diff --git a/backend/services/authorization-service/prisma/migrations/20251213151704_add_community_benefit_assessment_fields/migration.sql b/backend/services/authorization-service/prisma/migrations/20251213151704_add_community_benefit_assessment_fields/migration.sql new file mode 100644 index 00000000..c751117f --- /dev/null +++ b/backend/services/authorization-service/prisma/migrations/20251213151704_add_community_benefit_assessment_fields/migration.sql @@ -0,0 +1,33 @@ +-- AlterTable +ALTER TABLE "authorization_roles" ADD COLUMN "benefit_valid_until" TIMESTAMP(3), +ADD COLUMN "last_assessment_month" TEXT, +ADD COLUMN "monthly_trees_added" INTEGER NOT NULL DEFAULT 0, +ALTER COLUMN "user_id" SET DATA TYPE TEXT, +ALTER COLUMN "authorized_by" SET DATA TYPE TEXT, +ALTER COLUMN "revoked_by" SET DATA TYPE TEXT; + +-- AlterTable +ALTER TABLE "monthly_assessments" ALTER COLUMN "user_id" SET DATA TYPE TEXT, +ALTER COLUMN "bypassed_by" SET DATA TYPE TEXT; + +-- AlterTable +ALTER TABLE "monthly_bypasses" ALTER COLUMN "user_id" SET DATA TYPE TEXT, +ALTER COLUMN "granted_by" SET DATA TYPE TEXT, +ALTER COLUMN "approver1_id" SET DATA TYPE TEXT, +ALTER COLUMN "approver2_id" SET DATA TYPE TEXT, +ALTER COLUMN "approver3_id" SET DATA TYPE TEXT; + +-- AlterTable +ALTER TABLE "stickman_rankings" ALTER COLUMN "user_id" SET DATA TYPE TEXT; + +-- RenameIndex +ALTER INDEX "idx_authorization_roles_account_sequence" RENAME TO "authorization_roles_account_sequence_idx"; + +-- RenameIndex +ALTER INDEX "idx_monthly_assessments_account_sequence_month" RENAME TO "monthly_assessments_account_sequence_assessment_month_idx"; + +-- RenameIndex +ALTER INDEX "idx_monthly_bypasses_account_sequence_month" RENAME TO "monthly_bypasses_account_sequence_bypass_month_idx"; + +-- RenameIndex +ALTER INDEX "idx_stickman_rankings_account_sequence_month" RENAME TO "stickman_rankings_account_sequence_current_month_idx"; diff --git a/backend/services/authorization-service/prisma/migrations/migration_lock.toml b/backend/services/authorization-service/prisma/migrations/migration_lock.toml index 99e4f200..fbffa92c 100644 --- a/backend/services/authorization-service/prisma/migrations/migration_lock.toml +++ b/backend/services/authorization-service/prisma/migrations/migration_lock.toml @@ -1,3 +1,3 @@ # Please do not edit this file manually # It should be added in your version-control system (i.e. Git) -provider = "postgresql" +provider = "postgresql" \ No newline at end of file diff --git a/backend/services/authorization-service/prisma/schema.prisma b/backend/services/authorization-service/prisma/schema.prisma index 70ba3023..fd43f757 100644 --- a/backend/services/authorization-service/prisma/schema.prisma +++ b/backend/services/authorization-service/prisma/schema.prisma @@ -40,6 +40,11 @@ model AuthorizationRole { benefitActive Boolean @default(false) @map("benefit_active") benefitActivatedAt DateTime? @map("benefit_activated_at") benefitDeactivatedAt DateTime? @map("benefit_deactivated_at") + benefitValidUntil DateTime? @map("benefit_valid_until") // 权益有效期截止日期(当前月+下月末) + + // 月度考核追踪 + lastAssessmentMonth String? @map("last_assessment_month") // 上次考核月份 YYYY-MM + monthlyTreesAdded Int @default(0) @map("monthly_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 59aefc5e..13ac747e 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 @@ -11,7 +11,7 @@ import { } from '@/domain/repositories' import { AssessmentCalculatorService, ITeamStatisticsRepository } from '@/domain/services' import { EventPublisherService } from '@/infrastructure/kafka' -import { TEAM_STATISTICS_REPOSITORY } from '@/application/services' +import { TEAM_STATISTICS_REPOSITORY, AuthorizationApplicationService } from '@/application/services' @Injectable() export class MonthlyAssessmentScheduler { @@ -26,6 +26,7 @@ export class MonthlyAssessmentScheduler { @Inject(TEAM_STATISTICS_REPOSITORY) private readonly statsRepository: ITeamStatisticsRepository, private readonly eventPublisher: EventPublisherService, + private readonly authorizationAppService: AuthorizationApplicationService, ) {} /** @@ -182,4 +183,57 @@ export class MonthlyAssessmentScheduler { return map } + + /** + * 每天凌晨3点检查并处理过期的社区权益 + * + * 业务规则: + * - 检查所有 benefitValidUntil < 当前时间 且 benefitActive=true 的社区 + * - 如果当月新增树数 >= 10,续期 + * - 如果不达标,级联停用该社区及其所有上级社区 + */ + @Cron('0 3 * * *') + async processExpiredCommunityBenefits(): Promise { + 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点重置所有社区的月度新增树数 + * + * 注意:此任务必须在 processExpiredCommunityBenefits 之前执行 + * 因为要先检查上月的考核情况,再重置计数器 + */ + @Cron('0 0 1 * *') + async resetMonthlyTreeCounts(): Promise { + this.logger.log('[resetMonthlyTreeCounts] 开始重置月度新增树数...') + + try { + // 获取所有激活的社区 + const activeCommunities = await this.authorizationRepository.findAllActive(RoleType.COMMUNITY) + + let resetCount = 0 + for (const community of activeCommunities) { + if (community.benefitActive && community.monthlyTreesAdded > 0) { + community.resetMonthlyTrees() + await this.authorizationRepository.save(community) + resetCount++ + } + } + + this.logger.log(`[resetMonthlyTreeCounts] 重置完成: 已重置 ${resetCount} 个社区的月度计数`) + } catch (error) { + this.logger.error('[resetMonthlyTreeCounts] 月度树数重置失败', 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 bc5832df..1214ab0a 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 @@ -859,6 +859,8 @@ export class AuthorizationApplicationService { /** * 尝试激活授权权益 * 仅当权益未激活时执行激活操作 + * + * 对于社区权益:需要级联激活推荐链上所有父级社区 */ private async tryActivateBenefit(authorization: AuthorizationRole): Promise { if (authorization.benefitActive) { @@ -874,6 +876,232 @@ export class AuthorizationApplicationService { await this.authorizationRepository.save(authorization) await this.eventPublisher.publishAll(authorization.domainEvents) authorization.clearDomainEvents() + + // 如果是社区权益,需要级联激活上级社区 + if (authorization.roleType === RoleType.COMMUNITY) { + await this.cascadeActivateParentCommunities(authorization.userId.accountSequence) + } + } + + /** + * 级联激活上级社区权益 + * 当一个社区的权益被激活时,需要同时激活推荐链上所有父级社区的权益 + * + * 业务规则: + * - 从当前社区往上找,找到所有已授权但权益未激活的社区 + * - 将它们的权益都激活 + * - 总部社区不需要考核,不在此处理 + */ + private async cascadeActivateParentCommunities(accountSequence: string): Promise { + this.logger.log( + `[cascadeActivateParentCommunities] Starting cascade activation for communities above ${accountSequence}`, + ) + + // 1. 获取推荐链(不包括当前用户) + const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(accountSequence) + if (ancestorAccountSequences.length === 0) { + return + } + + // 2. 查找推荐链上所有社区授权(包括 benefitActive=false) + const ancestorCommunities = await this.authorizationRepository.findCommunityByAccountSequences( + ancestorAccountSequences, + ) + + // 3. 筛选出已授权但权益未激活的社区 + const inactiveCommunities = ancestorCommunities.filter( + (auth) => auth.status === AuthorizationStatus.AUTHORIZED && !auth.benefitActive, + ) + + if (inactiveCommunities.length === 0) { + this.logger.debug('[cascadeActivateParentCommunities] No inactive parent communities to activate') + return + } + + // 4. 激活这些社区的权益 + for (const community of inactiveCommunities) { + this.logger.log( + `[cascadeActivateParentCommunities] Cascade activating community benefit: ` + + `authorizationId=${community.authorizationId.value}, accountSequence=${community.userId.accountSequence}`, + ) + + community.activateBenefit() + await this.authorizationRepository.save(community) + await this.eventPublisher.publishAll(community.domainEvents) + community.clearDomainEvents() + } + + this.logger.log( + `[cascadeActivateParentCommunities] Cascade activated ${inactiveCommunities.length} parent communities`, + ) + } + + /** + * 级联停用社区权益 + * 当一个社区的月度考核失败时,需要停用该社区及其推荐链上所有父级社区的权益 + * + * 业务规则: + * - 从当前社区开始,往上找到所有已授权且权益已激活的社区 + * - 将它们的权益都停用,重新开始10棵树的初始考核 + * - 总部社区不受影响 + * + * @param accountSequence 月度考核失败的社区的 accountSequence + * @param reason 停用原因 + */ + async cascadeDeactivateCommunityBenefits( + accountSequence: string, + reason: string, + ): Promise<{ deactivatedCount: number }> { + this.logger.log( + `[cascadeDeactivateCommunityBenefits] Starting cascade deactivation from ${accountSequence}, reason=${reason}`, + ) + + // 1. 获取当前社区的授权 + const currentCommunity = await this.authorizationRepository.findByAccountSequenceAndRoleType( + accountSequence, + RoleType.COMMUNITY, + ) + + if (!currentCommunity) { + this.logger.warn(`[cascadeDeactivateCommunityBenefits] Community not found for ${accountSequence}`) + return { deactivatedCount: 0 } + } + + // 2. 收集需要停用的社区列表 + const communitiesToDeactivate: AuthorizationRole[] = [] + + // 如果当前社区权益已激活,加入停用列表 + if (currentCommunity.benefitActive) { + communitiesToDeactivate.push(currentCommunity) + } + + // 3. 获取推荐链上的所有父级社区 + const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(accountSequence) + if (ancestorAccountSequences.length > 0) { + const ancestorCommunities = await this.authorizationRepository.findCommunityByAccountSequences( + ancestorAccountSequences, + ) + + // 筛选出已授权且权益已激活的社区 + const activeCommunities = ancestorCommunities.filter( + (auth) => auth.status === AuthorizationStatus.AUTHORIZED && auth.benefitActive, + ) + + communitiesToDeactivate.push(...activeCommunities) + } + + if (communitiesToDeactivate.length === 0) { + this.logger.debug('[cascadeDeactivateCommunityBenefits] No active communities to deactivate') + return { deactivatedCount: 0 } + } + + // 4. 停用这些社区的权益 + for (const community of communitiesToDeactivate) { + this.logger.log( + `[cascadeDeactivateCommunityBenefits] Deactivating community benefit: ` + + `authorizationId=${community.authorizationId.value}, accountSequence=${community.userId.accountSequence}`, + ) + + community.deactivateBenefit(reason) + await this.authorizationRepository.save(community) + await this.eventPublisher.publishAll(community.domainEvents) + community.clearDomainEvents() + } + + this.logger.log( + `[cascadeDeactivateCommunityBenefits] Cascade deactivated ${communitiesToDeactivate.length} communities`, + ) + + return { deactivatedCount: communitiesToDeactivate.length } + } + + /** + * 处理过期的社区权益 + * 定时任务调用此方法来检查并处理过期的社区权益 + * + * 业务规则: + * - 查找所有 benefitValidUntil < 当前时间 且 benefitActive=true 的社区 + * - 检查其月度考核(当月新增10棵树) + * - 如果达标,续期;如果不达标,级联停用 + * + * @param limit 每次处理的最大数量 + */ + async processExpiredCommunityBenefits(limit = 100): Promise<{ + processedCount: number + renewedCount: number + deactivatedCount: number + }> { + const now = new Date() + this.logger.log(`[processExpiredCommunityBenefits] Starting at ${now.toISOString()}, limit=${limit}`) + + // 查找过期但仍激活的社区 + // 需要在 repository 中添加此查询方法 + const expiredCommunities = await this.findExpiredActiveCommunities(now, limit) + + if (expiredCommunities.length === 0) { + this.logger.debug('[processExpiredCommunityBenefits] No expired communities found') + return { processedCount: 0, renewedCount: 0, deactivatedCount: 0 } + } + + let renewedCount = 0 + let deactivatedCount = 0 + + for (const community of expiredCommunities) { + const accountSequence = community.userId.accountSequence + + // 获取团队统计数据,检查月度新增树数 + const teamStats = await this.statsRepository.findByAccountSequence(accountSequence) + const monthlyTreesAdded = community.monthlyTreesAdded + + this.logger.debug( + `[processExpiredCommunityBenefits] Checking community ${accountSequence}: ` + + `monthlyTreesAdded=${monthlyTreesAdded}, target=10`, + ) + + if (monthlyTreesAdded >= 10) { + // 达标,续期 + community.renewBenefit(monthlyTreesAdded) + await this.authorizationRepository.save(community) + renewedCount++ + + this.logger.log( + `[processExpiredCommunityBenefits] Community ${accountSequence} renewed, ` + + `new validUntil=${community.benefitValidUntil?.toISOString()}`, + ) + } else { + // 不达标,级联停用 + const result = await this.cascadeDeactivateCommunityBenefits( + accountSequence, + `月度考核不达标:本月新增${monthlyTreesAdded}棵,未达到10棵目标`, + ) + deactivatedCount += result.deactivatedCount + } + } + + this.logger.log( + `[processExpiredCommunityBenefits] Completed: processed=${expiredCommunities.length}, ` + + `renewed=${renewedCount}, deactivated=${deactivatedCount}`, + ) + + return { + processedCount: expiredCommunities.length, + renewedCount, + deactivatedCount, + } + } + + /** + * 查找过期但仍激活的社区 + * TODO: 后续优化可以移到 repository 层 + */ + private async findExpiredActiveCommunities(now: Date, limit: number): Promise { + // 获取所有激活的社区授权 + const activeCommunities = await this.authorizationRepository.findAllActive(RoleType.COMMUNITY) + + // 筛选出已过期的 + return activeCommunities + .filter((auth) => auth.benefitActive && auth.isBenefitExpired(now)) + .slice(0, limit) } /** 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 73976fd4..5b62af9e 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 @@ -43,6 +43,9 @@ export interface AuthorizationRoleProps { benefitActive: boolean benefitActivatedAt: Date | null benefitDeactivatedAt: Date | null + benefitValidUntil: Date | null // 权益有效期截止日期 + lastAssessmentMonth: string | null // 上次考核月份 YYYY-MM + monthlyTreesAdded: number // 当月新增树数 currentMonthIndex: number createdAt: Date updatedAt: Date @@ -75,6 +78,11 @@ export class AuthorizationRole extends AggregateRoot { private _benefitActive: boolean private _benefitActivatedAt: Date | null private _benefitDeactivatedAt: Date | null + private _benefitValidUntil: Date | null + + // 月度考核追踪 + private _lastAssessmentMonth: string | null + private _monthlyTreesAdded: number // 当前考核月份索引 private _currentMonthIndex: number @@ -137,6 +145,15 @@ export class AuthorizationRole extends AggregateRoot { get benefitDeactivatedAt(): Date | null { return this._benefitDeactivatedAt } + get benefitValidUntil(): Date | null { + return this._benefitValidUntil + } + get lastAssessmentMonth(): string | null { + return this._lastAssessmentMonth + } + get monthlyTreesAdded(): number { + return this._monthlyTreesAdded + } get currentMonthIndex(): number { return this._currentMonthIndex } @@ -171,6 +188,9 @@ export class AuthorizationRole extends AggregateRoot { this._benefitActive = props.benefitActive this._benefitActivatedAt = props.benefitActivatedAt this._benefitDeactivatedAt = props.benefitDeactivatedAt + this._benefitValidUntil = props.benefitValidUntil + this._lastAssessmentMonth = props.lastAssessmentMonth + this._monthlyTreesAdded = props.monthlyTreesAdded this._currentMonthIndex = props.currentMonthIndex this._createdAt = props.createdAt this._updatedAt = props.updatedAt @@ -181,6 +201,32 @@ export class AuthorizationRole extends AggregateRoot { return new AuthorizationRole(props) } + /** + * 计算权益有效期截止日期 + * 规则:激活当月 + 下月末 + * 例如:11月3日激活 -> 12月31日23:59:59 + */ + private static calculateBenefitValidUntil(activatedAt: Date): Date { + const year = activatedAt.getFullYear() + const month = activatedAt.getMonth() // 0-based + + // 下下个月的第一天的前一天 = 下个月的最后一天 + // 例如:11月 -> month=10 -> month+2=12 (1月) -> 12月31日 + const nextNextMonth = month + 2 + const validUntil = new Date(year, nextNextMonth, 0, 23, 59, 59, 999) + + return validUntil + } + + /** + * 获取当前月份字符串 YYYY-MM + */ + private static getCurrentMonthString(date: Date = new Date()): string { + const year = date.getFullYear() + const month = (date.getMonth() + 1).toString().padStart(2, '0') + return `${year}-${month}` + } + // 工厂方法 - 创建社区授权 static createCommunityAuth(params: { userId: UserId; communityName: string }): AuthorizationRole { const auth = new AuthorizationRole({ @@ -202,6 +248,9 @@ export class AuthorizationRole extends AggregateRoot { benefitActive: false, benefitActivatedAt: null, benefitDeactivatedAt: null, + benefitValidUntil: null, + lastAssessmentMonth: null, + monthlyTreesAdded: 0, currentMonthIndex: 0, createdAt: new Date(), updatedAt: new Date(), @@ -226,6 +275,7 @@ export class AuthorizationRole extends AggregateRoot { skipAssessment?: boolean }): AuthorizationRole { const skipAssessment = params.skipAssessment ?? false + const now = new Date() const auth = new AuthorizationRole({ authorizationId: AuthorizationId.generate(), userId: params.userId, @@ -234,7 +284,7 @@ export class AuthorizationRole extends AggregateRoot { regionName: params.communityName, status: AuthorizationStatus.AUTHORIZED, displayTitle: params.communityName, - authorizedAt: new Date(), + authorizedAt: now, authorizedBy: params.adminId, revokedAt: null, revokedBy: null, @@ -243,11 +293,14 @@ export class AuthorizationRole extends AggregateRoot { requireLocalPercentage: 0, exemptFromPercentageCheck: true, benefitActive: skipAssessment, - benefitActivatedAt: skipAssessment ? new Date() : null, + benefitActivatedAt: skipAssessment ? now : null, benefitDeactivatedAt: null, + benefitValidUntil: skipAssessment ? AuthorizationRole.calculateBenefitValidUntil(now) : null, + lastAssessmentMonth: skipAssessment ? AuthorizationRole.getCurrentMonthString(now) : null, + monthlyTreesAdded: 0, currentMonthIndex: skipAssessment ? 1 : 0, - createdAt: new Date(), - updatedAt: new Date(), + createdAt: now, + updatedAt: now, }) auth.addDomainEvent( @@ -268,6 +321,7 @@ export class AuthorizationRole extends AggregateRoot { provinceCode: string provinceName: string }): AuthorizationRole { + const now = new Date() const auth = new AuthorizationRole({ authorizationId: AuthorizationId.generate(), userId: params.userId, @@ -287,9 +341,12 @@ export class AuthorizationRole extends AggregateRoot { benefitActive: false, benefitActivatedAt: null, benefitDeactivatedAt: null, + benefitValidUntil: null, + lastAssessmentMonth: null, + monthlyTreesAdded: 0, currentMonthIndex: 0, - createdAt: new Date(), - updatedAt: new Date(), + createdAt: now, + updatedAt: now, }) auth.addDomainEvent( @@ -313,6 +370,7 @@ export class AuthorizationRole extends AggregateRoot { skipAssessment?: boolean }): AuthorizationRole { const skipAssessment = params.skipAssessment ?? false + const now = new Date() const auth = new AuthorizationRole({ authorizationId: AuthorizationId.generate(), userId: params.userId, @@ -321,7 +379,7 @@ export class AuthorizationRole extends AggregateRoot { regionName: params.provinceName, status: AuthorizationStatus.AUTHORIZED, displayTitle: params.provinceName, - authorizedAt: new Date(), + authorizedAt: now, authorizedBy: params.adminId, revokedAt: null, revokedBy: null, @@ -330,11 +388,14 @@ export class AuthorizationRole extends AggregateRoot { requireLocalPercentage: 0, exemptFromPercentageCheck: true, benefitActive: skipAssessment, - benefitActivatedAt: skipAssessment ? new Date() : null, + benefitActivatedAt: skipAssessment ? now : null, benefitDeactivatedAt: null, + benefitValidUntil: skipAssessment ? AuthorizationRole.calculateBenefitValidUntil(now) : null, + lastAssessmentMonth: skipAssessment ? AuthorizationRole.getCurrentMonthString(now) : null, + monthlyTreesAdded: 0, currentMonthIndex: skipAssessment ? 1 : 0, - createdAt: new Date(), - updatedAt: new Date(), + createdAt: now, + updatedAt: now, }) auth.addDomainEvent( @@ -356,6 +417,7 @@ export class AuthorizationRole extends AggregateRoot { cityCode: string cityName: string }): AuthorizationRole { + const now = new Date() const auth = new AuthorizationRole({ authorizationId: AuthorizationId.generate(), userId: params.userId, @@ -375,9 +437,12 @@ export class AuthorizationRole extends AggregateRoot { benefitActive: false, benefitActivatedAt: null, benefitDeactivatedAt: null, + benefitValidUntil: null, + lastAssessmentMonth: null, + monthlyTreesAdded: 0, currentMonthIndex: 0, - createdAt: new Date(), - updatedAt: new Date(), + createdAt: now, + updatedAt: now, }) auth.addDomainEvent( @@ -401,6 +466,7 @@ export class AuthorizationRole extends AggregateRoot { skipAssessment?: boolean }): AuthorizationRole { const skipAssessment = params.skipAssessment ?? false + const now = new Date() const auth = new AuthorizationRole({ authorizationId: AuthorizationId.generate(), userId: params.userId, @@ -409,7 +475,7 @@ export class AuthorizationRole extends AggregateRoot { regionName: params.cityName, status: AuthorizationStatus.AUTHORIZED, displayTitle: params.cityName, - authorizedAt: new Date(), + authorizedAt: now, authorizedBy: params.adminId, revokedAt: null, revokedBy: null, @@ -418,11 +484,14 @@ export class AuthorizationRole extends AggregateRoot { requireLocalPercentage: 0, exemptFromPercentageCheck: true, benefitActive: skipAssessment, - benefitActivatedAt: skipAssessment ? new Date() : null, + benefitActivatedAt: skipAssessment ? now : null, benefitDeactivatedAt: null, + benefitValidUntil: skipAssessment ? AuthorizationRole.calculateBenefitValidUntil(now) : null, + lastAssessmentMonth: skipAssessment ? AuthorizationRole.getCurrentMonthString(now) : null, + monthlyTreesAdded: 0, currentMonthIndex: skipAssessment ? 1 : 0, - createdAt: new Date(), - updatedAt: new Date(), + createdAt: now, + updatedAt: now, }) auth.addDomainEvent( @@ -447,6 +516,7 @@ export class AuthorizationRole extends AggregateRoot { skipAssessment?: boolean }): AuthorizationRole { const skipAssessment = params.skipAssessment ?? false + const now = new Date() const auth = new AuthorizationRole({ authorizationId: AuthorizationId.generate(), userId: params.userId, @@ -455,7 +525,7 @@ export class AuthorizationRole extends AggregateRoot { regionName: params.provinceName, status: AuthorizationStatus.AUTHORIZED, displayTitle: `授权${params.provinceName}`, - authorizedAt: new Date(), + authorizedAt: now, authorizedBy: params.adminId, revokedAt: null, revokedBy: null, @@ -464,11 +534,14 @@ export class AuthorizationRole extends AggregateRoot { requireLocalPercentage: 5.0, exemptFromPercentageCheck: false, benefitActive: skipAssessment, - benefitActivatedAt: skipAssessment ? new Date() : null, + benefitActivatedAt: skipAssessment ? now : null, benefitDeactivatedAt: null, + benefitValidUntil: skipAssessment ? AuthorizationRole.calculateBenefitValidUntil(now) : null, + lastAssessmentMonth: skipAssessment ? AuthorizationRole.getCurrentMonthString(now) : null, + monthlyTreesAdded: 0, currentMonthIndex: skipAssessment ? 1 : 0, - createdAt: new Date(), - updatedAt: new Date(), + createdAt: now, + updatedAt: now, }) auth.addDomainEvent( @@ -493,6 +566,7 @@ export class AuthorizationRole extends AggregateRoot { skipAssessment?: boolean }): AuthorizationRole { const skipAssessment = params.skipAssessment ?? false + const now = new Date() const auth = new AuthorizationRole({ authorizationId: AuthorizationId.generate(), userId: params.userId, @@ -501,7 +575,7 @@ export class AuthorizationRole extends AggregateRoot { regionName: params.cityName, status: AuthorizationStatus.AUTHORIZED, displayTitle: `授权${params.cityName}`, - authorizedAt: new Date(), + authorizedAt: now, authorizedBy: params.adminId, revokedAt: null, revokedBy: null, @@ -510,11 +584,14 @@ export class AuthorizationRole extends AggregateRoot { requireLocalPercentage: 5.0, exemptFromPercentageCheck: false, benefitActive: skipAssessment, - benefitActivatedAt: skipAssessment ? new Date() : null, + benefitActivatedAt: skipAssessment ? now : null, benefitDeactivatedAt: null, + benefitValidUntil: skipAssessment ? AuthorizationRole.calculateBenefitValidUntil(now) : null, + lastAssessmentMonth: skipAssessment ? AuthorizationRole.getCurrentMonthString(now) : null, + monthlyTreesAdded: 0, currentMonthIndex: skipAssessment ? 1 : 0, - createdAt: new Date(), - updatedAt: new Date(), + createdAt: now, + updatedAt: now, }) auth.addDomainEvent( @@ -534,17 +611,22 @@ export class AuthorizationRole extends AggregateRoot { /** * 激活权益(初始考核达标后) + * 设置有效期为当前月 + 下月末 */ activateBenefit(): void { if (this._benefitActive) { throw new DomainError('权益已激活') } + const now = new Date() this._status = AuthorizationStatus.AUTHORIZED this._benefitActive = true - this._benefitActivatedAt = new Date() + this._benefitActivatedAt = now + this._benefitValidUntil = AuthorizationRole.calculateBenefitValidUntil(now) + this._lastAssessmentMonth = AuthorizationRole.getCurrentMonthString(now) + this._monthlyTreesAdded = 0 this._currentMonthIndex = 1 - this._updatedAt = new Date() + this._updatedAt = now this.addDomainEvent( new BenefitActivatedEvent({ @@ -558,16 +640,21 @@ export class AuthorizationRole extends AggregateRoot { /** * 失效权益(月度考核不达标) + * 重置所有考核相关字段 */ deactivateBenefit(reason: string): void { if (!this._benefitActive) { return } + const now = new Date() this._benefitActive = false - this._benefitDeactivatedAt = new Date() + this._benefitDeactivatedAt = now + this._benefitValidUntil = null + this._lastAssessmentMonth = null + this._monthlyTreesAdded = 0 this._currentMonthIndex = 0 // 重置月份索引 - this._updatedAt = new Date() + this._updatedAt = now this.addDomainEvent( new BenefitDeactivatedEvent({ @@ -579,6 +666,48 @@ export class AuthorizationRole extends AggregateRoot { ) } + /** + * 续期权益(月度考核达标后) + * 延长有效期到下下月末 + */ + renewBenefit(treesAdded: number): void { + if (!this._benefitActive) { + throw new DomainError('权益未激活,无法续期') + } + + const now = new Date() + this._benefitValidUntil = AuthorizationRole.calculateBenefitValidUntil(now) + this._lastAssessmentMonth = AuthorizationRole.getCurrentMonthString(now) + this._monthlyTreesAdded = treesAdded + this._updatedAt = now + } + + /** + * 增加月度新增树数 + */ + addMonthlyTrees(treeCount: number): void { + this._monthlyTreesAdded += treeCount + this._updatedAt = new Date() + } + + /** + * 重置月度新增树数(每月初调用) + */ + resetMonthlyTrees(): void { + this._monthlyTreesAdded = 0 + this._updatedAt = new Date() + } + + /** + * 检查权益是否已过期 + */ + isBenefitExpired(checkDate: Date = new Date()): boolean { + if (!this._benefitActive || !this._benefitValidUntil) { + return false + } + return checkDate > this._benefitValidUntil + } + /** * 管理员授权 */ @@ -721,6 +850,9 @@ export class AuthorizationRole extends AggregateRoot { benefitActive: this._benefitActive, benefitActivatedAt: this._benefitActivatedAt, benefitDeactivatedAt: this._benefitDeactivatedAt, + benefitValidUntil: this._benefitValidUntil, + lastAssessmentMonth: this._lastAssessmentMonth, + monthlyTreesAdded: this._monthlyTreesAdded, 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 f6072840..debf551b 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 @@ -43,6 +43,9 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi benefitActive: data.benefitActive, benefitActivatedAt: data.benefitActivatedAt, benefitDeactivatedAt: data.benefitDeactivatedAt, + benefitValidUntil: data.benefitValidUntil, + lastAssessmentMonth: data.lastAssessmentMonth, + monthlyTreesAdded: data.monthlyTreesAdded, currentMonthIndex: data.currentMonthIndex, }, update: { @@ -58,6 +61,9 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi benefitActive: data.benefitActive, benefitActivatedAt: data.benefitActivatedAt, benefitDeactivatedAt: data.benefitDeactivatedAt, + benefitValidUntil: data.benefitValidUntil, + lastAssessmentMonth: data.lastAssessmentMonth, + monthlyTreesAdded: data.monthlyTreesAdded, currentMonthIndex: data.currentMonthIndex, }, }) @@ -384,6 +390,9 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi benefitActive: record.benefitActive, benefitActivatedAt: record.benefitActivatedAt, benefitDeactivatedAt: record.benefitDeactivatedAt, + benefitValidUntil: record.benefitValidUntil, + lastAssessmentMonth: record.lastAssessmentMonth, + monthlyTreesAdded: record.monthlyTreesAdded ?? 0, currentMonthIndex: record.currentMonthIndex, createdAt: record.createdAt, updatedAt: record.updatedAt,