521 lines
20 KiB
TypeScript
521 lines
20 KiB
TypeScript
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)
|
||
}
|
||
}
|
||
}
|