diff --git a/backend/services/authorization-service/Dockerfile b/backend/services/authorization-service/Dockerfile index 27addec5..85c2a843 100644 --- a/backend/services/authorization-service/Dockerfile +++ b/backend/services/authorization-service/Dockerfile @@ -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 diff --git a/backend/services/authorization-service/prisma/migrations/20251214100000_add_last_month_trees_added/migration.sql b/backend/services/authorization-service/prisma/migrations/20251214100000_add_last_month_trees_added/migration.sql new file mode 100644 index 00000000..6c84dcec --- /dev/null +++ b/backend/services/authorization-service/prisma/migrations/20251214100000_add_last_month_trees_added/migration.sql @@ -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 '当月新增树数(实时累计)'; diff --git a/backend/services/authorization-service/prisma/schema.prisma b/backend/services/authorization-service/prisma/schema.prisma index fd43f757..af9c1d67 100644 --- a/backend/services/authorization-service/prisma/schema.prisma +++ b/backend/services/authorization-service/prisma/schema.prisma @@ -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") diff --git a/backend/services/authorization-service/src/application/schedulers/monthly-assessment.scheduler.ts b/backend/services/authorization-service/src/application/schedulers/monthly-assessment.scheduler.ts index 13ac747e..f4f87aa9 100644 --- a/backend/services/authorization-service/src/application/schedulers/monthly-assessment.scheduler.ts +++ b/backend/services/authorization-service/src/application/schedulers/monthly-assessment.scheduler.ts @@ -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 { - this.logger.log('[resetMonthlyTreeCounts] 开始重置月度新增树数...') + async archiveAndResetMonthlyTreeCounts(): Promise { + 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) } } } diff --git a/backend/services/authorization-service/src/application/services/authorization-application.service.ts b/backend/services/authorization-service/src/application/services/authorization-application.service.ts index 1214ab0a..36c80dfd 100644 --- a/backend/services/authorization-service/src/application/services/authorization-application.service.ts +++ b/backend/services/authorization-service/src/application/services/authorization-application.service.ts @@ -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 } diff --git a/backend/services/authorization-service/src/domain/aggregates/authorization-role.aggregate.ts b/backend/services/authorization-service/src/domain/aggregates/authorization-role.aggregate.ts index 5b62af9e..17a7fae3 100644 --- a/backend/services/authorization-service/src/domain/aggregates/authorization-role.aggregate.ts +++ b/backend/services/authorization-service/src/domain/aggregates/authorization-role.aggregate.ts @@ -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, diff --git a/backend/services/authorization-service/src/infrastructure/persistence/repositories/authorization-role.repository.impl.ts b/backend/services/authorization-service/src/infrastructure/persistence/repositories/authorization-role.repository.impl.ts index debf551b..67bfc76e 100644 --- a/backend/services/authorization-service/src/infrastructure/persistence/repositories/authorization-role.repository.impl.ts +++ b/backend/services/authorization-service/src/infrastructure/persistence/repositories/authorization-role.repository.impl.ts @@ -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,