feat(authorization): 实现省区域/市区域自动升级机制
- 添加 createAutoUpgradedProvinceCompany/CityCompany 工厂方法 - 在 handleTreePlanted 中检查自动升级条件 - 省区域:团队累计5万棵时自动升级 - 市区域:团队累计1万棵时自动升级 - 扩展 ITeamStatisticsRepository 接口添加 getReferralChain 方法 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
0506a3547c
commit
7a264a0158
|
|
@ -675,6 +675,106 @@ export class AuthorizationRole extends AggregateRoot {
|
||||||
return auth
|
return auth
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 工厂方法 - 自动升级创建省区域(省团队伞下第一个累计达到5万棵)
|
||||||
|
// 与手动授权 createProvinceCompany 完全一致,仅 authorizedBy 为 null
|
||||||
|
static createAutoUpgradedProvinceCompany(params: {
|
||||||
|
userId: UserId
|
||||||
|
provinceCode: string
|
||||||
|
provinceName: string
|
||||||
|
}): AuthorizationRole {
|
||||||
|
const now = new Date()
|
||||||
|
const auth = new AuthorizationRole({
|
||||||
|
authorizationId: AuthorizationId.generate(),
|
||||||
|
userId: params.userId,
|
||||||
|
roleType: RoleType.PROVINCE_COMPANY,
|
||||||
|
regionCode: RegionCode.create(params.provinceCode),
|
||||||
|
regionName: params.provinceName,
|
||||||
|
status: AuthorizationStatus.AUTHORIZED,
|
||||||
|
displayTitle: params.provinceName,
|
||||||
|
authorizedAt: now,
|
||||||
|
authorizedBy: null, // 自动升级无管理员
|
||||||
|
revokedAt: null,
|
||||||
|
revokedBy: null,
|
||||||
|
revokeReason: null,
|
||||||
|
assessmentConfig: AssessmentConfig.forProvince(),
|
||||||
|
requireLocalPercentage: 0,
|
||||||
|
exemptFromPercentageCheck: true,
|
||||||
|
benefitActive: true, // 授权即激活(与手动授权一致)
|
||||||
|
benefitActivatedAt: now,
|
||||||
|
benefitDeactivatedAt: null,
|
||||||
|
benefitValidUntil: AuthorizationRole.calculateBenefitValidUntil(now),
|
||||||
|
lastAssessmentMonth: AuthorizationRole.getCurrentMonthString(now),
|
||||||
|
monthlyTreesAdded: 0,
|
||||||
|
lastMonthTreesAdded: 0,
|
||||||
|
currentMonthIndex: 1, // 从第1个月开始考核(与手动授权一致)
|
||||||
|
deletedAt: null,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
})
|
||||||
|
|
||||||
|
auth.addDomainEvent(
|
||||||
|
new ProvinceCompanyAuthorizedEvent({
|
||||||
|
authorizationId: auth.authorizationId.value,
|
||||||
|
userId: params.userId.value,
|
||||||
|
provinceCode: params.provinceCode,
|
||||||
|
provinceName: params.provinceName,
|
||||||
|
authorizedBy: null, // 自动升级
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return auth
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工厂方法 - 自动升级创建市区域(市团队伞下第一个累计达到1万棵)
|
||||||
|
// 与手动授权 createCityCompany 完全一致,仅 authorizedBy 为 null
|
||||||
|
static createAutoUpgradedCityCompany(params: {
|
||||||
|
userId: UserId
|
||||||
|
cityCode: string
|
||||||
|
cityName: string
|
||||||
|
}): AuthorizationRole {
|
||||||
|
const now = new Date()
|
||||||
|
const auth = new AuthorizationRole({
|
||||||
|
authorizationId: AuthorizationId.generate(),
|
||||||
|
userId: params.userId,
|
||||||
|
roleType: RoleType.CITY_COMPANY,
|
||||||
|
regionCode: RegionCode.create(params.cityCode),
|
||||||
|
regionName: params.cityName,
|
||||||
|
status: AuthorizationStatus.AUTHORIZED,
|
||||||
|
displayTitle: params.cityName,
|
||||||
|
authorizedAt: now,
|
||||||
|
authorizedBy: null, // 自动升级无管理员
|
||||||
|
revokedAt: null,
|
||||||
|
revokedBy: null,
|
||||||
|
revokeReason: null,
|
||||||
|
assessmentConfig: AssessmentConfig.forCity(),
|
||||||
|
requireLocalPercentage: 0,
|
||||||
|
exemptFromPercentageCheck: true,
|
||||||
|
benefitActive: true, // 授权即激活(与手动授权一致)
|
||||||
|
benefitActivatedAt: now,
|
||||||
|
benefitDeactivatedAt: null,
|
||||||
|
benefitValidUntil: AuthorizationRole.calculateBenefitValidUntil(now),
|
||||||
|
lastAssessmentMonth: AuthorizationRole.getCurrentMonthString(now),
|
||||||
|
monthlyTreesAdded: 0,
|
||||||
|
lastMonthTreesAdded: 0,
|
||||||
|
currentMonthIndex: 1, // 从第1个月开始考核(与手动授权一致)
|
||||||
|
deletedAt: null,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
})
|
||||||
|
|
||||||
|
auth.addDomainEvent(
|
||||||
|
new CityCompanyAuthorizedEvent({
|
||||||
|
authorizationId: auth.authorizationId.value,
|
||||||
|
userId: params.userId.value,
|
||||||
|
cityCode: params.cityCode,
|
||||||
|
cityName: params.cityName,
|
||||||
|
authorizedBy: null, // 自动升级
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return auth
|
||||||
|
}
|
||||||
|
|
||||||
// 工厂方法 - Admin授权省团队
|
// 工厂方法 - Admin授权省团队
|
||||||
static createAuthProvinceCompanyByAdmin(params: {
|
static createAuthProvinceCompanyByAdmin(params: {
|
||||||
userId: UserId
|
userId: UserId
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ export class ProvinceCompanyAuthorizedEvent extends DomainEvent {
|
||||||
userId: string
|
userId: string
|
||||||
provinceCode: string
|
provinceCode: string
|
||||||
provinceName: string
|
provinceName: string
|
||||||
authorizedBy: string
|
authorizedBy: string | null // null 表示自动升级
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(data: {
|
constructor(data: {
|
||||||
|
|
@ -104,7 +104,7 @@ export class ProvinceCompanyAuthorizedEvent extends DomainEvent {
|
||||||
userId: string
|
userId: string
|
||||||
provinceCode: string
|
provinceCode: string
|
||||||
provinceName: string
|
provinceName: string
|
||||||
authorizedBy: string
|
authorizedBy: string | null
|
||||||
}) {
|
}) {
|
||||||
super()
|
super()
|
||||||
this.aggregateId = data.authorizationId
|
this.aggregateId = data.authorizationId
|
||||||
|
|
@ -121,7 +121,7 @@ export class CityCompanyAuthorizedEvent extends DomainEvent {
|
||||||
userId: string
|
userId: string
|
||||||
cityCode: string
|
cityCode: string
|
||||||
cityName: string
|
cityName: string
|
||||||
authorizedBy: string
|
authorizedBy: string | null // null 表示自动升级
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(data: {
|
constructor(data: {
|
||||||
|
|
@ -129,7 +129,7 @@ export class CityCompanyAuthorizedEvent extends DomainEvent {
|
||||||
userId: string
|
userId: string
|
||||||
cityCode: string
|
cityCode: string
|
||||||
cityName: string
|
cityName: string
|
||||||
authorizedBy: string
|
authorizedBy: string | null
|
||||||
}) {
|
}) {
|
||||||
super()
|
super()
|
||||||
this.aggregateId = data.authorizationId
|
this.aggregateId = data.authorizationId
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ export interface TeamStatistics {
|
||||||
export interface ITeamStatisticsRepository {
|
export interface ITeamStatisticsRepository {
|
||||||
findByUserId(userId: string): Promise<TeamStatistics | null>
|
findByUserId(userId: string): Promise<TeamStatistics | null>
|
||||||
findByAccountSequence(accountSequence: string): Promise<TeamStatistics | null>
|
findByAccountSequence(accountSequence: string): Promise<TeamStatistics | null>
|
||||||
|
/** 获取用户的祖先链(推荐链),返回从直接推荐人到根节点的 accountSequence 列表 */
|
||||||
|
getReferralChain(accountSequence: string): Promise<string[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AssessmentCalculatorService {
|
export class AssessmentCalculatorService {
|
||||||
|
|
|
||||||
|
|
@ -183,6 +183,9 @@ export class EventConsumerController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. 检查自动升级条件(省区域/市区域)
|
||||||
|
await this.checkAutoUpgrade(teamStats)
|
||||||
|
|
||||||
this.logger.log(`[PLANTING] Completed processing tree planted event for user ${userId}`)
|
this.logger.log(`[PLANTING] Completed processing tree planted event for user ${userId}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`[PLANTING] Error processing tree planted for user ${userId}:`, error)
|
this.logger.error(`[PLANTING] Error processing tree planted for user ${userId}:`, error)
|
||||||
|
|
@ -283,4 +286,204 @@ export class EventConsumerController {
|
||||||
await this.eventPublisher.publishAll(assessment.domainEvents)
|
await this.eventPublisher.publishAll(assessment.domainEvents)
|
||||||
assessment.clearDomainEvents()
|
assessment.clearDomainEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动升级阈值常量
|
||||||
|
*/
|
||||||
|
private static readonly PROVINCE_UPGRADE_THRESHOLD = 50000 // 5万棵
|
||||||
|
private static readonly CITY_UPGRADE_THRESHOLD = 10000 // 1万棵
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查并执行自动升级
|
||||||
|
* 业务规则:
|
||||||
|
* - 省团队账号伞下第一个累计达到5万棵的账户自动获得该省区域授权
|
||||||
|
* - 市团队账号伞下第一个累计达到1万棵的账户自动获得该市区域授权
|
||||||
|
*/
|
||||||
|
private async checkAutoUpgrade(teamStats: TeamStatistics): Promise<void> {
|
||||||
|
const userId = teamStats.userId
|
||||||
|
const accountSequence = teamStats.accountSequence
|
||||||
|
const totalTeamCount = teamStats.totalTeamPlantingCount
|
||||||
|
|
||||||
|
this.logger.debug(`[AUTO-UPGRADE] Checking auto upgrade for user ${userId}, total team count: ${totalTeamCount}`)
|
||||||
|
|
||||||
|
// 检查省区域升级(5万棵)
|
||||||
|
if (totalTeamCount >= EventConsumerController.PROVINCE_UPGRADE_THRESHOLD) {
|
||||||
|
await this.checkProvinceAutoUpgrade(teamStats)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查市区域升级(1万棵)
|
||||||
|
if (totalTeamCount >= EventConsumerController.CITY_UPGRADE_THRESHOLD) {
|
||||||
|
await this.checkCityAutoUpgrade(teamStats)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查省区域自动升级
|
||||||
|
* 条件:用户是某省团队的伞下成员,且累计达到5万棵,且该省尚无省区域授权
|
||||||
|
*/
|
||||||
|
private async checkProvinceAutoUpgrade(teamStats: TeamStatistics): Promise<void> {
|
||||||
|
const userId = UserId.create(teamStats.userId, teamStats.accountSequence)
|
||||||
|
const accountSequence = teamStats.accountSequence
|
||||||
|
|
||||||
|
this.logger.debug(`[AUTO-UPGRADE] Checking province auto upgrade for ${accountSequence}`)
|
||||||
|
|
||||||
|
// 1. 检查用户是否已有省区域授权
|
||||||
|
const existingProvince = await this.authorizationRepository.findByAccountSequenceAndRoleType(
|
||||||
|
accountSequence,
|
||||||
|
RoleType.PROVINCE_COMPANY,
|
||||||
|
)
|
||||||
|
if (existingProvince && existingProvince.status !== AuthorizationStatus.REVOKED) {
|
||||||
|
this.logger.debug(`[AUTO-UPGRADE] User ${accountSequence} already has province company authorization`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查用户是否已有市区域授权(互斥)
|
||||||
|
const existingCity = await this.authorizationRepository.findByAccountSequenceAndRoleType(
|
||||||
|
accountSequence,
|
||||||
|
RoleType.CITY_COMPANY,
|
||||||
|
)
|
||||||
|
if (existingCity && existingCity.status !== AuthorizationStatus.REVOKED) {
|
||||||
|
this.logger.debug(`[AUTO-UPGRADE] User ${accountSequence} already has city company authorization, cannot auto upgrade to province`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 获取用户的祖先链,查找上级省团队
|
||||||
|
const ancestorSeqs = await this.statsRepository.getReferralChain(accountSequence) as unknown as string[]
|
||||||
|
if (ancestorSeqs.length === 0) {
|
||||||
|
this.logger.debug(`[AUTO-UPGRADE] No ancestors found for ${accountSequence}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 查找祖先链中的省团队授权
|
||||||
|
const ancestorAuthProvinces = await this.authorizationRepository.findAuthProvinceByAccountSequences(ancestorSeqs)
|
||||||
|
if (ancestorAuthProvinces.length === 0) {
|
||||||
|
this.logger.debug(`[AUTO-UPGRADE] No auth province found in ancestor chain for ${accountSequence}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 取最近的上级省团队
|
||||||
|
let nearestAuthProvince: AuthorizationRole | null = null
|
||||||
|
for (const ancestorSeq of ancestorSeqs) {
|
||||||
|
const found = ancestorAuthProvinces.find(auth => auth.userId.accountSequence === ancestorSeq)
|
||||||
|
if (found && found.benefitActive) {
|
||||||
|
nearestAuthProvince = found
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nearestAuthProvince) {
|
||||||
|
this.logger.debug(`[AUTO-UPGRADE] No active auth province found in ancestor chain for ${accountSequence}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const provinceCode = nearestAuthProvince.regionCode.value
|
||||||
|
const provinceName = nearestAuthProvince.regionName
|
||||||
|
|
||||||
|
// 6. 检查该省是否已有省区域授权
|
||||||
|
const existingProvinceRegion = await this.authorizationRepository.findProvinceCompanyByRegion(provinceCode)
|
||||||
|
if (existingProvinceRegion) {
|
||||||
|
this.logger.debug(`[AUTO-UPGRADE] Province ${provinceName} already has province company authorization`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 执行自动升级
|
||||||
|
this.logger.log(`[AUTO-UPGRADE] Auto upgrading user ${accountSequence} to province company: ${provinceName}`)
|
||||||
|
|
||||||
|
const authorization = AuthorizationRole.createAutoUpgradedProvinceCompany({
|
||||||
|
userId,
|
||||||
|
provinceCode,
|
||||||
|
provinceName,
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.authorizationRepository.save(authorization)
|
||||||
|
await this.eventPublisher.publishAll(authorization.domainEvents)
|
||||||
|
authorization.clearDomainEvents()
|
||||||
|
|
||||||
|
this.logger.log(`[AUTO-UPGRADE] Successfully auto upgraded user ${accountSequence} to province company: ${provinceName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查市区域自动升级
|
||||||
|
* 条件:用户是某市团队的伞下成员,且累计达到1万棵,且该市尚无市区域授权
|
||||||
|
*/
|
||||||
|
private async checkCityAutoUpgrade(teamStats: TeamStatistics): Promise<void> {
|
||||||
|
const userId = UserId.create(teamStats.userId, teamStats.accountSequence)
|
||||||
|
const accountSequence = teamStats.accountSequence
|
||||||
|
|
||||||
|
this.logger.debug(`[AUTO-UPGRADE] Checking city auto upgrade for ${accountSequence}`)
|
||||||
|
|
||||||
|
// 1. 检查用户是否已有市区域授权
|
||||||
|
const existingCity = await this.authorizationRepository.findByAccountSequenceAndRoleType(
|
||||||
|
accountSequence,
|
||||||
|
RoleType.CITY_COMPANY,
|
||||||
|
)
|
||||||
|
if (existingCity && existingCity.status !== AuthorizationStatus.REVOKED) {
|
||||||
|
this.logger.debug(`[AUTO-UPGRADE] User ${accountSequence} already has city company authorization`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查用户是否已有省区域授权(互斥)
|
||||||
|
const existingProvince = await this.authorizationRepository.findByAccountSequenceAndRoleType(
|
||||||
|
accountSequence,
|
||||||
|
RoleType.PROVINCE_COMPANY,
|
||||||
|
)
|
||||||
|
if (existingProvince && existingProvince.status !== AuthorizationStatus.REVOKED) {
|
||||||
|
this.logger.debug(`[AUTO-UPGRADE] User ${accountSequence} already has province company authorization, cannot auto upgrade to city`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 获取用户的祖先链,查找上级市团队
|
||||||
|
const ancestorSeqs = await this.statsRepository.getReferralChain(accountSequence) as unknown as string[]
|
||||||
|
if (ancestorSeqs.length === 0) {
|
||||||
|
this.logger.debug(`[AUTO-UPGRADE] No ancestors found for ${accountSequence}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 查找祖先链中的市团队授权
|
||||||
|
const ancestorAuthCities = await this.authorizationRepository.findAuthCityByAccountSequences(ancestorSeqs)
|
||||||
|
if (ancestorAuthCities.length === 0) {
|
||||||
|
this.logger.debug(`[AUTO-UPGRADE] No auth city found in ancestor chain for ${accountSequence}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 取最近的上级市团队
|
||||||
|
let nearestAuthCity: AuthorizationRole | null = null
|
||||||
|
for (const ancestorSeq of ancestorSeqs) {
|
||||||
|
const found = ancestorAuthCities.find(auth => auth.userId.accountSequence === ancestorSeq)
|
||||||
|
if (found && found.benefitActive) {
|
||||||
|
nearestAuthCity = found
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nearestAuthCity) {
|
||||||
|
this.logger.debug(`[AUTO-UPGRADE] No active auth city found in ancestor chain for ${accountSequence}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const cityCode = nearestAuthCity.regionCode.value
|
||||||
|
const cityName = nearestAuthCity.regionName
|
||||||
|
|
||||||
|
// 6. 检查该市是否已有市区域授权
|
||||||
|
const existingCityRegion = await this.authorizationRepository.findCityCompanyByRegion(cityCode)
|
||||||
|
if (existingCityRegion) {
|
||||||
|
this.logger.debug(`[AUTO-UPGRADE] City ${cityName} already has city company authorization`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 执行自动升级
|
||||||
|
this.logger.log(`[AUTO-UPGRADE] Auto upgrading user ${accountSequence} to city company: ${cityName}`)
|
||||||
|
|
||||||
|
const authorization = AuthorizationRole.createAutoUpgradedCityCompany({
|
||||||
|
userId,
|
||||||
|
cityCode,
|
||||||
|
cityName,
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.authorizationRepository.save(authorization)
|
||||||
|
await this.eventPublisher.publishAll(authorization.domainEvents)
|
||||||
|
authorization.clearDomainEvents()
|
||||||
|
|
||||||
|
this.logger.log(`[AUTO-UPGRADE] Successfully auto upgraded user ${accountSequence} to city company: ${cityName}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ describe('Domain Services Integration Tests', () => {
|
||||||
const mockTeamStatisticsRepository: jest.Mocked<ITeamStatisticsRepository> = {
|
const mockTeamStatisticsRepository: jest.Mocked<ITeamStatisticsRepository> = {
|
||||||
findByUserId: jest.fn(),
|
findByUserId: jest.fn(),
|
||||||
findByAccountSequence: jest.fn(),
|
findByAccountSequence: jest.fn(),
|
||||||
|
getReferralChain: jest.fn(),
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue