feat(authorization-service): 省团队授权级联激活/停用及月度考核

- 添加 cascadeActivateParentAuthProvinces: 当省团队授权激活时级联激活上级省团队
- 添加 cascadeDeactivateAuthProvinceBenefits: 月度考核失败时级联停用上级省团队
- 添加 processExpiredAuthProvinceBenefits: 月度考核处理(500棵树/月)
- 修改 tryActivateBenefit 支持 AUTH_PROVINCE_COMPANY 级联激活
- 定时任务: 每天凌晨5点检查省团队权益过期
- 定时任务: 每月1号存档并重置省团队月度新增树数

🤖 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-13 22:32:39 -08:00
parent 049a13c97e
commit 4d58d4be6c
2 changed files with 285 additions and 0 deletions

View File

@ -246,6 +246,9 @@ export class MonthlyAssessmentScheduler {
// 同时处理市团队授权
await this.archiveAndResetAuthCityMonthlyTreeCounts()
// 同时处理省团队授权
await this.archiveAndResetAuthProvinceMonthlyTreeCounts()
} catch (error) {
this.logger.error('[archiveAndResetMonthlyTreeCounts] 月度树数存档失败', error)
}
@ -286,6 +289,41 @@ export class MonthlyAssessmentScheduler {
}
}
/**
*
*
*
* - lastMonthTreesAdded
* - monthlyTreesAdded 0
*/
private async archiveAndResetAuthProvinceMonthlyTreeCounts(): Promise<void> {
this.logger.log('[archiveAndResetAuthProvinceMonthlyTreeCounts] 开始存档省团队月度新增树数...')
try {
// 获取所有激活的省团队授权
const activeAuthProvinces = await this.authorizationRepository.findAllActive(RoleType.AUTH_PROVINCE_COMPANY)
let archivedCount = 0
for (const authProvince of activeAuthProvinces) {
if (authProvince.benefitActive) {
// 存档当月数据到 lastMonthTreesAdded然后重置 monthlyTreesAdded
authProvince.archiveAndResetMonthlyTrees()
await this.authorizationRepository.save(authProvince)
archivedCount++
this.logger.debug(
`[archiveAndResetAuthProvinceMonthlyTreeCounts] 省团队授权 ${authProvince.userId.accountSequence}: ` +
`存档=${authProvince.lastMonthTreesAdded}, 当月已重置=0`,
)
}
}
this.logger.log(`[archiveAndResetAuthProvinceMonthlyTreeCounts] 存档完成: 已处理 ${archivedCount} 个省团队授权`)
} catch (error) {
this.logger.error('[archiveAndResetAuthProvinceMonthlyTreeCounts] 省团队月度树数存档失败', error)
}
}
/**
* 4
*
@ -309,4 +347,28 @@ export class MonthlyAssessmentScheduler {
this.logger.error('[processExpiredAuthCityBenefits] 市团队授权权益过期检查失败', error)
}
}
/**
* 5
*
*
* - benefitValidUntil < benefitActive=true
* - >= 500
* -
*/
@Cron('0 5 * * *')
async processExpiredAuthProvinceBenefits(): Promise<void> {
this.logger.log('[processExpiredAuthProvinceBenefits] 开始检查省团队授权权益过期情况...')
try {
const result = await this.authorizationAppService.processExpiredAuthProvinceBenefits(100)
this.logger.log(
`[processExpiredAuthProvinceBenefits] 处理完成: ` +
`已处理=${result.processedCount}, 已续期=${result.renewedCount}, 已停用=${result.deactivatedCount}`,
)
} catch (error) {
this.logger.error('[processExpiredAuthProvinceBenefits] 省团队授权权益过期检查失败', error)
}
}
}

View File

@ -886,6 +886,11 @@ export class AuthorizationApplicationService {
if (authorization.roleType === RoleType.AUTH_CITY_COMPANY) {
await this.cascadeActivateParentAuthCities(authorization.userId.accountSequence)
}
// 如果是省团队授权权益,需要级联激活上级省团队授权
if (authorization.roleType === RoleType.AUTH_PROVINCE_COMPANY) {
await this.cascadeActivateParentAuthProvinces(authorization.userId.accountSequence)
}
}
/**
@ -1238,6 +1243,224 @@ export class AuthorizationApplicationService {
)
}
/**
*
*
*
*
* -
* -
* -
*/
private async cascadeActivateParentAuthProvinces(accountSequence: string): Promise<void> {
this.logger.log(
`[cascadeActivateParentAuthProvinces] Starting cascade activation for auth provinces above ${accountSequence}`,
)
// 1. 获取推荐链(不包括当前用户)
const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(accountSequence)
if (ancestorAccountSequences.length === 0) {
return
}
// 2. 查找推荐链上所有省团队授权(包括 benefitActive=false
const ancestorAuthProvinces = await this.authorizationRepository.findAuthProvinceByAccountSequences(
ancestorAccountSequences,
)
// 3. 筛选出已授权但权益未激活的省团队授权
const inactiveAuthProvinces = ancestorAuthProvinces.filter(
(auth) => auth.status === AuthorizationStatus.AUTHORIZED && !auth.benefitActive,
)
if (inactiveAuthProvinces.length === 0) {
this.logger.debug('[cascadeActivateParentAuthProvinces] No inactive parent auth provinces to activate')
return
}
// 4. 激活这些省团队授权的权益
for (const authProvince of inactiveAuthProvinces) {
this.logger.log(
`[cascadeActivateParentAuthProvinces] Cascade activating auth province benefit: ` +
`authorizationId=${authProvince.authorizationId.value}, accountSequence=${authProvince.userId.accountSequence}`,
)
authProvince.activateBenefit()
await this.authorizationRepository.save(authProvince)
await this.eventPublisher.publishAll(authProvince.domainEvents)
authProvince.clearDomainEvents()
}
this.logger.log(
`[cascadeActivateParentAuthProvinces] Cascade activated ${inactiveAuthProvinces.length} parent auth provinces`,
)
}
/**
*
*
*
*
* -
* - 500
*/
async cascadeDeactivateAuthProvinceBenefits(
accountSequence: string,
reason: string,
): Promise<{ deactivatedCount: number }> {
this.logger.log(
`[cascadeDeactivateAuthProvinceBenefits] Starting cascade deactivation from ${accountSequence}, reason=${reason}`,
)
// 1. 获取当前省团队授权
const currentAuthProvince = await this.authorizationRepository.findByAccountSequenceAndRoleType(
accountSequence,
RoleType.AUTH_PROVINCE_COMPANY,
)
if (!currentAuthProvince) {
this.logger.warn(`[cascadeDeactivateAuthProvinceBenefits] Auth province not found for ${accountSequence}`)
return { deactivatedCount: 0 }
}
// 2. 收集需要停用的省团队授权列表
const authProvincesToDeactivate: AuthorizationRole[] = []
// 如果当前省团队授权权益已激活,加入停用列表
if (currentAuthProvince.benefitActive) {
authProvincesToDeactivate.push(currentAuthProvince)
}
// 3. 获取推荐链上的所有父级省团队授权
const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(accountSequence)
if (ancestorAccountSequences.length > 0) {
const ancestorAuthProvinces = await this.authorizationRepository.findAuthProvinceByAccountSequences(
ancestorAccountSequences,
)
// 筛选出已授权且权益已激活的省团队授权
const activeAuthProvinces = ancestorAuthProvinces.filter(
(auth) => auth.status === AuthorizationStatus.AUTHORIZED && auth.benefitActive,
)
authProvincesToDeactivate.push(...activeAuthProvinces)
}
if (authProvincesToDeactivate.length === 0) {
this.logger.debug('[cascadeDeactivateAuthProvinceBenefits] No active auth provinces to deactivate')
return { deactivatedCount: 0 }
}
// 4. 停用这些省团队授权的权益
for (const authProvince of authProvincesToDeactivate) {
this.logger.log(
`[cascadeDeactivateAuthProvinceBenefits] Deactivating auth province benefit: ` +
`authorizationId=${authProvince.authorizationId.value}, accountSequence=${authProvince.userId.accountSequence}`,
)
authProvince.deactivateBenefit(reason)
await this.authorizationRepository.save(authProvince)
await this.eventPublisher.publishAll(authProvince.domainEvents)
authProvince.clearDomainEvents()
}
this.logger.log(
`[cascadeDeactivateAuthProvinceBenefits] Cascade deactivated ${authProvincesToDeactivate.length} auth provinces`,
)
return { deactivatedCount: authProvincesToDeactivate.length }
}
/**
*
*
*
*
* - benefitValidUntil < benefitActive=true
* - 500
* -
*
* @param limit
*/
async processExpiredAuthProvinceBenefits(limit = 100): Promise<{
processedCount: number
renewedCount: number
deactivatedCount: number
}> {
const now = new Date()
this.logger.log(`[processExpiredAuthProvinceBenefits] Starting at ${now.toISOString()}, limit=${limit}`)
// 查找过期但仍激活的省团队授权
const expiredAuthProvinces = await this.findExpiredActiveAuthProvinces(now, limit)
if (expiredAuthProvinces.length === 0) {
this.logger.debug('[processExpiredAuthProvinceBenefits] No expired auth provinces found')
return { processedCount: 0, renewedCount: 0, deactivatedCount: 0 }
}
let renewedCount = 0
let deactivatedCount = 0
for (const authProvince of expiredAuthProvinces) {
const accountSequence = authProvince.userId.accountSequence
// 使用 getTreesForAssessment 获取正确的考核数据
const treesForAssessment = authProvince.getTreesForAssessment(now)
this.logger.debug(
`[processExpiredAuthProvinceBenefits] Checking auth province ${accountSequence}: ` +
`treesForAssessment=${treesForAssessment}, ` +
`monthlyTreesAdded=${authProvince.monthlyTreesAdded}, ` +
`lastMonthTreesAdded=${authProvince.lastMonthTreesAdded}, ` +
`benefitValidUntil=${authProvince.benefitValidUntil?.toISOString()}, target=500`,
)
if (treesForAssessment >= 500) {
// 达标,续期
authProvince.renewBenefit(treesForAssessment)
await this.authorizationRepository.save(authProvince)
renewedCount++
this.logger.log(
`[processExpiredAuthProvinceBenefits] Auth province ${accountSequence} renewed, ` +
`trees=${treesForAssessment}, new validUntil=${authProvince.benefitValidUntil?.toISOString()}`,
)
} else {
// 不达标,级联停用
const result = await this.cascadeDeactivateAuthProvinceBenefits(
accountSequence,
`月度考核不达标:考核期内新增${treesForAssessment}未达到500棵目标`,
)
deactivatedCount += result.deactivatedCount
}
}
this.logger.log(
`[processExpiredAuthProvinceBenefits] Completed: processed=${expiredAuthProvinces.length}, ` +
`renewed=${renewedCount}, deactivated=${deactivatedCount}`,
)
return {
processedCount: expiredAuthProvinces.length,
renewedCount,
deactivatedCount,
}
}
/**
*
*/
private async findExpiredActiveAuthProvinces(
checkDate: Date,
limit: number,
): Promise<AuthorizationRole[]> {
return this.authorizationRepository.findExpiredActiveByRoleType(
RoleType.AUTH_PROVINCE_COMPANY,
checkDate,
limit,
)
}
/**
*
*