From bf63852a0f2ea46fcec10af98a4e7c40e503c4d7 Mon Sep 17 00:00:00 2001 From: hailin Date: Tue, 23 Dec 2025 03:56:38 -0800 Subject: [PATCH] =?UTF-8?q?feat(authorization):=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=B8=82/=E7=9C=81=E5=9B=A2=E9=98=9F=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=8D=87=E7=BA=A7=E4=B8=BA=E5=B8=82/=E7=9C=81=E5=8C=BA?= =?UTF-8?q?=E5=9F=9F=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 findAllActiveAuthProvinceCompanies 和 findAllActiveAuthCityCompanies 仓储方法 - 在认种事件处理中添加 checkAllTeamAutoUpgrade 检查所有已激活团队 - 市团队达到1万棵自动升级为市区域(CITY_COMPANY) - 省团队达到5万棵自动升级为省区域(PROVINCE_COMPANY) - 保持原有手动授权功能不变 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../authorization-role.repository.ts | 14 ++ .../kafka/event-consumer.controller.ts | 200 +++++++++++++++++- .../authorization-role.repository.impl.ts | 28 +++ .../test/domain-services.integration-spec.ts | 2 + 4 files changed, 243 insertions(+), 1 deletion(-) diff --git a/backend/services/authorization-service/src/domain/repositories/authorization-role.repository.ts b/backend/services/authorization-service/src/domain/repositories/authorization-role.repository.ts index e3b0e67d..4ac4e0cd 100644 --- a/backend/services/authorization-service/src/domain/repositories/authorization-role.repository.ts +++ b/backend/services/authorization-service/src/domain/repositories/authorization-role.repository.ts @@ -116,4 +116,18 @@ export interface IAuthorizationRoleRepository { * 用于审计和历史记录查询 */ findByIdIncludeDeleted(authorizationId: AuthorizationId): Promise + + // ============ 自动升级检查方法 ============ + + /** + * 查找所有权益已激活的省团队(AUTH_PROVINCE_COMPANY)授权 + * 用于自动升级检查:省团队 → 省区域 + */ + findAllActiveAuthProvinceCompanies(): Promise + + /** + * 查找所有权益已激活的市团队(AUTH_CITY_COMPANY)授权 + * 用于自动升级检查:市团队 → 市区域 + */ + findAllActiveAuthCityCompanies(): Promise } diff --git a/backend/services/authorization-service/src/infrastructure/kafka/event-consumer.controller.ts b/backend/services/authorization-service/src/infrastructure/kafka/event-consumer.controller.ts index 88016f15..e0c9517a 100644 --- a/backend/services/authorization-service/src/infrastructure/kafka/event-consumer.controller.ts +++ b/backend/services/authorization-service/src/infrastructure/kafka/event-consumer.controller.ts @@ -183,9 +183,13 @@ export class EventConsumerController { } } - // 4. 检查自动升级条件(省区域/市区域) + // 4. 检查自动升级条件(省区域/市区域)- 仅针对当前用户的祖先链 await this.checkAutoUpgrade(teamStats) + // 5. 检查所有已激活市/省团队的自动升级条件 + // 每次认种都可能导致某个市/省团队达到升级阈值 + await this.checkAllTeamAutoUpgrade() + this.logger.log(`[PLANTING] Completed processing tree planted event for user ${userId}`) } catch (error) { this.logger.error(`[PLANTING] Error processing tree planted for user ${userId}:`, error) @@ -486,4 +490,198 @@ export class EventConsumerController { this.logger.log(`[AUTO-UPGRADE] Successfully auto upgraded user ${accountSequence} to city company: ${cityName}`) } + + /** + * 检查所有已激活市/省团队的自动升级条件 + * 业务规则: + * - 已激活权益的省团队(AUTH_PROVINCE_COMPANY)用户,如果团队认种数达到5万棵,自动升级为省区域(PROVINCE_COMPANY) + * - 已激活权益的市团队(AUTH_CITY_COMPANY)用户,如果团队认种数达到1万棵,自动升级为市区域(CITY_COMPANY) + */ + private async checkAllTeamAutoUpgrade(): Promise { + this.logger.debug('[TEAM-AUTO-UPGRADE] Starting check for all active team authorizations') + + // 并行检查省团队和市团队 + await Promise.all([ + this.checkAllAuthProvinceUpgrade(), + this.checkAllAuthCityUpgrade(), + ]) + + this.logger.debug('[TEAM-AUTO-UPGRADE] Completed check for all active team authorizations') + } + + /** + * 检查所有已激活省团队的自动升级条件 + */ + private async checkAllAuthProvinceUpgrade(): Promise { + // 1. 获取所有权益已激活的省团队授权 + const activeAuthProvinces = await this.authorizationRepository.findAllActiveAuthProvinceCompanies() + if (activeAuthProvinces.length === 0) { + this.logger.debug('[TEAM-AUTO-UPGRADE] No active auth province companies found') + return + } + + this.logger.debug(`[TEAM-AUTO-UPGRADE] Found ${activeAuthProvinces.length} active auth province companies`) + + // 2. 逐个检查是否达到升级阈值 + for (const authProvince of activeAuthProvinces) { + await this.checkAuthProvinceUpgrade(authProvince) + } + } + + /** + * 检查单个省团队是否可以升级为省区域 + */ + private async checkAuthProvinceUpgrade(authProvince: AuthorizationRole): Promise { + const accountSequence = authProvince.userId.accountSequence + const provinceCode = authProvince.regionCode.value + const provinceName = authProvince.regionName + + // 1. 获取该用户的团队统计 + const teamStats = await this.statsRepository.findByAccountSequence(accountSequence) + if (!teamStats) { + this.logger.debug(`[TEAM-AUTO-UPGRADE] No team stats found for auth province ${accountSequence}`) + return + } + + const totalTeamCount = teamStats.totalTeamPlantingCount + + // 2. 检查是否达到省区域升级阈值(5万棵) + if (totalTeamCount < EventConsumerController.PROVINCE_UPGRADE_THRESHOLD) { + this.logger.debug(`[TEAM-AUTO-UPGRADE] Auth province ${accountSequence} has ${totalTeamCount} trees, not reaching ${EventConsumerController.PROVINCE_UPGRADE_THRESHOLD} threshold`) + return + } + + this.logger.log(`[TEAM-AUTO-UPGRADE] Auth province ${accountSequence} reached ${totalTeamCount} trees, checking upgrade eligibility`) + + // 3. 检查该用户是否已有省区域授权 + const existingProvinceCompany = await this.authorizationRepository.findByAccountSequenceAndRoleType( + accountSequence, + RoleType.PROVINCE_COMPANY, + ) + if (existingProvinceCompany && existingProvinceCompany.status !== AuthorizationStatus.REVOKED) { + this.logger.debug(`[TEAM-AUTO-UPGRADE] User ${accountSequence} already has province company authorization`) + return + } + + // 4. 检查该用户是否已有市区域授权(互斥) + const existingCityCompany = await this.authorizationRepository.findByAccountSequenceAndRoleType( + accountSequence, + RoleType.CITY_COMPANY, + ) + if (existingCityCompany && existingCityCompany.status !== AuthorizationStatus.REVOKED) { + this.logger.debug(`[TEAM-AUTO-UPGRADE] User ${accountSequence} already has city company authorization, cannot upgrade to province`) + return + } + + // 5. 检查该省是否已有省区域授权 + const existingProvinceRegion = await this.authorizationRepository.findProvinceCompanyByRegion(provinceCode) + if (existingProvinceRegion) { + this.logger.debug(`[TEAM-AUTO-UPGRADE] Province ${provinceName} already has province company authorization`) + return + } + + // 6. 执行自动升级:省团队 → 省区域 + this.logger.log(`[TEAM-AUTO-UPGRADE] Auto upgrading auth province ${accountSequence} to province company: ${provinceName}`) + + const userId = UserId.create(teamStats.userId, accountSequence) + const authorization = AuthorizationRole.createAutoUpgradedProvinceCompany({ + userId, + provinceCode, + provinceName, + }) + + await this.authorizationRepository.save(authorization) + await this.eventPublisher.publishAll(authorization.domainEvents) + authorization.clearDomainEvents() + + this.logger.log(`[TEAM-AUTO-UPGRADE] Successfully auto upgraded auth province ${accountSequence} to province company: ${provinceName}`) + } + + /** + * 检查所有已激活市团队的自动升级条件 + */ + private async checkAllAuthCityUpgrade(): Promise { + // 1. 获取所有权益已激活的市团队授权 + const activeAuthCities = await this.authorizationRepository.findAllActiveAuthCityCompanies() + if (activeAuthCities.length === 0) { + this.logger.debug('[TEAM-AUTO-UPGRADE] No active auth city companies found') + return + } + + this.logger.debug(`[TEAM-AUTO-UPGRADE] Found ${activeAuthCities.length} active auth city companies`) + + // 2. 逐个检查是否达到升级阈值 + for (const authCity of activeAuthCities) { + await this.checkAuthCityUpgrade(authCity) + } + } + + /** + * 检查单个市团队是否可以升级为市区域 + */ + private async checkAuthCityUpgrade(authCity: AuthorizationRole): Promise { + const accountSequence = authCity.userId.accountSequence + const cityCode = authCity.regionCode.value + const cityName = authCity.regionName + + // 1. 获取该用户的团队统计 + const teamStats = await this.statsRepository.findByAccountSequence(accountSequence) + if (!teamStats) { + this.logger.debug(`[TEAM-AUTO-UPGRADE] No team stats found for auth city ${accountSequence}`) + return + } + + const totalTeamCount = teamStats.totalTeamPlantingCount + + // 2. 检查是否达到市区域升级阈值(1万棵) + if (totalTeamCount < EventConsumerController.CITY_UPGRADE_THRESHOLD) { + this.logger.debug(`[TEAM-AUTO-UPGRADE] Auth city ${accountSequence} has ${totalTeamCount} trees, not reaching ${EventConsumerController.CITY_UPGRADE_THRESHOLD} threshold`) + return + } + + this.logger.log(`[TEAM-AUTO-UPGRADE] Auth city ${accountSequence} reached ${totalTeamCount} trees, checking upgrade eligibility`) + + // 3. 检查该用户是否已有市区域授权 + const existingCityCompany = await this.authorizationRepository.findByAccountSequenceAndRoleType( + accountSequence, + RoleType.CITY_COMPANY, + ) + if (existingCityCompany && existingCityCompany.status !== AuthorizationStatus.REVOKED) { + this.logger.debug(`[TEAM-AUTO-UPGRADE] User ${accountSequence} already has city company authorization`) + return + } + + // 4. 检查该用户是否已有省区域授权(互斥) + const existingProvinceCompany = await this.authorizationRepository.findByAccountSequenceAndRoleType( + accountSequence, + RoleType.PROVINCE_COMPANY, + ) + if (existingProvinceCompany && existingProvinceCompany.status !== AuthorizationStatus.REVOKED) { + this.logger.debug(`[TEAM-AUTO-UPGRADE] User ${accountSequence} already has province company authorization, cannot upgrade to city`) + return + } + + // 5. 检查该市是否已有市区域授权 + const existingCityRegion = await this.authorizationRepository.findCityCompanyByRegion(cityCode) + if (existingCityRegion) { + this.logger.debug(`[TEAM-AUTO-UPGRADE] City ${cityName} already has city company authorization`) + return + } + + // 6. 执行自动升级:市团队 → 市区域 + this.logger.log(`[TEAM-AUTO-UPGRADE] Auto upgrading auth city ${accountSequence} to city company: ${cityName}`) + + const userId = UserId.create(teamStats.userId, accountSequence) + const authorization = AuthorizationRole.createAutoUpgradedCityCompany({ + userId, + cityCode, + cityName, + }) + + await this.authorizationRepository.save(authorization) + await this.eventPublisher.publishAll(authorization.domainEvents) + authorization.clearDomainEvents() + + this.logger.log(`[TEAM-AUTO-UPGRADE] Successfully auto upgraded auth city ${accountSequence} to city company: ${cityName}`) + } } diff --git a/backend/services/authorization-service/src/infrastructure/persistence/repositories/authorization-role.repository.impl.ts b/backend/services/authorization-service/src/infrastructure/persistence/repositories/authorization-role.repository.impl.ts index fa6ef00e..f3c1c954 100644 --- a/backend/services/authorization-service/src/infrastructure/persistence/repositories/authorization-role.repository.impl.ts +++ b/backend/services/authorization-service/src/infrastructure/persistence/repositories/authorization-role.repository.impl.ts @@ -447,6 +447,34 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi return record ? this.toDomain(record) : null } + // ============ 自动升级检查方法 ============ + + async findAllActiveAuthProvinceCompanies(): Promise { + const records = await this.prisma.authorizationRole.findMany({ + where: { + roleType: RoleType.AUTH_PROVINCE_COMPANY, + status: AuthorizationStatus.AUTHORIZED, + benefitActive: true, + ...this.notDeleted, + }, + orderBy: { createdAt: 'asc' }, + }) + return records.map((record) => this.toDomain(record)) + } + + async findAllActiveAuthCityCompanies(): Promise { + const records = await this.prisma.authorizationRole.findMany({ + where: { + roleType: RoleType.AUTH_CITY_COMPANY, + status: AuthorizationStatus.AUTHORIZED, + benefitActive: true, + ...this.notDeleted, + }, + orderBy: { createdAt: 'asc' }, + }) + return records.map((record) => this.toDomain(record)) + } + private toDomain(record: any): AuthorizationRole { const props: AuthorizationRoleProps = { authorizationId: AuthorizationId.create(record.id), diff --git a/backend/services/authorization-service/test/domain-services.integration-spec.ts b/backend/services/authorization-service/test/domain-services.integration-spec.ts index 8df26743..914dd77a 100644 --- a/backend/services/authorization-service/test/domain-services.integration-spec.ts +++ b/backend/services/authorization-service/test/domain-services.integration-spec.ts @@ -44,6 +44,8 @@ describe('Domain Services Integration Tests', () => { findAllByUserIdIncludeDeleted: jest.fn(), findAllByAccountSequenceIncludeDeleted: jest.fn(), findByIdIncludeDeleted: jest.fn(), + findAllActiveAuthProvinceCompanies: jest.fn(), + findAllActiveAuthCityCompanies: jest.fn(), } const mockMonthlyAssessmentRepository: jest.Mocked = {