From 049a13c97ef6e986c145aa395ef5d4ca63f8cba5 Mon Sep 17 00:00:00 2001 From: hailin Date: Sat, 13 Dec 2025 22:04:59 -0800 Subject: [PATCH] =?UTF-8?q?feat(authorization-service):=20=E5=B8=82?= =?UTF-8?q?=E5=9B=A2=E9=98=9F=E6=8E=88=E6=9D=83=E7=BA=A7=E8=81=94=E6=BF=80?= =?UTF-8?q?=E6=B4=BB/=E5=81=9C=E7=94=A8=E5=8F=8A=E6=9C=88=E5=BA=A6?= =?UTF-8?q?=E8=80=83=E6=A0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 cascadeActivateParentAuthCities: 当市团队授权激活时级联激活上级市团队 - 添加 cascadeDeactivateAuthCityBenefits: 月度考核失败时级联停用上级市团队 - 添加 processExpiredAuthCityBenefits: 月度考核处理(100棵树/月) - 添加 findExpiredActiveByRoleType 仓储方法 - 修改 tryActivateBenefit 支持 AUTH_CITY_COMPANY 级联激活 - 定时任务: 每天凌晨4点检查市团队权益过期 - 定时任务: 每月1号存档并重置市团队月度新增树数 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../monthly-assessment.scheduler.ts | 62 +++++ .../authorization-application.service.ts | 223 ++++++++++++++++++ .../authorization-role.repository.ts | 9 + .../authorization-role.repository.impl.ts | 20 ++ 4 files changed, 314 insertions(+) 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 f4f87aa9..f34caf5a 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 @@ -243,8 +243,70 @@ export class MonthlyAssessmentScheduler { } this.logger.log(`[archiveAndResetMonthlyTreeCounts] 存档完成: 已处理 ${archivedCount} 个社区`) + + // 同时处理市团队授权 + await this.archiveAndResetAuthCityMonthlyTreeCounts() } catch (error) { this.logger.error('[archiveAndResetMonthlyTreeCounts] 月度树数存档失败', error) } } + + /** + * 存档并重置所有市团队授权的月度新增树数 + * + * 业务规则: + * - 将当月业绩存档到 lastMonthTreesAdded(用于月度考核) + * - 重置 monthlyTreesAdded 为0(开始新月累计) + */ + private async archiveAndResetAuthCityMonthlyTreeCounts(): Promise { + 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) + } + } + + /** + * 每天凌晨4点检查并处理过期的市团队授权权益 + * + * 业务规则: + * - 检查所有 benefitValidUntil < 当前时间 且 benefitActive=true 的市团队授权 + * - 如果当月新增树数 >= 100,续期 + * - 如果不达标,级联停用该市团队授权及其所有上级市团队授权 + */ + @Cron('0 4 * * *') + async processExpiredAuthCityBenefits(): Promise { + 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) + } + } } 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 82c326fd..b43dcb09 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 @@ -881,6 +881,11 @@ export class AuthorizationApplicationService { if (authorization.roleType === RoleType.COMMUNITY) { await this.cascadeActivateParentCommunities(authorization.userId.accountSequence) } + + // 如果是市团队授权权益,需要级联激活上级市团队授权 + if (authorization.roleType === RoleType.AUTH_CITY_COMPANY) { + await this.cascadeActivateParentAuthCities(authorization.userId.accountSequence) + } } /** @@ -1015,6 +1020,224 @@ export class AuthorizationApplicationService { return { deactivatedCount: communitiesToDeactivate.length } } + /** + * 级联激活上级市团队授权权益 + * 当一个市团队授权的权益被激活时,需要同时激活推荐链上所有父级市团队授权的权益 + * + * 业务规则: + * - 从当前市团队授权往上找,找到所有已授权但权益未激活的市团队授权 + * - 将它们的权益都激活 + * - 系统账户不需要考核,不在此处理 + */ + private async cascadeActivateParentAuthCities(accountSequence: string): Promise { + this.logger.log( + `[cascadeActivateParentAuthCities] Starting cascade activation for auth cities above ${accountSequence}`, + ) + + // 1. 获取推荐链(不包括当前用户) + const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(accountSequence) + if (ancestorAccountSequences.length === 0) { + return + } + + // 2. 查找推荐链上所有市团队授权(包括 benefitActive=false) + const ancestorAuthCities = await this.authorizationRepository.findAuthCityByAccountSequences( + ancestorAccountSequences, + ) + + // 3. 筛选出已授权但权益未激活的市团队授权 + const inactiveAuthCities = ancestorAuthCities.filter( + (auth) => auth.status === AuthorizationStatus.AUTHORIZED && !auth.benefitActive, + ) + + if (inactiveAuthCities.length === 0) { + this.logger.debug('[cascadeActivateParentAuthCities] No inactive parent auth cities to activate') + return + } + + // 4. 激活这些市团队授权的权益 + for (const authCity of inactiveAuthCities) { + this.logger.log( + `[cascadeActivateParentAuthCities] Cascade activating auth city benefit: ` + + `authorizationId=${authCity.authorizationId.value}, accountSequence=${authCity.userId.accountSequence}`, + ) + + authCity.activateBenefit() + await this.authorizationRepository.save(authCity) + await this.eventPublisher.publishAll(authCity.domainEvents) + authCity.clearDomainEvents() + } + + this.logger.log( + `[cascadeActivateParentAuthCities] Cascade activated ${inactiveAuthCities.length} parent auth cities`, + ) + } + + /** + * 级联停用市团队授权权益 + * 当一个市团队授权的月度考核失败时,需要停用该市团队授权及其推荐链上所有父级市团队授权的权益 + * + * 业务规则: + * - 从当前市团队授权开始,往上找到所有已授权且权益已激活的市团队授权 + * - 将它们的权益都停用,重新开始100棵树的初始考核 + */ + async cascadeDeactivateAuthCityBenefits( + accountSequence: string, + reason: string, + ): Promise<{ deactivatedCount: number }> { + this.logger.log( + `[cascadeDeactivateAuthCityBenefits] Starting cascade deactivation from ${accountSequence}, reason=${reason}`, + ) + + // 1. 获取当前市团队授权 + const currentAuthCity = await this.authorizationRepository.findByAccountSequenceAndRoleType( + accountSequence, + RoleType.AUTH_CITY_COMPANY, + ) + + if (!currentAuthCity) { + this.logger.warn(`[cascadeDeactivateAuthCityBenefits] Auth city not found for ${accountSequence}`) + return { deactivatedCount: 0 } + } + + // 2. 收集需要停用的市团队授权列表 + const authCitiesToDeactivate: AuthorizationRole[] = [] + + // 如果当前市团队授权权益已激活,加入停用列表 + if (currentAuthCity.benefitActive) { + authCitiesToDeactivate.push(currentAuthCity) + } + + // 3. 获取推荐链上的所有父级市团队授权 + const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(accountSequence) + if (ancestorAccountSequences.length > 0) { + const ancestorAuthCities = await this.authorizationRepository.findAuthCityByAccountSequences( + ancestorAccountSequences, + ) + + // 筛选出已授权且权益已激活的市团队授权 + const activeAuthCities = ancestorAuthCities.filter( + (auth) => auth.status === AuthorizationStatus.AUTHORIZED && auth.benefitActive, + ) + + authCitiesToDeactivate.push(...activeAuthCities) + } + + if (authCitiesToDeactivate.length === 0) { + this.logger.debug('[cascadeDeactivateAuthCityBenefits] No active auth cities to deactivate') + return { deactivatedCount: 0 } + } + + // 4. 停用这些市团队授权的权益 + for (const authCity of authCitiesToDeactivate) { + this.logger.log( + `[cascadeDeactivateAuthCityBenefits] Deactivating auth city benefit: ` + + `authorizationId=${authCity.authorizationId.value}, accountSequence=${authCity.userId.accountSequence}`, + ) + + authCity.deactivateBenefit(reason) + await this.authorizationRepository.save(authCity) + await this.eventPublisher.publishAll(authCity.domainEvents) + authCity.clearDomainEvents() + } + + this.logger.log( + `[cascadeDeactivateAuthCityBenefits] Cascade deactivated ${authCitiesToDeactivate.length} auth cities`, + ) + + return { deactivatedCount: authCitiesToDeactivate.length } + } + + /** + * 处理过期的市团队授权权益 + * 定时任务调用此方法来检查并处理过期的市团队授权权益 + * + * 业务规则: + * - 查找所有 benefitValidUntil < 当前时间 且 benefitActive=true 的市团队授权 + * - 检查其月度考核(当月新增100棵树) + * - 如果达标,续期;如果不达标,级联停用 + * + * @param limit 每次处理的最大数量 + */ + async processExpiredAuthCityBenefits(limit = 100): Promise<{ + processedCount: number + renewedCount: number + deactivatedCount: number + }> { + const now = new Date() + this.logger.log(`[processExpiredAuthCityBenefits] Starting at ${now.toISOString()}, limit=${limit}`) + + // 查找过期但仍激活的市团队授权 + const expiredAuthCities = await this.findExpiredActiveAuthCities(now, limit) + + if (expiredAuthCities.length === 0) { + this.logger.debug('[processExpiredAuthCityBenefits] No expired auth cities found') + return { processedCount: 0, renewedCount: 0, deactivatedCount: 0 } + } + + let renewedCount = 0 + let deactivatedCount = 0 + + for (const authCity of expiredAuthCities) { + const accountSequence = authCity.userId.accountSequence + + // 使用 getTreesForAssessment 获取正确的考核数据 + const treesForAssessment = authCity.getTreesForAssessment(now) + + this.logger.debug( + `[processExpiredAuthCityBenefits] Checking auth city ${accountSequence}: ` + + `treesForAssessment=${treesForAssessment}, ` + + `monthlyTreesAdded=${authCity.monthlyTreesAdded}, ` + + `lastMonthTreesAdded=${authCity.lastMonthTreesAdded}, ` + + `benefitValidUntil=${authCity.benefitValidUntil?.toISOString()}, target=100`, + ) + + if (treesForAssessment >= 100) { + // 达标,续期 + authCity.renewBenefit(treesForAssessment) + await this.authorizationRepository.save(authCity) + renewedCount++ + + this.logger.log( + `[processExpiredAuthCityBenefits] Auth city ${accountSequence} renewed, ` + + `trees=${treesForAssessment}, new validUntil=${authCity.benefitValidUntil?.toISOString()}`, + ) + } else { + // 不达标,级联停用 + const result = await this.cascadeDeactivateAuthCityBenefits( + accountSequence, + `月度考核不达标:考核期内新增${treesForAssessment}棵,未达到100棵目标`, + ) + deactivatedCount += result.deactivatedCount + } + } + + this.logger.log( + `[processExpiredAuthCityBenefits] Completed: processed=${expiredAuthCities.length}, ` + + `renewed=${renewedCount}, deactivated=${deactivatedCount}`, + ) + + return { + processedCount: expiredAuthCities.length, + renewedCount, + deactivatedCount, + } + } + + /** + * 查找过期但仍激活的市团队授权 + */ + private async findExpiredActiveAuthCities( + checkDate: Date, + limit: number, + ): Promise { + return this.authorizationRepository.findExpiredActiveByRoleType( + RoleType.AUTH_CITY_COMPANY, + checkDate, + limit, + ) + } + /** * 处理过期的社区权益 * 定时任务调用此方法来检查并处理过期的社区权益 diff --git a/backend/services/authorization-service/src/domain/repositories/authorization-role.repository.ts b/backend/services/authorization-service/src/domain/repositories/authorization-role.repository.ts index 4c3d3c78..0aee6576 100644 --- a/backend/services/authorization-service/src/domain/repositories/authorization-role.repository.ts +++ b/backend/services/authorization-service/src/domain/repositories/authorization-role.repository.ts @@ -83,4 +83,13 @@ export interface IAuthorizationRoleRepository { * 查找指定城市的正式市公司授权(用于市区域权益分配) */ findCityCompanyByRegion(cityCode: string): Promise + /** + * 查找指定角色类型中权益已激活但已过期的授权 + * 用于月度考核定时任务 + */ + findExpiredActiveByRoleType( + roleType: RoleType, + checkDate: Date, + limit: number, + ): Promise } 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 67bfc76e..53478414 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 @@ -369,6 +369,26 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi return record ? this.toDomain(record) : null } + async findExpiredActiveByRoleType( + roleType: RoleType, + checkDate: Date, + limit: number, + ): Promise { + const records = await this.prisma.authorizationRole.findMany({ + where: { + roleType: roleType, + status: AuthorizationStatus.AUTHORIZED, + benefitActive: true, + benefitValidUntil: { + lt: checkDate, + }, + }, + take: limit, + orderBy: { benefitValidUntil: 'asc' }, + }) + return records.map((record) => this.toDomain(record)) + } + private toDomain(record: any): AuthorizationRole { const props: AuthorizationRoleProps = { authorizationId: AuthorizationId.create(record.id),