import { Injectable, Inject, Logger } from '@nestjs/common' import { AuthorizationRole, MonthlyAssessment } from '@/domain/aggregates' import { LadderTargetRule } from '@/domain/entities' import { UserId, AdminUserId, RegionCode, AuthorizationId, Month, } from '@/domain/value-objects' import { RoleType, AuthorizationStatus } from '@/domain/enums' import { IAuthorizationRoleRepository, AUTHORIZATION_ROLE_REPOSITORY, IMonthlyAssessmentRepository, MONTHLY_ASSESSMENT_REPOSITORY, } from '@/domain/repositories' import { AuthorizationValidatorService, IReferralRepository, ITeamStatisticsRepository, TeamStatistics, } from '@/domain/services' import { EventPublisherService } from '@/infrastructure/kafka' import { ReferralServiceClient, IdentityServiceClient } from '@/infrastructure/external' import { ApplicationError, NotFoundError } from '@/shared/exceptions' import { ApplyCommunityAuthCommand, ApplyCommunityAuthResult, ApplyAuthProvinceCompanyCommand, ApplyAuthProvinceCompanyResult, ApplyAuthCityCompanyCommand, ApplyAuthCityCompanyResult, GrantCommunityCommand, GrantProvinceCompanyCommand, GrantCityCompanyCommand, GrantAuthProvinceCompanyCommand, GrantAuthCityCompanyCommand, RevokeAuthorizationCommand, GrantMonthlyBypassCommand, ExemptLocalPercentageCheckCommand, SelfApplyAuthorizationCommand, } from '@/application/commands' import { SelfApplyAuthorizationResponseDto, UserAuthorizationStatusResponseDto, SelfApplyAuthorizationType, TeamChainOccupiedRegionDto, } from '@/api/dto/request/self-apply-authorization.dto' import { AuthorizationDTO, StickmanRankingDTO, CommunityHierarchyDTO } from '@/application/dto' export const REFERRAL_REPOSITORY = Symbol('IReferralRepository') export const TEAM_STATISTICS_REPOSITORY = Symbol('ITeamStatisticsRepository') @Injectable() export class AuthorizationApplicationService { private readonly logger = new Logger(AuthorizationApplicationService.name) private readonly validatorService = new AuthorizationValidatorService() constructor( @Inject(AUTHORIZATION_ROLE_REPOSITORY) private readonly authorizationRepository: IAuthorizationRoleRepository, @Inject(MONTHLY_ASSESSMENT_REPOSITORY) private readonly assessmentRepository: IMonthlyAssessmentRepository, @Inject(REFERRAL_REPOSITORY) private readonly referralRepository: IReferralRepository, @Inject(TEAM_STATISTICS_REPOSITORY) private readonly statsRepository: ITeamStatisticsRepository, private readonly eventPublisher: EventPublisherService, private readonly referralServiceClient: ReferralServiceClient, private readonly identityServiceClient: IdentityServiceClient, ) {} /** * 申请社区授权 */ async applyCommunityAuth( command: ApplyCommunityAuthCommand, ): Promise { const userId = UserId.create(command.userId, command.accountSequence) // 1. 检查是否已有社区授权 const existing = await this.authorizationRepository.findByAccountSequenceAndRoleType( userId.accountSequence, RoleType.COMMUNITY, ) if (existing && existing.status !== AuthorizationStatus.REVOKED) { throw new ApplicationError('您已申请过社区授权') } // 2. 创建社区授权 const authorization = AuthorizationRole.createCommunityAuth({ userId, communityName: command.communityName, }) // 3. 检查初始考核(10棵)- 使用下级团队认种数(不含自己) const teamStats = await this.statsRepository.findByAccountSequence(userId.accountSequence) const subordinateTreeCount = teamStats?.subordinateTeamPlantingCount || 0 if (subordinateTreeCount >= authorization.getInitialTarget()) { // 达标,激活权益 authorization.activateBenefit() } await this.authorizationRepository.save(authorization) await this.eventPublisher.publishAll(authorization.domainEvents) authorization.clearDomainEvents() return { authorizationId: authorization.authorizationId.value, status: authorization.status, benefitActive: authorization.benefitActive, message: authorization.benefitActive ? '社区权益已激活' : `需要下级团队累计认种达到${authorization.getInitialTarget()}棵才能激活`, currentTreeCount: subordinateTreeCount, requiredTreeCount: authorization.getInitialTarget(), } } /** * 申请授权省公司 */ async applyAuthProvinceCompany( command: ApplyAuthProvinceCompanyCommand, ): Promise { const userId = UserId.create(command.userId, command.accountSequence) const regionCode = RegionCode.create(command.provinceCode) // 1. 验证授权申请(团队内唯一性) const validation = await this.validatorService.validateAuthorizationRequest( userId, RoleType.AUTH_PROVINCE_COMPANY, regionCode, this.referralRepository, this.authorizationRepository, ) if (!validation.isValid) { throw new ApplicationError(validation.errorMessage!) } // 2. 创建授权 const authorization = AuthorizationRole.createAuthProvinceCompany({ userId, provinceCode: command.provinceCode, provinceName: command.provinceName, }) // 3. 检查初始考核(500棵)- 使用下级团队认种数(不含自己) const teamStats = await this.statsRepository.findByAccountSequence(userId.accountSequence) const subordinateTreeCount = teamStats?.subordinateTeamPlantingCount || 0 if (subordinateTreeCount >= authorization.getInitialTarget()) { // 达标,激活权益并创建首月考核 authorization.activateBenefit() await this.createInitialAssessment(authorization, teamStats!) } await this.authorizationRepository.save(authorization) await this.eventPublisher.publishAll(authorization.domainEvents) authorization.clearDomainEvents() return { authorizationId: authorization.authorizationId.value, status: authorization.status, benefitActive: authorization.benefitActive, displayTitle: authorization.displayTitle, message: authorization.benefitActive ? '授权省公司权益已激活,开始阶梯考核' : `需要下级团队累计认种达到${authorization.getInitialTarget()}棵才能激活`, currentTreeCount: subordinateTreeCount, requiredTreeCount: authorization.getInitialTarget(), } } /** * 申请授权市公司 */ async applyAuthCityCompany( command: ApplyAuthCityCompanyCommand, ): Promise { const userId = UserId.create(command.userId, command.accountSequence) const regionCode = RegionCode.create(command.cityCode) // 1. 验证 const validation = await this.validatorService.validateAuthorizationRequest( userId, RoleType.AUTH_CITY_COMPANY, regionCode, this.referralRepository, this.authorizationRepository, ) if (!validation.isValid) { throw new ApplicationError(validation.errorMessage!) } // 2. 创建授权 const authorization = AuthorizationRole.createAuthCityCompany({ userId, cityCode: command.cityCode, cityName: command.cityName, }) // 3. 检查初始考核(100棵)- 使用下级团队认种数(不含自己) const teamStats = await this.statsRepository.findByAccountSequence(userId.accountSequence) const subordinateTreeCount = teamStats?.subordinateTeamPlantingCount || 0 if (subordinateTreeCount >= authorization.getInitialTarget()) { authorization.activateBenefit() await this.createInitialAssessment(authorization, teamStats!) } await this.authorizationRepository.save(authorization) await this.eventPublisher.publishAll(authorization.domainEvents) authorization.clearDomainEvents() return { authorizationId: authorization.authorizationId.value, status: authorization.status, benefitActive: authorization.benefitActive, displayTitle: authorization.displayTitle, message: authorization.benefitActive ? '授权市公司权益已激活,开始阶梯考核' : `需要下级团队累计认种达到${authorization.getInitialTarget()}棵才能激活`, currentTreeCount: subordinateTreeCount, requiredTreeCount: authorization.getInitialTarget(), } } /** * 管理员直接授权社区 * * 业务规则: * - 一个用户只能拥有一个社区角色 * - 社区名称全局唯一,不允许重复 */ async grantCommunity(command: GrantCommunityCommand): Promise { const userId = UserId.create(command.userId, command.accountSequence) const adminId = AdminUserId.create(command.adminId, command.adminAccountSequence) // 0. 检查用户是否已认种(授权前置条件) await this.ensureUserHasPlanted(command.accountSequence) // 1. 检查用户是否已有社区角色 const existingUserCommunity = await this.authorizationRepository.findByAccountSequenceAndRoleType( command.accountSequence, RoleType.COMMUNITY, ) if (existingUserCommunity) { throw new ApplicationError( `用户 ${command.accountSequence} 已拥有社区角色「${existingUserCommunity.displayTitle}」,不能重复授权`, ) } // 2. 检查社区名称是否已被使用 const existingCommunityName = await this.authorizationRepository.findCommunityByName(command.communityName) if (existingCommunityName) { throw new ApplicationError( `社区名称「${command.communityName}」已被使用,请选择其他名称`, ) } // 3. 创建社区授权 const authorization = AuthorizationRole.createCommunity({ userId, communityName: command.communityName, adminId, skipAssessment: command.skipAssessment, }) await this.authorizationRepository.save(authorization) await this.eventPublisher.publishAll(authorization.domainEvents) authorization.clearDomainEvents() } /** * 管理员授权正式省公司(省区域) * * 业务规则: * - 同一个用户不能同时拥有省区域和市区域两种身份 * - 同一个省份只允许授权给一个账户(按省份唯一) */ async grantProvinceCompany(command: GrantProvinceCompanyCommand): Promise { // 0. 检查用户是否已认种(授权前置条件) await this.ensureUserHasPlanted(command.accountSequence) const userId = UserId.create(command.userId, command.accountSequence) const adminId = AdminUserId.create(command.adminId, command.adminAccountSequence) // 1. 检查用户是否已有市区域授权(省区域和市区域互斥) const existingCityCompany = await this.authorizationRepository.findByAccountSequenceAndRoleType( command.accountSequence, RoleType.CITY_COMPANY, ) if (existingCityCompany && existingCityCompany.status !== AuthorizationStatus.REVOKED) { throw new ApplicationError( `用户 ${command.accountSequence} 已拥有市区域角色「${existingCityCompany.regionName}」,不能同时拥有省区域角色`, ) } // 2. 检查用户是否已有省区域授权(一个用户只能有一个省区域) const existingProvinceCompany = await this.authorizationRepository.findByAccountSequenceAndRoleType( command.accountSequence, RoleType.PROVINCE_COMPANY, ) if (existingProvinceCompany && existingProvinceCompany.status !== AuthorizationStatus.REVOKED) { throw new ApplicationError( `用户 ${command.accountSequence} 已拥有省区域角色「${existingProvinceCompany.regionName}」,不能重复授权`, ) } // 3. 检查该省份是否已有省区域授权(同一省份只能授权给一个账户) const existingProvinceRegion = await this.authorizationRepository.findProvinceCompanyByRegion(command.provinceCode) if (existingProvinceRegion) { throw new ApplicationError( `省份「${command.provinceName}」已有省区域角色授权给用户 ${existingProvinceRegion.userId.accountSequence},不能重复授权`, ) } // 4. 创建授权 const authorization = AuthorizationRole.createProvinceCompany({ userId, provinceCode: command.provinceCode, provinceName: command.provinceName, adminId, skipAssessment: command.skipAssessment, }) await this.authorizationRepository.save(authorization) await this.eventPublisher.publishAll(authorization.domainEvents) authorization.clearDomainEvents() } /** * 管理员授权正式市公司(市区域) * * 业务规则: * - 同一个用户不能同时拥有市区域和省区域两种身份 * - 同一个城市只允许一个市区域角色被授权 */ async grantCityCompany(command: GrantCityCompanyCommand): Promise { // 0. 检查用户是否已认种(授权前置条件) await this.ensureUserHasPlanted(command.accountSequence) const userId = UserId.create(command.userId, command.accountSequence) const adminId = AdminUserId.create(command.adminId, command.adminAccountSequence) // 1. 检查用户是否已有省区域授权(市区域和省区域互斥) const existingProvinceCompany = await this.authorizationRepository.findByAccountSequenceAndRoleType( command.accountSequence, RoleType.PROVINCE_COMPANY, ) if (existingProvinceCompany && existingProvinceCompany.status !== AuthorizationStatus.REVOKED) { throw new ApplicationError( `用户 ${command.accountSequence} 已拥有省区域角色「${existingProvinceCompany.regionName}」,不能同时拥有市区域角色`, ) } // 2. 检查用户是否已有市区域授权(一个用户只能有一个市区域) const existingCityCompany = await this.authorizationRepository.findByAccountSequenceAndRoleType( command.accountSequence, RoleType.CITY_COMPANY, ) if (existingCityCompany && existingCityCompany.status !== AuthorizationStatus.REVOKED) { throw new ApplicationError( `用户 ${command.accountSequence} 已拥有市区域角色「${existingCityCompany.regionName}」,不能重复授权`, ) } // 3. 检查该城市是否已有市区域授权(同一城市全局唯一) const existingCityRegion = await this.authorizationRepository.findCityCompanyByRegion(command.cityCode) if (existingCityRegion && existingCityRegion.status !== AuthorizationStatus.REVOKED) { throw new ApplicationError( `城市「${command.cityName}」已有市区域角色授权给用户 ${existingCityRegion.userId.accountSequence},不能重复授权`, ) } // 4. 创建授权 const authorization = AuthorizationRole.createCityCompany({ userId, cityCode: command.cityCode, cityName: command.cityName, adminId, skipAssessment: command.skipAssessment, }) await this.authorizationRepository.save(authorization) await this.eventPublisher.publishAll(authorization.domainEvents) authorization.clearDomainEvents() } /** * 管理员授权授权省公司(省团队) * Admin直接授权,跳过用户申请流程 * 需要验证团队内唯一性:同一推荐链上不能有重复的相同省份授权 */ async grantAuthProvinceCompany(command: GrantAuthProvinceCompanyCommand): Promise { // 0. 检查用户是否已认种(授权前置条件) await this.ensureUserHasPlanted(command.accountSequence) const userId = UserId.create(command.userId, command.accountSequence) const adminId = AdminUserId.create(command.adminId, command.adminAccountSequence) const regionCode = RegionCode.create(command.provinceCode) // 1. 验证团队内唯一性(同一推荐链上不能有重复的相同省份授权) const validation = await this.validatorService.validateAuthorizationRequest( userId, RoleType.AUTH_PROVINCE_COMPANY, regionCode, this.referralRepository, this.authorizationRepository, ) if (!validation.isValid) { throw new ApplicationError(validation.errorMessage!) } // 2. 创建授权 const authorization = AuthorizationRole.createAuthProvinceCompanyByAdmin({ userId, provinceCode: command.provinceCode, provinceName: command.provinceName, adminId, skipAssessment: command.skipAssessment, }) await this.authorizationRepository.save(authorization) await this.eventPublisher.publishAll(authorization.domainEvents) authorization.clearDomainEvents() } /** * 管理员授权授权市公司(市团队) * Admin直接授权,跳过用户申请流程 * 需要验证团队内唯一性:同一推荐链上不能有重复的相同城市授权 */ async grantAuthCityCompany(command: GrantAuthCityCompanyCommand): Promise { // 0. 检查用户是否已认种(授权前置条件) await this.ensureUserHasPlanted(command.accountSequence) const userId = UserId.create(command.userId, command.accountSequence) const adminId = AdminUserId.create(command.adminId, command.adminAccountSequence) const regionCode = RegionCode.create(command.cityCode) // 1. 验证团队内唯一性(同一推荐链上不能有重复的相同城市授权) const validation = await this.validatorService.validateAuthorizationRequest( userId, RoleType.AUTH_CITY_COMPANY, regionCode, this.referralRepository, this.authorizationRepository, ) if (!validation.isValid) { throw new ApplicationError(validation.errorMessage!) } // 2. 创建授权 const authorization = AuthorizationRole.createAuthCityCompanyByAdmin({ userId, cityCode: command.cityCode, cityName: command.cityName, adminId, skipAssessment: command.skipAssessment, }) await this.authorizationRepository.save(authorization) await this.eventPublisher.publishAll(authorization.domainEvents) authorization.clearDomainEvents() } /** * 撤销授权 */ async revokeAuthorization(command: RevokeAuthorizationCommand): Promise { const authorization = await this.authorizationRepository.findById( AuthorizationId.create(command.authorizationId), ) if (!authorization) { throw new NotFoundError('授权不存在') } // Note: We need the adminId from somewhere, for now using a placeholder // In a real scenario, we would need to fetch the admin's userId from the accountSequence const adminId = AdminUserId.create('admin', command.adminAccountSequence) authorization.revoke(adminId, command.reason) await this.authorizationRepository.save(authorization) await this.eventPublisher.publishAll(authorization.domainEvents) authorization.clearDomainEvents() } /** * 授予单月豁免 */ async grantMonthlyBypass(command: GrantMonthlyBypassCommand): Promise { const assessment = await this.assessmentRepository.findByAuthorizationAndMonth( AuthorizationId.create(command.authorizationId), Month.create(command.month), ) if (!assessment) { throw new NotFoundError('考核记录不存在') } // Note: We need the adminId from somewhere, for now using a placeholder const adminId = AdminUserId.create('admin', command.adminAccountSequence) assessment.grantBypass(adminId) await this.assessmentRepository.save(assessment) await this.eventPublisher.publishAll(assessment.domainEvents) assessment.clearDomainEvents() } /** * 豁免占比考核 */ async exemptLocalPercentageCheck(command: ExemptLocalPercentageCheckCommand): Promise { const authorization = await this.authorizationRepository.findById( AuthorizationId.create(command.authorizationId), ) if (!authorization) { throw new NotFoundError('授权不存在') } // Note: We need the adminId from somewhere, for now using a placeholder const adminId = AdminUserId.create('admin', command.adminAccountSequence) authorization.exemptLocalPercentageCheck(adminId) await this.authorizationRepository.save(authorization) await this.eventPublisher.publishAll(authorization.domainEvents) authorization.clearDomainEvents() } /** * 查询用户授权列表 */ async getUserAuthorizations(accountSequence: string): Promise { const authorizations = await this.authorizationRepository.findByAccountSequence( accountSequence, ) // 查询用户团队统计数据 const teamStats = await this.statsRepository.findByAccountSequence(accountSequence) const currentTreeCount = teamStats?.totalTeamPlantingCount || 0 return authorizations.map((auth) => this.toAuthorizationDTO(auth, currentTreeCount)) } /** * 查询用户授权详情 */ async getAuthorizationById(authorizationId: string): Promise { const authorization = await this.authorizationRepository.findById( AuthorizationId.create(authorizationId), ) if (!authorization) return null // 查询用户团队统计数据 const teamStats = await this.statsRepository.findByAccountSequence(authorization.userId.accountSequence) const currentTreeCount = teamStats?.totalTeamPlantingCount || 0 return this.toAuthorizationDTO(authorization, currentTreeCount) } /** * 查询火柴人排名数据 * * 业务规则:查询全系统该角色类型的所有排名数据(不按区域过滤) * 只要有任何一个用户达标,所有申请了该角色类型授权的用户都能看到排名 */ async getStickmanRanking( month: string, roleType: RoleType, regionCode: string, currentUserId?: string, ): Promise { this.logger.log( `[getStickmanRanking] 查询火柴人排名: month=${month}, roleType=${roleType}, regionCode=${regionCode}, currentUserId=${currentUserId}`, ) // 查询全系统该角色类型的所有评估记录(不按区域过滤) const assessments = await this.assessmentRepository.findRankingsByMonthAndRoleType( Month.create(month), roleType, ) this.logger.log( `[getStickmanRanking] 查询到 ${assessments.length} 条评估记录`, ) if (assessments.length === 0) { this.logger.warn( `[getStickmanRanking] 没有找到任何评估记录,month=${month}, roleType=${roleType}`, ) return [] } // 批量获取用户信息 const userIds = assessments.map(a => a.userId.value) const userInfoMap = await this.identityServiceClient.batchGetUserInfo(userIds) const rankings: StickmanRankingDTO[] = [] const finalTarget = LadderTargetRule.getFinalTarget(roleType) for (const assessment of assessments) { const userInfo = userInfoMap.get(assessment.userId.value) this.logger.debug( `[getStickmanRanking] 处理评估记录: userId=${assessment.userId.value}, ` + `regionCode=${assessment.regionCode.value}, cumulativeCompleted=${assessment.cumulativeCompleted}`, ) rankings.push({ id: assessment.authorizationId.value, userId: assessment.userId.value, authorizationId: assessment.authorizationId.value, roleType: assessment.roleType, regionCode: assessment.regionCode.value, nickname: userInfo?.nickname || `用户${assessment.userId.accountSequence.slice(-4)}`, avatarUrl: userInfo?.avatarUrl, completedCount: assessment.cumulativeCompleted, monthlyEarnings: 0, // TODO: 从奖励服务获取本月可结算收益 isCurrentUser: currentUserId ? assessment.userId.value === currentUserId : false, ranking: assessment.rankingInRegion || 0, isFirstPlace: assessment.isFirstPlace, cumulativeCompleted: assessment.cumulativeCompleted, cumulativeTarget: assessment.cumulativeTarget, finalTarget, progressPercentage: (assessment.cumulativeCompleted / finalTarget) * 100, exceedRatio: assessment.exceedRatio, monthlyRewardUsdt: 0, // TODO: 从奖励服务获取 monthlyRewardRwad: 0, }) } // 按完成数量降序排序 rankings.sort((a, b) => b.completedCount - a.completedCount) this.logger.log( `[getStickmanRanking] 返回 ${rankings.length} 条排名数据`, ) return rankings } // 辅助方法 private async createInitialAssessment( authorization: AuthorizationRole, teamStats: TeamStatistics, ): Promise { const currentMonth = Month.current() const target = LadderTargetRule.getTarget(authorization.roleType, 1) const assessment = MonthlyAssessment.create({ authorizationId: authorization.authorizationId, userId: authorization.userId, roleType: authorization.roleType, regionCode: authorization.regionCode, assessmentMonth: currentMonth, monthIndex: 1, monthlyTarget: target.monthlyTarget, cumulativeTarget: target.cumulativeTarget, }) // 立即评估首月 const localTeamCount = this.getLocalTeamCount( teamStats, authorization.roleType, authorization.regionCode, ) assessment.assess({ cumulativeCompleted: teamStats.totalTeamPlantingCount, localTeamCount, totalTeamCount: teamStats.totalTeamPlantingCount, requireLocalPercentage: authorization.requireLocalPercentage, exemptFromPercentageCheck: authorization.exemptFromPercentageCheck, }) await this.assessmentRepository.save(assessment) await this.eventPublisher.publishAll(assessment.domainEvents) assessment.clearDomainEvents() } private getLocalTeamCount( teamStats: TeamStatistics, roleType: RoleType, regionCode: RegionCode, ): number { if (roleType === RoleType.AUTH_PROVINCE_COMPANY) { return teamStats.getProvinceTeamCount(regionCode.value) } else if (roleType === RoleType.AUTH_CITY_COMPANY) { return teamStats.getCityTeamCount(regionCode.value) } return 0 } private toAuthorizationDTO(auth: AuthorizationRole, currentTreeCount: number): AuthorizationDTO { // 获取月度考核目标(社区固定10,其他类型根据阶梯规则) let monthlyTargetTreeCount = 0 if (auth.roleType === RoleType.COMMUNITY) { monthlyTargetTreeCount = 10 // 社区固定每月新增10棵 } else if (auth.benefitActive && auth.currentMonthIndex > 0) { // 省/市公司使用阶梯目标 const target = LadderTargetRule.getTarget(auth.roleType, auth.currentMonthIndex) monthlyTargetTreeCount = target.monthlyTarget } return { authorizationId: auth.authorizationId.value, userId: auth.userId.value, roleType: auth.roleType, regionCode: auth.regionCode.value, regionName: auth.regionName, status: auth.status, displayTitle: auth.displayTitle, benefitActive: auth.benefitActive, currentMonthIndex: auth.currentMonthIndex, requireLocalPercentage: auth.requireLocalPercentage, exemptFromPercentageCheck: auth.exemptFromPercentageCheck, // 考核进度字段 initialTargetTreeCount: auth.getInitialTarget(), currentTreeCount, monthlyTargetTreeCount, createdAt: auth.createdAt, updatedAt: auth.updatedAt, } } /** * 获取用户的社区层级信息 * - myCommunity: 我的社区授权(如果有) * - parentCommunity: 上级社区(沿推荐链往上找最近的,如果没有则返回总部社区) * - childCommunities: 下级社区(在我的团队中找最近的社区) */ async getCommunityHierarchy(accountSequence: string): Promise { this.logger.debug(`[getCommunityHierarchy] accountSequence=${accountSequence}`) // 1. 查询我的社区授权 const myCommunity = await this.authorizationRepository.findByAccountSequenceAndRoleType( accountSequence, RoleType.COMMUNITY, ) // 2. 获取我的祖先链(推荐链) const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(accountSequence) this.logger.debug(`[getCommunityHierarchy] ancestorPath: ${ancestorAccountSequences.join(',')}`) // 3. 查找上级社区(在祖先链中找最近的有社区授权的用户) let parentCommunityAuth: AuthorizationRole | null = null if (ancestorAccountSequences.length > 0) { const ancestorCommunities = await this.authorizationRepository.findActiveCommunityByAccountSequences( ancestorAccountSequences, ) // 找最近的(ancestorAccountSequences 是从直接推荐人到根节点的顺序) if (ancestorCommunities.length > 0) { // 按祖先链顺序找第一个匹配的 for (const ancestorSeq of ancestorAccountSequences) { const found = ancestorCommunities.find( (auth) => auth.userId.accountSequence === ancestorSeq, ) if (found) { parentCommunityAuth = found break } } } } // 4. 获取我的团队成员 const teamMemberAccountSequences = await this.referralServiceClient.getTeamMembers(accountSequence) this.logger.debug(`[getCommunityHierarchy] teamMembers count: ${teamMemberAccountSequences.length}`) // 5. 查找下级社区(在团队成员中找最近的有社区授权的用户) // "最近" 的定义:直接下级优先,然后是下级的下级,以此类推 // 由于 getTeamMembers 返回的是广度优先遍历结果,可以直接使用顺序 let childCommunityAuths: AuthorizationRole[] = [] if (teamMemberAccountSequences.length > 0) { const teamCommunities = await this.authorizationRepository.findActiveCommunityByAccountSequences( teamMemberAccountSequences, ) // 只保留"最近的"下级社区 // 如果一个社区的上级不在我的直接团队成员中,或者其上级就是我,则它是"最近的" // 简化实现:返回所有团队中的社区,前端可以根据需要过滤 // 但按用户要求"只计算最近的那个",这里需要做过滤 // 算法:如果某个社区 A 的祖先中有另一个社区 B 也在团队中,则 A 不是最近的 const communityAccountSeqs = new Set(teamCommunities.map((c) => c.userId.accountSequence)) for (const comm of teamCommunities) { // 获取这个社区成员的祖先链 const commAncestors = await this.referralServiceClient.getReferralChain(comm.userId.accountSequence) // 检查这个社区是否有"更近"的祖先社区 let hasCloserAncestorCommunity = false for (const ancestorSeq of commAncestors) { // 如果祖先是我,停止检查 if (ancestorSeq === accountSequence) { break } // 如果祖先也是社区且在我的团队中,则当前社区不是最近的 if (communityAccountSeqs.has(ancestorSeq)) { hasCloserAncestorCommunity = true break } } if (!hasCloserAncestorCommunity) { childCommunityAuths.push(comm) } } } // 6. 构建响应 const HEADQUARTERS_COMMUNITY = { authorizationId: 'headquarters', accountSequence: '0', communityName: '总部社区', userId: undefined, isHeadquarters: true, } return { myCommunity: myCommunity && myCommunity.status === AuthorizationStatus.AUTHORIZED ? { authorizationId: myCommunity.authorizationId.value, accountSequence: myCommunity.userId.accountSequence, communityName: myCommunity.displayTitle, userId: myCommunity.userId.value, isHeadquarters: false, } : null, parentCommunity: parentCommunityAuth ? { authorizationId: parentCommunityAuth.authorizationId.value, accountSequence: parentCommunityAuth.userId.accountSequence, communityName: parentCommunityAuth.displayTitle, userId: parentCommunityAuth.userId.value, isHeadquarters: false, } : HEADQUARTERS_COMMUNITY, childCommunities: childCommunityAuths.map((auth) => ({ authorizationId: auth.authorizationId.value, accountSequence: auth.userId.accountSequence, communityName: auth.displayTitle, userId: auth.userId.value, isHeadquarters: false, })), hasParentCommunity: parentCommunityAuth !== null, childCommunityCount: childCommunityAuths.length, } } /** * 查找用户推荐链中最近的社区授权用户 * 用于 reward-service 分配社区权益 * @returns accountSequence of nearest community authorization holder, or null */ async findNearestAuthorizedCommunity(accountSequence: string): Promise { this.logger.debug(`[findNearestAuthorizedCommunity] accountSequence=${accountSequence}`) // 获取用户的祖先链(推荐链) const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(accountSequence) if (ancestorAccountSequences.length === 0) { return null } // 在祖先链中找最近的有社区授权的用户 const ancestorCommunities = await this.authorizationRepository.findActiveCommunityByAccountSequences( ancestorAccountSequences, ) if (ancestorCommunities.length === 0) { return null } // 按祖先链顺序找第一个匹配的 for (const ancestorSeq of ancestorAccountSequences) { const found = ancestorCommunities.find( (auth) => auth.userId.accountSequence === ancestorSeq, ) if (found) { return found.userId.accountSequence } } return null } /** * 查找用户推荐链中最近的省公司授权用户(匹配指定省份) * 用于 reward-service 分配省团队权益 * @returns accountSequence of nearest province authorization holder, or null */ async findNearestAuthorizedProvince( accountSequence: string, provinceCode: string, ): Promise { this.logger.debug( `[findNearestAuthorizedProvince] accountSequence=${accountSequence}, provinceCode=${provinceCode}`, ) // 获取用户的祖先链(推荐链) const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(accountSequence) if (ancestorAccountSequences.length === 0) { return null } // 在祖先链中找最近的有省公司授权且匹配省份代码的用户 const ancestorProvinces = await this.authorizationRepository.findActiveProvinceByAccountSequencesAndRegion( ancestorAccountSequences, provinceCode, ) if (ancestorProvinces.length === 0) { return null } // 按祖先链顺序找第一个匹配的 for (const ancestorSeq of ancestorAccountSequences) { const found = ancestorProvinces.find( (auth) => auth.userId.accountSequence === ancestorSeq, ) if (found) { return found.userId.accountSequence } } return null } /** * 查找用户推荐链中最近的市公司授权用户(匹配指定城市) * 用于 reward-service 分配市团队权益 * @returns accountSequence of nearest city authorization holder, or null */ async findNearestAuthorizedCity( accountSequence: string, cityCode: string, ): Promise { this.logger.debug( `[findNearestAuthorizedCity] accountSequence=${accountSequence}, cityCode=${cityCode}`, ) // 获取用户的祖先链(推荐链) const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(accountSequence) if (ancestorAccountSequences.length === 0) { return null } // 在祖先链中找最近的有市公司授权且匹配城市代码的用户 const ancestorCities = await this.authorizationRepository.findActiveCityByAccountSequencesAndRegion( ancestorAccountSequences, cityCode, ) if (ancestorCities.length === 0) { return null } // 按祖先链顺序找第一个匹配的 for (const ancestorSeq of ancestorAccountSequences) { const found = ancestorCities.find( (auth) => auth.userId.accountSequence === ancestorSeq, ) if (found) { return found.userId.accountSequence } } return null } /** * 检查用户是否已认种至少一棵树 * 授权前置条件:用户必须先认种才能被授权任何角色 */ private async ensureUserHasPlanted(accountSequence: string): Promise { const teamStats = await this.referralServiceClient.findByAccountSequence(accountSequence) const selfPlantingCount = teamStats?.selfPlantingCount || 0 if (selfPlantingCount < 1) { throw new ApplicationError( `用户 ${accountSequence} 尚未认种任何树,无法授权。请先至少认种1棵树后再进行授权操作。`, ) } this.logger.debug( `[ensureUserHasPlanted] User ${accountSequence} has planted ${selfPlantingCount} tree(s), authorization allowed`, ) } /** * 尝试激活授权权益 * 仅当权益未激活时执行激活操作 * * 对于社区权益:需要级联激活推荐链上所有父级社区 */ private async tryActivateBenefit(authorization: AuthorizationRole): Promise { if (authorization.benefitActive) { return // 已激活,无需操作 } this.logger.log( `[tryActivateBenefit] Activating benefit for authorization ${authorization.authorizationId.value}, ` + `role=${authorization.roleType}, accountSequence=${authorization.userId.accountSequence}`, ) authorization.activateBenefit() await this.authorizationRepository.save(authorization) await this.eventPublisher.publishAll(authorization.domainEvents) authorization.clearDomainEvents() // 如果是社区权益,需要级联激活上级社区 if (authorization.roleType === RoleType.COMMUNITY) { await this.cascadeActivateParentCommunities(authorization.userId.accountSequence) } // 如果是市团队授权权益,需要级联激活上级市团队授权 if (authorization.roleType === RoleType.AUTH_CITY_COMPANY) { await this.cascadeActivateParentAuthCities(authorization.userId.accountSequence) } // 如果是省团队授权权益,需要级联激活上级省团队授权 if (authorization.roleType === RoleType.AUTH_PROVINCE_COMPANY) { await this.cascadeActivateParentAuthProvinces(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 } } /** * 级联激活上级市团队授权权益 * 当一个市团队授权的权益被激活时,需要同时激活推荐链上所有父级市团队授权的权益 * * 业务规则: * - 从当前市团队授权往上找,找到所有已授权但权益未激活的市团队授权 * - 将它们的权益都激活 * - 系统账户不需要考核,不在此处理 */ 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, ) } /** * 级联激活上级省团队授权权益 * 当一个省团队授权的权益被激活时,需要同时激活推荐链上所有父级省团队授权的权益 * * 业务规则: * - 从当前省团队授权往上找,找到所有已授权但权益未激活的省团队授权 * - 将它们的权益都激活 * - 系统账户不需要考核,不在此处理 */ private async cascadeActivateParentAuthProvinces(accountSequence: string): Promise { this.logger.log( `[cascadeActivateParentAuthProvinces] Starting cascade activation for auth provinces above ${accountSequence}`, ) // 1. 获取推荐链(不包括当前用户) const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(accountSequence) if (ancestorAccountSequences.length === 0) { return } // 2. 查找推荐链上所有省团队授权(包括 benefitActive=false) const ancestorAuthProvinces = await this.authorizationRepository.findAuthProvinceByAccountSequences( ancestorAccountSequences, ) // 3. 筛选出已授权但权益未激活的省团队授权 const inactiveAuthProvinces = ancestorAuthProvinces.filter( (auth) => auth.status === AuthorizationStatus.AUTHORIZED && !auth.benefitActive, ) if (inactiveAuthProvinces.length === 0) { this.logger.debug('[cascadeActivateParentAuthProvinces] No inactive parent auth provinces to activate') return } // 4. 激活这些省团队授权的权益 for (const authProvince of inactiveAuthProvinces) { this.logger.log( `[cascadeActivateParentAuthProvinces] Cascade activating auth province benefit: ` + `authorizationId=${authProvince.authorizationId.value}, accountSequence=${authProvince.userId.accountSequence}`, ) authProvince.activateBenefit() await this.authorizationRepository.save(authProvince) await this.eventPublisher.publishAll(authProvince.domainEvents) authProvince.clearDomainEvents() } this.logger.log( `[cascadeActivateParentAuthProvinces] Cascade activated ${inactiveAuthProvinces.length} parent auth provinces`, ) } /** * 级联停用省团队授权权益 * 当一个省团队授权的月度考核失败时,需要停用该省团队授权及其推荐链上所有父级省团队授权的权益 * * 业务规则: * - 从当前省团队授权开始,往上找到所有已授权且权益已激活的省团队授权 * - 将它们的权益都停用,重新开始500棵树的初始考核 */ async cascadeDeactivateAuthProvinceBenefits( accountSequence: string, reason: string, ): Promise<{ deactivatedCount: number }> { this.logger.log( `[cascadeDeactivateAuthProvinceBenefits] Starting cascade deactivation from ${accountSequence}, reason=${reason}`, ) // 1. 获取当前省团队授权 const currentAuthProvince = await this.authorizationRepository.findByAccountSequenceAndRoleType( accountSequence, RoleType.AUTH_PROVINCE_COMPANY, ) if (!currentAuthProvince) { this.logger.warn(`[cascadeDeactivateAuthProvinceBenefits] Auth province not found for ${accountSequence}`) return { deactivatedCount: 0 } } // 2. 收集需要停用的省团队授权列表 const authProvincesToDeactivate: AuthorizationRole[] = [] // 如果当前省团队授权权益已激活,加入停用列表 if (currentAuthProvince.benefitActive) { authProvincesToDeactivate.push(currentAuthProvince) } // 3. 获取推荐链上的所有父级省团队授权 const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(accountSequence) if (ancestorAccountSequences.length > 0) { const ancestorAuthProvinces = await this.authorizationRepository.findAuthProvinceByAccountSequences( ancestorAccountSequences, ) // 筛选出已授权且权益已激活的省团队授权 const activeAuthProvinces = ancestorAuthProvinces.filter( (auth) => auth.status === AuthorizationStatus.AUTHORIZED && auth.benefitActive, ) authProvincesToDeactivate.push(...activeAuthProvinces) } if (authProvincesToDeactivate.length === 0) { this.logger.debug('[cascadeDeactivateAuthProvinceBenefits] No active auth provinces to deactivate') return { deactivatedCount: 0 } } // 4. 停用这些省团队授权的权益 for (const authProvince of authProvincesToDeactivate) { this.logger.log( `[cascadeDeactivateAuthProvinceBenefits] Deactivating auth province benefit: ` + `authorizationId=${authProvince.authorizationId.value}, accountSequence=${authProvince.userId.accountSequence}`, ) authProvince.deactivateBenefit(reason) await this.authorizationRepository.save(authProvince) await this.eventPublisher.publishAll(authProvince.domainEvents) authProvince.clearDomainEvents() } this.logger.log( `[cascadeDeactivateAuthProvinceBenefits] Cascade deactivated ${authProvincesToDeactivate.length} auth provinces`, ) return { deactivatedCount: authProvincesToDeactivate.length } } /** * 处理过期的省团队授权权益 * 定时任务调用此方法来检查并处理过期的省团队授权权益 * * 业务规则: * - 查找所有 benefitValidUntil < 当前时间 且 benefitActive=true 的省团队授权 * - 检查其月度考核(当月新增500棵树) * - 如果达标,续期;如果不达标,级联停用 * * @param limit 每次处理的最大数量 */ async processExpiredAuthProvinceBenefits(limit = 100): Promise<{ processedCount: number renewedCount: number deactivatedCount: number }> { const now = new Date() this.logger.log(`[processExpiredAuthProvinceBenefits] Starting at ${now.toISOString()}, limit=${limit}`) // 查找过期但仍激活的省团队授权 const expiredAuthProvinces = await this.findExpiredActiveAuthProvinces(now, limit) if (expiredAuthProvinces.length === 0) { this.logger.debug('[processExpiredAuthProvinceBenefits] No expired auth provinces found') return { processedCount: 0, renewedCount: 0, deactivatedCount: 0 } } let renewedCount = 0 let deactivatedCount = 0 for (const authProvince of expiredAuthProvinces) { const accountSequence = authProvince.userId.accountSequence // 使用 getTreesForAssessment 获取正确的考核数据 const treesForAssessment = authProvince.getTreesForAssessment(now) this.logger.debug( `[processExpiredAuthProvinceBenefits] Checking auth province ${accountSequence}: ` + `treesForAssessment=${treesForAssessment}, ` + `monthlyTreesAdded=${authProvince.monthlyTreesAdded}, ` + `lastMonthTreesAdded=${authProvince.lastMonthTreesAdded}, ` + `benefitValidUntil=${authProvince.benefitValidUntil?.toISOString()}, target=500`, ) if (treesForAssessment >= 500) { // 达标,续期 authProvince.renewBenefit(treesForAssessment) await this.authorizationRepository.save(authProvince) renewedCount++ this.logger.log( `[processExpiredAuthProvinceBenefits] Auth province ${accountSequence} renewed, ` + `trees=${treesForAssessment}, new validUntil=${authProvince.benefitValidUntil?.toISOString()}`, ) } else { // 不达标,级联停用 const result = await this.cascadeDeactivateAuthProvinceBenefits( accountSequence, `月度考核不达标:考核期内新增${treesForAssessment}棵,未达到500棵目标`, ) deactivatedCount += result.deactivatedCount } } this.logger.log( `[processExpiredAuthProvinceBenefits] Completed: processed=${expiredAuthProvinces.length}, ` + `renewed=${renewedCount}, deactivated=${deactivatedCount}`, ) return { processedCount: expiredAuthProvinces.length, renewedCount, deactivatedCount, } } /** * 查找过期但仍激活的省团队授权 */ private async findExpiredActiveAuthProvinces( checkDate: Date, limit: number, ): Promise { return this.authorizationRepository.findExpiredActiveByRoleType( RoleType.AUTH_PROVINCE_COMPANY, checkDate, limit, ) } /** * 处理过期的社区权益 * 定时任务调用此方法来检查并处理过期的社区权益 * * 业务规则: * - 查找所有 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 // 使用 getTreesForAssessment 获取正确的考核数据 // - 有效期在上月末 → 用 lastMonthTreesAdded(存档数据) // - 有效期在当月末 → 用 monthlyTreesAdded(当月数据) const treesForAssessment = community.getTreesForAssessment(now) this.logger.debug( `[processExpiredCommunityBenefits] Checking community ${accountSequence}: ` + `treesForAssessment=${treesForAssessment}, ` + `monthlyTreesAdded=${community.monthlyTreesAdded}, ` + `lastMonthTreesAdded=${community.lastMonthTreesAdded}, ` + `benefitValidUntil=${community.benefitValidUntil?.toISOString()}, target=10`, ) if (treesForAssessment >= 10) { // 达标,续期 community.renewBenefit(treesForAssessment) await this.authorizationRepository.save(community) renewedCount++ this.logger.log( `[processExpiredCommunityBenefits] Community ${accountSequence} renewed, ` + `trees=${treesForAssessment}, new validUntil=${community.benefitValidUntil?.toISOString()}`, ) } else { // 不达标,级联停用 const result = await this.cascadeDeactivateCommunityBenefits( accountSequence, `月度考核不达标:考核期内新增${treesForAssessment}棵,未达到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) } /** * 获取社区权益分配方案 * 根据考核规则计算每棵树的社区权益应该分配给谁 * * 规则: * 1. 找到认种用户推荐链上最近的社区 * 2. 如果该社区 benefitActive=true,全部权益给该社区 * 3. 如果该社区 benefitActive=false: * - 计算该社区还需要多少棵才能达到初始考核(10棵) * - 考核前的部分给上级社区或总部 * - 考核后的部分给该社区(同时激活权益) * 4. 如果没有社区,全部给总部 */ async getCommunityRewardDistribution( accountSequence: string, treeCount: number, ): Promise<{ distributions: Array<{ accountSequence: string treeCount: number reason: string }> }> { this.logger.debug( `[getCommunityRewardDistribution] accountSequence=${accountSequence}, treeCount=${treeCount}`, ) const HEADQUARTERS_ACCOUNT_SEQUENCE = '1' // 总部社区账号 // 1. 获取用户的祖先链(推荐链) const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(accountSequence) if (ancestorAccountSequences.length === 0) { // 无推荐链,全部给总部 return { distributions: [ { accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, treeCount, reason: '无推荐链,进总部社区', }, ], } } // 2. 查找祖先链中所有社区授权(包括 benefitActive=false 的) const ancestorCommunities = await this.authorizationRepository.findCommunityByAccountSequences( ancestorAccountSequences, ) if (ancestorCommunities.length === 0) { // 推荐链上没有社区,全部给总部 return { distributions: [ { accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, treeCount, reason: '推荐链上无社区授权,进总部社区', }, ], } } // 3. 按祖先链顺序找最近的社区 let nearestCommunity: typeof ancestorCommunities[0] | null = null let nearestCommunityIndex = -1 for (let i = 0; i < ancestorAccountSequences.length; i++) { const ancestorSeq = ancestorAccountSequences[i] const found = ancestorCommunities.find( (auth) => auth.userId.accountSequence === ancestorSeq, ) if (found) { nearestCommunity = found nearestCommunityIndex = i break } } if (!nearestCommunity) { // 这种情况理论上不应该发生,但作为兜底 return { distributions: [ { accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, treeCount, reason: '未找到匹配的社区,进总部社区', }, ], } } // 4. 检查最近社区的权益状态 if (nearestCommunity.benefitActive) { // 权益已激活,全部给该社区 // 累加月度新增树数(用于月度考核) nearestCommunity.addMonthlyTrees(treeCount) await this.authorizationRepository.save(nearestCommunity) return { distributions: [ { accountSequence: nearestCommunity.userId.accountSequence, treeCount, reason: '社区权益已激活', }, ], } } // 5. 权益未激活,需要计算考核分配 // 获取该社区的团队统计数据 - 使用下级团队认种数(不含自己) const communityStats = await this.statsRepository.findByAccountSequence( nearestCommunity.userId.accountSequence, ) const rawSubordinateCount = communityStats?.subordinateTeamPlantingCount ?? 0 // 重要:由于 referral-service 和 reward-service 都消费同一个 Kafka 事件, // 存在竞态条件,此时查询到的 subordinateTeamPlantingCount 可能已经包含了本次认种。 // 因此需要减去本次认种数量来还原"认种前"的下级团队数。 // 注意:如果 referral-service 还没处理完,rawSubordinateCount 可能还是旧值, // 此时 currentTeamCount 可能为负数,需要取 max(0, ...) const currentTeamCount = Math.max(0, rawSubordinateCount - treeCount) const initialTarget = nearestCommunity.getInitialTarget() // 社区初始考核目标:10棵 this.logger.debug( `[getCommunityRewardDistribution] Community ${nearestCommunity.userId.accountSequence} ` + `benefitActive=false, rawSubordinateCount=${rawSubordinateCount}, treeCount=${treeCount}, ` + `currentTeamCount(before)=${currentTeamCount}, initialTarget=${initialTarget}`, ) // 6. 查找上级社区(用于接收考核前的权益) let parentCommunityAccountSequence: string = HEADQUARTERS_ACCOUNT_SEQUENCE let parentCommunityReason = '上级为总部社区' // 从最近社区之后继续查找上级社区 for (let i = nearestCommunityIndex + 1; i < ancestorAccountSequences.length; i++) { const ancestorSeq = ancestorAccountSequences[i] const found = ancestorCommunities.find( (auth) => auth.userId.accountSequence === ancestorSeq && auth.benefitActive, ) if (found) { parentCommunityAccountSequence = found.userId.accountSequence parentCommunityReason = '上级社区权益已激活' break } } // 7. 计算分配方案 const distributions: Array<{ accountSequence: string treeCount: number reason: string }> = [] if (currentTeamCount >= initialTarget) { // 已达标但权益未激活(可能是月度考核失败),全部给该社区 // 注:这种情况下应该由系统自动激活权益,但这里作为兜底处理 distributions.push({ accountSequence: nearestCommunity.userId.accountSequence, treeCount, reason: '已达初始考核目标', }) // 自动激活权益 await this.tryActivateBenefit(nearestCommunity) // 累加月度新增树数(用于月度考核) nearestCommunity.addMonthlyTrees(treeCount) await this.authorizationRepository.save(nearestCommunity) } else { // 未达标,需要拆分 // toReachTarget: 还差多少棵达到考核目标(包括达标那一棵) // 业务规则:第1-10棵全部给上级的上级/总部,第11棵开始才给该社区 // 例如:目标10棵,当前2棵 -> toReachTarget = 8(第3-10棵给上级,第11棵开始给自己) const toReachTarget = Math.max(0, initialTarget - currentTeamCount) const afterPlantingCount = currentTeamCount + treeCount // 本次认种后的总数 if (afterPlantingCount <= initialTarget) { // 本次认种后仍未超过目标(包括刚好达标),全部给上级/总部 distributions.push({ accountSequence: parentCommunityAccountSequence, treeCount, reason: `初始考核中(${currentTeamCount}+${treeCount}=${afterPlantingCount}/${initialTarget}),${parentCommunityReason}`, }) // 如果刚好达标,激活权益(但本批次树全部给上级) if (afterPlantingCount === initialTarget) { await this.tryActivateBenefit(nearestCommunity) } } else { // 本次认种跨越考核达标点 (afterPlantingCount > initialTarget) // 达标前的部分(包括第10棵)给上级/总部 if (toReachTarget > 0) { distributions.push({ accountSequence: parentCommunityAccountSequence, treeCount: toReachTarget, reason: `初始考核(${currentTeamCount}+${toReachTarget}=${initialTarget}/${initialTarget}),${parentCommunityReason}`, }) } // 超过达标点的部分(第11棵开始),给该社区 const afterTargetCount = treeCount - toReachTarget if (afterTargetCount > 0) { distributions.push({ accountSequence: nearestCommunity.userId.accountSequence, treeCount: afterTargetCount, reason: `考核达标后权益生效(第${initialTarget + 1}棵起)`, }) } // 自动激活权益(本次认种使其达标) await this.tryActivateBenefit(nearestCommunity) // 激活后累加月度新增树数(只计算归自己的那部分) if (afterTargetCount > 0) { nearestCommunity.addMonthlyTrees(afterTargetCount) await this.authorizationRepository.save(nearestCommunity) } } } this.logger.debug( `[getCommunityRewardDistribution] Result: ${JSON.stringify(distributions)}`, ) return { distributions } } /** * 获取省团队权益分配方案 (20 USDT) * * 规则: * 1. 找到认种用户推荐链上最近的授权省公司(AUTH_PROVINCE_COMPANY) * 2. 如果该授权省公司 benefitActive=true,全部权益给该用户 * 3. 如果该授权省公司 benefitActive=false: * - 计算还需要多少棵才能达到初始考核(500棵) * - 考核前的部分给上级/总部 * - 考核后的部分给该用户 * 4. 如果没有授权省公司,全部给总部 */ async getProvinceTeamRewardDistribution( accountSequence: string, provinceCode: string, treeCount: number, ): Promise<{ distributions: Array<{ accountSequence: string treeCount: number reason: string }> }> { this.logger.debug( `[getProvinceTeamRewardDistribution] accountSequence=${accountSequence}, provinceCode=${provinceCode}, treeCount=${treeCount}`, ) // 系统省团队账户ID格式: 7 + 省份代码 const systemProvinceTeamAccountSequence = `7${provinceCode.padStart(6, '0')}` // 1. 获取用户的祖先链 const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(accountSequence) if (ancestorAccountSequences.length === 0) { return { distributions: [ { accountSequence: systemProvinceTeamAccountSequence, treeCount, reason: '无推荐链,进系统省团队账户' }, ], } } // 2. 查找祖先链中所有授权省公司(包括 benefitActive=false) // 注意:省团队收益不再要求省份匹配,只要推荐链上有省团队授权即可获得收益 const ancestorAuthProvinces = await this.authorizationRepository.findAuthProvinceByAccountSequences( ancestorAccountSequences, ) if (ancestorAuthProvinces.length === 0) { return { distributions: [ { accountSequence: systemProvinceTeamAccountSequence, treeCount, reason: '推荐链上无授权省公司,进系统省团队账户' }, ], } } // 3. 按祖先链顺序找最近的授权省公司 let nearestAuthProvince: typeof ancestorAuthProvinces[0] | null = null let nearestIndex = -1 for (let i = 0; i < ancestorAccountSequences.length; i++) { const ancestorSeq = ancestorAccountSequences[i] const found = ancestorAuthProvinces.find( (auth) => auth.userId.accountSequence === ancestorSeq, ) if (found) { nearestAuthProvince = found nearestIndex = i break } } if (!nearestAuthProvince) { return { distributions: [ { accountSequence: systemProvinceTeamAccountSequence, treeCount, reason: '未找到匹配的授权省公司,进系统省团队账户' }, ], } } // 4. 检查权益状态 if (nearestAuthProvince.benefitActive) { // 权益已激活,全部给该省团队 // 累加月度新增树数(用于月度考核) nearestAuthProvince.addMonthlyTrees(treeCount) await this.authorizationRepository.save(nearestAuthProvince) return { distributions: [ { accountSequence: nearestAuthProvince.userId.accountSequence, treeCount, reason: '省团队权益已激活' }, ], } } // 5. 权益未激活,计算考核分配 - 使用下级团队认种数(不含自己) const stats = await this.statsRepository.findByAccountSequence(nearestAuthProvince.userId.accountSequence) const rawSubordinateCount = stats?.subordinateTeamPlantingCount ?? 0 // 修复竞态条件:减去本次认种数量来还原"认种前"的下级团队数 const currentTeamCount = Math.max(0, rawSubordinateCount - treeCount) const initialTarget = nearestAuthProvince.getInitialTarget() // 500棵 this.logger.debug( `[getProvinceTeamRewardDistribution] rawSubordinateCount=${rawSubordinateCount}, treeCount=${treeCount}, currentTeamCount(before)=${currentTeamCount}`, ) // 6. 查找上级(用于接收考核前的权益) let parentAccountSequence: string = systemProvinceTeamAccountSequence let parentReason = '上级为系统省团队账户' for (let i = nearestIndex + 1; i < ancestorAccountSequences.length; i++) { const ancestorSeq = ancestorAccountSequences[i] const found = ancestorAuthProvinces.find( (auth) => auth.userId.accountSequence === ancestorSeq && auth.benefitActive, ) if (found) { parentAccountSequence = found.userId.accountSequence parentReason = '上级授权省公司权益已激活' break } } // 7. 计算分配 const distributions: Array<{ accountSequence: string; treeCount: number; reason: string }> = [] if (currentTeamCount >= initialTarget) { // 已达标但权益未激活(可能是月度考核失败),全部给该省团队 // 注:这种情况下应该由系统自动激活权益,但这里作为兜底处理 distributions.push({ accountSequence: nearestAuthProvince.userId.accountSequence, treeCount, reason: '已达初始考核目标', }) // 自动激活权益 await this.tryActivateBenefit(nearestAuthProvince) // 累加月度新增树数(用于月度考核) nearestAuthProvince.addMonthlyTrees(treeCount) await this.authorizationRepository.save(nearestAuthProvince) } else { // toReachTarget: 还差多少棵达到考核目标(包括达标那一棵) // 业务规则:达标前的全部给上级/总部,超过达标点后才给该省团队 const toReachTarget = Math.max(0, initialTarget - currentTeamCount) const afterPlantingCount = currentTeamCount + treeCount if (afterPlantingCount <= initialTarget) { // 本次认种后仍未超过目标(包括刚好达标),全部给上级/总部 distributions.push({ accountSequence: parentAccountSequence, treeCount, reason: `初始考核中(${currentTeamCount}+${treeCount}=${afterPlantingCount}/${initialTarget}),${parentReason}`, }) // 如果刚好达标,激活权益(但本批次树全部给上级) if (afterPlantingCount === initialTarget) { await this.tryActivateBenefit(nearestAuthProvince) } } else { // 本次认种跨越考核达标点 (afterPlantingCount > initialTarget) // 达标前的部分(包括第500棵)给上级/总部 if (toReachTarget > 0) { distributions.push({ accountSequence: parentAccountSequence, treeCount: toReachTarget, reason: `初始考核(${currentTeamCount}+${toReachTarget}=${initialTarget}/${initialTarget}),${parentReason}`, }) } // 超过达标点的部分(第501棵开始),给该省团队 const afterTargetCount = treeCount - toReachTarget if (afterTargetCount > 0) { distributions.push({ accountSequence: nearestAuthProvince.userId.accountSequence, treeCount: afterTargetCount, reason: `考核达标后权益生效(第${initialTarget + 1}棵起)`, }) } // 自动激活权益(本次认种使其达标) await this.tryActivateBenefit(nearestAuthProvince) // 激活后累加月度新增树数(只计算归自己的那部分) if (afterTargetCount > 0) { nearestAuthProvince.addMonthlyTrees(afterTargetCount) await this.authorizationRepository.save(nearestAuthProvince) } } } this.logger.debug(`[getProvinceTeamRewardDistribution] Result: ${JSON.stringify(distributions)}`) return { distributions } } /** * 获取省区域权益分配方案 (15 USDT + 1%算力) * * 规则: * 1. 查找该省份是否有正式省公司(PROVINCE_COMPANY) * 2. 如果有且 benefitActive=true,权益进该省公司自己的账户,并累加当月新增树数 * 3. 如果有但 benefitActive=false(考核中): * - 使用阶梯目标(第1月150,第2月300,...,第9月11750) * - 计算还需要多少棵才能达到当月目标 * - 考核前的部分进系统省账户 * - 考核后的部分给该省公司 * - 第一个月达标150棵立即激活权益 * 4. 如果没有正式省公司,全部进系统省账户 */ async getProvinceAreaRewardDistribution( provinceCode: string, treeCount: number, ): Promise<{ distributions: Array<{ accountSequence: string treeCount: number reason: string isSystemAccount: boolean }> }> { this.logger.debug( `[getProvinceAreaRewardDistribution] provinceCode=${provinceCode}, treeCount=${treeCount}`, ) // 系统省账户ID格式: 9 + 省份代码 const systemProvinceAccountId = `9${provinceCode.padStart(6, '0')}` // 查找该省份的正式省公司 const provinceCompany = await this.authorizationRepository.findProvinceCompanyByRegion(provinceCode) if (!provinceCompany) { // 无正式省公司,全部进系统省账户 return { distributions: [ { accountSequence: systemProvinceAccountId, treeCount, reason: '无正式省公司授权,进系统省账户', isSystemAccount: true, }, ], } } if (provinceCompany.benefitActive) { // 正式省公司权益已激活,进该省公司账户 // 累加当月新增树数用于月度考核 provinceCompany.addMonthlyTrees(treeCount) await this.authorizationRepository.save(provinceCompany) return { distributions: [ { accountSequence: provinceCompany.userId.accountSequence, treeCount, reason: '省区域权益已激活', isSystemAccount: false, }, ], } } // 权益未激活,使用阶梯目标计算考核分配 // 使用当前月份索引获取阶梯目标(第一个月为150棵) const monthIndex = provinceCompany.currentMonthIndex || 1 const ladderTarget = LadderTargetRule.getTarget(RoleType.PROVINCE_COMPANY, monthIndex) const initialTarget = ladderTarget.monthlyTarget // 第一个月150棵 // 使用 monthlyTreesAdded 作为当前累计认种数 const currentTeamCount = provinceCompany.monthlyTreesAdded this.logger.debug( `[getProvinceAreaRewardDistribution] monthIndex=${monthIndex}, currentTeamCount=${currentTeamCount}, treeCount=${treeCount}, initialTarget=${initialTarget}`, ) const distributions: Array<{ accountSequence: string treeCount: number reason: string isSystemAccount: boolean }> = [] if (currentTeamCount >= initialTarget) { // 已达标但权益未激活,全部给该省公司 distributions.push({ accountSequence: provinceCompany.userId.accountSequence, treeCount, reason: '已达初始考核目标', isSystemAccount: false, }) // 累加当月新增树数 provinceCompany.addMonthlyTrees(treeCount) // 自动激活权益 await this.tryActivateBenefit(provinceCompany) } else { // toReachTarget: 还差多少棵达到考核目标(包括达标那一棵) // 业务规则:达标前的全部进系统省账户,超过达标点后才给该省公司 const toReachTarget = Math.max(0, initialTarget - currentTeamCount) const afterPlantingCount = currentTeamCount + treeCount if (afterPlantingCount <= initialTarget) { // 本次认种后仍未超过目标(包括刚好达标),全部进系统省账户 distributions.push({ accountSequence: systemProvinceAccountId, treeCount, reason: `初始考核中(${currentTeamCount}+${treeCount}=${afterPlantingCount}/${initialTarget}),进系统省账户`, isSystemAccount: true, }) // 累加当月新增树数 provinceCompany.addMonthlyTrees(treeCount) await this.authorizationRepository.save(provinceCompany) // 如果刚好达标,激活权益(但本批次树全部进系统省账户) if (afterPlantingCount === initialTarget) { await this.tryActivateBenefit(provinceCompany) } } else { // 本次认种跨越考核达标点 (afterPlantingCount > initialTarget) // 达标前的部分进系统省账户 if (toReachTarget > 0) { distributions.push({ accountSequence: systemProvinceAccountId, treeCount: toReachTarget, reason: `初始考核(${currentTeamCount}+${toReachTarget}=${initialTarget}/${initialTarget}),进系统省账户`, isSystemAccount: true, }) } // 超过达标点的部分,给该省公司 const afterTargetCount = treeCount - toReachTarget if (afterTargetCount > 0) { distributions.push({ accountSequence: provinceCompany.userId.accountSequence, treeCount: afterTargetCount, reason: `考核达标后权益生效(第${initialTarget + 1}棵起)`, isSystemAccount: false, }) } // 累加当月新增树数 provinceCompany.addMonthlyTrees(treeCount) // 自动激活权益(本次认种使其达标) await this.tryActivateBenefit(provinceCompany) } } this.logger.debug(`[getProvinceAreaRewardDistribution] Result: ${JSON.stringify(distributions)}`) return { distributions } } /** * 获取市团队权益分配方案 (40 USDT) * * 规则: * 1. 找到认种用户推荐链上最近的授权市公司(AUTH_CITY_COMPANY) * 2. 如果该授权市公司 benefitActive=true,全部权益给该用户 * 3. 如果该授权市公司 benefitActive=false: * - 计算还需要多少棵才能达到初始考核(100棵) * - 考核前的部分给上级/总部 * - 考核后的部分给该用户 * 4. 如果没有授权市公司,全部给总部 */ async getCityTeamRewardDistribution( accountSequence: string, cityCode: string, treeCount: number, ): Promise<{ distributions: Array<{ accountSequence: string treeCount: number reason: string }> }> { this.logger.debug( `[getCityTeamRewardDistribution] accountSequence=${accountSequence}, cityCode=${cityCode}, treeCount=${treeCount}`, ) // 系统市团队账户ID格式: 6 + 城市代码 const systemCityTeamAccountSequence = `6${cityCode.padStart(6, '0')}` // 1. 获取用户的祖先链 const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(accountSequence) if (ancestorAccountSequences.length === 0) { return { distributions: [ { accountSequence: systemCityTeamAccountSequence, treeCount, reason: '无推荐链,进系统市团队账户' }, ], } } // 2. 查找祖先链中所有授权市公司(包括 benefitActive=false) // 注意:市团队收益不再要求城市匹配,只要推荐链上有市团队授权即可获得收益 const ancestorAuthCities = await this.authorizationRepository.findAuthCityByAccountSequences( ancestorAccountSequences, ) if (ancestorAuthCities.length === 0) { return { distributions: [ { accountSequence: systemCityTeamAccountSequence, treeCount, reason: '推荐链上无授权市公司,进系统市团队账户' }, ], } } // 3. 按祖先链顺序找最近的授权市公司 let nearestAuthCity: typeof ancestorAuthCities[0] | null = null let nearestIndex = -1 for (let i = 0; i < ancestorAccountSequences.length; i++) { const ancestorSeq = ancestorAccountSequences[i] const found = ancestorAuthCities.find( (auth) => auth.userId.accountSequence === ancestorSeq, ) if (found) { nearestAuthCity = found nearestIndex = i break } } if (!nearestAuthCity) { return { distributions: [ { accountSequence: systemCityTeamAccountSequence, treeCount, reason: '未找到匹配的授权市公司,进系统市团队账户' }, ], } } // 4. 检查权益状态 if (nearestAuthCity.benefitActive) { // 权益已激活,全部给该市团队 // 累加月度新增树数(用于月度考核) nearestAuthCity.addMonthlyTrees(treeCount) await this.authorizationRepository.save(nearestAuthCity) return { distributions: [ { accountSequence: nearestAuthCity.userId.accountSequence, treeCount, reason: '市团队权益已激活' }, ], } } // 5. 权益未激活,计算考核分配 - 使用下级团队认种数(不含自己) const stats = await this.statsRepository.findByAccountSequence(nearestAuthCity.userId.accountSequence) const rawSubordinateCount = stats?.subordinateTeamPlantingCount ?? 0 // 修复竞态条件:减去本次认种数量来还原"认种前"的下级团队数 const currentTeamCount = Math.max(0, rawSubordinateCount - treeCount) const initialTarget = nearestAuthCity.getInitialTarget() // 100棵 this.logger.debug( `[getCityTeamRewardDistribution] rawSubordinateCount=${rawSubordinateCount}, treeCount=${treeCount}, currentTeamCount(before)=${currentTeamCount}`, ) // 6. 查找上级 let parentAccountSequence: string = systemCityTeamAccountSequence let parentReason = '上级为系统市团队账户' for (let i = nearestIndex + 1; i < ancestorAccountSequences.length; i++) { const ancestorSeq = ancestorAccountSequences[i] const found = ancestorAuthCities.find( (auth) => auth.userId.accountSequence === ancestorSeq && auth.benefitActive, ) if (found) { parentAccountSequence = found.userId.accountSequence parentReason = '上级授权市公司权益已激活' break } } // 7. 计算分配 const distributions: Array<{ accountSequence: string; treeCount: number; reason: string }> = [] if (currentTeamCount >= initialTarget) { // 已达标但权益未激活(可能是月度考核失败),全部给该市团队 // 注:这种情况下应该由系统自动激活权益,但这里作为兜底处理 distributions.push({ accountSequence: nearestAuthCity.userId.accountSequence, treeCount, reason: '已达初始考核目标', }) // 自动激活权益 await this.tryActivateBenefit(nearestAuthCity) // 累加月度新增树数(用于月度考核) nearestAuthCity.addMonthlyTrees(treeCount) await this.authorizationRepository.save(nearestAuthCity) } else { // toReachTarget: 还差多少棵达到考核目标(包括达标那一棵) // 业务规则:达标前的全部给上级/总部,超过达标点后才给该市团队 const toReachTarget = Math.max(0, initialTarget - currentTeamCount) const afterPlantingCount = currentTeamCount + treeCount if (afterPlantingCount <= initialTarget) { // 本次认种后仍未超过目标(包括刚好达标),全部给上级/总部 distributions.push({ accountSequence: parentAccountSequence, treeCount, reason: `初始考核中(${currentTeamCount}+${treeCount}=${afterPlantingCount}/${initialTarget}),${parentReason}`, }) // 如果刚好达标,激活权益(但本批次树全部给上级) if (afterPlantingCount === initialTarget) { await this.tryActivateBenefit(nearestAuthCity) } } else { // 本次认种跨越考核达标点 (afterPlantingCount > initialTarget) // 达标前的部分(包括第100棵)给上级/总部 if (toReachTarget > 0) { distributions.push({ accountSequence: parentAccountSequence, treeCount: toReachTarget, reason: `初始考核(${currentTeamCount}+${toReachTarget}=${initialTarget}/${initialTarget}),${parentReason}`, }) } // 超过达标点的部分(第101棵开始),给该市团队 const afterTargetCount = treeCount - toReachTarget if (afterTargetCount > 0) { distributions.push({ accountSequence: nearestAuthCity.userId.accountSequence, treeCount: afterTargetCount, reason: `考核达标后权益生效(第${initialTarget + 1}棵起)`, }) } // 自动激活权益(本次认种使其达标) await this.tryActivateBenefit(nearestAuthCity) // 激活后累加月度新增树数(只计算归自己的那部分) if (afterTargetCount > 0) { nearestAuthCity.addMonthlyTrees(afterTargetCount) await this.authorizationRepository.save(nearestAuthCity) } } } this.logger.debug(`[getCityTeamRewardDistribution] Result: ${JSON.stringify(distributions)}`) return { distributions } } /** * 获取市区域权益分配方案 (35 USDT + 2%算力) * * 规则: * 1. 查找该城市是否有正式市公司(CITY_COMPANY) * 2. 如果有且 benefitActive=true,权益进该市公司自己的账户,并累加 monthlyTreesAdded * 3. 如果有但 benefitActive=false(考核中): * - 使用阶梯考核目标(第一个月30棵,逐月递增) * - 计算还需要多少棵才能达到当前月份的考核目标 * - 考核前的部分进系统市账户 * - 考核后的部分给该市公司 * 4. 如果没有正式市公司,全部进系统市账户 */ async getCityAreaRewardDistribution( cityCode: string, treeCount: number, ): Promise<{ distributions: Array<{ accountSequence: string treeCount: number reason: string isSystemAccount: boolean }> }> { this.logger.debug( `[getCityAreaRewardDistribution] cityCode=${cityCode}, treeCount=${treeCount}`, ) // 系统市账户ID格式: 8 + 城市代码 const systemCityAccountId = `8${cityCode.padStart(6, '0')}` // 查找该城市的正式市公司 const cityCompany = await this.authorizationRepository.findCityCompanyByRegion(cityCode) if (!cityCompany) { // 无正式市公司,全部进系统市账户 return { distributions: [ { accountSequence: systemCityAccountId, treeCount, reason: '无正式市公司授权,进系统市账户', isSystemAccount: true, }, ], } } if (cityCompany.benefitActive) { // 正式市公司权益已激活,进该市公司账户 // 累加月度新增树数(用于后续月度考核) cityCompany.addMonthlyTrees(treeCount) await this.authorizationRepository.save(cityCompany) return { distributions: [ { accountSequence: cityCompany.userId.accountSequence, treeCount, reason: '市区域权益已激活', isSystemAccount: false, }, ], } } // 权益未激活,计算考核分配 - 使用阶梯目标 // 对于未激活的正式市公司,使用第一个月的目标(30棵)进行初始考核 const monthIndex = cityCompany.currentMonthIndex || 1 const ladderTarget = LadderTargetRule.getTarget(RoleType.CITY_COMPANY, monthIndex) const initialTarget = ladderTarget.monthlyTarget // 第一个月30棵 // 获取当前月度新增树数 const rawMonthlyCount = cityCompany.monthlyTreesAdded ?? 0 // 修复竞态条件:减去本次认种数量来还原"认种前"的月度数 const currentMonthlyCount = Math.max(0, rawMonthlyCount - treeCount) this.logger.debug( `[getCityAreaRewardDistribution] rawMonthlyCount=${rawMonthlyCount}, treeCount=${treeCount}, currentMonthlyCount(before)=${currentMonthlyCount}, initialTarget=${initialTarget}, monthIndex=${monthIndex}`, ) const distributions: Array<{ accountSequence: string treeCount: number reason: string isSystemAccount: boolean }> = [] if (currentMonthlyCount >= initialTarget) { // 已达标但权益未激活,全部给该市公司 distributions.push({ accountSequence: cityCompany.userId.accountSequence, treeCount, reason: '已达初始考核目标', isSystemAccount: false, }) // 自动激活权益 await this.tryActivateBenefit(cityCompany) } else { // toReachTarget: 还差多少棵达到考核目标(包括达标那一棵) // 业务规则:达标前的全部进系统市账户,超过达标点后才给该市公司 const toReachTarget = Math.max(0, initialTarget - currentMonthlyCount) const afterPlantingCount = currentMonthlyCount + treeCount if (afterPlantingCount <= initialTarget) { // 本次认种后仍未超过目标(包括刚好达标),全部进系统市账户 distributions.push({ accountSequence: systemCityAccountId, treeCount, reason: `初始考核中(${currentMonthlyCount}+${treeCount}=${afterPlantingCount}/${initialTarget}),进系统市账户`, isSystemAccount: true, }) // 累加月度新增树数(用于月底考核) cityCompany.addMonthlyTrees(treeCount) await this.authorizationRepository.save(cityCompany) // 如果刚好达标,激活权益(但本批次树全部进系统市账户) if (afterPlantingCount === initialTarget) { await this.tryActivateBenefit(cityCompany) } } else { // 本次认种跨越考核达标点 (afterPlantingCount > initialTarget) // 达标前的部分进系统市账户 if (toReachTarget > 0) { distributions.push({ accountSequence: systemCityAccountId, treeCount: toReachTarget, reason: `初始考核(${currentMonthlyCount}+${toReachTarget}=${initialTarget}/${initialTarget}),进系统市账户`, isSystemAccount: true, }) } // 超过达标点的部分,给该市公司 const afterTargetCount = treeCount - toReachTarget if (afterTargetCount > 0) { distributions.push({ accountSequence: cityCompany.userId.accountSequence, treeCount: afterTargetCount, reason: `考核达标后权益生效(第${initialTarget + 1}棵起)`, isSystemAccount: false, }) } // 累加月度新增树数 cityCompany.addMonthlyTrees(treeCount) await this.authorizationRepository.save(cityCompany) // 自动激活权益(本次认种使其达标) await this.tryActivateBenefit(cityCompany) } } this.logger.debug(`[getCityAreaRewardDistribution] Result: ${JSON.stringify(distributions)}`) return { distributions } } /** * 处理过期的正式市公司权益(月度考核) * * 业务规则: * - 检查所有 benefitValidUntil < 当前时间 且 benefitActive=true 的正式市公司 * - 获取当前月份索引对应的阶梯目标 * - 如果月度新增树数 >= 阶梯目标,续期并递增月份索引 * - 如果不达标,停用权益并重置月份索引为0 */ async processExpiredCityCompanyBenefits( limit: number, ): Promise<{ processedCount: number; renewedCount: number; deactivatedCount: number }> { const now = new Date() const expiredCityCompanies = await this.authorizationRepository.findExpiredActiveByRoleType( RoleType.CITY_COMPANY, now, limit, ) let renewedCount = 0 let deactivatedCount = 0 for (const cityCompany of expiredCityCompanies) { // 获取当前月份索引对应的阶梯目标 const monthIndex = cityCompany.currentMonthIndex || 1 const ladderTarget = LadderTargetRule.getTarget(RoleType.CITY_COMPANY, monthIndex) const monthlyTarget = ladderTarget.monthlyTarget // 获取用于考核的树数 const treesForAssessment = cityCompany.getTreesForAssessment(now) this.logger.debug( `[processExpiredCityCompanyBenefits] ${cityCompany.userId.accountSequence}: ` + `monthIndex=${monthIndex}, target=${monthlyTarget}, trees=${treesForAssessment}`, ) if (treesForAssessment >= monthlyTarget) { // 达标:续期权益并递增月份索引 cityCompany.renewBenefit(treesForAssessment) cityCompany.incrementMonthIndex() await this.authorizationRepository.save(cityCompany) renewedCount++ this.logger.log( `[processExpiredCityCompanyBenefits] ${cityCompany.userId.accountSequence} 考核达标,续期成功`, ) } else { // 不达标:停用权益 cityCompany.deactivateBenefit(`月度考核不达标(${treesForAssessment}/${monthlyTarget})`) await this.authorizationRepository.save(cityCompany) await this.eventPublisher.publishAll(cityCompany.domainEvents) cityCompany.clearDomainEvents() deactivatedCount++ this.logger.log( `[processExpiredCityCompanyBenefits] ${cityCompany.userId.accountSequence} 考核不达标,权益已停用`, ) } } return { processedCount: expiredCityCompanies.length, renewedCount, deactivatedCount, } } /** * 获取过期的正式市公司授权列表 */ async findExpiredCityCompanyBenefits( checkDate: Date, limit: number, ): Promise { return this.authorizationRepository.findExpiredActiveByRoleType( RoleType.CITY_COMPANY, checkDate, limit, ) } /** * 处理过期的正式省公司权益 * * 业务规则: * - 检查所有 benefitValidUntil < 当前时间 且 benefitActive=true 的正式省公司 * - 使用阶梯目标(第1月150,第2月300,...,第9月11750) * - 如果当月新增树数达标,续期并递增月份索引 * - 如果不达标,停用权益并重置月份索引到1 */ async processExpiredProvinceCompanyBenefits( limit: number, ): Promise<{ processedCount: number; renewedCount: number; deactivatedCount: number }> { const now = new Date() const expiredProvinceCompanies = await this.authorizationRepository.findExpiredActiveByRoleType( RoleType.PROVINCE_COMPANY, now, limit, ) let renewedCount = 0 let deactivatedCount = 0 for (const provinceCompany of expiredProvinceCompanies) { // 获取当前月份索引对应的阶梯目标 const monthIndex = provinceCompany.currentMonthIndex || 1 const ladderTarget = LadderTargetRule.getTarget(RoleType.PROVINCE_COMPANY, monthIndex) const monthlyTarget = ladderTarget.monthlyTarget // 获取用于考核的树数 const treesForAssessment = provinceCompany.getTreesForAssessment(now) this.logger.debug( `[processExpiredProvinceCompanyBenefits] ${provinceCompany.userId.accountSequence}: ` + `monthIndex=${monthIndex}, target=${monthlyTarget}, trees=${treesForAssessment}`, ) if (treesForAssessment >= monthlyTarget) { // 达标:续期权益并递增月份索引 provinceCompany.renewBenefit(treesForAssessment) provinceCompany.incrementMonthIndex() await this.authorizationRepository.save(provinceCompany) renewedCount++ this.logger.log( `[processExpiredProvinceCompanyBenefits] ${provinceCompany.userId.accountSequence} 考核达标,续期成功`, ) } else { // 不达标:停用权益 provinceCompany.deactivateBenefit(`月度考核不达标(${treesForAssessment}/${monthlyTarget})`) await this.authorizationRepository.save(provinceCompany) await this.eventPublisher.publishAll(provinceCompany.domainEvents) provinceCompany.clearDomainEvents() deactivatedCount++ this.logger.log( `[processExpiredProvinceCompanyBenefits] ${provinceCompany.userId.accountSequence} 考核不达标,权益已停用`, ) } } return { processedCount: expiredProvinceCompanies.length, renewedCount, deactivatedCount, } } /** * 获取过期的正式省公司授权列表 */ async findExpiredProvinceCompanyBenefits( checkDate: Date, limit: number, ): Promise { return this.authorizationRepository.findExpiredActiveByRoleType( RoleType.PROVINCE_COMPANY, checkDate, limit, ) } // ============================================ // 自助申请授权相关方法 // ============================================ /** * 获取用户授权状态 * 用于自助申请页面显示用户当前状态 */ async getUserAuthorizationStatus( accountSequence: string, ): Promise { this.logger.log(`[getUserAuthorizationStatus] accountSequence=${accountSequence}`) // 1. 获取用户认种数据 const teamStats = await this.statsRepository.findByAccountSequence(accountSequence) const hasPlanted = (teamStats?.selfPlantingCount || 0) > 0 const plantedCount = teamStats?.selfPlantingCount || 0 // 2. 获取用户已有的授权 const authorizations = await this.authorizationRepository.findByAccountSequence(accountSequence) const existingAuthorizations = authorizations .filter(auth => auth.status !== AuthorizationStatus.REVOKED) .map(auth => this.mapRoleTypeToDisplayName(auth.roleType)) // 3. 查找团队链中已被占用的城市和省份 const teamChainOccupiedCities = await this.findTeamChainOccupiedRegions( accountSequence, RoleType.AUTH_CITY_COMPANY, ) const teamChainOccupiedProvinces = await this.findTeamChainOccupiedRegions( accountSequence, RoleType.AUTH_PROVINCE_COMPANY, ) return { hasPlanted, plantedCount, existingAuthorizations, teamChainOccupiedCities, teamChainOccupiedProvinces, } } /** * 自助申请授权 */ async selfApplyAuthorization( command: SelfApplyAuthorizationCommand, ): Promise { this.logger.log( `[selfApplyAuthorization] userId=${command.userId}, type=${command.type}, ` + `photos=${command.officePhotoUrls.length}`, ) // 1. 验证用户已认种 const teamStats = await this.statsRepository.findByAccountSequence(command.accountSequence) const selfPlantingCount = teamStats?.selfPlantingCount || 0 if (selfPlantingCount <= 0) { throw new ApplicationError('申请授权需要先完成认种') } // 2. 验证照片数量 if (command.officePhotoUrls.length < 2) { throw new ApplicationError('请至少上传2张办公室照片') } if (command.officePhotoUrls.length > 6) { throw new ApplicationError('最多上传6张办公室照片') } // 3. 根据申请类型处理 let result: SelfApplyAuthorizationResponseDto switch (command.type) { case SelfApplyAuthorizationType.COMMUNITY: if (!command.communityName) { throw new ApplicationError('申请社区授权需要提供社区名称') } result = await this.processCommunityApplication(command) break case SelfApplyAuthorizationType.CITY_TEAM: if (!command.cityCode || !command.cityName) { throw new ApplicationError('申请市团队授权需要提供城市信息') } result = await this.processCityTeamApplication(command) break case SelfApplyAuthorizationType.PROVINCE_TEAM: if (!command.provinceCode || !command.provinceName) { throw new ApplicationError('申请省团队授权需要提供省份信息') } result = await this.processProvinceTeamApplication(command) break default: throw new ApplicationError('不支持的授权类型') } return result } /** * 处理社区授权申请 */ private async processCommunityApplication( command: SelfApplyAuthorizationCommand, ): Promise { // 检查是否已有社区授权 const existing = await this.authorizationRepository.findByAccountSequenceAndRoleType( command.accountSequence, RoleType.COMMUNITY, ) if (existing && existing.status !== AuthorizationStatus.REVOKED) { throw new ApplicationError('您已拥有社区授权') } // 直接创建授权(自助申请的社区授权直接生效,状态为 AUTHORIZED) const userId = UserId.create(command.userId, command.accountSequence) const authorization = AuthorizationRole.createSelfAppliedCommunity({ userId, communityName: command.communityName!, }) // 检查初始考核 const teamStats = await this.statsRepository.findByAccountSequence(command.accountSequence) const subordinateTreeCount = teamStats?.subordinateTeamPlantingCount || 0 if (subordinateTreeCount >= authorization.getInitialTarget()) { authorization.activateBenefit() } await this.authorizationRepository.save(authorization) await this.eventPublisher.publishAll(authorization.domainEvents) authorization.clearDomainEvents() return { authorizationId: authorization.authorizationId.value, type: SelfApplyAuthorizationType.COMMUNITY, grantedAt: new Date(), benefitsActivated: authorization.benefitActive, } } /** * 处理市团队授权申请 */ private async processCityTeamApplication( command: SelfApplyAuthorizationCommand, ): Promise { // 检查用户是否已拥有省团队授权(市团队和省团队互斥,只能二选一) const userAuthorizations = await this.authorizationRepository.findByAccountSequence(command.accountSequence) const hasProvinceTeam = userAuthorizations.some( auth => auth.roleType === RoleType.AUTH_PROVINCE_COMPANY && auth.status !== AuthorizationStatus.REVOKED, ) if (hasProvinceTeam) { throw new ApplicationError('您已拥有省团队授权,市团队和省团队只能二选一') } // 检查是否已有该市的授权市公司(全局唯一性) const existingList = await this.authorizationRepository.findActiveByRoleTypeAndRegion( RoleType.AUTH_CITY_COMPANY, RegionCode.create(command.cityCode!), ) if (existingList.length > 0) { throw new ApplicationError('该市已有授权市公司') } // 检查团队链中是否已有人申请该城市(团队链唯一性) const occupiedCities = await this.findTeamChainOccupiedRegions( command.accountSequence, RoleType.AUTH_CITY_COMPANY, ) const isCityOccupiedInChain = occupiedCities.some(r => r.regionCode === command.cityCode) if (isCityOccupiedInChain) { throw new ApplicationError(`您的团队链中已有人申请了「${command.cityName}」的市团队授权`) } // 创建授权市公司授权(自助申请直接生效,状态为 AUTHORIZED) const userId = UserId.create(command.userId, command.accountSequence) const authorization = AuthorizationRole.createSelfAppliedAuthCityCompany({ userId, cityCode: command.cityCode!, cityName: command.cityName!, }) // 检查初始考核(100棵) const teamStats = await this.statsRepository.findByAccountSequence(command.accountSequence) const subordinateTreeCount = teamStats?.subordinateTeamPlantingCount || 0 if (subordinateTreeCount >= authorization.getInitialTarget()) { authorization.activateBenefit() } await this.authorizationRepository.save(authorization) await this.eventPublisher.publishAll(authorization.domainEvents) authorization.clearDomainEvents() return { authorizationId: authorization.authorizationId.value, type: SelfApplyAuthorizationType.CITY_TEAM, grantedAt: new Date(), benefitsActivated: authorization.benefitActive, } } /** * 处理省团队授权申请 */ private async processProvinceTeamApplication( command: SelfApplyAuthorizationCommand, ): Promise { // 检查用户是否已拥有市团队授权(市团队和省团队互斥,只能二选一) const userAuthorizations = await this.authorizationRepository.findByAccountSequence(command.accountSequence) const hasCityTeam = userAuthorizations.some( auth => auth.roleType === RoleType.AUTH_CITY_COMPANY && auth.status !== AuthorizationStatus.REVOKED, ) if (hasCityTeam) { throw new ApplicationError('您已拥有市团队授权,市团队和省团队只能二选一') } // 检查是否已有该省的授权省公司(全局唯一性) const existingList = await this.authorizationRepository.findActiveByRoleTypeAndRegion( RoleType.AUTH_PROVINCE_COMPANY, RegionCode.create(command.provinceCode!), ) if (existingList.length > 0) { throw new ApplicationError('该省已有授权省公司') } // 检查团队链中是否已有人申请该省份(团队链唯一性) const occupiedProvinces = await this.findTeamChainOccupiedRegions( command.accountSequence, RoleType.AUTH_PROVINCE_COMPANY, ) const isProvinceOccupiedInChain = occupiedProvinces.some(r => r.regionCode === command.provinceCode) if (isProvinceOccupiedInChain) { throw new ApplicationError(`您的团队链中已有人申请了「${command.provinceName}」的省团队授权`) } // 创建授权省公司授权(自助申请直接生效,状态为 AUTHORIZED) const userId = UserId.create(command.userId, command.accountSequence) const authorization = AuthorizationRole.createSelfAppliedAuthProvinceCompany({ userId, provinceCode: command.provinceCode!, provinceName: command.provinceName!, }) // 检查初始考核(500棵) const teamStats = await this.statsRepository.findByAccountSequence(command.accountSequence) const subordinateTreeCount = teamStats?.subordinateTeamPlantingCount || 0 if (subordinateTreeCount >= authorization.getInitialTarget()) { authorization.activateBenefit() } await this.authorizationRepository.save(authorization) await this.eventPublisher.publishAll(authorization.domainEvents) authorization.clearDomainEvents() return { authorizationId: authorization.authorizationId.value, type: SelfApplyAuthorizationType.PROVINCE_TEAM, grantedAt: new Date(), benefitsActivated: authorization.benefitActive, } } /** * 查找团队链中指定类型的所有已占用区域 * 遍历用户的祖先链(推荐链),收集所有已被占用的城市/省份 * * @returns 已占用区域列表(包含持有者信息和区域信息) */ private async findTeamChainOccupiedRegions( accountSequence: string, roleType: RoleType.AUTH_CITY_COMPANY | RoleType.AUTH_PROVINCE_COMPANY, ): Promise { const occupiedRegions: TeamChainOccupiedRegionDto[] = [] try { // 获取用户的祖先链(推荐链) const ancestorChain = await this.referralServiceClient.getReferralChain(accountSequence) if (!ancestorChain || ancestorChain.length === 0) { return occupiedRegions } // 遍历祖先链,收集所有持有该类型授权的区域 for (const ancestorAccountSeq of ancestorChain) { const authorizations = await this.authorizationRepository.findByAccountSequence(ancestorAccountSeq) const matchingAuths = authorizations.filter( auth => auth.roleType === roleType && auth.status !== AuthorizationStatus.REVOKED, ) for (const auth of matchingAuths) { // 获取用户昵称 const userInfo = await this.identityServiceClient.getUserInfo(auth.userId.value) occupiedRegions.push({ accountSequence: ancestorAccountSeq, nickname: userInfo?.nickname || `用户${ancestorAccountSeq}`, regionCode: auth.regionCode?.value || '', regionName: auth.regionName || '', }) } } return occupiedRegions } catch (error) { this.logger.error(`[findTeamChainOccupiedRegions] Error: ${error}`) return occupiedRegions } } private mapRoleTypeToDisplayName(roleType: RoleType): string { const mapping: Record = { [RoleType.COMMUNITY]: '社区', [RoleType.AUTH_CITY_COMPANY]: '市团队', [RoleType.AUTH_PROVINCE_COMPANY]: '省团队', [RoleType.CITY_COMPANY]: '市区域', [RoleType.PROVINCE_COMPANY]: '省区域', } return mapping[roleType] || roleType } }