rwadurian/backend/services/authorization-service/src/application/schedulers/monthly-assessment.schedule...

521 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<void> {
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<void> {
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<void> {
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<string, AuthorizationRole[]> {
const map = new Map<string, AuthorizationRole[]>()
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<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)
}
}
/**
* 每天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)
}
}
}