fix(authorization): use subordinate team count for assessment and auto-activate benefit

1. Assessment calculation now uses subordinate team count (excluding self planting)
   - Added selfPlantingCount to referral-service API response
   - Added subordinateTeamPlantingCount getter to TeamStatistics interface
   - Updated all assessment checks to use subordinateTeamPlantingCount

2. Auto-activate benefit when assessment target is reached
   - Added tryActivateBenefit helper method
   - Community, province team, and city team reward distribution methods
     now automatically activate benefit when target is reached

3. Fixed event consumer to support AUTHORIZED status (admin grants)
   - Previously only checked PENDING status for activation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-11 19:33:39 -08:00
parent f94e7283d5
commit 9819130f98
5 changed files with 94 additions and 42 deletions

View File

@ -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<void> {
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)
}
}

View File

@ -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
}

View File

@ -13,6 +13,7 @@ interface ReferralTeamStatsResponse {
userId: string;
accountSequence: string;
totalTeamPlantingCount: number;
selfPlantingCount: number;
provinceCityDistribution: Record<string, Record<string, number>> | 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<string, Record<string, number>> | 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);
}
/**

View File

@ -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)

View File

@ -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) {