feat(authorization-service): 正式省公司(PROVINCE_COMPANY)阶梯考核及月度考核

- 添加 PROVINCE_COMPANY_LADDER 阶梯目标配置 (150→300→600→1200→2400→4800→9600→19200→11750)
- 修改 getProvinceAreaRewardDistribution 使用阶梯目标判断激活条件
- 添加 processExpiredProvinceCompanyBenefits 月度考核方法
- 添加每月最后一天23:59定时任务执行省公司月度考核
- 添加月度数据存档方法 archiveAndResetProvinceCompanyMonthlyTreeCounts

🤖 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 23:43:41 -08:00
parent 1896dd6b4c
commit 95e1cdffba
3 changed files with 202 additions and 9 deletions

View File

@ -252,6 +252,9 @@ export class MonthlyAssessmentScheduler {
// 同时处理正式市公司
await this.archiveAndResetCityCompanyMonthlyTreeCounts()
// 同时处理正式省公司
await this.archiveAndResetProvinceCompanyMonthlyTreeCounts()
} catch (error) {
this.logger.error('[archiveAndResetMonthlyTreeCounts] 月度树数存档失败', error)
}
@ -444,4 +447,74 @@ export class MonthlyAssessmentScheduler {
this.logger.error('[archiveAndResetCityCompanyMonthlyTreeCounts] 正式市公司月度树数存档失败', error)
}
}
/**
* 每天23:59检查是否是当月最后一天(PROVINCE_COMPANY)
*
*
* - benefitValidUntil < benefitActive=true
* - 使11502300...911750
* -
* - 1
*/
@Cron('59 23 * * *')
async processExpiredProvinceCompanyBenefits(): Promise<void> {
// 判断是否是当月最后一天
const now = new Date()
const tomorrow = new Date(now)
tomorrow.setDate(tomorrow.getDate() + 1)
// 如果明天是1号说明今天是当月最后一天
if (tomorrow.getDate() !== 1) {
return
}
this.logger.log('[processExpiredProvinceCompanyBenefits] 今天是月末,开始检查正式省公司权益过期情况...')
try {
const result = await this.authorizationAppService.processExpiredProvinceCompanyBenefits(100)
this.logger.log(
`[processExpiredProvinceCompanyBenefits] 处理完成: ` +
`已处理=${result.processedCount}, 已续期=${result.renewedCount}, 已停用=${result.deactivatedCount}`,
)
} catch (error) {
this.logger.error('[processExpiredProvinceCompanyBenefits] 正式省公司权益过期检查失败', error)
}
}
/**
*
*
*
* - lastMonthTreesAdded
* - monthlyTreesAdded 0
*/
private async archiveAndResetProvinceCompanyMonthlyTreeCounts(): Promise<void> {
this.logger.log('[archiveAndResetProvinceCompanyMonthlyTreeCounts] 开始存档正式省公司月度新增树数...')
try {
// 获取所有激活的正式省公司
const activeProvinceCompanies = await this.authorizationRepository.findAllActive(RoleType.PROVINCE_COMPANY)
let archivedCount = 0
for (const provinceCompany of activeProvinceCompanies) {
if (provinceCompany.benefitActive) {
// 存档当月数据到 lastMonthTreesAdded然后重置 monthlyTreesAdded
provinceCompany.archiveAndResetMonthlyTrees()
await this.authorizationRepository.save(provinceCompany)
archivedCount++
this.logger.debug(
`[archiveAndResetProvinceCompanyMonthlyTreeCounts] 正式省公司 ${provinceCompany.userId.accountSequence}: ` +
`存档=${provinceCompany.lastMonthTreesAdded}, 当月已重置=0`,
)
}
}
this.logger.log(`[archiveAndResetProvinceCompanyMonthlyTreeCounts] 存档完成: 已处理 ${archivedCount} 个正式省公司`)
} catch (error) {
this.logger.error('[archiveAndResetProvinceCompanyMonthlyTreeCounts] 正式省公司月度树数存档失败', error)
}
}
}

View File

