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:
parent
b705c6aa91
commit
6bd6c6b5be
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 '当月新增树数(实时累计)';
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -209,31 +209,42 @@ export class MonthlyAssessmentScheduler {
|
|||
}
|
||||
|
||||
/**
|
||||
* 每月1号凌晨0点重置所有社区的月度新增树数
|
||||
* 每月1号凌晨0点存档并重置所有社区的月度新增树数
|
||||
*
|
||||
* 注意:此任务必须在 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue