import { Injectable, Inject, Logger } from '@nestjs/common' import { Cron, CronExpression } from '@nestjs/schedule' import { AuthorizationRole } from '@/domain/aggregates' import { Month, RegionCode } from '@/domain/value-objects' import { RoleType, AssessmentResult } from '@/domain/enums' import { IAuthorizationRoleRepository, AUTHORIZATION_ROLE_REPOSITORY, IMonthlyAssessmentRepository, MONTHLY_ASSESSMENT_REPOSITORY, } from '@/domain/repositories' import { AssessmentCalculatorService, ITeamStatisticsRepository } from '@/domain/services' import { EventPublisherService } from '@/infrastructure/kafka' import { TEAM_STATISTICS_REPOSITORY, AuthorizationApplicationService } from '@/application/services' @Injectable() export class MonthlyAssessmentScheduler { private readonly logger = new Logger(MonthlyAssessmentScheduler.name) private readonly calculatorService = new AssessmentCalculatorService() constructor( @Inject(AUTHORIZATION_ROLE_REPOSITORY) private readonly authorizationRepository: IAuthorizationRoleRepository, @Inject(MONTHLY_ASSESSMENT_REPOSITORY) private readonly assessmentRepository: IMonthlyAssessmentRepository, @Inject(TEAM_STATISTICS_REPOSITORY) private readonly statsRepository: ITeamStatisticsRepository, private readonly eventPublisher: EventPublisherService, private readonly authorizationAppService: AuthorizationApplicationService, ) {} /** * 每月1号凌晨2点执行月度考核 */ @Cron('0 2 1 * *') async executeMonthlyAssessment(): Promise { this.logger.log('开始执行月度考核...') const previousMonth = Month.current().previous() try { // 1. 获取所有激活的授权 const activeAuths = await this.authorizationRepository.findAllActive() // 2. 按角色类型和区域分组处理 const groupedByRoleAndRegion = this.groupByRoleAndRegion(activeAuths) for (const [key, auths] of groupedByRoleAndRegion) { const [roleTypeStr, regionCodeStr] = key.split('|') const roleType = roleTypeStr as RoleType // 跳过正式省市公司(无月度考核) if (roleType === RoleType.PROVINCE_COMPANY || roleType === RoleType.CITY_COMPANY) { continue } // 执行考核并排名 const assessments = await this.calculatorService.assessAndRankRegion( roleType, RegionCode.create(regionCodeStr), previousMonth, this.authorizationRepository, this.statsRepository, this.assessmentRepository, ) // 保存考核结果 await this.assessmentRepository.saveAll(assessments) // 处理不达标的授权 for (const assessment of assessments) { if (assessment.result === AssessmentResult.FAIL) { const auth = auths.find((a) => a.authorizationId.equals(assessment.authorizationId), ) if (auth) { // 权益失效 auth.deactivateBenefit('月度考核不达标') await this.authorizationRepository.save(auth) await this.eventPublisher.publishAll(auth.domainEvents) auth.clearDomainEvents() } } else if (assessment.isPassed()) { // 达标,递增月份索引 const auth = auths.find((a) => a.authorizationId.equals(assessment.authorizationId), ) if (auth) { auth.incrementMonthIndex() await this.authorizationRepository.save(auth) } } await this.eventPublisher.publishAll(assessment.domainEvents) assessment.clearDomainEvents() } } this.logger.log('月度考核执行完成') } catch (error) { this.logger.error('月度考核执行失败', error) throw error } } /** * 每10分钟更新火柴人排名数据 */ @Cron('*/10 * * * *') async updateStickmanRankings(): Promise { this.logger.log('开始更新火柴人排名数据...') try { const currentMonth = Month.current() // 获取所有激活的授权省/市公司 const activeAuths = await this.authorizationRepository.findAllActive() const provinceAuths = activeAuths.filter( (a) => a.roleType === RoleType.AUTH_PROVINCE_COMPANY, ) const cityAuths = activeAuths.filter( (a) => a.roleType === RoleType.AUTH_CITY_COMPANY, ) // 按区域分组并更新排名 const provinceRegions = new Set(provinceAuths.map((a) => a.regionCode.value)) const cityRegions = new Set(cityAuths.map((a) => a.regionCode.value)) for (const regionCode of provinceRegions) { await this.updateRegionRankings( RoleType.AUTH_PROVINCE_COMPANY, regionCode, currentMonth, ) } for (const regionCode of cityRegions) { await this.updateRegionRankings( RoleType.AUTH_CITY_COMPANY, regionCode, currentMonth, ) } this.logger.log('火柴人排名数据更新完成') } catch (error) { this.logger.error('火柴人排名数据更新失败', error) } } private async updateRegionRankings( roleType: RoleType, regionCode: string, currentMonth: Month, ): Promise { const assessments = await this.calculatorService.assessAndRankRegion( roleType, RegionCode.create(regionCode), currentMonth, this.authorizationRepository, this.statsRepository, this.assessmentRepository, ) await this.assessmentRepository.saveAll(assessments) } private groupByRoleAndRegion( authorizations: AuthorizationRole[], ): Map { const map = new Map() for (const auth of authorizations) { const key = `${auth.roleType}|${auth.regionCode.value}` const list = map.get(key) || [] list.push(auth) map.set(key, list) } return map } /** * 每天凌晨3点检查并处理过期的社区权益 * * 业务规则: * - 检查所有 benefitValidUntil < 当前时间 且 benefitActive=true 的社区 * - 如果当月新增树数 >= 10,续期 * - 如果不达标,级联停用该社区及其所有上级社区 */ @Cron('0 3 * * *') async processExpiredCommunityBenefits(): Promise { this.logger.log('[processExpiredCommunityBenefits] 开始检查社区权益过期情况...') try { const result = await this.authorizationAppService.processExpiredCommunityBenefits(100) this.logger.log( `[processExpiredCommunityBenefits] 处理完成: ` + `已处理=${result.processedCount}, 已续期=${result.renewedCount}, 已停用=${result.deactivatedCount}`, ) } catch (error) { this.logger.error('[processExpiredCommunityBenefits] 社区权益过期检查失败', error) } } /** * 每月1号凌晨0点存档并重置所有社区的月度新增树数 * * 业务规则: * - 将当月业绩存档到 lastMonthTreesAdded(用于月度考核) * - 重置 monthlyTreesAdded 为0(开始新月累计) * * 注意:考核时会根据 benefitValidUntil 判断使用哪个月的数据 * - 有效期在上月末 → 用 lastMonthTreesAdded * - 有效期在当月末 → 用 monthlyTreesAdded */ @Cron('0 0 1 * *') async archiveAndResetMonthlyTreeCounts(): Promise { this.logger.log('[archiveAndResetMonthlyTreeCounts] 开始存档并重置月度新增树数...') try { // 获取所有激活的社区 const activeCommunities = await this.authorizationRepository.findAllActive(RoleType.COMMUNITY) let archivedCount = 0 for (const community of activeCommunities) { if (community.benefitActive) { // 存档当月数据到 lastMonthTreesAdded,然后重置 monthlyTreesAdded community.archiveAndResetMonthlyTrees() await this.authorizationRepository.save(community) archivedCount++ this.logger.debug( `[archiveAndResetMonthlyTreeCounts] 社区 ${community.userId.accountSequence}: ` + `存档=${community.lastMonthTreesAdded}, 当月已重置=0`, ) } } this.logger.log(`[archiveAndResetMonthlyTreeCounts] 存档完成: 已处理 ${archivedCount} 个社区`) // 同时处理市团队授权 await this.archiveAndResetAuthCityMonthlyTreeCounts() // 同时处理省团队授权 await this.archiveAndResetAuthProvinceMonthlyTreeCounts() // 同时处理正式市公司 await this.archiveAndResetCityCompanyMonthlyTreeCounts() // 同时处理正式省公司 await this.archiveAndResetProvinceCompanyMonthlyTreeCounts() } catch (error) { this.logger.error('[archiveAndResetMonthlyTreeCounts] 月度树数存档失败', error) } } /** * 存档并重置所有市团队授权的月度新增树数 * * 业务规则: * - 将当月业绩存档到 lastMonthTreesAdded(用于月度考核) * - 重置 monthlyTreesAdded 为0(开始新月累计) */ private async archiveAndResetAuthCityMonthlyTreeCounts(): Promise { 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) } } /** * 存档并重置所有省团队授权的月度新增树数 * * 业务规则: * - 将当月业绩存档到 lastMonthTreesAdded(用于月度考核) * - 重置 monthlyTreesAdded 为0(开始新月累计) */ private async archiveAndResetAuthProvinceMonthlyTreeCounts(): Promise { this.logger.log('[archiveAndResetAuthProvinceMonthlyTreeCounts] 开始存档省团队月度新增树数...') try { // 获取所有激活的省团队授权 const activeAuthProvinces = await this.authorizationRepository.findAllActive(RoleType.AUTH_PROVINCE_COMPANY) let archivedCount = 0 for (const authProvince of activeAuthProvinces) { if (authProvince.benefitActive) { // 存档当月数据到 lastMonthTreesAdded,然后重置 monthlyTreesAdded authProvince.archiveAndResetMonthlyTrees() await this.authorizationRepository.save(authProvince) archivedCount++ this.logger.debug( `[archiveAndResetAuthProvinceMonthlyTreeCounts] 省团队授权 ${authProvince.userId.accountSequence}: ` + `存档=${authProvince.lastMonthTreesAdded}, 当月已重置=0`, ) } } this.logger.log(`[archiveAndResetAuthProvinceMonthlyTreeCounts] 存档完成: 已处理 ${archivedCount} 个省团队授权`) } catch (error) { this.logger.error('[archiveAndResetAuthProvinceMonthlyTreeCounts] 省团队月度树数存档失败', error) } } /** * 每天凌晨4点检查并处理过期的市团队授权权益 * * 业务规则: * - 检查所有 benefitValidUntil < 当前时间 且 benefitActive=true 的市团队授权 * - 如果当月新增树数 >= 100,续期 * - 如果不达标,级联停用该市团队授权及其所有上级市团队授权 */ @Cron('0 4 * * *') async processExpiredAuthCityBenefits(): Promise { 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) } } /** * 每天凌晨5点检查并处理过期的省团队授权权益 * * 业务规则: * - 检查所有 benefitValidUntil < 当前时间 且 benefitActive=true 的省团队授权 * - 如果当月新增树数 >= 500,续期 * - 如果不达标,级联停用该省团队授权及其所有上级省团队授权 */ @Cron('0 5 * * *') async processExpiredAuthProvinceBenefits(): Promise { this.logger.log('[processExpiredAuthProvinceBenefits] 开始检查省团队授权权益过期情况...') try { const result = await this.authorizationAppService.processExpiredAuthProvinceBenefits(100) this.logger.log( `[processExpiredAuthProvinceBenefits] 处理完成: ` + `已处理=${result.processedCount}, 已续期=${result.renewedCount}, 已停用=${result.deactivatedCount}`, ) } catch (error) { 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 { // 判断是否是当月最后一天 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 { 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) } } /** * 每天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) } } }