feat(authorization-service): 实现月度考核数据存档机制(方案B)

- 新增 lastMonthTreesAdded 字段,用于存档上月业绩数据
- 新增 archiveAndResetMonthlyTreeCounts 定时任务:每月1日0:00将当月数据存档后重置
- 新增 getTreesForAssessment() 方法:根据 benefitValidUntil 判断使用当月或上月数据
- 修复月度考核时序问题:数据重置(0:00)在考核(3:00)之前执行

业务规则:
- 严格自然月统计,11月的业绩不计入12月
- 激活当月免考核,考核激活当月的下一个月
- 权益有效期在上月末 → 使用 lastMonthTreesAdded
- 权益有效期在当月末 → 使用 monthlyTreesAdded

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-13 20:39:16 -08:00
parent b705c6aa91
commit 6bd6c6b5be
7 changed files with 106 additions and 23 deletions

View File

@ -11,11 +11,15 @@ COPY prisma ./prisma/
RUN npm ci
# Generate Prisma client (dummy DATABASE_URL for build time only)
RUN DATABASE_URL="postgresql://user:pass@localhost:5432/db" npx prisma generate
# Force regenerate to ensure latest schema is used
RUN rm -rf node_modules/.prisma && DATABASE_URL="postgresql://user:pass@localhost:5432/db" npx prisma generate
# Copy source code
COPY . .
# Regenerate Prisma client after COPY to ensure latest schema is used
RUN DATABASE_URL="postgresql://user:pass@localhost:5432/db" npx prisma generate
# Build application
RUN npm run build

View File

@ -0,0 +1,10 @@
-- 添加上月新增树数字段(用于月度考核存档)
-- 业务规则每月1日0:00将 monthly_trees_added 存档到 last_month_trees_added然后重置 monthly_trees_added
-- 考核时根据 benefit_valid_until 判断使用哪个字段
ALTER TABLE "authorization_roles"
ADD COLUMN IF NOT EXISTS "last_month_trees_added" INTEGER NOT NULL DEFAULT 0;
-- 添加注释
COMMENT ON COLUMN "authorization_roles"."last_month_trees_added" IS '上月新增树数(考核用存档)';
COMMENT ON COLUMN "authorization_roles"."monthly_trees_added" IS '当月新增树数(实时累计)';

View File

@ -45,6 +45,7 @@ model AuthorizationRole {
// 月度考核追踪
lastAssessmentMonth String? @map("last_assessment_month") // 上次考核月份 YYYY-MM
monthlyTreesAdded Int @default(0) @map("monthly_trees_added") // 当月新增树数
lastMonthTreesAdded Int @default(0) @map("last_month_trees_added") // 上月新增树数(考核用存档)
// 当前考核月份索引
currentMonthIndex Int @default(0) @map("current_month_index")

View File

@ -209,31 +209,42 @@ export class MonthlyAssessmentScheduler {
}
/**
* 10
* 10
*
* processExpiredCommunityBenefits
*
*
* - lastMonthTreesAdded
* - monthlyTreesAdded 0
*
* benefitValidUntil 使
* - lastMonthTreesAdded
* - monthlyTreesAdded
*/
@Cron('0 0 1 * *')
async resetMonthlyTreeCounts(): Promise<void> {
this.logger.log('[resetMonthlyTreeCounts] 开始重置月度新增树数...')
async archiveAndResetMonthlyTreeCounts(): Promise<void> {
this.logger.log('[archiveAndResetMonthlyTreeCounts] 开始存档并重置月度新增树数...')
try {
// 获取所有激活的社区
const activeCommunities = await this.authorizationRepository.findAllActive(RoleType.COMMUNITY)
let resetCount = 0
let archivedCount = 0
for (const community of activeCommunities) {
if (community.benefitActive && community.monthlyTreesAdded > 0) {
community.resetMonthlyTrees()
if (community.benefitActive) {
// 存档当月数据到 lastMonthTreesAdded然后重置 monthlyTreesAdded
community.archiveAndResetMonthlyTrees()
await this.authorizationRepository.save(community)
resetCount++
archivedCount++
this.logger.debug(
`[archiveAndResetMonthlyTreeCounts] 社区 ${community.userId.accountSequence}: ` +
`存档=${community.lastMonthTreesAdded}, 当月已重置=0`,
)
}
}
this.logger.log(`[resetMonthlyTreeCounts] 重置完成: 已重置 ${resetCount} 个社区的月度计数`)
this.logger.log(`[archiveAndResetMonthlyTreeCounts] 存档完成: 已处理 ${archivedCount} 个社区`)
} catch (error) {
this.logger.error('[resetMonthlyTreeCounts] 月度树数重置失败', error)
this.logger.error('[archiveAndResetMonthlyTreeCounts] 月度树数存档失败', error)
}
}
}

View File

@ -1049,30 +1049,34 @@ export class AuthorizationApplicationService {
for (const community of expiredCommunities) {
const accountSequence = community.userId.accountSequence
// 获取团队统计数据,检查月度新增树数
const teamStats = await this.statsRepository.findByAccountSequence(accountSequence)
const monthlyTreesAdded = community.monthlyTreesAdded
// 使用 getTreesForAssessment 获取正确的考核数据
// - 有效期在上月末 → 用 lastMonthTreesAdded存档数据
// - 有效期在当月末 → 用 monthlyTreesAdded当月数据
const treesForAssessment = community.getTreesForAssessment(now)
this.logger.debug(
`[processExpiredCommunityBenefits] Checking community ${accountSequence}: ` +
`monthlyTreesAdded=${monthlyTreesAdded}, target=10`,
`treesForAssessment=${treesForAssessment}, ` +
`monthlyTreesAdded=${community.monthlyTreesAdded}, ` +
`lastMonthTreesAdded=${community.lastMonthTreesAdded}, ` +
`benefitValidUntil=${community.benefitValidUntil?.toISOString()}, target=10`,
)
if (monthlyTreesAdded >= 10) {
if (treesForAssessment >= 10) {
// 达标,续期
community.renewBenefit(monthlyTreesAdded)
community.renewBenefit(treesForAssessment)
await this.authorizationRepository.save(community)
renewedCount++
this.logger.log(
`[processExpiredCommunityBenefits] Community ${accountSequence} renewed, ` +
`new validUntil=${community.benefitValidUntil?.toISOString()}`,
`trees=${treesForAssessment}, new validUntil=${community.benefitValidUntil?.toISOString()}`,
)
} else {
// 不达标,级联停用
const result = await this.cascadeDeactivateCommunityBenefits(
accountSequence,
`月度考核不达标:本月新增${monthlyTreesAdded}未达到10棵目标`,
`月度考核不达标:考核期内新增${treesForAssessment}未达到10棵目标`,
)
deactivatedCount += result.deactivatedCount
}

View File

@ -46,6 +46,7 @@ export interface AuthorizationRoleProps {
benefitValidUntil: Date | null // 权益有效期截止日期
lastAssessmentMonth: string | null // 上次考核月份 YYYY-MM
monthlyTreesAdded: number // 当月新增树数
lastMonthTreesAdded: number // 上月新增树数(考核用存档)
currentMonthIndex: number
createdAt: Date
updatedAt: Date
@ -83,6 +84,7 @@ export class AuthorizationRole extends AggregateRoot {
// 月度考核追踪
private _lastAssessmentMonth: string | null
private _monthlyTreesAdded: number
private _lastMonthTreesAdded: number // 上月新增树数(考核用存档)
// 当前考核月份索引
private _currentMonthIndex: number
@ -154,6 +156,9 @@ export class AuthorizationRole extends AggregateRoot {
get monthlyTreesAdded(): number {
return this._monthlyTreesAdded
}
get lastMonthTreesAdded(): number {
return this._lastMonthTreesAdded
}
get currentMonthIndex(): number {
return this._currentMonthIndex
}
@ -191,6 +196,7 @@ export class AuthorizationRole extends AggregateRoot {
this._benefitValidUntil = props.benefitValidUntil
this._lastAssessmentMonth = props.lastAssessmentMonth
this._monthlyTreesAdded = props.monthlyTreesAdded
this._lastMonthTreesAdded = props.lastMonthTreesAdded
this._currentMonthIndex = props.currentMonthIndex
this._createdAt = props.createdAt
this._updatedAt = props.updatedAt
@ -251,6 +257,7 @@ export class AuthorizationRole extends AggregateRoot {
benefitValidUntil: null,
lastAssessmentMonth: null,
monthlyTreesAdded: 0,
lastMonthTreesAdded: 0,
currentMonthIndex: 0,
createdAt: new Date(),
updatedAt: new Date(),
@ -298,6 +305,7 @@ export class AuthorizationRole extends AggregateRoot {
benefitValidUntil: skipAssessment ? AuthorizationRole.calculateBenefitValidUntil(now) : null,
lastAssessmentMonth: skipAssessment ? AuthorizationRole.getCurrentMonthString(now) : null,
monthlyTreesAdded: 0,
lastMonthTreesAdded: 0,
currentMonthIndex: skipAssessment ? 1 : 0,
createdAt: now,
updatedAt: now,
@ -344,6 +352,7 @@ export class AuthorizationRole extends AggregateRoot {
benefitValidUntil: null,
lastAssessmentMonth: null,
monthlyTreesAdded: 0,
lastMonthTreesAdded: 0,
currentMonthIndex: 0,
createdAt: now,
updatedAt: now,
@ -393,6 +402,7 @@ export class AuthorizationRole extends AggregateRoot {
benefitValidUntil: skipAssessment ? AuthorizationRole.calculateBenefitValidUntil(now) : null,
lastAssessmentMonth: skipAssessment ? AuthorizationRole.getCurrentMonthString(now) : null,
monthlyTreesAdded: 0,
lastMonthTreesAdded: 0,
currentMonthIndex: skipAssessment ? 1 : 0,
createdAt: now,
updatedAt: now,
@ -440,6 +450,7 @@ export class AuthorizationRole extends AggregateRoot {
benefitValidUntil: null,
lastAssessmentMonth: null,
monthlyTreesAdded: 0,
lastMonthTreesAdded: 0,
currentMonthIndex: 0,
createdAt: now,
updatedAt: now,
@ -489,6 +500,7 @@ export class AuthorizationRole extends AggregateRoot {
benefitValidUntil: skipAssessment ? AuthorizationRole.calculateBenefitValidUntil(now) : null,
lastAssessmentMonth: skipAssessment ? AuthorizationRole.getCurrentMonthString(now) : null,
monthlyTreesAdded: 0,
lastMonthTreesAdded: 0,
currentMonthIndex: skipAssessment ? 1 : 0,
createdAt: now,
updatedAt: now,
@ -539,6 +551,7 @@ export class AuthorizationRole extends AggregateRoot {
benefitValidUntil: skipAssessment ? AuthorizationRole.calculateBenefitValidUntil(now) : null,
lastAssessmentMonth: skipAssessment ? AuthorizationRole.getCurrentMonthString(now) : null,
monthlyTreesAdded: 0,
lastMonthTreesAdded: 0,
currentMonthIndex: skipAssessment ? 1 : 0,
createdAt: now,
updatedAt: now,
@ -589,6 +602,7 @@ export class AuthorizationRole extends AggregateRoot {
benefitValidUntil: skipAssessment ? AuthorizationRole.calculateBenefitValidUntil(now) : null,
lastAssessmentMonth: skipAssessment ? AuthorizationRole.getCurrentMonthString(now) : null,
monthlyTreesAdded: 0,
lastMonthTreesAdded: 0,
currentMonthIndex: skipAssessment ? 1 : 0,
createdAt: now,
updatedAt: now,
@ -653,6 +667,7 @@ export class AuthorizationRole extends AggregateRoot {
this._benefitValidUntil = null
this._lastAssessmentMonth = null
this._monthlyTreesAdded = 0
this._lastMonthTreesAdded = 0
this._currentMonthIndex = 0 // 重置月份索引
this._updatedAt = now
@ -691,11 +706,45 @@ export class AuthorizationRole extends AggregateRoot {
}
/**
*
* 每月1日0:00调用
* lastMonthTreesAdded
*/
archiveAndResetMonthlyTrees(): void {
this._lastMonthTreesAdded = this._monthlyTreesAdded // 存档
this._monthlyTreesAdded = 0 // 重置
this._updatedAt = new Date()
}
/**
*
* benefitValidUntil 使
* - lastMonthTreesAdded
* - monthlyTreesAdded
*/
getTreesForAssessment(checkDate: Date = new Date()): number {
if (!this._benefitValidUntil) return 0
const validUntilMonth = this._benefitValidUntil.getMonth()
const validUntilYear = this._benefitValidUntil.getFullYear()
const checkMonth = checkDate.getMonth()
const checkYear = checkDate.getFullYear()
// 如果有效期年月 < 检查年月,说明有效期在上月或更早
if (validUntilYear < checkYear ||
(validUntilYear === checkYear && validUntilMonth < checkMonth)) {
// 有效期在上月末或更早 → 用存档数据
return this._lastMonthTreesAdded
} else {
// 有效期在当月末 → 用当月数据
return this._monthlyTreesAdded
}
}
/**
* @deprecated 使 archiveAndResetMonthlyTrees
*/
resetMonthlyTrees(): void {
this._monthlyTreesAdded = 0
this._updatedAt = new Date()
this.archiveAndResetMonthlyTrees()
}
/**
@ -853,6 +902,7 @@ export class AuthorizationRole extends AggregateRoot {
benefitValidUntil: this._benefitValidUntil,
lastAssessmentMonth: this._lastAssessmentMonth,
monthlyTreesAdded: this._monthlyTreesAdded,
lastMonthTreesAdded: this._lastMonthTreesAdded,
currentMonthIndex: this._currentMonthIndex,
createdAt: this._createdAt,
updatedAt: this._updatedAt,

View File

@ -46,6 +46,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi
benefitValidUntil: data.benefitValidUntil,
lastAssessmentMonth: data.lastAssessmentMonth,
monthlyTreesAdded: data.monthlyTreesAdded,
lastMonthTreesAdded: data.lastMonthTreesAdded,
currentMonthIndex: data.currentMonthIndex,
},
update: {
@ -64,6 +65,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi
benefitValidUntil: data.benefitValidUntil,
lastAssessmentMonth: data.lastAssessmentMonth,
monthlyTreesAdded: data.monthlyTreesAdded,
lastMonthTreesAdded: data.lastMonthTreesAdded,
currentMonthIndex: data.currentMonthIndex,
},
})
@ -393,6 +395,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi
benefitValidUntil: record.benefitValidUntil,
lastAssessmentMonth: record.lastAssessmentMonth,
monthlyTreesAdded: record.monthlyTreesAdded ?? 0,
lastMonthTreesAdded: record.lastMonthTreesAdded ?? 0,
currentMonthIndex: record.currentMonthIndex,
createdAt: record.createdAt,
updatedAt: record.updatedAt,