From 95e1cdffba74c1c0de1f64d593744460275bd627 Mon Sep 17 00:00:00 2001 From: hailin Date: Sat, 13 Dec 2025 23:43:41 -0800 Subject: [PATCH] =?UTF-8?q?feat(authorization-service):=20=E6=AD=A3?= =?UTF-8?q?=E5=BC=8F=E7=9C=81=E5=85=AC=E5=8F=B8(PROVINCE=5FCOMPANY)?= =?UTF-8?q?=E9=98=B6=E6=A2=AF=E8=80=83=E6=A0=B8=E5=8F=8A=E6=9C=88=E5=BA=A6?= =?UTF-8?q?=E8=80=83=E6=A0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 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 --- .../monthly-assessment.scheduler.ts | 73 +++++++++++ .../authorization-application.service.ts | 116 ++++++++++++++++-- .../entities/ladder-target-rule.entity.ts | 22 ++++ 3 files changed, 202 insertions(+), 9 deletions(-) diff --git a/backend/services/authorization-service/src/application/schedulers/monthly-assessment.scheduler.ts b/backend/services/authorization-service/src/application/schedulers/monthly-assessment.scheduler.ts index ca97e485..735c734b 100644 --- a/backend/services/authorization-service/src/application/schedulers/monthly-assessment.scheduler.ts +++ b/backend/services/authorization-service/src/application/schedulers/monthly-assessment.scheduler.ts @@ -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 { + // 判断是否是当月最后一天 + 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 { + 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) + } + } } diff --git a/backend/services/authorization-service/src/application/services/authorization-application.service.ts b/backend/services/authorization-service/src/application/services/authorization-application.service.ts index d4cef8d0..50993f0b 100644 --- a/backend/services/authorization-service/src/application/services/authorization-application.service.ts +++ b/backend/services/authorization-service/src/application/services/authorization-application.service.ts @@ -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 { + return this.authorizationRepository.findExpiredActiveByRoleType( + RoleType.PROVINCE_COMPANY, + checkDate, + limit, + ) + } } diff --git a/backend/services/authorization-service/src/domain/entities/ladder-target-rule.entity.ts b/backend/services/authorization-service/src/domain/entities/ladder-target-rule.entity.ts index 0e1c3939..e85ac2d3 100644 --- a/backend/services/authorization-service/src/domain/entities/ladder-target-rule.entity.ts +++ b/backend/services/authorization-service/src/domain/entities/ladder-target-rule.entity.ts @@ -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: