feat(authorization-service): 市团队授权级联激活/停用及月度考核
- 添加 cascadeActivateParentAuthCities: 当市团队授权激活时级联激活上级市团队 - 添加 cascadeDeactivateAuthCityBenefits: 月度考核失败时级联停用上级市团队 - 添加 processExpiredAuthCityBenefits: 月度考核处理(100棵树/月) - 添加 findExpiredActiveByRoleType 仓储方法 - 修改 tryActivateBenefit 支持 AUTH_CITY_COMPANY 级联激活 - 定时任务: 每天凌晨4点检查市团队权益过期 - 定时任务: 每月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:
parent
4a2a1e3855
commit
049a13c97e
|
|
@ -243,8 +243,70 @@ export class MonthlyAssessmentScheduler {
|
|||
}
|
||||
|
||||
this.logger.log(`[archiveAndResetMonthlyTreeCounts] 存档完成: 已处理 ${archivedCount} 个社区`)
|
||||
|
||||
// 同时处理市团队授权
|
||||
await this.archiveAndResetAuthCityMonthlyTreeCounts()
|
||||
} catch (error) {
|
||||
this.logger.error('[archiveAndResetMonthlyTreeCounts] 月度树数存档失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 存档并重置所有市团队授权的月度新增树数
|
||||
*
|
||||
* 业务规则:
|
||||
* - 将当月业绩存档到 lastMonthTreesAdded(用于月度考核)
|
||||
* - 重置 monthlyTreesAdded 为0(开始新月累计)
|
||||
*/
|
||||
private async archiveAndResetAuthCityMonthlyTreeCounts(): Promise<void> {
|
||||
this.logger.log('[archiveAndResetAuthCityMonthlyTreeCounts] 开始存档市团队月度新增树数...')
|
||||
|
||||
try {
|
||||
// 获取所有激活的市团队授权
|
||||
const activeAuthCities = await this.authorizationRepository.findAllActive(RoleType.AUTH_CITY_COMPANY)
|
||||
|
||||
let archivedCount = 0
|
||||
for (const authCity of activeAuthCities) {
|
||||
if (authCity.benefitActive) {
|
||||
// 存档当月数据到 lastMonthTreesAdded,然后重置 monthlyTreesAdded
|
||||
authCity.archiveAndResetMonthlyTrees()
|
||||
await this.authorizationRepository.save(authCity)
|
||||
archivedCount++
|
||||
|
||||
this.logger.debug(
|
||||
`[archiveAndResetAuthCityMonthlyTreeCounts] 市团队授权 ${authCity.userId.accountSequence}: ` +
|
||||
`存档=${authCity.lastMonthTreesAdded}, 当月已重置=0`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(`[archiveAndResetAuthCityMonthlyTreeCounts] 存档完成: 已处理 ${archivedCount} 个市团队授权`)
|
||||
} catch (error) {
|
||||
this.logger.error('[archiveAndResetAuthCityMonthlyTreeCounts] 市团队月度树数存档失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 每天凌晨4点检查并处理过期的市团队授权权益
|
||||
*
|
||||
* 业务规则:
|
||||
* - 检查所有 benefitValidUntil < 当前时间 且 benefitActive=true 的市团队授权
|
||||
* - 如果当月新增树数 >= 100,续期
|
||||
* - 如果不达标,级联停用该市团队授权及其所有上级市团队授权
|
||||
*/
|
||||
@Cron('0 4 * * *')
|
||||
async processExpiredAuthCityBenefits(): Promise<void> {
|
||||
this.logger.log('[processExpiredAuthCityBenefits] 开始检查市团队授权权益过期情况...')
|
||||
|
||||
try {
|
||||
const result = await this.authorizationAppService.processExpiredAuthCityBenefits(100)
|
||||
|
||||
this.logger.log(
|
||||
`[processExpiredAuthCityBenefits] 处理完成: ` +
|
||||
`已处理=${result.processedCount}, 已续期=${result.renewedCount}, 已停用=${result.deactivatedCount}`,
|
||||
)
|
||||
} catch (error) {
|
||||
this.logger.error('[processExpiredAuthCityBenefits] 市团队授权权益过期检查失败', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -881,6 +881,11 @@ export class AuthorizationApplicationService {
|
|||
if (authorization.roleType === RoleType.COMMUNITY) {
|
||||
await this.cascadeActivateParentCommunities(authorization.userId.accountSequence)
|
||||
}
|
||||
|
||||
// 如果是市团队授权权益,需要级联激活上级市团队授权
|
||||
if (authorization.roleType === RoleType.AUTH_CITY_COMPANY) {
|
||||
await this.cascadeActivateParentAuthCities(authorization.userId.accountSequence)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1015,6 +1020,224 @@ export class AuthorizationApplicationService {
|
|||
return { deactivatedCount: communitiesToDeactivate.length }
|
||||
}
|
||||
|
||||
/**
|
||||
* 级联激活上级市团队授权权益
|
||||
* 当一个市团队授权的权益被激活时,需要同时激活推荐链上所有父级市团队授权的权益
|
||||
*
|
||||
* 业务规则:
|
||||
* - 从当前市团队授权往上找,找到所有已授权但权益未激活的市团队授权
|
||||
* - 将它们的权益都激活
|
||||
* - 系统账户不需要考核,不在此处理
|
||||
*/
|
||||
private async cascadeActivateParentAuthCities(accountSequence: string): Promise<void> {
|
||||
this.logger.log(
|
||||
`[cascadeActivateParentAuthCities] Starting cascade activation for auth cities above ${accountSequence}`,
|
||||
)
|
||||
|
||||
// 1. 获取推荐链(不包括当前用户)
|
||||
const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(accountSequence)
|
||||
if (ancestorAccountSequences.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 查找推荐链上所有市团队授权(包括 benefitActive=false)
|
||||
const ancestorAuthCities = await this.authorizationRepository.findAuthCityByAccountSequences(
|
||||
ancestorAccountSequences,
|
||||
)
|
||||
|
||||
// 3. 筛选出已授权但权益未激活的市团队授权
|
||||
const inactiveAuthCities = ancestorAuthCities.filter(
|
||||
(auth) => auth.status === AuthorizationStatus.AUTHORIZED && !auth.benefitActive,
|
||||
)
|
||||
|
||||
if (inactiveAuthCities.length === 0) {
|
||||
this.logger.debug('[cascadeActivateParentAuthCities] No inactive parent auth cities to activate')
|
||||
return
|
||||
}
|
||||
|
||||
// 4. 激活这些市团队授权的权益
|
||||
for (const authCity of inactiveAuthCities) {
|
||||
this.logger.log(
|
||||
`[cascadeActivateParentAuthCities] Cascade activating auth city benefit: ` +
|
||||
`authorizationId=${authCity.authorizationId.value}, accountSequence=${authCity.userId.accountSequence}`,
|
||||
)
|
||||
|
||||
authCity.activateBenefit()
|
||||
await this.authorizationRepository.save(authCity)
|
||||
await this.eventPublisher.publishAll(authCity.domainEvents)
|
||||
authCity.clearDomainEvents()
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`[cascadeActivateParentAuthCities] Cascade activated ${inactiveAuthCities.length} parent auth cities`,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 级联停用市团队授权权益
|
||||
* 当一个市团队授权的月度考核失败时,需要停用该市团队授权及其推荐链上所有父级市团队授权的权益
|
||||
*
|
||||
* 业务规则:
|
||||
* - 从当前市团队授权开始,往上找到所有已授权且权益已激活的市团队授权
|
||||
* - 将它们的权益都停用,重新开始100棵树的初始考核
|
||||
*/
|
||||
async cascadeDeactivateAuthCityBenefits(
|
||||
accountSequence: string,
|
||||
reason: string,
|
||||
): Promise<{ deactivatedCount: number }> {
|
||||
this.logger.log(
|
||||
`[cascadeDeactivateAuthCityBenefits] Starting cascade deactivation from ${accountSequence}, reason=${reason}`,
|
||||
)
|
||||
|
||||
// 1. 获取当前市团队授权
|
||||
const currentAuthCity = await this.authorizationRepository.findByAccountSequenceAndRoleType(
|
||||
accountSequence,
|
||||
RoleType.AUTH_CITY_COMPANY,
|
||||
)
|
||||
|
||||
if (!currentAuthCity) {
|
||||
this.logger.warn(`[cascadeDeactivateAuthCityBenefits] Auth city not found for ${accountSequence}`)
|
||||
return { deactivatedCount: 0 }
|
||||
}
|
||||
|
||||
// 2. 收集需要停用的市团队授权列表
|
||||
const authCitiesToDeactivate: AuthorizationRole[] = []
|
||||
|
||||
// 如果当前市团队授权权益已激活,加入停用列表
|
||||
if (currentAuthCity.benefitActive) {
|
||||
authCitiesToDeactivate.push(currentAuthCity)
|
||||
}
|
||||
|
||||
// 3. 获取推荐链上的所有父级市团队授权
|
||||
const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(accountSequence)
|
||||
if (ancestorAccountSequences.length > 0) {
|
||||
const ancestorAuthCities = await this.authorizationRepository.findAuthCityByAccountSequences(
|
||||
ancestorAccountSequences,
|
||||
)
|
||||
|
||||
// 筛选出已授权且权益已激活的市团队授权
|
||||
const activeAuthCities = ancestorAuthCities.filter(
|
||||
(auth) => auth.status === AuthorizationStatus.AUTHORIZED && auth.benefitActive,
|
||||
)
|
||||
|
||||
authCitiesToDeactivate.push(...activeAuthCities)
|
||||
}
|
||||
|
||||
if (authCitiesToDeactivate.length === 0) {
|
||||
this.logger.debug('[cascadeDeactivateAuthCityBenefits] No active auth cities to deactivate')
|
||||
return { deactivatedCount: 0 }
|
||||
}
|
||||
|
||||
// 4. 停用这些市团队授权的权益
|
||||
for (const authCity of authCitiesToDeactivate) {
|
||||
this.logger.log(
|
||||
`[cascadeDeactivateAuthCityBenefits] Deactivating auth city benefit: ` +
|
||||
`authorizationId=${authCity.authorizationId.value}, accountSequence=${authCity.userId.accountSequence}`,
|
||||
)
|
||||
|
||||
authCity.deactivateBenefit(reason)
|
||||
await this.authorizationRepository.save(authCity)
|
||||
await this.eventPublisher.publishAll(authCity.domainEvents)
|
||||
authCity.clearDomainEvents()
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`[cascadeDeactivateAuthCityBenefits] Cascade deactivated ${authCitiesToDeactivate.length} auth cities`,
|
||||
)
|
||||
|
||||
return { deactivatedCount: authCitiesToDeactivate.length }
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理过期的市团队授权权益
|
||||
* 定时任务调用此方法来检查并处理过期的市团队授权权益
|
||||
*
|
||||
* 业务规则:
|
||||
* - 查找所有 benefitValidUntil < 当前时间 且 benefitActive=true 的市团队授权
|
||||
* - 检查其月度考核(当月新增100棵树)
|
||||
* - 如果达标,续期;如果不达标,级联停用
|
||||
*
|
||||
* @param limit 每次处理的最大数量
|
||||
*/
|
||||
async processExpiredAuthCityBenefits(limit = 100): Promise<{
|
||||
processedCount: number
|
||||
renewedCount: number
|
||||
deactivatedCount: number
|
||||
}> {
|
||||
const now = new Date()
|
||||
this.logger.log(`[processExpiredAuthCityBenefits] Starting at ${now.toISOString()}, limit=${limit}`)
|
||||
|
||||
// 查找过期但仍激活的市团队授权
|
||||
const expiredAuthCities = await this.findExpiredActiveAuthCities(now, limit)
|
||||
|
||||
if (expiredAuthCities.length === 0) {
|
||||
this.logger.debug('[processExpiredAuthCityBenefits] No expired auth cities found')
|
||||
return { processedCount: 0, renewedCount: 0, deactivatedCount: 0 }
|
||||
}
|
||||
|
||||
let renewedCount = 0
|
||||
let deactivatedCount = 0
|
||||
|
||||
for (const authCity of expiredAuthCities) {
|
||||
const accountSequence = authCity.userId.accountSequence
|
||||
|
||||
// 使用 getTreesForAssessment 获取正确的考核数据
|
||||
const treesForAssessment = authCity.getTreesForAssessment(now)
|
||||
|
||||
this.logger.debug(
|
||||
`[processExpiredAuthCityBenefits] Checking auth city ${accountSequence}: ` +
|
||||
`treesForAssessment=${treesForAssessment}, ` +
|
||||
`monthlyTreesAdded=${authCity.monthlyTreesAdded}, ` +
|
||||
`lastMonthTreesAdded=${authCity.lastMonthTreesAdded}, ` +
|
||||
`benefitValidUntil=${authCity.benefitValidUntil?.toISOString()}, target=100`,
|
||||
)
|
||||
|
||||
if (treesForAssessment >= 100) {
|
||||
// 达标,续期
|
||||
authCity.renewBenefit(treesForAssessment)
|
||||
await this.authorizationRepository.save(authCity)
|
||||
renewedCount++
|
||||
|
||||
this.logger.log(
|
||||
`[processExpiredAuthCityBenefits] Auth city ${accountSequence} renewed, ` +
|
||||
`trees=${treesForAssessment}, new validUntil=${authCity.benefitValidUntil?.toISOString()}`,
|
||||
)
|
||||
} else {
|
||||
// 不达标,级联停用
|
||||
const result = await this.cascadeDeactivateAuthCityBenefits(
|
||||
accountSequence,
|
||||
`月度考核不达标:考核期内新增${treesForAssessment}棵,未达到100棵目标`,
|
||||
)
|
||||
deactivatedCount += result.deactivatedCount
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`[processExpiredAuthCityBenefits] Completed: processed=${expiredAuthCities.length}, ` +
|
||||
`renewed=${renewedCount}, deactivated=${deactivatedCount}`,
|
||||
)
|
||||
|
||||
return {
|
||||
processedCount: expiredAuthCities.length,
|
||||
renewedCount,
|
||||
deactivatedCount,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找过期但仍激活的市团队授权
|
||||
*/
|
||||
private async findExpiredActiveAuthCities(
|
||||
checkDate: Date,
|
||||
limit: number,
|
||||
): Promise<AuthorizationRole[]> {
|
||||
return this.authorizationRepository.findExpiredActiveByRoleType(
|
||||
RoleType.AUTH_CITY_COMPANY,
|
||||
checkDate,
|
||||
limit,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理过期的社区权益
|
||||
* 定时任务调用此方法来检查并处理过期的社区权益
|
||||
|
|
|
|||
|
|
@ -83,4 +83,13 @@ export interface IAuthorizationRoleRepository {
|
|||
* 查找指定城市的正式市公司授权(用于市区域权益分配)
|
||||
*/
|
||||
findCityCompanyByRegion(cityCode: string): Promise<AuthorizationRole | null>
|
||||
/**
|
||||
* 查找指定角色类型中权益已激活但已过期的授权
|
||||
* 用于月度考核定时任务
|
||||
*/
|
||||
findExpiredActiveByRoleType(
|
||||
roleType: RoleType,
|
||||
checkDate: Date,
|
||||
limit: number,
|
||||
): Promise<AuthorizationRole[]>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -369,6 +369,26 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi
|
|||
return record ? this.toDomain(record) : null
|
||||
}
|
||||
|
||||
async findExpiredActiveByRoleType(
|
||||
roleType: RoleType,
|
||||
checkDate: Date,
|
||||
limit: number,
|
||||
): Promise<AuthorizationRole[]> {
|
||||
const records = await this.prisma.authorizationRole.findMany({
|
||||
where: {
|
||||
roleType: roleType,
|
||||
status: AuthorizationStatus.AUTHORIZED,
|
||||
benefitActive: true,
|
||||
benefitValidUntil: {
|
||||
lt: checkDate,
|
||||
},
|
||||
},
|
||||
take: limit,
|
||||
orderBy: { benefitValidUntil: 'asc' },
|
||||
})
|
||||
return records.map((record) => this.toDomain(record))
|
||||
}
|
||||
|
||||
private toDomain(record: any): AuthorizationRole {
|
||||
const props: AuthorizationRoleProps = {
|
||||
authorizationId: AuthorizationId.create(record.id),
|
||||
|
|
|
|||
Loading…
Reference in New Issue