diff --git a/backend/services/authorization-service/src/application/services/authorization-application.service.ts b/backend/services/authorization-service/src/application/services/authorization-application.service.ts index bf77b6f4..4cea93d6 100644 --- a/backend/services/authorization-service/src/application/services/authorization-application.service.ts +++ b/backend/services/authorization-service/src/application/services/authorization-application.service.ts @@ -85,11 +85,11 @@ export class AuthorizationApplicationService { communityName: command.communityName, }) - // 3. 检查初始考核(10棵) + // 3. 检查初始考核(10棵)- 使用下级团队认种数(不含自己) const teamStats = await this.statsRepository.findByAccountSequence(userId.accountSequence) - const totalTreeCount = teamStats?.totalTeamPlantingCount || 0 + const subordinateTreeCount = teamStats?.subordinateTeamPlantingCount || 0 - if (totalTreeCount >= authorization.getInitialTarget()) { + if (subordinateTreeCount >= authorization.getInitialTarget()) { // 达标,激活权益 authorization.activateBenefit() } @@ -104,8 +104,8 @@ export class AuthorizationApplicationService { benefitActive: authorization.benefitActive, message: authorization.benefitActive ? '社区权益已激活' - : `需要团队累计认种达到${authorization.getInitialTarget()}棵才能激活`, - currentTreeCount: totalTreeCount, + : `需要下级团队累计认种达到${authorization.getInitialTarget()}棵才能激活`, + currentTreeCount: subordinateTreeCount, requiredTreeCount: authorization.getInitialTarget(), } } @@ -139,11 +139,11 @@ export class AuthorizationApplicationService { provinceName: command.provinceName, }) - // 3. 检查初始考核(500棵) + // 3. 检查初始考核(500棵)- 使用下级团队认种数(不含自己) const teamStats = await this.statsRepository.findByAccountSequence(userId.accountSequence) - const totalTreeCount = teamStats?.totalTeamPlantingCount || 0 + const subordinateTreeCount = teamStats?.subordinateTeamPlantingCount || 0 - if (totalTreeCount >= authorization.getInitialTarget()) { + if (subordinateTreeCount >= authorization.getInitialTarget()) { // 达标,激活权益并创建首月考核 authorization.activateBenefit() await this.createInitialAssessment(authorization, teamStats!) @@ -160,8 +160,8 @@ export class AuthorizationApplicationService { displayTitle: authorization.displayTitle, message: authorization.benefitActive ? '授权省公司权益已激活,开始阶梯考核' - : `需要团队累计认种达到${authorization.getInitialTarget()}棵才能激活`, - currentTreeCount: totalTreeCount, + : `需要下级团队累计认种达到${authorization.getInitialTarget()}棵才能激活`, + currentTreeCount: subordinateTreeCount, requiredTreeCount: authorization.getInitialTarget(), } } @@ -195,11 +195,11 @@ export class AuthorizationApplicationService { cityName: command.cityName, }) - // 3. 检查初始考核(100棵) + // 3. 检查初始考核(100棵)- 使用下级团队认种数(不含自己) const teamStats = await this.statsRepository.findByAccountSequence(userId.accountSequence) - const totalTreeCount = teamStats?.totalTeamPlantingCount || 0 + const subordinateTreeCount = teamStats?.subordinateTeamPlantingCount || 0 - if (totalTreeCount >= authorization.getInitialTarget()) { + if (subordinateTreeCount >= authorization.getInitialTarget()) { authorization.activateBenefit() await this.createInitialAssessment(authorization, teamStats!) } @@ -215,8 +215,8 @@ export class AuthorizationApplicationService { displayTitle: authorization.displayTitle, message: authorization.benefitActive ? '授权市公司权益已激活,开始阶梯考核' - : `需要团队累计认种达到${authorization.getInitialTarget()}棵才能激活`, - currentTreeCount: totalTreeCount, + : `需要下级团队累计认种达到${authorization.getInitialTarget()}棵才能激活`, + currentTreeCount: subordinateTreeCount, requiredTreeCount: authorization.getInitialTarget(), } } @@ -748,6 +748,26 @@ export class AuthorizationApplicationService { return null } + /** + * 尝试激活授权权益 + * 仅当权益未激活时执行激活操作 + */ + private async tryActivateBenefit(authorization: AuthorizationRole): Promise { + if (authorization.benefitActive) { + return // 已激活,无需操作 + } + + this.logger.log( + `[tryActivateBenefit] Activating benefit for authorization ${authorization.authorizationId.value}, ` + + `role=${authorization.roleType}, accountSequence=${authorization.userId.accountSequence}`, + ) + + authorization.activateBenefit() + await this.authorizationRepository.save(authorization) + await this.eventPublisher.publishAll(authorization.domainEvents) + authorization.clearDomainEvents() + } + /** * 获取社区权益分配方案 * 根据考核规则计算每棵树的社区权益应该分配给谁 @@ -855,23 +875,23 @@ export class AuthorizationApplicationService { } // 5. 权益未激活,需要计算考核分配 - // 获取该社区的团队统计数据(当前已完成的认种数量) + // 获取该社区的团队统计数据 - 使用下级团队认种数(不含自己) const communityStats = await this.statsRepository.findByAccountSequence( nearestCommunity.userId.accountSequence, ) - const rawTeamCount = communityStats?.totalTeamPlantingCount ?? 0 + const rawSubordinateCount = communityStats?.subordinateTeamPlantingCount ?? 0 // 重要:由于 referral-service 和 reward-service 都消费同一个 Kafka 事件, - // 存在竞态条件,此时查询到的 totalTeamPlantingCount 可能已经包含了本次认种。 - // 因此需要减去本次认种数量来还原"认种前"的团队数。 - // 注意:如果 referral-service 还没处理完,rawTeamCount 可能还是旧值, + // 存在竞态条件,此时查询到的 subordinateTeamPlantingCount 可能已经包含了本次认种。 + // 因此需要减去本次认种数量来还原"认种前"的下级团队数。 + // 注意:如果 referral-service 还没处理完,rawSubordinateCount 可能还是旧值, // 此时 currentTeamCount 可能为负数,需要取 max(0, ...) - const currentTeamCount = Math.max(0, rawTeamCount - treeCount) + const currentTeamCount = Math.max(0, rawSubordinateCount - treeCount) const initialTarget = nearestCommunity.getInitialTarget() // 社区初始考核目标:10棵 this.logger.debug( `[getCommunityRewardDistribution] Community ${nearestCommunity.userId.accountSequence} ` + - `benefitActive=false, rawTeamCount=${rawTeamCount}, treeCount=${treeCount}, ` + + `benefitActive=false, rawSubordinateCount=${rawSubordinateCount}, treeCount=${treeCount}, ` + `currentTeamCount(before)=${currentTeamCount}, initialTarget=${initialTarget}`, ) @@ -907,6 +927,9 @@ export class AuthorizationApplicationService { treeCount, reason: '已达初始考核目标', }) + + // 自动激活权益 + await this.tryActivateBenefit(nearestCommunity) } else { // 未达标,需要拆分 const remaining = initialTarget - currentTeamCount // 还差多少棵达标 @@ -932,6 +955,9 @@ export class AuthorizationApplicationService { treeCount: treeCount - remaining, reason: `考核达标后权益生效`, }) + + // 自动激活权益(本次认种使其达标) + await this.tryActivateBenefit(nearestCommunity) } } @@ -1029,15 +1055,15 @@ export class AuthorizationApplicationService { } } - // 5. 权益未激活,计算考核分配 + // 5. 权益未激活,计算考核分配 - 使用下级团队认种数(不含自己) const stats = await this.statsRepository.findByAccountSequence(nearestAuthProvince.userId.accountSequence) - const rawTeamCount = stats?.totalTeamPlantingCount ?? 0 - // 修复竞态条件:减去本次认种数量来还原"认种前"的团队数 - const currentTeamCount = Math.max(0, rawTeamCount - treeCount) + const rawSubordinateCount = stats?.subordinateTeamPlantingCount ?? 0 + // 修复竞态条件:减去本次认种数量来还原"认种前"的下级团队数 + const currentTeamCount = Math.max(0, rawSubordinateCount - treeCount) const initialTarget = nearestAuthProvince.getInitialTarget() // 500棵 this.logger.debug( - `[getProvinceTeamRewardDistribution] rawTeamCount=${rawTeamCount}, treeCount=${treeCount}, currentTeamCount(before)=${currentTeamCount}`, + `[getProvinceTeamRewardDistribution] rawSubordinateCount=${rawSubordinateCount}, treeCount=${treeCount}, currentTeamCount(before)=${currentTeamCount}`, ) // 6. 查找上级(用于接收考核前的权益) @@ -1065,6 +1091,8 @@ export class AuthorizationApplicationService { treeCount, reason: '已达初始考核目标', }) + // 自动激活权益 + await this.tryActivateBenefit(nearestAuthProvince) } else { const remaining = initialTarget - currentTeamCount @@ -1085,6 +1113,8 @@ export class AuthorizationApplicationService { treeCount: treeCount - remaining, reason: '考核达标后权益生效', }) + // 自动激活权益(本次认种使其达标) + await this.tryActivateBenefit(nearestAuthProvince) } } @@ -1239,15 +1269,15 @@ export class AuthorizationApplicationService { } } - // 5. 权益未激活,计算考核分配 + // 5. 权益未激活,计算考核分配 - 使用下级团队认种数(不含自己) const stats = await this.statsRepository.findByAccountSequence(nearestAuthCity.userId.accountSequence) - const rawTeamCount = stats?.totalTeamPlantingCount ?? 0 - // 修复竞态条件:减去本次认种数量来还原"认种前"的团队数 - const currentTeamCount = Math.max(0, rawTeamCount - treeCount) + const rawSubordinateCount = stats?.subordinateTeamPlantingCount ?? 0 + // 修复竞态条件:减去本次认种数量来还原"认种前"的下级团队数 + const currentTeamCount = Math.max(0, rawSubordinateCount - treeCount) const initialTarget = nearestAuthCity.getInitialTarget() // 100棵 this.logger.debug( - `[getCityTeamRewardDistribution] rawTeamCount=${rawTeamCount}, treeCount=${treeCount}, currentTeamCount(before)=${currentTeamCount}`, + `[getCityTeamRewardDistribution] rawSubordinateCount=${rawSubordinateCount}, treeCount=${treeCount}, currentTeamCount(before)=${currentTeamCount}`, ) // 6. 查找上级 @@ -1275,6 +1305,8 @@ export class AuthorizationApplicationService { treeCount, reason: '已达初始考核目标', }) + // 自动激活权益 + await this.tryActivateBenefit(nearestAuthCity) } else { const remaining = initialTarget - currentTeamCount @@ -1295,6 +1327,8 @@ export class AuthorizationApplicationService { treeCount: treeCount - remaining, reason: '考核达标后权益生效', }) + // 自动激活权益(本次认种使其达标) + await this.tryActivateBenefit(nearestAuthCity) } } diff --git a/backend/services/authorization-service/src/domain/services/assessment-calculator.service.ts b/backend/services/authorization-service/src/domain/services/assessment-calculator.service.ts index b5186204..3a3bcc9d 100644 --- a/backend/services/authorization-service/src/domain/services/assessment-calculator.service.ts +++ b/backend/services/authorization-service/src/domain/services/assessment-calculator.service.ts @@ -8,6 +8,9 @@ export interface TeamStatistics { userId: string accountSequence: bigint totalTeamPlantingCount: number + selfPlantingCount: number + /** 下级团队认种数(不包括自己)= totalTeamPlantingCount - selfPlantingCount */ + get subordinateTeamPlantingCount(): number getProvinceTeamCount(provinceCode: string): number getCityTeamCount(cityCode: string): number } diff --git a/backend/services/authorization-service/src/infrastructure/external/referral-service.client.ts b/backend/services/authorization-service/src/infrastructure/external/referral-service.client.ts index e926b392..e2806f78 100644 --- a/backend/services/authorization-service/src/infrastructure/external/referral-service.client.ts +++ b/backend/services/authorization-service/src/infrastructure/external/referral-service.client.ts @@ -13,6 +13,7 @@ interface ReferralTeamStatsResponse { userId: string; accountSequence: string; totalTeamPlantingCount: number; + selfPlantingCount: number; provinceCityDistribution: Record> | null; } @@ -42,9 +43,15 @@ class TeamStatisticsAdapter implements TeamStatistics { public readonly userId: string, public readonly accountSequence: bigint, public readonly totalTeamPlantingCount: number, + public readonly selfPlantingCount: number, private readonly provinceCityDistribution: Record> | null, ) {} + /** 下级团队认种数(不包括自己) */ + get subordinateTeamPlantingCount(): number { + return Math.max(0, this.totalTeamPlantingCount - this.selfPlantingCount); + } + getProvinceTeamCount(provinceCode: string): number { if (!this.provinceCityDistribution || !this.provinceCityDistribution[provinceCode]) { return 0; @@ -125,6 +132,7 @@ export class ReferralServiceClient implements ITeamStatisticsRepository, OnModul data.userId, BigInt(data.accountSequence || 0), data.totalTeamPlantingCount, + data.selfPlantingCount || 0, data.provinceCityDistribution, ); } catch (error) { @@ -156,12 +164,13 @@ export class ReferralServiceClient implements ITeamStatisticsRepository, OnModul } const data = response.data; - this.logger.debug(`[HTTP] Got stats for accountSequence ${accountSequence}: totalTeamPlantingCount=${data.totalTeamPlantingCount}`); + this.logger.debug(`[HTTP] Got stats for accountSequence ${accountSequence}: totalTeamPlantingCount=${data.totalTeamPlantingCount}, selfPlantingCount=${data.selfPlantingCount}`); return new TeamStatisticsAdapter( data.userId, BigInt(data.accountSequence || accountSequence.toString()), data.totalTeamPlantingCount, + data.selfPlantingCount || 0, data.provinceCityDistribution, ); } catch (error) { @@ -175,7 +184,7 @@ export class ReferralServiceClient implements ITeamStatisticsRepository, OnModul * 创建空的统计数据 */ private createEmptyStats(userId: string, accountSequence: bigint): TeamStatistics { - return new TeamStatisticsAdapter(userId, accountSequence, 0, null); + return new TeamStatisticsAdapter(userId, accountSequence, 0, 0, null); } /** 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 15627fbf..edc422de 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 @@ -136,8 +136,9 @@ export class EventConsumerController { return } - const totalTreeCount = teamStats.totalTeamPlantingCount - this.logger.debug(`[PLANTING] User ${userId} total team planting count: ${totalTreeCount}`) + // 使用下级团队认种数(不含自己)进行考核判断 + const subordinateTreeCount = teamStats.subordinateTeamPlantingCount + this.logger.debug(`[PLANTING] User ${userId} subordinate team planting count: ${subordinateTreeCount}`) // 2. 获取用户所有授权 const authorizations = await this.authorizationRepository.findByUserId( @@ -151,14 +152,15 @@ export class EventConsumerController { // 3. 处理每个授权 for (const auth of authorizations) { - this.logger.debug(`[PLANTING] Processing authorization: ${auth.authorizationId.value}, role=${auth.roleType}, benefitActive=${auth.benefitActive}`) + this.logger.debug(`[PLANTING] Processing authorization: ${auth.authorizationId.value}, role=${auth.roleType}, benefitActive=${auth.benefitActive}, status=${auth.status}`) // 3.1 检查初始考核(权益未激活的情况) - if (!auth.benefitActive && auth.status === AuthorizationStatus.PENDING) { + // 支持 PENDING(用户申请)和 AUTHORIZED(管理员授权)两种状态 + if (!auth.benefitActive && (auth.status === AuthorizationStatus.PENDING || auth.status === AuthorizationStatus.AUTHORIZED)) { const initialTarget = auth.getInitialTarget() - this.logger.debug(`[PLANTING] Checking initial target: ${totalTreeCount}/${initialTarget}`) + this.logger.debug(`[PLANTING] Checking initial target: ${subordinateTreeCount}/${initialTarget}`) - if (totalTreeCount >= initialTarget) { + if (subordinateTreeCount >= initialTarget) { this.logger.log(`[PLANTING] User ${userId} reached initial target for ${auth.roleType}, activating benefit`) auth.activateBenefit() await this.authorizationRepository.save(auth) diff --git a/backend/services/referral-service/src/api/controllers/internal-team-statistics.controller.ts b/backend/services/referral-service/src/api/controllers/internal-team-statistics.controller.ts index a89a4404..e3c9b35a 100644 --- a/backend/services/referral-service/src/api/controllers/internal-team-statistics.controller.ts +++ b/backend/services/referral-service/src/api/controllers/internal-team-statistics.controller.ts @@ -31,6 +31,7 @@ export class InternalTeamStatisticsController { userId: { type: 'string' }, accountSequence: { type: 'string' }, totalTeamPlantingCount: { type: 'number' }, + selfPlantingCount: { type: 'number' }, provinceCityDistribution: { type: 'object' }, }, }, @@ -51,7 +52,8 @@ export class InternalTeamStatisticsController { return { userId: stats.userId.toString(), accountSequence: '0', // userId 查询时无法获取 accountSequence - totalTeamPlantingCount: stats.teamPlantingCount, // 使用 teamPlantingCount 作为团队总量 + totalTeamPlantingCount: stats.teamPlantingCount, // 团队总认种(含自己) + selfPlantingCount: stats.personalPlantingCount, // 自己的认种数 provinceCityDistribution: distribution.toJson(), }; } catch (error) { @@ -72,6 +74,7 @@ export class InternalTeamStatisticsController { userId: { type: 'string' }, accountSequence: { type: 'string' }, totalTeamPlantingCount: { type: 'number' }, + selfPlantingCount: { type: 'number' }, provinceCityDistribution: { type: 'object' }, }, }, @@ -94,7 +97,8 @@ export class InternalTeamStatisticsController { return { userId: stats.userId.toString(), accountSequence: accountSequence, - totalTeamPlantingCount: stats.teamPlantingCount, + totalTeamPlantingCount: stats.teamPlantingCount, // 团队总认种(含自己) + selfPlantingCount: stats.personalPlantingCount, // 自己的认种数 provinceCityDistribution: distribution.toJson(), }; } catch (error) {