feat(authorization-service): 正式市公司(CITY_COMPANY)阶梯考核及月度考核
- 添加 CITY_COMPANY_LADDER 阶梯目标配置 (30→60→120→240→480→960→1920→3840→2350) - 修改 getCityAreaRewardDistribution 使用阶梯目标激活权益 - 添加 processExpiredCityCompanyBenefits 月度考核处理方法 - 添加月末23:59考核定时任务 (判断是否当月最后一天) - 添加正式市公司月度数据存档定时任务 业务规则: - 第一个月达标30棵树即可激活权益 - 每月最后一天23:59执行考核 - 达标续期并递增月份索引 - 不达标停用权益 🤖 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
4d58d4be6c
commit
1896dd6b4c
|
|
@ -249,6 +249,9 @@ export class MonthlyAssessmentScheduler {
|
|||
|
||||
// 同时处理省团队授权
|
||||
await this.archiveAndResetAuthProvinceMonthlyTreeCounts()
|
||||
|
||||
// 同时处理正式市公司
|
||||
await this.archiveAndResetCityCompanyMonthlyTreeCounts()
|
||||
} catch (error) {
|
||||
this.logger.error('[archiveAndResetMonthlyTreeCounts] 月度树数存档失败', error)
|
||||
}
|
||||
|
|
@ -371,4 +374,74 @@ export class MonthlyAssessmentScheduler {
|
|||
this.logger.error('[processExpiredAuthProvinceBenefits] 省团队授权权益过期检查失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 每天23:59检查是否是当月最后一天,如果是则执行正式市公司(CITY_COMPANY)月度考核
|
||||
*
|
||||
* 业务规则:
|
||||
* - 检查所有 benefitValidUntil < 当前时间 且 benefitActive=true 的正式市公司
|
||||
* - 使用阶梯目标(第1月30,第2月60,...,第9月2350)
|
||||
* - 如果当月新增树数达标,续期并递增月份索引
|
||||
* - 如果不达标,停用权益并重置月份索引到1
|
||||
*/
|
||||
@Cron('59 23 * * *')
|
||||
async processExpiredCityCompanyBenefits(): 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('[processExpiredCityCompanyBenefits] 今天是月末,开始检查正式市公司权益过期情况...')
|
||||
|
||||
try {
|
||||
const result = await this.authorizationAppService.processExpiredCityCompanyBenefits(100)
|
||||
|
||||
this.logger.log(
|
||||
`[processExpiredCityCompanyBenefits] 处理完成: ` +
|
||||
`已处理=${result.processedCount}, 已续期=${result.renewedCount}, 已停用=${result.deactivatedCount}`,
|
||||
)
|
||||
} catch (error) {
|
||||
this.logger.error('[processExpiredCityCompanyBenefits] 正式市公司权益过期检查失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 存档并重置所有正式市公司的月度新增树数
|
||||
*
|
||||
* 业务规则:
|
||||
* - 将当月业绩存档到 lastMonthTreesAdded(用于月度考核)
|
||||
* - 重置 monthlyTreesAdded 为0(开始新月累计)
|
||||
*/
|
||||
private async archiveAndResetCityCompanyMonthlyTreeCounts(): Promise<void> {
|
||||
this.logger.log('[archiveAndResetCityCompanyMonthlyTreeCounts] 开始存档正式市公司月度新增树数...')
|
||||
|
||||
try {
|
||||
// 获取所有激活的正式市公司
|
||||
const activeCityCompanies = await this.authorizationRepository.findAllActive(RoleType.CITY_COMPANY)
|
||||
|
||||
let archivedCount = 0
|
||||
for (const cityCompany of activeCityCompanies) {
|
||||
if (cityCompany.benefitActive) {
|
||||
// 存档当月数据到 lastMonthTreesAdded,然后重置 monthlyTreesAdded
|
||||
cityCompany.archiveAndResetMonthlyTrees()
|
||||
await this.authorizationRepository.save(cityCompany)
|
||||
archivedCount++
|
||||
|
||||
this.logger.debug(
|
||||
`[archiveAndResetCityCompanyMonthlyTreeCounts] 正式市公司 ${cityCompany.userId.accountSequence}: ` +
|
||||
`存档=${cityCompany.lastMonthTreesAdded}, 当月已重置=0`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(`[archiveAndResetCityCompanyMonthlyTreeCounts] 存档完成: 已处理 ${archivedCount} 个正式市公司`)
|
||||
} catch (error) {
|
||||
this.logger.error('[archiveAndResetCityCompanyMonthlyTreeCounts] 正式市公司月度树数存档失败', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2307,9 +2307,10 @@ export class AuthorizationApplicationService {
|
|||
*
|
||||
* 规则:
|
||||
* 1. 查找该城市是否有正式市公司(CITY_COMPANY)
|
||||
* 2. 如果有且 benefitActive=true,权益进该市公司自己的账户
|
||||
* 2. 如果有且 benefitActive=true,权益进该市公司自己的账户,并累加 monthlyTreesAdded
|
||||
* 3. 如果有但 benefitActive=false(考核中):
|
||||
* - 计算还需要多少棵才能达到初始考核(1万棵)
|
||||
* - 使用阶梯考核目标(第一个月30棵,逐月递增)
|
||||
* - 计算还需要多少棵才能达到当前月份的考核目标
|
||||
* - 考核前的部分进系统市账户
|
||||
* - 考核后的部分给该市公司
|
||||
* 4. 如果没有正式市公司,全部进系统市账户
|
||||
|
|
@ -2351,6 +2352,10 @@ export class AuthorizationApplicationService {
|
|||
|
||||
if (cityCompany.benefitActive) {
|
||||
// 正式市公司权益已激活,进该市公司账户
|
||||
// 累加月度新增树数(用于后续月度考核)
|
||||
cityCompany.addMonthlyTrees(treeCount)
|
||||
await this.authorizationRepository.save(cityCompany)
|
||||
|
||||
return {
|
||||
distributions: [
|
||||
{
|
||||
|
|
@ -2363,15 +2368,19 @@ export class AuthorizationApplicationService {
|
|||
}
|
||||
}
|
||||
|
||||
// 权益未激活,计算考核分配 - 使用下级团队认种数(不含自己)
|
||||
const stats = await this.statsRepository.findByAccountSequence(cityCompany.userId.accountSequence)
|
||||
const rawSubordinateCount = stats?.subordinateTeamPlantingCount ?? 0
|
||||
// 修复竞态条件:减去本次认种数量来还原"认种前"的下级团队数
|
||||
const currentTeamCount = Math.max(0, rawSubordinateCount - treeCount)
|
||||
const initialTarget = cityCompany.getInitialTarget() // 1万棵
|
||||
// 权益未激活,计算考核分配 - 使用阶梯目标
|
||||
// 对于未激活的正式市公司,使用第一个月的目标(30棵)进行初始考核
|
||||
const monthIndex = cityCompany.currentMonthIndex || 1
|
||||
const ladderTarget = LadderTargetRule.getTarget(RoleType.CITY_COMPANY, monthIndex)
|
||||
const initialTarget = ladderTarget.monthlyTarget // 第一个月30棵
|
||||
|
||||
// 获取当前月度新增树数
|
||||
const rawMonthlyCount = cityCompany.monthlyTreesAdded ?? 0
|
||||
// 修复竞态条件:减去本次认种数量来还原"认种前"的月度数
|
||||
const currentMonthlyCount = Math.max(0, rawMonthlyCount - treeCount)
|
||||
|
||||
this.logger.debug(
|
||||
`[getCityAreaRewardDistribution] rawSubordinateCount=${rawSubordinateCount}, treeCount=${treeCount}, currentTeamCount(before)=${currentTeamCount}, initialTarget=${initialTarget}`,
|
||||
`[getCityAreaRewardDistribution] rawMonthlyCount=${rawMonthlyCount}, treeCount=${treeCount}, currentMonthlyCount(before)=${currentMonthlyCount}, initialTarget=${initialTarget}, monthIndex=${monthIndex}`,
|
||||
)
|
||||
|
||||
const distributions: Array<{
|
||||
|
|
@ -2381,7 +2390,7 @@ export class AuthorizationApplicationService {
|
|||
isSystemAccount: boolean
|
||||
}> = []
|
||||
|
||||
if (currentTeamCount >= initialTarget) {
|
||||
if (currentMonthlyCount >= initialTarget) {
|
||||
// 已达标但权益未激活,全部给该市公司
|
||||
distributions.push({
|
||||
accountSequence: cityCompany.userId.accountSequence,
|
||||
|
|
@ -2394,18 +2403,22 @@ export class AuthorizationApplicationService {
|
|||
} else {
|
||||
// toReachTarget: 还差多少棵达到考核目标(包括达标那一棵)
|
||||
// 业务规则:达标前的全部进系统市账户,超过达标点后才给该市公司
|
||||
const toReachTarget = Math.max(0, initialTarget - currentTeamCount)
|
||||
const afterPlantingCount = currentTeamCount + treeCount
|
||||
const toReachTarget = Math.max(0, initialTarget - currentMonthlyCount)
|
||||
const afterPlantingCount = currentMonthlyCount + treeCount
|
||||
|
||||
if (afterPlantingCount <= initialTarget) {
|
||||
// 本次认种后仍未超过目标(包括刚好达标),全部进系统市账户
|
||||
distributions.push({
|
||||
accountSequence: systemCityAccountId,
|
||||
treeCount,
|
||||
reason: `初始考核中(${currentTeamCount}+${treeCount}=${afterPlantingCount}/${initialTarget}),进系统市账户`,
|
||||
reason: `初始考核中(${currentMonthlyCount}+${treeCount}=${afterPlantingCount}/${initialTarget}),进系统市账户`,
|
||||
isSystemAccount: true,
|
||||
})
|
||||
|
||||
// 累加月度新增树数(用于月底考核)
|
||||
cityCompany.addMonthlyTrees(treeCount)
|
||||
await this.authorizationRepository.save(cityCompany)
|
||||
|
||||
// 如果刚好达标,激活权益(但本批次树全部进系统市账户)
|
||||
if (afterPlantingCount === initialTarget) {
|
||||
await this.tryActivateBenefit(cityCompany)
|
||||
|
|
@ -2417,7 +2430,7 @@ export class AuthorizationApplicationService {
|
|||
distributions.push({
|
||||
accountSequence: systemCityAccountId,
|
||||
treeCount: toReachTarget,
|
||||
reason: `初始考核(${currentTeamCount}+${toReachTarget}=${initialTarget}/${initialTarget}),进系统市账户`,
|
||||
reason: `初始考核(${currentMonthlyCount}+${toReachTarget}=${initialTarget}/${initialTarget}),进系统市账户`,
|
||||
isSystemAccount: true,
|
||||
})
|
||||
}
|
||||
|
|
@ -2431,6 +2444,11 @@ export class AuthorizationApplicationService {
|
|||
isSystemAccount: false,
|
||||
})
|
||||
}
|
||||
|
||||
// 累加月度新增树数
|
||||
cityCompany.addMonthlyTrees(treeCount)
|
||||
await this.authorizationRepository.save(cityCompany)
|
||||
|
||||
// 自动激活权益(本次认种使其达标)
|
||||
await this.tryActivateBenefit(cityCompany)
|
||||
}
|
||||
|
|
@ -2439,4 +2457,87 @@ export class AuthorizationApplicationService {
|
|||
this.logger.debug(`[getCityAreaRewardDistribution] Result: ${JSON.stringify(distributions)}`)
|
||||
return { distributions }
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理过期的正式市公司权益(月度考核)
|
||||
*
|
||||
* 业务规则:
|
||||
* - 检查所有 benefitValidUntil < 当前时间 且 benefitActive=true 的正式市公司
|
||||
* - 获取当前月份索引对应的阶梯目标
|
||||
* - 如果月度新增树数 >= 阶梯目标,续期并递增月份索引
|
||||
* - 如果不达标,停用权益并重置月份索引为0
|
||||
*/
|
||||
async processExpiredCityCompanyBenefits(
|
||||
limit: number,
|
||||
): Promise<{ processedCount: number; renewedCount: number; deactivatedCount: number }> {
|
||||
const now = new Date()
|
||||
const expiredCityCompanies = await this.authorizationRepository.findExpiredActiveByRoleType(
|
||||
RoleType.CITY_COMPANY,
|
||||
now,
|
||||
limit,
|
||||
)
|
||||
|
||||
let renewedCount = 0
|
||||
let deactivatedCount = 0
|
||||
|
||||
for (const cityCompany of expiredCityCompanies) {
|
||||
// 获取当前月份索引对应的阶梯目标
|
||||
const monthIndex = cityCompany.currentMonthIndex || 1
|
||||
const ladderTarget = LadderTargetRule.getTarget(RoleType.CITY_COMPANY, monthIndex)
|
||||
const monthlyTarget = ladderTarget.monthlyTarget
|
||||
|
||||
// 获取用于考核的树数
|
||||
const treesForAssessment = cityCompany.getTreesForAssessment(now)
|
||||
|
||||
this.logger.debug(
|
||||
`[processExpiredCityCompanyBenefits] ${cityCompany.userId.accountSequence}: ` +
|
||||
`monthIndex=${monthIndex}, target=${monthlyTarget}, trees=${treesForAssessment}`,
|
||||
)
|
||||
|
||||
if (treesForAssessment >= monthlyTarget) {
|
||||
// 达标:续期权益并递增月份索引
|
||||
cityCompany.renewBenefit(treesForAssessment)
|
||||
cityCompany.incrementMonthIndex()
|
||||
await this.authorizationRepository.save(cityCompany)
|
||||
renewedCount++
|
||||
|
||||
this.logger.log(
|
||||
`[processExpiredCityCompanyBenefits] ${cityCompany.userId.accountSequence} 考核达标,续期成功`,
|
||||
)
|
||||
} else {
|
||||
// 不达标:停用权益
|
||||
cityCompany.deactivateBenefit(`月度考核不达标(${treesForAssessment}/${monthlyTarget})`)
|
||||
await this.authorizationRepository.save(cityCompany)
|
||||
|
||||
await this.eventPublisher.publishAll(cityCompany.domainEvents)
|
||||
cityCompany.clearDomainEvents()
|
||||
|
||||
deactivatedCount++
|
||||
|
||||
this.logger.log(
|
||||
`[processExpiredCityCompanyBenefits] ${cityCompany.userId.accountSequence} 考核不达标,权益已停用`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
processedCount: expiredCityCompanies.length,
|
||||
renewedCount,
|
||||
deactivatedCount,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取过期的正式市公司授权列表
|
||||
*/
|
||||
async findExpiredCityCompanyBenefits(
|
||||
checkDate: Date,
|
||||
limit: number,
|
||||
): Promise<AuthorizationRole[]> {
|
||||
return this.authorizationRepository.findExpiredActiveByRoleType(
|
||||
RoleType.CITY_COMPANY,
|
||||
checkDate,
|
||||
limit,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export class LadderTargetRule {
|
|||
new LadderTargetRule(RoleType.AUTH_PROVINCE_COMPANY, 9, 11750, 50000),
|
||||
]
|
||||
|
||||
// 市代阶梯目标表
|
||||
// 市团队授权阶梯目标表 (AUTH_CITY_COMPANY)
|
||||
static readonly CITY_LADDER: LadderTargetRule[] = [
|
||||
new LadderTargetRule(RoleType.AUTH_CITY_COMPANY, 1, 30, 30),
|
||||
new LadderTargetRule(RoleType.AUTH_CITY_COMPANY, 2, 60, 90),
|
||||
|
|
@ -35,6 +35,20 @@ export class LadderTargetRule {
|
|||
new LadderTargetRule(RoleType.AUTH_CITY_COMPANY, 9, 2350, 10000),
|
||||
]
|
||||
|
||||
// 正式市公司/市区域权益阶梯目标表 (CITY_COMPANY)
|
||||
// 与市团队授权使用相同的阶梯目标
|
||||
static readonly CITY_COMPANY_LADDER: LadderTargetRule[] = [
|
||||
new LadderTargetRule(RoleType.CITY_COMPANY, 1, 30, 30),
|
||||
new LadderTargetRule(RoleType.CITY_COMPANY, 2, 60, 90),
|
||||
new LadderTargetRule(RoleType.CITY_COMPANY, 3, 120, 210),
|
||||
new LadderTargetRule(RoleType.CITY_COMPANY, 4, 240, 450),
|
||||
new LadderTargetRule(RoleType.CITY_COMPANY, 5, 480, 930),
|
||||
new LadderTargetRule(RoleType.CITY_COMPANY, 6, 960, 1890),
|
||||
new LadderTargetRule(RoleType.CITY_COMPANY, 7, 1920, 3810),
|
||||
new LadderTargetRule(RoleType.CITY_COMPANY, 8, 3840, 7650),
|
||||
new LadderTargetRule(RoleType.CITY_COMPANY, 9, 2350, 10000),
|
||||
]
|
||||
|
||||
// 社区固定目标
|
||||
static readonly COMMUNITY_FIXED: LadderTargetRule = new LadderTargetRule(
|
||||
RoleType.COMMUNITY,
|
||||
|
|
@ -57,6 +71,10 @@ export class LadderTargetRule {
|
|||
const cityIndex = Math.min(monthIndex, 9) - 1
|
||||
return this.CITY_LADDER[cityIndex >= 0 ? cityIndex : 0]
|
||||
|
||||
case RoleType.CITY_COMPANY:
|
||||
const cityCompanyIndex = Math.min(monthIndex, 9) - 1
|
||||
return this.CITY_COMPANY_LADDER[cityCompanyIndex >= 0 ? cityCompanyIndex : 0]
|
||||
|
||||
case RoleType.COMMUNITY:
|
||||
return this.COMMUNITY_FIXED
|
||||
|
||||
|
|
@ -74,6 +92,8 @@ export class LadderTargetRule {
|
|||
return 50000
|
||||
case RoleType.AUTH_CITY_COMPANY:
|
||||
return 10000
|
||||
case RoleType.CITY_COMPANY:
|
||||
return 10000
|
||||
case RoleType.COMMUNITY:
|
||||
return 10
|
||||
default:
|
||||
|
|
@ -90,6 +110,8 @@ export class LadderTargetRule {
|
|||
return this.PROVINCE_LADDER
|
||||
case RoleType.AUTH_CITY_COMPANY:
|
||||
return this.CITY_LADDER
|
||||
case RoleType.CITY_COMPANY:
|
||||
return this.CITY_COMPANY_LADDER
|
||||
case RoleType.COMMUNITY:
|
||||
return [this.COMMUNITY_FIXED]
|
||||
default:
|
||||
|
|
|
|||
Loading…
Reference in New Issue