@ -1978,11 +1978,13 @@ export class AuthorizationApplicationService {
*
*
* 1. PROVINCE_COMPANY
* 2. benefitActive=true
* 2. benefitActive=true
* 3. benefitActive=false
* - (5)
* - 使11502300...911750
* -
* -
* -
* - 150
* 4.
*/
async getProvinceAreaRewardDistribution(
@ -2022,6 +2024,10 @@ export class AuthorizationApplicationService {
if (provinceCompany.benefitActive) {
// 正式省公司权益已激活,进该省公司账户
// 累加当月新增树数用于月度考核
provinceCompany.addMonthlyTrees(treeCount)
await this.authorizationRepository.save(provinceCompany)
return {
distributions: [
{
@ -2034,15 +2040,17 @@ export class AuthorizationApplicationService {
}
}
// 权益未激活,计算考核分配 - 使用下级团队认种数(不含自己)
const stats = await this.statsRepository.findByAccountSequence(provinceCompany.userId.accountSequence)
const rawSubordinateCount = stats?.subordinateTeamPlantingCount ?? 0
// 修复竞态条件:减去本次认种数量来还原"认种前"的下级团队数
const currentTeamCount = Math.max(0, rawSubordinateCount - treeCount)
const initialTarget = provinceCompany.getInitialTarget() // 5万棵
// 权益未激活,使用阶梯目标计算考核分配
// 使用当前月份索引获取阶梯目标第一个月为150棵
const monthIndex = provinceCompany.currentMonthIndex || 1
const ladderTarget = LadderTargetRule.getTarget(RoleType.PROVINCE_COMPANY, monthIndex)
const initialTarget = ladderTarget.monthlyTarget // 第一个月150棵
// 使用 monthlyTreesAdded 作为当前累计认种数
const currentTeamCount = provinceCompany.monthlyTreesAdded
this.logger.debug(
`[getProvinceAreaRewardDistribution] rawSubordinateCount=${rawSubordinateCount}, treeCount=${treeCount}, currentTeamCount(before)=${currentTeamCount}, initialTarget=${initialTarget}`,
`[getProvinceAreaRewardDistribution] monthIndex=${monthIndex}, currentTeamCount=${currentTeamCount}, treeCount=${treeCount}, initialTarget=${initialTarget}`,
)
const distributions: Array<{
@ -2060,6 +2068,8 @@ export class AuthorizationApplicationService {
reason: '已达初始考核目标',
isSystemAccount: false,
})
// 累加当月新增树数
provinceCompany.addMonthlyTrees(treeCount)
// 自动激活权益
await this.tryActivateBenefit(provinceCompany)
} else {
@ -2076,6 +2086,9 @@ export class AuthorizationApplicationService {
reason: `初始考核中(${currentTeamCount}+${treeCount}=${afterPlantingCount}/${initialTarget}),进系统省账户`,
isSystemAccount: true,
})
// 累加当月新增树数
provinceCompany.addMonthlyTrees(treeCount)
await this.authorizationRepository.save(provinceCompany)
// 如果刚好达标,激活权益(但本批次树全部进系统省账户)
if (afterPlantingCount === initialTarget) {
@ -2102,6 +2115,8 @@ export class AuthorizationApplicationService {
isSystemAccount: false,
})
}
// 累加当月新增树数
provinceCompany.addMonthlyTrees(treeCount)
// 自动激活权益(本次认种使其达标)
await this.tryActivateBenefit(provinceCompany)
}
@ -2540,4 +2555,87 @@ export class AuthorizationApplicationService {
limit,
)
}
/**
*
*
*
* - benefitValidUntil < benefitActive=true
* - 使11502300...911750
* -
* - 1
*/
async processExpiredProvinceCompanyBenefits(
limit: number,
): Promise<{ processedCount: number; renewedCount: number; deactivatedCount: number }> {
const now = new Date()
const expiredProvinceCompanies = await this.authorizationRepository.findExpiredActiveByRoleType(
RoleType.PROVINCE_COMPANY,
now,
limit,
)
let renewedCount = 0
let deactivatedCount = 0
for (const provinceCompany of expiredProvinceCompanies) {
// 获取当前月份索引对应的阶梯目标
const monthIndex = provinceCompany.currentMonthIndex || 1
const ladderTarget = LadderTargetRule.getTarget(RoleType.PROVINCE_COMPANY, monthIndex)
const monthlyTarget = ladderTarget.monthlyTarget
// 获取用于考核的树数
const treesForAssessment = provinceCompany.getTreesForAssessment(now)
this.logger.debug(
`[processExpiredProvinceCompanyBenefits] ${provinceCompany.userId.accountSequence}: ` +
`monthIndex=${monthIndex}, target=${monthlyTarget}, trees=${treesForAssessment}`,
)
if (treesForAssessment >= monthlyTarget) {
// 达标:续期权益并递增月份索引
provinceCompany.renewBenefit(treesForAssessment)
provinceCompany.incrementMonthIndex()
await this.authorizationRepository.save(provinceCompany)
renewedCount++
this.logger.log(
`[processExpiredProvinceCompanyBenefits] ${provinceCompany.userId.accountSequence} 考核达标,续期成功`,
)
} else {
// 不达标:停用权益
provinceCompany.deactivateBenefit(`月度考核不达标(${treesForAssessment}/${monthlyTarget})`)
await this.authorizationRepository.save(provinceCompany)
await this.eventPublisher.publishAll(provinceCompany.domainEvents)
provinceCompany.clearDomainEvents()
deactivatedCount++
this.logger.log(
`[processExpiredProvinceCompanyBenefits] ${provinceCompany.userId.accountSequence} 考核不达标,权益已停用`,
)
}
}
return {
processedCount: expiredProvinceCompanies.length,
renewedCount,
deactivatedCount,
}
}
/**
*
*/
async findExpiredProvinceCompanyBenefits(
checkDate: Date,
limit: number,
): Promise<AuthorizationRole[]> {
return this.authorizationRepository.findExpiredActiveByRoleType(
RoleType.PROVINCE_COMPANY,
checkDate,
limit,
)
}
}

View File

@ -49,6 +49,20 @@ export class LadderTargetRule {
new LadderTargetRule(RoleType.CITY_COMPANY, 9, 2350, 10000),
]
// 正式省公司/省区域权益阶梯目标表 (PROVINCE_COMPANY)
// 与省团队授权使用相同的阶梯目标
static readonly PROVINCE_COMPANY_LADDER: LadderTargetRule[] = [
new LadderTargetRule(RoleType.PROVINCE_COMPANY, 1, 150, 150),
new LadderTargetRule(RoleType.PROVINCE_COMPANY, 2, 300, 450),
new LadderTargetRule(RoleType.PROVINCE_COMPANY, 3, 600, 1050),
new LadderTargetRule(RoleType.PROVINCE_COMPANY, 4, 1200, 2250),
new LadderTargetRule(RoleType.PROVINCE_COMPANY, 5, 2400, 4650),
new LadderTargetRule(RoleType.PROVINCE_COMPANY, 6, 4800, 9450),
new LadderTargetRule(RoleType.PROVINCE_COMPANY, 7, 9600, 19050),
new LadderTargetRule(RoleType.PROVINCE_COMPANY, 8, 19200, 38250),
new LadderTargetRule(RoleType.PROVINCE_COMPANY, 9, 11750, 50000),
]
// 社区固定目标
static readonly COMMUNITY_FIXED: LadderTargetRule = new LadderTargetRule(
RoleType.COMMUNITY,
@ -75,6 +89,10 @@ export class LadderTargetRule {
const cityCompanyIndex = Math.min(monthIndex, 9) - 1
return this.CITY_COMPANY_LADDER[cityCompanyIndex >= 0 ? cityCompanyIndex : 0]
case RoleType.PROVINCE_COMPANY:
const provinceCompanyIndex = Math.min(monthIndex, 9) - 1
return this.PROVINCE_COMPANY_LADDER[provinceCompanyIndex >= 0 ? provinceCompanyIndex : 0]
case RoleType.COMMUNITY:
return this.COMMUNITY_FIXED
@ -94,6 +112,8 @@ export class LadderTargetRule {
return 10000
case RoleType.CITY_COMPANY:
return 10000
case RoleType.PROVINCE_COMPANY:
return 50000
case RoleType.COMMUNITY:
return 10
default:
@ -112,6 +132,8 @@ export class LadderTargetRule {
return this.CITY_LADDER
case RoleType.CITY_COMPANY:
return this.CITY_COMPANY_LADDER
case RoleType.PROVINCE_COMPANY:
return this.PROVINCE_COMPANY_LADDER
case RoleType.COMMUNITY:
return [this.COMMUNITY_FIXED]
default: