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:
parent
1896dd6b4c
commit
95e1cdffba
|
|
@ -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 的正式省公司
|
||||
* - 使用阶梯目标(第1月150,第2月300,...,第9月11750)
|
||||
* - 如果当月新增树数达标,续期并递增月份索引
|
||||
* - 如果不达标,停用权益并重置月份索引到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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1978,11 +1978,13 @@ export class AuthorizationApplicationService {
|
|||
*
|
||||
* 规则:
|
||||
* 1. 查找该省份是否有正式省公司(PROVINCE_COMPANY)
|
||||
* 2. 如果有且 benefitActive=true,权益进该省公司自己的账户
|
||||
* 2. 如果有且 benefitActive=true,权益进该省公司自己的账户,并累加当月新增树数
|
||||
* 3. 如果有但 benefitActive=false(考核中):
|
||||
* - 计算还需要多少棵才能达到初始考核(5万棵)
|
||||
* - 使用阶梯目标(第1月150,第2月300,...,第9月11750)
|
||||
* - 计算还需要多少棵才能达到当月目标
|
||||
* - 考核前的部分进系统省账户
|
||||
* - 考核后的部分给该省公司
|
||||
* - 第一个月达标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 的正式省公司
|
||||
* - 使用阶梯目标(第1月150,第2月300,...,第9月11750)
|
||||
* - 如果当月新增树数达标,续期并递增月份索引
|
||||
* - 如果不达标,停用权益并重置月份索引到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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in New Issue