3107 lines
115 KiB
TypeScript
3107 lines
115 KiB
TypeScript
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
|
||
}
|
||
}
|