rwadurian/backend/services/authorization-service/src/application/services/authorization-application.s...

3107 lines
115 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<ApplyCommunityAuthResult> {
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<ApplyAuthProvinceCompanyResult> {
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<ApplyAuthCityCompanyResult> {
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<void> {
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<void> {
// 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<void> {
// 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<void> {
// 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<void> {
// 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<void> {
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<void> {
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<void> {
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<AuthorizationDTO[]> {
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<AuthorizationDTO | null> {
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<StickmanRankingDTO[]> {
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<void> {
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<CommunityHierarchyDTO> {
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<string | null> {
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<string | null> {
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<string | null> {
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<void> {
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<void> {
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<void> {
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<void> {
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<AuthorizationRole[]> {
return this.authorizationRepository.findExpiredActiveByRoleType(
RoleType.AUTH_CITY_COMPANY,
checkDate,
limit,
)
}
/**
* 级联激活上级省团队授权权益
* 当一个省团队授权的权益被激活时,需要同时激活推荐链上所有父级省团队授权的权益
*
* 业务规则:
* - 从当前省团队授权往上找,找到所有已授权但权益未激活的省团队授权
* - 将它们的权益都激活
* - 系统账户不需要考核,不在此处理
*/
private async cascadeActivateParentAuthProvinces(accountSequence: string): Promise<void> {
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<AuthorizationRole[]> {
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<AuthorizationRole[]> {
// 获取所有激活的社区授权
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<AuthorizationRole[]> {
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<AuthorizationRole[]> {
return this.authorizationRepository.findExpiredActiveByRoleType(
RoleType.PROVINCE_COMPANY,
checkDate,
limit,
)
}
// ============================================
// 自助申请授权相关方法
// ============================================
/**
* 获取用户授权状态
* 用于自助申请页面显示用户当前状态
*/
async getUserAuthorizationStatus(
accountSequence: string,
): Promise<UserAuthorizationStatusResponseDto> {
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<SelfApplyAuthorizationResponseDto> {
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<SelfApplyAuthorizationResponseDto> {
// 检查是否已有社区授权
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<SelfApplyAuthorizationResponseDto> {
// 检查用户是否已拥有省团队授权(市团队和省团队互斥,只能二选一)
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<SelfApplyAuthorizationResponseDto> {
// 检查用户是否已拥有市团队授权(市团队和省团队互斥,只能二选一)
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<TeamChainOccupiedRegionDto[]> {
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, string> = {
[RoleType.COMMUNITY]: '社区',
[RoleType.AUTH_CITY_COMPANY]: '市团队',
[RoleType.AUTH_PROVINCE_COMPANY]: '省团队',
[RoleType.CITY_COMPANY]: '市区域',
[RoleType.PROVINCE_COMPANY]: '省区域',
}
return mapping[roleType] || roleType
}
}