97 KiB
97 KiB
Authorization Service 开发指导
项目概述
Authorization Service 是 RWA 榴莲皇后平台的授权管理微服务,负责社区授权、省/市公司授权、阶梯式考核管理、月度考核评估与排名、权益激活/失效管理等功能。
核心职责 ✅
- 社区/省公司/市公司授权管理
- 阶梯式考核规则执行
- 月度考核评估与排名
- 权益激活/失效管理
- 单月免考核授权(豁免功能)
- 自有团队占比考核
- 授权限制验证(团队内唯一性)
- 火柴人排名显示数据支持
- 认种限制功能(账户限时限量、总量限制)
不负责 ❌
- 团队统计数据计算(Referral Context)
- 奖励金额计算(Reward Context)
- 用户基本信息管理(Identity Context)
- 实际认种操作(Planting Context)
核心业务需求
1. 授权省公司功能
功能说明:
- 可以给账户授权成为《授权省公司》
- 授权后前端头像显示金色字体,如"授权湖南省"
- 取消授权后头像标识立即消失
- 可以给多个账号同时授权成为同一个省的《授权省公司》
团队权益:
- 省公司团队权益:20 USDT/棵
初始考核:
- 伞下团队认种总量达到500棵后权益自动生效
- 考核的500棵权益归上级省公司所有(无上级则归总部社区账户)
2. 授权市公司功能
功能说明:
- 可以给账户授权成为《授权市公司》
- 授权后前端头像显示金色字体,如"授权长沙市"
- 取消授权后头像标识立即消失
- 可以给多个账号同时授权成为同一个市的《授权市公司》
团队权益:
- 市公司团队权益:40 USDT/棵
初始考核:
- 伞下团队认种总量达到100棵后权益自动生效
- 考核的100棵权益归上级市公司所有(无上级则归总部社区账户)
3. 正式省公司授权
功能说明:
- 可以给账户授权成为《省公司》(正式)
- 授权后前端头像显示"湖南省"(无"授权"二字)
- 取消授权后头像标识立即消失
区域权益:
- 省区域权益:15 USDT/棵 + 1%算力
- 授权后该省新认种产生的区域权益直接进入省公司账户
- 取消后进入系统省公司账户
4. 正式市公司授权
功能说明:
- 可以给账户授权成为《市公司》(正式)
- 授权后前端头像显示"长沙市"(无"授权"二字)
- 取消授权后头像标识立即消失
区域权益:
- 市区域权益:35 USDT/棵 + 2%算力
- 授权后该市新认种产生的区域权益直接进入市公司账户
- 取消后进入系统市公司账户
5. 社区授权功能
功能说明:
- 可以给账户授权成为《量子社区》等社区
社区权益:
- 社区权益:80 USDT/棵
初始考核:
- 伞下团队认种总量达到10棵后权益自动生效
- 考核的10棵权益归上级社区所有(无上级则归总部社区账户)
月度考核:
- 每月需新增10棵
- 上月未达成10棵则权益失效
- 失效后重新从0开始考核10棵
6. 阶梯式考核规则
省代阶梯考核目标
| 考核月 | 当月目标 | 累计目标 |
|---|---|---|
| 第1个月 | 150 | 150 |
| 第2个月 | 300 | 450 |
| 第3个月 | 600 | 1,050 |
| 第4个月 | 1,200 | 2,250 |
| 第5个月 | 2,400 | 4,650 |
| 第6个月 | 4,800 | 9,450 |
| 第7个月 | 9,600 | 19,050 |
| 第8个月 | 19,200 | 38,250 |
| 第9个月 | 11,750 | 50,000 |
市代阶梯考核目标
| 考核月 | 当月目标 | 累计目标 |
|---|---|---|
| 第1个月 | 30 | 30 |
| 第2个月 | 60 | 90 |
| 第3个月 | 120 | 210 |
| 第4个月 | 240 | 450 |
| 第5个月 | 480 | 930 |
| 第6个月 | 960 | 1,890 |
| 第7个月 | 1,920 | 3,810 |
| 第8个月 | 3,840 | 7,650 |
| 第9个月 | 2,350 | 10,000 |
7. 授权限制规则
限制规则:
① 一个账号只能申请一个省代或一个市代
② 团队内唯一性:某账号申请某省/市授权后,其上级团队无法再申请同一省/市授权
- 举例:D申请《授权长沙市》后,C、B、A都无法再授权《授权长沙市》
- 申请时提示"本团队已有人申请"
③ 自有团队占比考核:本地用户(本省/市)占比默认5%才能参与评选第一名
④ 可单独豁免:给单个账号授权《不考核自有团队本省/市公司占比》
8. 火柴人排名显示
显示规则:
- 每个获得《授权省/市公司》的用户头像下方显示火柴人排名
- 火柴人动态奔跑方式展示
- 每个火柴人头上显示团队完成数量
- 脚下显示昵称
- 进度条目标:省50,000棵,市10,000棵
- 目标处图标为红旗
- 显示本月累计可发放收益(USDT与RWAD)
- 火柴人根据完成数量处于进度条对应位置
- 即使只有一个账户申请也显示完整排名信息
排名规则:
- 两用户同时达成累计目标时,取超越累计目标占比最多者为第一名
- 超越累计目标占比 = 实际完成目标 ÷ 累计目标
- 占比一致时,取先完成的为第一名
9. 单月豁免授权
单月社区权益授权:
① 只能对已授权的社区账户使用
② 授权后该社区账号本月不需要完成考核目标直接享有社区权益
单月省代权益授权:
① 只能对已授权的《授权省公司》账户使用
② 授权后该省代账号本月不需要完成累计考核目标直接享有团队权益
③ 下月继续考核上月累计目标(举例:第3个月获得豁免,第4个月继续考核第3个月的累计目标)
单月市代权益授权:
① 只能对已授权的《授权市公司》账户使用
② 授权后该市代账号本月不需要完成累计考核目标直接享有团队权益
③ 下月继续考核上月累计目标
10. 认种限制功能
账户限时限认种量:
- 举例:限制5天之内每个账户只能认种1棵
- 开关及参数可设置
总量限认种功能:
- 举例:设置为限制2天之内,总限种量为100棵
- 认种达到100棵后,所有账户将无法认种
- 直至超出设置时间或关闭此功能才能继续认种
11. 后台管理分层级
管理规则:
- 涉及后台数据修改或授权需要三个后台账号授权通过才能修改
- 后台记录各个账号授权的事件
技术架构
架构模式
- DDD(领域驱动设计)
- 六边形架构(Hexagonal Architecture)
- Presentation Layer (API Controllers)
- Application Layer (Use Cases)
- Domain Layer (Entities, Aggregates, Services)
- Infrastructure Layer (Repos, External Services)
技术栈
- 框架: NestJS + TypeScript
- 数据库: PostgreSQL + Prisma ORM
- 缓存: Redis
- 消息队列: Kafka
- 定时任务: @nestjs/schedule
数据模型设计
Prisma Schema
// schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// ============ 授权角色表 ============
model AuthorizationRole {
id String @id @default(uuid())
userId String @map("user_id")
roleType RoleType @map("role_type")
regionCode String @map("region_code")
regionName String @map("region_name")
status AuthorizationStatus @default(PENDING)
displayTitle String @map("display_title")
// 授权信息
authorizedAt DateTime? @map("authorized_at")
authorizedBy String? @map("authorized_by")
revokedAt DateTime? @map("revoked_at")
revokedBy String? @map("revoked_by")
revokeReason String? @map("revoke_reason")
// 考核配置
initialTargetTreeCount Int @map("initial_target_tree_count")
monthlyTargetType MonthlyTargetType @map("monthly_target_type")
// 自有团队占比
requireLocalPercentage Decimal @default(5.0) @map("require_local_percentage") @db.Decimal(5, 2)
exemptFromPercentageCheck Boolean @default(false) @map("exempt_from_percentage_check")
// 权益状态
benefitActive Boolean @default(false) @map("benefit_active")
benefitActivatedAt DateTime? @map("benefit_activated_at")
benefitDeactivatedAt DateTime? @map("benefit_deactivated_at")
// 当前考核月份索引
currentMonthIndex Int @default(0) @map("current_month_index")
// 时间戳
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// 关联
assessments MonthlyAssessment[]
bypassRecords MonthlyBypass[]
@@unique([userId, roleType, regionCode])
@@index([userId])
@@index([roleType, regionCode])
@@index([status])
@@index([roleType, status])
@@map("authorization_roles")
}
// ============ 月度考核表 ============
model MonthlyAssessment {
id String @id @default(uuid())
authorizationId String @map("authorization_id")
userId String @map("user_id")
roleType RoleType @map("role_type")
regionCode String @map("region_code")
// 考核月份
assessmentMonth String @map("assessment_month") // YYYY-MM
monthIndex Int @map("month_index") // 第几个月考核
// 考核目标
monthlyTarget Int @map("monthly_target")
cumulativeTarget Int @map("cumulative_target")
// 完成情况
monthlyCompleted Int @default(0) @map("monthly_completed")
cumulativeCompleted Int @default(0) @map("cumulative_completed")
completedAt DateTime? @map("completed_at") // 达标时间(用于排名)
// 自有团队占比
localTeamCount Int @default(0) @map("local_team_count")
totalTeamCount Int @default(0) @map("total_team_count")
localPercentage Decimal @default(0) @map("local_percentage") @db.Decimal(5, 2)
localPercentagePass Boolean @default(false) @map("local_percentage_pass")
// 超越目标占比
exceedRatio Decimal @default(0) @map("exceed_ratio") @db.Decimal(10, 4)
// 考核结果
result AssessmentResult @default(NOT_ASSESSED)
// 排名
rankingInRegion Int? @map("ranking_in_region")
isFirstPlace Boolean @default(false) @map("is_first_place")
// 豁免
isBypassed Boolean @default(false) @map("is_bypassed")
bypassedBy String? @map("bypassed_by")
bypassedAt DateTime? @map("bypassed_at")
// 时间戳
assessedAt DateTime? @map("assessed_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// 关联
authorization AuthorizationRole @relation(fields: [authorizationId], references: [id])
@@unique([authorizationId, assessmentMonth])
@@index([userId, assessmentMonth])
@@index([roleType, regionCode, assessmentMonth])
@@index([assessmentMonth, result])
@@index([assessmentMonth, roleType, exceedRatio(sort: Desc)])
@@map("monthly_assessments")
}
// ============ 单月豁免记录表 ============
model MonthlyBypass {
id String @id @default(uuid())
authorizationId String @map("authorization_id")
userId String @map("user_id")
roleType RoleType @map("role_type")
bypassMonth String @map("bypass_month") // YYYY-MM
// 授权信息
grantedBy String @map("granted_by")
grantedAt DateTime @map("granted_at")
reason String?
// 审批信息(三人授权)
approver1Id String @map("approver1_id")
approver1At DateTime @map("approver1_at")
approver2Id String? @map("approver2_id")
approver2At DateTime? @map("approver2_at")
approver3Id String? @map("approver3_id")
approver3At DateTime? @map("approver3_at")
approvalStatus ApprovalStatus @default(PENDING) @map("approval_status")
createdAt DateTime @default(now()) @map("created_at")
authorization AuthorizationRole @relation(fields: [authorizationId], references: [id])
@@unique([authorizationId, bypassMonth])
@@index([userId, bypassMonth])
@@map("monthly_bypasses")
}
// ============ 阶梯考核目标配置表 ============
model LadderTargetConfig {
id String @id @default(uuid())
roleType RoleType @map("role_type")
monthIndex Int @map("month_index")
monthlyTarget Int @map("monthly_target")
cumulativeTarget Int @map("cumulative_target")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@unique([roleType, monthIndex])
@@map("ladder_target_configs")
}
// ============ 认种限制配置表 ============
model PlantingRestriction {
id String @id @default(uuid())
restrictionType RestrictionType @map("restriction_type")
// 账户限制配置
accountLimitDays Int? @map("account_limit_days")
accountLimitCount Int? @map("account_limit_count")
// 总量限制配置
totalLimitDays Int? @map("total_limit_days")
totalLimitCount Int? @map("total_limit_count")
currentTotalCount Int @default(0) @map("current_total_count")
// 生效时间
startAt DateTime @map("start_at")
endAt DateTime @map("end_at")
isActive Boolean @default(true) @map("is_active")
createdBy String @map("created_by")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("planting_restrictions")
}
// ============ 管理员授权审批表 ============
model AdminApproval {
id String @id @default(uuid())
operationType OperationType @map("operation_type")
targetId String @map("target_id")
targetType String @map("target_type")
requestData Json @map("request_data")
// 审批状态
status ApprovalStatus @default(PENDING)
// 审批人
requesterId String @map("requester_id")
approver1Id String? @map("approver1_id")
approver1At DateTime? @map("approver1_at")
approver2Id String? @map("approver2_id")
approver2At DateTime? @map("approver2_at")
approver3Id String? @map("approver3_id")
approver3At DateTime? @map("approver3_at")
// 完成信息
completedAt DateTime? @map("completed_at")
rejectedBy String? @map("rejected_by")
rejectedAt DateTime? @map("rejected_at")
rejectReason String? @map("reject_reason")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([status])
@@index([targetId, targetType])
@@map("admin_approvals")
}
// ============ 授权操作日志表 ============
model AuthorizationAuditLog {
id String @id @default(uuid())
operationType String @map("operation_type")
targetUserId String @map("target_user_id")
targetRoleType RoleType? @map("target_role_type")
targetRegionCode String? @map("target_region_code")
operatorId String @map("operator_id")
operatorRole String @map("operator_role")
beforeState Json? @map("before_state")
afterState Json? @map("after_state")
ipAddress String? @map("ip_address")
userAgent String? @map("user_agent")
createdAt DateTime @default(now()) @map("created_at")
@@index([targetUserId])
@@index([operatorId])
@@index([createdAt])
@@map("authorization_audit_logs")
}
// ============ 省市热度统计表 ============
model RegionHeatMap {
id String @id @default(uuid())
regionCode String @map("region_code")
regionName String @map("region_name")
regionType RegionType @map("region_type")
totalPlantings Int @default(0) @map("total_plantings")
monthlyPlantings Int @default(0) @map("monthly_plantings")
weeklyPlantings Int @default(0) @map("weekly_plantings")
dailyPlantings Int @default(0) @map("daily_plantings")
activeUsers Int @default(0) @map("active_users")
authCompanyCount Int @default(0) @map("auth_company_count")
heatScore Decimal @default(0) @map("heat_score") @db.Decimal(10, 2)
updatedAt DateTime @updatedAt @map("updated_at")
@@unique([regionCode, regionType])
@@map("region_heat_maps")
}
// ============ 火柴人排名视图数据表 ============
model StickmanRanking {
id String @id @default(uuid())
userId String @map("user_id")
authorizationId String @map("authorization_id")
roleType RoleType @map("role_type")
regionCode String @map("region_code")
regionName String @map("region_name")
// 用户信息
nickname String
avatarUrl String? @map("avatar_url")
// 进度信息
currentMonth String @map("current_month")
cumulativeCompleted Int @map("cumulative_completed")
cumulativeTarget Int @map("cumulative_target")
progressPercentage Decimal @map("progress_percentage") @db.Decimal(5, 2)
exceedRatio Decimal @map("exceed_ratio") @db.Decimal(10, 4)
// 排名
ranking Int
isFirstPlace Boolean @map("is_first_place")
// 本月收益
monthlyRewardUsdt Decimal @map("monthly_reward_usdt") @db.Decimal(18, 2)
monthlyRewardRwad Decimal @map("monthly_reward_rwad") @db.Decimal(18, 8)
updatedAt DateTime @updatedAt @map("updated_at")
@@unique([authorizationId, currentMonth])
@@index([roleType, regionCode, currentMonth])
@@map("stickman_rankings")
}
// ============ 系统配置表 ============
model AuthorizationConfig {
id String @id @default(uuid())
configKey String @unique @map("config_key")
configValue String @map("config_value")
description String?
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("authorization_configs")
}
// ============ 枚举定义 ============
enum RoleType {
COMMUNITY // 社区
AUTH_PROVINCE_COMPANY // 授权省公司(团队权益20U)
PROVINCE_COMPANY // 正式省公司(区域权益15U+1%算力)
AUTH_CITY_COMPANY // 授权市公司(团队权益40U)
CITY_COMPANY // 正式市公司(区域权益35U+2%算力)
}
enum AuthorizationStatus {
PENDING // 待授权/待考核
AUTHORIZED // 已授权
REVOKED // 已撤销
}
enum AssessmentResult {
NOT_ASSESSED // 未考核
PASS // 达标
FAIL // 未达标
BYPASSED // 豁免
}
enum MonthlyTargetType {
NONE // 无月度考核(正式省市公司)
FIXED // 固定目标(社区:10棵/月)
LADDER // 阶梯目标(授权省市公司)
}
enum RestrictionType {
ACCOUNT_LIMIT // 账户限时限量
TOTAL_LIMIT // 总量限制
}
enum ApprovalStatus {
PENDING // 待审批
APPROVED // 已通过
REJECTED // 已拒绝
}
enum OperationType {
GRANT_AUTHORIZATION // 授予授权
REVOKE_AUTHORIZATION // 撤销授权
GRANT_BYPASS // 授予豁免
EXEMPT_PERCENTAGE // 豁免占比考核
MODIFY_CONFIG // 修改配置
}
enum RegionType {
PROVINCE // 省
CITY // 市
}
领域层设计
值对象(Value Objects)
// domain/value-objects/authorization-id.vo.ts
export class AuthorizationId {
constructor(public readonly value: string) {
if (!value) {
throw new DomainError('授权ID不能为空')
}
}
static generate(): AuthorizationId {
return new AuthorizationId(uuidv4())
}
static create(value: string): AuthorizationId {
return new AuthorizationId(value)
}
equals(other: AuthorizationId): boolean {
return this.value === other.value
}
}
// domain/value-objects/region-code.vo.ts
export class RegionCode {
constructor(public readonly value: string) {
if (!value) {
throw new DomainError('区域代码不能为空')
}
}
static create(value: string): RegionCode {
return new RegionCode(value)
}
equals(other: RegionCode): boolean {
return this.value === other.value
}
}
// domain/value-objects/month.vo.ts
export class Month {
constructor(public readonly value: string) {
if (!/^\d{4}-\d{2}$/.test(value)) {
throw new DomainError('月份格式错误,应为YYYY-MM')
}
}
static current(): Month {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
return new Month(`${year}-${month}`)
}
static create(value: string): Month {
return new Month(value)
}
next(): Month {
const [year, month] = this.value.split('-').map(Number)
const nextMonth = month === 12 ? 1 : month + 1
const nextYear = month === 12 ? year + 1 : year
return new Month(`${nextYear}-${String(nextMonth).padStart(2, '0')}`)
}
previous(): Month {
const [year, month] = this.value.split('-').map(Number)
const prevMonth = month === 1 ? 12 : month - 1
const prevYear = month === 1 ? year - 1 : year
return new Month(`${prevYear}-${String(prevMonth).padStart(2, '0')}`)
}
equals(other: Month): boolean {
return this.value === other.value
}
}
// domain/value-objects/assessment-config.vo.ts
export class AssessmentConfig {
constructor(
public readonly initialTargetTreeCount: number,
public readonly monthlyTargetType: MonthlyTargetType
) {}
static forCommunity(): AssessmentConfig {
return new AssessmentConfig(10, MonthlyTargetType.FIXED)
}
static forAuthProvince(): AssessmentConfig {
return new AssessmentConfig(500, MonthlyTargetType.LADDER)
}
static forProvince(): AssessmentConfig {
return new AssessmentConfig(0, MonthlyTargetType.NONE)
}
static forAuthCity(): AssessmentConfig {
return new AssessmentConfig(100, MonthlyTargetType.LADDER)
}
static forCity(): AssessmentConfig {
return new AssessmentConfig(0, MonthlyTargetType.NONE)
}
}
// domain/value-objects/benefit-amount.vo.ts
export class BenefitAmount {
constructor(
public readonly usdtPerTree: number,
public readonly computingPowerPercentage: number
) {}
static forCommunity(): BenefitAmount {
return new BenefitAmount(80, 0)
}
static forAuthProvince(): BenefitAmount {
return new BenefitAmount(20, 0)
}
static forProvince(): BenefitAmount {
return new BenefitAmount(15, 1)
}
static forAuthCity(): BenefitAmount {
return new BenefitAmount(40, 0)
}
static forCity(): BenefitAmount {
return new BenefitAmount(35, 2)
}
static forRoleType(roleType: RoleType): BenefitAmount {
switch (roleType) {
case RoleType.COMMUNITY:
return this.forCommunity()
case RoleType.AUTH_PROVINCE_COMPANY:
return this.forAuthProvince()
case RoleType.PROVINCE_COMPANY:
return this.forProvince()
case RoleType.AUTH_CITY_COMPANY:
return this.forAuthCity()
case RoleType.CITY_COMPANY:
return this.forCity()
default:
throw new DomainError(`未知角色类型: ${roleType}`)
}
}
}
聚合根(Aggregates)
// domain/aggregates/authorization-role.aggregate.ts
export class AuthorizationRole extends AggregateRoot {
private readonly _authorizationId: AuthorizationId
private readonly _userId: UserId
private readonly _roleType: RoleType
private readonly _regionCode: RegionCode
private readonly _regionName: string
private _status: AuthorizationStatus
private _displayTitle: string
// 授权信息
private _authorizedAt: Date | null
private _authorizedBy: AdminUserId | null
private _revokedAt: Date | null
private _revokedBy: AdminUserId | null
private _revokeReason: string | null
// 考核配置
private readonly _assessmentConfig: AssessmentConfig
// 自有团队占比
private _requireLocalPercentage: number
private _exemptFromPercentageCheck: boolean
// 权益状态
private _benefitActive: boolean
private _benefitActivatedAt: Date | null
private _benefitDeactivatedAt: Date | null
// 当前考核月份索引
private _currentMonthIndex: number
private readonly _createdAt: Date
private _updatedAt: Date
// Getters
get authorizationId(): AuthorizationId { return this._authorizationId }
get userId(): UserId { return this._userId }
get roleType(): RoleType { return this._roleType }
get regionCode(): RegionCode { return this._regionCode }
get regionName(): string { return this._regionName }
get status(): AuthorizationStatus { return this._status }
get displayTitle(): string { return this._displayTitle }
get assessmentConfig(): AssessmentConfig { return this._assessmentConfig }
get requireLocalPercentage(): number { return this._requireLocalPercentage }
get exemptFromPercentageCheck(): boolean { return this._exemptFromPercentageCheck }
get benefitActive(): boolean { return this._benefitActive }
get currentMonthIndex(): number { return this._currentMonthIndex }
get isActive(): boolean { return this._status === AuthorizationStatus.AUTHORIZED }
// 工厂方法
static createCommunityAuth(params: {
userId: UserId
communityName: string
}): AuthorizationRole {
const auth = new AuthorizationRole(
AuthorizationId.generate(),
params.userId,
RoleType.COMMUNITY,
RegionCode.create(params.communityName),
params.communityName,
AuthorizationStatus.PENDING,
params.communityName,
null, null, null, null, null,
AssessmentConfig.forCommunity(),
0, true,
false, null, null,
0,
new Date(), new Date()
)
auth.addDomainEvent(new CommunityAuthRequestedEvent({
authorizationId: auth.authorizationId.value,
userId: params.userId.value,
communityName: params.communityName
}))
return auth
}
static createAuthProvinceCompany(params: {
userId: UserId
provinceCode: string
provinceName: string
}): AuthorizationRole {
const auth = new AuthorizationRole(
AuthorizationId.generate(),
params.userId,
RoleType.AUTH_PROVINCE_COMPANY,
RegionCode.create(params.provinceCode),
params.provinceName,
AuthorizationStatus.PENDING,
`授权${params.provinceName}`,
null, null, null, null, null,
AssessmentConfig.forAuthProvince(),
5.0, false,
false, null, null,
0,
new Date(), new Date()
)
auth.addDomainEvent(new AuthProvinceCompanyRequestedEvent({
authorizationId: auth.authorizationId.value,
userId: params.userId.value,
provinceCode: params.provinceCode,
provinceName: params.provinceName
}))
return auth
}
static createProvinceCompany(params: {
userId: UserId
provinceCode: string
provinceName: string
adminId: AdminUserId
}): AuthorizationRole {
const auth = new AuthorizationRole(
AuthorizationId.generate(),
params.userId,
RoleType.PROVINCE_COMPANY,
RegionCode.create(params.provinceCode),
params.provinceName,
AuthorizationStatus.AUTHORIZED,
params.provinceName,
new Date(), params.adminId, null, null, null,
AssessmentConfig.forProvince(),
0, true,
true, new Date(), null,
0,
new Date(), new Date()
)
auth.addDomainEvent(new ProvinceCompanyAuthorizedEvent({
authorizationId: auth.authorizationId.value,
userId: params.userId.value,
provinceCode: params.provinceCode,
provinceName: params.provinceName,
authorizedBy: params.adminId.value
}))
return auth
}
static createAuthCityCompany(params: {
userId: UserId
cityCode: string
cityName: string
}): AuthorizationRole {
const auth = new AuthorizationRole(
AuthorizationId.generate(),
params.userId,
RoleType.AUTH_CITY_COMPANY,
RegionCode.create(params.cityCode),
params.cityName,
AuthorizationStatus.PENDING,
`授权${params.cityName}`,
null, null, null, null, null,
AssessmentConfig.forAuthCity(),
5.0, false,
false, null, null,
0,
new Date(), new Date()
)
auth.addDomainEvent(new AuthCityCompanyRequestedEvent({
authorizationId: auth.authorizationId.value,
userId: params.userId.value,
cityCode: params.cityCode,
cityName: params.cityName
}))
return auth
}
static createCityCompany(params: {
userId: UserId
cityCode: string
cityName: string
adminId: AdminUserId
}): AuthorizationRole {
const auth = new AuthorizationRole(
AuthorizationId.generate(),
params.userId,
RoleType.CITY_COMPANY,
RegionCode.create(params.cityCode),
params.cityName,
AuthorizationStatus.AUTHORIZED,
params.cityName,
new Date(), params.adminId, null, null, null,
AssessmentConfig.forCity(),
0, true,
true, new Date(), null,
0,
new Date(), new Date()
)
auth.addDomainEvent(new CityCompanyAuthorizedEvent({
authorizationId: auth.authorizationId.value,
userId: params.userId.value,
cityCode: params.cityCode,
cityName: params.cityName,
authorizedBy: params.adminId.value
}))
return auth
}
// 核心领域行为
/**
* 激活权益(初始考核达标后)
*/
activateBenefit(): void {
if (this._benefitActive) {
throw new DomainError('权益已激活')
}
this._status = AuthorizationStatus.AUTHORIZED
this._benefitActive = true
this._benefitActivatedAt = new Date()
this._currentMonthIndex = 1
this._updatedAt = new Date()
this.addDomainEvent(new BenefitActivatedEvent({
authorizationId: this._authorizationId.value,
userId: this._userId.value,
roleType: this._roleType,
regionCode: this._regionCode.value
}))
}
/**
* 失效权益(月度考核不达标)
*/
deactivateBenefit(reason: string): void {
if (!this._benefitActive) {
return
}
this._benefitActive = false
this._benefitDeactivatedAt = new Date()
this._currentMonthIndex = 0 // 重置月份索引
this._updatedAt = new Date()
this.addDomainEvent(new BenefitDeactivatedEvent({
authorizationId: this._authorizationId.value,
userId: this._userId.value,
roleType: this._roleType,
reason
}))
}
/**
* 管理员授权
*/
authorize(adminId: AdminUserId): void {
if (this._status === AuthorizationStatus.AUTHORIZED) {
throw new DomainError('已授权,无需重复授权')
}
this._status = AuthorizationStatus.AUTHORIZED
this._authorizedAt = new Date()
this._authorizedBy = adminId
this._updatedAt = new Date()
this.addDomainEvent(new RoleAuthorizedEvent({
authorizationId: this._authorizationId.value,
userId: this._userId.value,
roleType: this._roleType,
regionCode: this._regionCode.value,
authorizedBy: adminId.value
}))
}
/**
* 撤销授权
*/
revoke(adminId: AdminUserId, reason: string): void {
if (this._status === AuthorizationStatus.REVOKED) {
throw new DomainError('已撤销')
}
this._status = AuthorizationStatus.REVOKED
this._revokedAt = new Date()
this._revokedBy = adminId
this._revokeReason = reason
this._benefitActive = false
this._benefitDeactivatedAt = new Date()
this._updatedAt = new Date()
this.addDomainEvent(new RoleRevokedEvent({
authorizationId: this._authorizationId.value,
userId: this._userId.value,
roleType: this._roleType,
regionCode: this._regionCode.value,
reason,
revokedBy: adminId.value
}))
}
/**
* 豁免占比考核
*/
exemptLocalPercentageCheck(adminId: AdminUserId): void {
this._exemptFromPercentageCheck = true
this._updatedAt = new Date()
this.addDomainEvent(new PercentageCheckExemptedEvent({
authorizationId: this._authorizationId.value,
userId: this._userId.value,
exemptedBy: adminId.value
}))
}
/**
* 取消豁免占比考核
*/
cancelExemptLocalPercentageCheck(): void {
this._exemptFromPercentageCheck = false
this._updatedAt = new Date()
}
/**
* 递增考核月份
*/
incrementMonthIndex(): void {
this._currentMonthIndex += 1
this._updatedAt = new Date()
}
/**
* 获取初始考核目标
*/
getInitialTarget(): number {
return this._assessmentConfig.initialTargetTreeCount
}
/**
* 检查是否需要占比考核
*/
needsLocalPercentageCheck(): boolean {
return !this._exemptFromPercentageCheck &&
this._requireLocalPercentage > 0 &&
(this._roleType === RoleType.AUTH_PROVINCE_COMPANY ||
this._roleType === RoleType.AUTH_CITY_COMPANY)
}
/**
* 检查是否需要阶梯考核
*/
needsLadderAssessment(): boolean {
return this._assessmentConfig.monthlyTargetType === MonthlyTargetType.LADDER
}
/**
* 检查是否需要固定月度考核
*/
needsFixedAssessment(): boolean {
return this._assessmentConfig.monthlyTargetType === MonthlyTargetType.FIXED
}
// 私有构造函数
private constructor(
authorizationId: AuthorizationId,
userId: UserId,
roleType: RoleType,
regionCode: RegionCode,
regionName: string,
status: AuthorizationStatus,
displayTitle: string,
authorizedAt: Date | null,
authorizedBy: AdminUserId | null,
revokedAt: Date | null,
revokedBy: AdminUserId | null,
revokeReason: string | null,
assessmentConfig: AssessmentConfig,
requireLocalPercentage: number,
exemptFromPercentageCheck: boolean,
benefitActive: boolean,
benefitActivatedAt: Date | null,
benefitDeactivatedAt: Date | null,
currentMonthIndex: number,
createdAt: Date,
updatedAt: Date
) {
super()
this._authorizationId = authorizationId
this._userId = userId
this._roleType = roleType
this._regionCode = regionCode
this._regionName = regionName
this._status = status
this._displayTitle = displayTitle
this._authorizedAt = authorizedAt
this._authorizedBy = authorizedBy
this._revokedAt = revokedAt
this._revokedBy = revokedBy
this._revokeReason = revokeReason
this._assessmentConfig = assessmentConfig
this._requireLocalPercentage = requireLocalPercentage
this._exemptFromPercentageCheck = exemptFromPercentageCheck
this._benefitActive = benefitActive
this._benefitActivatedAt = benefitActivatedAt
this._benefitDeactivatedAt = benefitDeactivatedAt
this._currentMonthIndex = currentMonthIndex
this._createdAt = createdAt
this._updatedAt = updatedAt
}
}
// domain/aggregates/monthly-assessment.aggregate.ts
export class MonthlyAssessment extends AggregateRoot {
private readonly _assessmentId: AssessmentId
private readonly _authorizationId: AuthorizationId
private readonly _userId: UserId
private readonly _roleType: RoleType
private readonly _regionCode: RegionCode
// 考核月份
private readonly _assessmentMonth: Month
private readonly _monthIndex: number
// 考核目标
private readonly _monthlyTarget: number
private readonly _cumulativeTarget: number
// 完成情况
private _monthlyCompleted: number
private _cumulativeCompleted: number
private _completedAt: Date | null
// 自有团队占比
private _localTeamCount: number
private _totalTeamCount: number
private _localPercentage: number
private _localPercentagePass: boolean
// 超越目标占比
private _exceedRatio: number
// 考核结果
private _result: AssessmentResult
// 排名
private _rankingInRegion: number | null
private _isFirstPlace: boolean
// 豁免
private _isBypassed: boolean
private _bypassedBy: AdminUserId | null
private _bypassedAt: Date | null
private _assessedAt: Date | null
private readonly _createdAt: Date
private _updatedAt: Date
// Getters
get assessmentId(): AssessmentId { return this._assessmentId }
get authorizationId(): AuthorizationId { return this._authorizationId }
get userId(): UserId { return this._userId }
get roleType(): RoleType { return this._roleType }
get assessmentMonth(): Month { return this._assessmentMonth }
get monthIndex(): number { return this._monthIndex }
get monthlyTarget(): number { return this._monthlyTarget }
get cumulativeTarget(): number { return this._cumulativeTarget }
get cumulativeCompleted(): number { return this._cumulativeCompleted }
get result(): AssessmentResult { return this._result }
get exceedRatio(): number { return this._exceedRatio }
get rankingInRegion(): number | null { return this._rankingInRegion }
get isFirstPlace(): boolean { return this._isFirstPlace }
get completedAt(): Date | null { return this._completedAt }
// 工厂方法
static create(params: {
authorizationId: AuthorizationId
userId: UserId
roleType: RoleType
regionCode: RegionCode
assessmentMonth: Month
monthIndex: number
monthlyTarget: number
cumulativeTarget: number
}): MonthlyAssessment {
return new MonthlyAssessment(
AssessmentId.generate(),
params.authorizationId,
params.userId,
params.roleType,
params.regionCode,
params.assessmentMonth,
params.monthIndex,
params.monthlyTarget,
params.cumulativeTarget,
0, 0, null,
0, 0, 0, false,
0,
AssessmentResult.NOT_ASSESSED,
null, false,
false, null, null,
null,
new Date(), new Date()
)
}
/**
* 执行考核评估
*/
assess(params: {
cumulativeCompleted: number
localTeamCount: number
totalTeamCount: number
requireLocalPercentage: number
exemptFromPercentageCheck: boolean
}): void {
this._cumulativeCompleted = params.cumulativeCompleted
this._localTeamCount = params.localTeamCount
this._totalTeamCount = params.totalTeamCount
// 计算本地占比
if (params.totalTeamCount > 0) {
this._localPercentage = (params.localTeamCount / params.totalTeamCount) * 100
} else {
this._localPercentage = 0
}
// 判断占比是否达标
this._localPercentagePass = params.exemptFromPercentageCheck ||
this._localPercentage >= params.requireLocalPercentage
// 计算超越比例
if (this._cumulativeTarget > 0) {
this._exceedRatio = params.cumulativeCompleted / this._cumulativeTarget
} else {
this._exceedRatio = 0
}
// 记录达标时间(用于同比例时的排名)
const cumulativePass = params.cumulativeCompleted >= this._cumulativeTarget
if (cumulativePass && !this._completedAt) {
this._completedAt = new Date()
}
// 判断考核结果
if (this._isBypassed) {
this._result = AssessmentResult.BYPASSED
} else if (cumulativePass && this._localPercentagePass) {
this._result = AssessmentResult.PASS
this.addDomainEvent(new MonthlyAssessmentPassedEvent({
assessmentId: this._assessmentId.value,
userId: this._userId.value,
roleType: this._roleType,
month: this._assessmentMonth.value,
cumulativeCompleted: params.cumulativeCompleted,
cumulativeTarget: this._cumulativeTarget
}))
} else {
this._result = AssessmentResult.FAIL
this.addDomainEvent(new MonthlyAssessmentFailedEvent({
assessmentId: this._assessmentId.value,
userId: this._userId.value,
roleType: this._roleType,
month: this._assessmentMonth.value,
cumulativeCompleted: params.cumulativeCompleted,
cumulativeTarget: this._cumulativeTarget,
reason: !cumulativePass ? '累计目标未达成' : '本地占比不足'
}))
}
this._assessedAt = new Date()
this._updatedAt = new Date()
}
/**
* 授予单月豁免
*/
grantBypass(adminId: AdminUserId): void {
if (this._isBypassed) {
throw new DomainError('已授予豁免')
}
this._isBypassed = true
this._bypassedBy = adminId
this._bypassedAt = new Date()
this._result = AssessmentResult.BYPASSED
this._updatedAt = new Date()
this.addDomainEvent(new MonthlyBypassGrantedEvent({
assessmentId: this._assessmentId.value,
userId: this._userId.value,
roleType: this._roleType,
month: this._assessmentMonth.value,
grantedBy: adminId.value
}))
}
/**
* 设置排名
*/
setRanking(rank: number, isFirst: boolean): void {
this._rankingInRegion = rank
this._isFirstPlace = isFirst
this._updatedAt = new Date()
if (isFirst) {
this.addDomainEvent(new FirstPlaceAchievedEvent({
assessmentId: this._assessmentId.value,
userId: this._userId.value,
roleType: this._roleType,
regionCode: this._regionCode.value,
month: this._assessmentMonth.value
}))
}
}
/**
* 是否达标
*/
isPassed(): boolean {
return this._result === AssessmentResult.PASS ||
this._result === AssessmentResult.BYPASSED
}
// 私有构造函数
private constructor(
assessmentId: AssessmentId,
authorizationId: AuthorizationId,
userId: UserId,
roleType: RoleType,
regionCode: RegionCode,
assessmentMonth: Month,
monthIndex: number,
monthlyTarget: number,
cumulativeTarget: number,
monthlyCompleted: number,
cumulativeCompleted: number,
completedAt: Date | null,
localTeamCount: number,
totalTeamCount: number,
localPercentage: number,
localPercentagePass: boolean,
exceedRatio: number,
result: AssessmentResult,
rankingInRegion: number | null,
isFirstPlace: boolean,
isBypassed: boolean,
bypassedBy: AdminUserId | null,
bypassedAt: Date | null,
assessedAt: Date | null,
createdAt: Date,
updatedAt: Date
) {
super()
this._assessmentId = assessmentId
this._authorizationId = authorizationId
this._userId = userId
this._roleType = roleType
this._regionCode = regionCode
this._assessmentMonth = assessmentMonth
this._monthIndex = monthIndex
this._monthlyTarget = monthlyTarget
this._cumulativeTarget = cumulativeTarget
this._monthlyCompleted = monthlyCompleted
this._cumulativeCompleted = cumulativeCompleted
this._completedAt = completedAt
this._localTeamCount = localTeamCount
this._totalTeamCount = totalTeamCount
this._localPercentage = localPercentage
this._localPercentagePass = localPercentagePass
this._exceedRatio = exceedRatio
this._result = result
this._rankingInRegion = rankingInRegion
this._isFirstPlace = isFirstPlace
this._isBypassed = isBypassed
this._bypassedBy = bypassedBy
this._bypassedAt = bypassedAt
this._assessedAt = assessedAt
this._createdAt = createdAt
this._updatedAt = updatedAt
}
}
实体(Entities)
// domain/entities/ladder-target-rule.entity.ts
export class LadderTargetRule {
constructor(
public readonly roleType: RoleType,
public readonly monthIndex: number,
public readonly monthlyTarget: number,
public readonly cumulativeTarget: number
) {}
// 省代阶梯目标表
static readonly PROVINCE_LADDER: LadderTargetRule[] = [
new LadderTargetRule(RoleType.AUTH_PROVINCE_COMPANY, 1, 150, 150),
new LadderTargetRule(RoleType.AUTH_PROVINCE_COMPANY, 2, 300, 450),
new LadderTargetRule(RoleType.AUTH_PROVINCE_COMPANY, 3, 600, 1050),
new LadderTargetRule(RoleType.AUTH_PROVINCE_COMPANY, 4, 1200, 2250),
new LadderTargetRule(RoleType.AUTH_PROVINCE_COMPANY, 5, 2400, 4650),
new LadderTargetRule(RoleType.AUTH_PROVINCE_COMPANY, 6, 4800, 9450),
new LadderTargetRule(RoleType.AUTH_PROVINCE_COMPANY, 7, 9600, 19050),
new LadderTargetRule(RoleType.AUTH_PROVINCE_COMPANY, 8, 19200, 38250),
new LadderTargetRule(RoleType.AUTH_PROVINCE_COMPANY, 9, 11750, 50000)
]
// 市代阶梯目标表
static readonly CITY_LADDER: LadderTargetRule[] = [
new LadderTargetRule(RoleType.AUTH_CITY_COMPANY, 1, 30, 30),
new LadderTargetRule(RoleType.AUTH_CITY_COMPANY, 2, 60, 90),
new LadderTargetRule(RoleType.AUTH_CITY_COMPANY, 3, 120, 210),
new LadderTargetRule(RoleType.AUTH_CITY_COMPANY, 4, 240, 450),
new LadderTargetRule(RoleType.AUTH_CITY_COMPANY, 5, 480, 930),
new LadderTargetRule(RoleType.AUTH_CITY_COMPANY, 6, 960, 1890),
new LadderTargetRule(RoleType.AUTH_CITY_COMPANY, 7, 1920, 3810),
new LadderTargetRule(RoleType.AUTH_CITY_COMPANY, 8, 3840, 7650),
new LadderTargetRule(RoleType.AUTH_CITY_COMPANY, 9, 2350, 10000)
]
// 社区固定目标
static readonly COMMUNITY_FIXED: LadderTargetRule =
new LadderTargetRule(RoleType.COMMUNITY, 1, 10, 10)
/**
* 获取指定角色和月份的目标
*/
static getTarget(roleType: RoleType, monthIndex: number): LadderTargetRule {
switch (roleType) {
case RoleType.AUTH_PROVINCE_COMPANY:
// 超过9个月后使用第9个月的目标
const provinceIndex = Math.min(monthIndex, 9) - 1
return this.PROVINCE_LADDER[provinceIndex]
case RoleType.AUTH_CITY_COMPANY:
const cityIndex = Math.min(monthIndex, 9) - 1
return this.CITY_LADDER[cityIndex]
case RoleType.COMMUNITY:
return this.COMMUNITY_FIXED
default:
throw new DomainError(`不支持的角色类型: ${roleType}`)
}
}
/**
* 获取最终累计目标
*/
static getFinalTarget(roleType: RoleType): number {
switch (roleType) {
case RoleType.AUTH_PROVINCE_COMPANY:
return 50000
case RoleType.AUTH_CITY_COMPANY:
return 10000
case RoleType.COMMUNITY:
return 10
default:
return 0
}
}
}
领域服务(Domain Services)
// domain/services/authorization-validator.service.ts
export class AuthorizationValidatorService {
/**
* 验证授权申请(团队内唯一性)
*/
async validateAuthorizationRequest(
userId: UserId,
roleType: RoleType,
regionCode: RegionCode,
referralRepository: IReferralRepository,
authorizationRepository: IAuthorizationRoleRepository
): Promise<ValidationResult> {
// 1. 检查用户是否已有同类型授权
const existingAuth = await authorizationRepository.findByUserIdAndRoleType(
userId,
roleType
)
if (existingAuth) {
return ValidationResult.failure('一个账号只能申请一个省或市的授权')
}
// 2. 检查团队内唯一性(上下级不能重复)
const relationship = await referralRepository.findByUserId(userId)
if (!relationship) {
return ValidationResult.success()
}
// 检查所有上级
const ancestors = await referralRepository.getAllAncestors(userId)
for (const ancestorId of ancestors) {
const ancestorAuth = await authorizationRepository.findByUserIdRoleTypeAndRegion(
ancestorId,
roleType,
regionCode
)
if (ancestorAuth && ancestorAuth.status !== AuthorizationStatus.REVOKED) {
return ValidationResult.failure(
`本团队已有人申请该${this.getRoleTypeName(roleType)}授权`
)
}
}
// 检查所有下级
const descendants = await referralRepository.getAllDescendants(userId)
for (const descendantId of descendants) {
const descendantAuth = await authorizationRepository.findByUserIdRoleTypeAndRegion(
descendantId,
roleType,
regionCode
)
if (descendantAuth && descendantAuth.status !== AuthorizationStatus.REVOKED) {
return ValidationResult.failure(
`本团队已有人申请该${this.getRoleTypeName(roleType)}授权`
)
}
}
return ValidationResult.success()
}
private getRoleTypeName(roleType: RoleType): string {
switch (roleType) {
case RoleType.AUTH_PROVINCE_COMPANY:
return '省'
case RoleType.AUTH_CITY_COMPANY:
return '市'
default:
return ''
}
}
}
// domain/value-objects/validation-result.vo.ts
export class ValidationResult {
private constructor(
public readonly isValid: boolean,
public readonly errorMessage: string | null
) {}
static success(): ValidationResult {
return new ValidationResult(true, null)
}
static failure(message: string): ValidationResult {
return new ValidationResult(false, message)
}
}
// domain/services/assessment-calculator.service.ts
export class AssessmentCalculatorService {
/**
* 计算月度考核
*/
async calculateMonthlyAssessment(
authorization: AuthorizationRole,
assessmentMonth: Month,
teamStats: TeamStatistics,
repository: IMonthlyAssessmentRepository
): Promise<MonthlyAssessment> {
// 1. 查找或创建本月考核
let assessment = await repository.findByAuthorizationAndMonth(
authorization.authorizationId,
assessmentMonth
)
if (!assessment) {
// 获取目标
const monthIndex = authorization.currentMonthIndex || 1
const target = LadderTargetRule.getTarget(authorization.roleType, monthIndex)
assessment = MonthlyAssessment.create({
authorizationId: authorization.authorizationId,
userId: authorization.userId,
roleType: authorization.roleType,
regionCode: authorization.regionCode,
assessmentMonth,
monthIndex,
monthlyTarget: target.monthlyTarget,
cumulativeTarget: target.cumulativeTarget
})
}
// 2. 执行考核
const localTeamCount = this.getLocalTeamCount(
teamStats,
authorization.roleType,
authorization.regionCode
)
assessment.assess({
cumulativeCompleted: teamStats.totalTeamPlantingCount,
localTeamCount,
totalTeamCount: teamStats.totalTeamPlantingCount,
requireLocalPercentage: authorization.requireLocalPercentage,
exemptFromPercentageCheck: authorization.exemptFromPercentageCheck
})
return assessment
}
/**
* 批量评估并排名
*/
async assessAndRankRegion(
roleType: RoleType,
regionCode: RegionCode,
assessmentMonth: Month,
authorizationRepository: IAuthorizationRoleRepository,
statsRepository: ITeamStatisticsRepository,
assessmentRepository: IMonthlyAssessmentRepository
): Promise<MonthlyAssessment[]> {
// 1. 查找该区域的所有激活授权
const authorizations = await authorizationRepository.findActiveByRoleTypeAndRegion(
roleType,
regionCode
)
// 2. 计算所有考核
const assessments: MonthlyAssessment[] = []
for (const auth of authorizations) {
const teamStats = await statsRepository.findByUserId(auth.userId)
if (!teamStats) continue
const assessment = await this.calculateMonthlyAssessment(
auth,
assessmentMonth,
teamStats,
assessmentRepository
)
assessments.push(assessment)
}
// 3. 排名规则:
// - 按超越比例降序排列
// - 比例相同时,按达标时间升序排列(先完成的排前面)
assessments.sort((a, b) => {
// 先按超越比例降序
if (b.exceedRatio !== a.exceedRatio) {
return b.exceedRatio - a.exceedRatio
}
// 比例相同时按达标时间升序
if (a.completedAt && b.completedAt) {
return a.completedAt.getTime() - b.completedAt.getTime()
}
// 有达标时间的排前面
if (a.completedAt) return -1
if (b.completedAt) return 1
return 0
})
// 4. 设置排名
assessments.forEach((assessment, index) => {
assessment.setRanking(index + 1, index === 0)
})
return assessments
}
private getLocalTeamCount(
teamStats: TeamStatistics,
roleType: RoleType,
regionCode: RegionCode
): number {
if (roleType === RoleType.AUTH_PROVINCE_COMPANY ||
roleType === RoleType.PROVINCE_COMPANY) {
return teamStats.getProvinceTeamCount(regionCode.value)
} else if (roleType === RoleType.AUTH_CITY_COMPANY ||
roleType === RoleType.CITY_COMPANY) {
return teamStats.getCityTeamCount(regionCode.value)
}
return 0
}
}
// domain/services/planting-restriction.service.ts
export class PlantingRestrictionService {
/**
* 检查用户是否可以认种
*/
async canUserPlant(
userId: UserId,
treeCount: number,
restrictionRepository: IPlantingRestrictionRepository,
userPlantingRepository: IUserPlantingRecordRepository
): Promise<RestrictionCheckResult> {
const now = new Date()
// 1. 检查账户限时限量
const accountRestriction = await restrictionRepository.findActiveAccountRestriction()
if (accountRestriction) {
const userPlantingCount = await userPlantingRepository.countUserPlantingsInPeriod(
userId,
accountRestriction.startAt,
accountRestriction.endAt
)
if (userPlantingCount + treeCount > accountRestriction.accountLimitCount!) {
return RestrictionCheckResult.blocked(
`限制期内每个账户只能认种${accountRestriction.accountLimitCount}棵,` +
`您已认种${userPlantingCount}棵`
)
}
}
// 2. 检查总量限制
const totalRestriction = await restrictionRepository.findActiveTotalRestriction()
if (totalRestriction) {
if (totalRestriction.currentTotalCount + treeCount > totalRestriction.totalLimitCount!) {
return RestrictionCheckResult.blocked(
`系统限制期内总认种量为${totalRestriction.totalLimitCount}棵,` +
`当前已认种${totalRestriction.currentTotalCount}棵`
)
}
}
return RestrictionCheckResult.allowed()
}
}
// domain/value-objects/restriction-check-result.vo.ts
export class RestrictionCheckResult {
private constructor(
public readonly allowed: boolean,
public readonly message: string | null
) {}
static allowed(): RestrictionCheckResult {
return new RestrictionCheckResult(true, null)
}
static blocked(message: string): RestrictionCheckResult {
return new RestrictionCheckResult(false, message)
}
}
仓储接口(Repository Interfaces)
// domain/repositories/authorization-role.repository.ts
export interface IAuthorizationRoleRepository {
save(authorization: AuthorizationRole): Promise<void>
findById(authorizationId: AuthorizationId): Promise<AuthorizationRole | null>
findByUserIdAndRoleType(userId: UserId, roleType: RoleType): Promise<AuthorizationRole | null>
findByUserIdRoleTypeAndRegion(userId: UserId, roleType: RoleType, regionCode: RegionCode): Promise<AuthorizationRole | null>
findByUserId(userId: UserId): Promise<AuthorizationRole[]>
findActiveByRoleTypeAndRegion(roleType: RoleType, regionCode: RegionCode): Promise<AuthorizationRole[]>
findAllActive(roleType?: RoleType): Promise<AuthorizationRole[]>
findPendingByUserId(userId: UserId): Promise<AuthorizationRole[]>
}
// domain/repositories/monthly-assessment.repository.ts
export interface IMonthlyAssessmentRepository {
save(assessment: MonthlyAssessment): Promise<void>
saveAll(assessments: MonthlyAssessment[]): Promise<void>
findById(assessmentId: AssessmentId): Promise<MonthlyAssessment | null>
findByAuthorizationAndMonth(authorizationId: AuthorizationId, month: Month): Promise<MonthlyAssessment | null>
findByUserAndMonth(userId: UserId, month: Month): Promise<MonthlyAssessment[]>
findFirstByAuthorization(authorizationId: AuthorizationId): Promise<MonthlyAssessment | null>
findByMonthAndRegion(month: Month, roleType: RoleType, regionCode: RegionCode): Promise<MonthlyAssessment[]>
findRankingsByMonthAndRegion(month: Month, roleType: RoleType, regionCode: RegionCode): Promise<MonthlyAssessment[]>
}
// domain/repositories/planting-restriction.repository.ts
export interface IPlantingRestrictionRepository {
save(restriction: PlantingRestriction): Promise<void>
findActiveAccountRestriction(): Promise<PlantingRestriction | null>
findActiveTotalRestriction(): Promise<PlantingRestriction | null>
incrementTotalCount(restrictionId: string, count: number): Promise<void>
}
应用层设计
应用服务
// application/services/authorization-application.service.ts
@Injectable()
export class AuthorizationApplicationService {
constructor(
private readonly authorizationRepository: IAuthorizationRoleRepository,
private readonly assessmentRepository: IMonthlyAssessmentRepository,
private readonly referralRepository: IReferralRepository,
private readonly statsRepository: ITeamStatisticsRepository,
private readonly validatorService: AuthorizationValidatorService,
private readonly calculatorService: AssessmentCalculatorService,
private readonly eventBus: EventBus,
private readonly unitOfWork: UnitOfWork
) {}
/**
* 申请社区授权
*/
async applyCommunityAuth(command: ApplyCommunityAuthCommand): Promise<ApplyCommunityAuthResult> {
return await this.unitOfWork.execute(async () => {
const userId = UserId.create(command.userId)
// 1. 检查是否已有社区授权
const existing = await this.authorizationRepository.findByUserIdAndRoleType(
userId,
RoleType.COMMUNITY
)
if (existing) {
throw new ApplicationError('您已申请过社区授权')
}
// 2. 创建社区授权
const authorization = AuthorizationRole.createCommunityAuth({
userId,
communityName: command.communityName
})
// 3. 检查初始考核(10棵)
const teamStats = await this.statsRepository.findByUserId(userId)
const totalTreeCount = teamStats?.totalTeamPlantingCount || 0
if (totalTreeCount >= authorization.getInitialTarget()) {
// 达标,激活权益
authorization.activateBenefit()
}
await this.authorizationRepository.save(authorization)
await this.eventBus.publishAll(authorization.domainEvents)
authorization.clearDomainEvents()
return {
authorizationId: authorization.authorizationId.value,
status: authorization.status,
benefitActive: authorization.benefitActive,
message: authorization.benefitActive
? '社区权益已激活'
: `需要团队累计认种达到${authorization.getInitialTarget()}棵才能激活`,
currentTreeCount: totalTreeCount,
requiredTreeCount: authorization.getInitialTarget()
}
})
}
/**
* 申请授权省公司
*/
async applyAuthProvinceCompany(
command: ApplyAuthProvinceCompanyCommand
): Promise<ApplyAuthProvinceCompanyResult> {
return await this.unitOfWork.execute(async () => {
const userId = UserId.create(command.userId)
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.findByUserId(userId)
const totalTreeCount = teamStats?.totalTeamPlantingCount || 0
if (totalTreeCount >= authorization.getInitialTarget()) {
// 达标,激活权益并创建首月考核
authorization.activateBenefit()
await this.createInitialAssessment(authorization, teamStats!)
}
await this.authorizationRepository.save(authorization)
await this.eventBus.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: totalTreeCount,
requiredTreeCount: authorization.getInitialTarget()
}
})
}
/**
* 申请授权市公司
*/
async applyAuthCityCompany(
command: ApplyAuthCityCompanyCommand
): Promise<ApplyAuthCityCompanyResult> {
return await this.unitOfWork.execute(async () => {
const userId = UserId.create(command.userId)
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.findByUserId(userId)
const totalTreeCount = teamStats?.totalTeamPlantingCount || 0
if (totalTreeCount >= authorization.getInitialTarget()) {
authorization.activateBenefit()
await this.createInitialAssessment(authorization, teamStats!)
}
await this.authorizationRepository.save(authorization)
await this.eventBus.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: totalTreeCount,
requiredTreeCount: authorization.getInitialTarget()
}
})
}
/**
* 管理员授权正式省公司
*/
async grantProvinceCompany(
command: GrantProvinceCompanyCommand
): Promise<void> {
return await this.unitOfWork.execute(async () => {
const userId = UserId.create(command.userId)
const adminId = AdminUserId.create(command.adminId)
const authorization = AuthorizationRole.createProvinceCompany({
userId,
provinceCode: command.provinceCode,
provinceName: command.provinceName,
adminId
})
await this.authorizationRepository.save(authorization)
await this.eventBus.publishAll(authorization.domainEvents)
authorization.clearDomainEvents()
})
}
/**
* 管理员授权正式市公司
*/
async grantCityCompany(
command: GrantCityCompanyCommand
): Promise<void> {
return await this.unitOfWork.execute(async () => {
const userId = UserId.create(command.userId)
const adminId = AdminUserId.create(command.adminId)
const authorization = AuthorizationRole.createCityCompany({
userId,
cityCode: command.cityCode,
cityName: command.cityName,
adminId
})
await this.authorizationRepository.save(authorization)
await this.eventBus.publishAll(authorization.domainEvents)
authorization.clearDomainEvents()
})
}
/**
* 撤销授权
*/
async revokeAuthorization(command: RevokeAuthorizationCommand): Promise<void> {
return await this.unitOfWork.execute(async () => {
const authorization = await this.authorizationRepository.findById(
AuthorizationId.create(command.authorizationId)
)
if (!authorization) {
throw new ApplicationError('授权不存在')
}
authorization.revoke(
AdminUserId.create(command.adminId),
command.reason
)
await this.authorizationRepository.save(authorization)
await this.eventBus.publishAll(authorization.domainEvents)
authorization.clearDomainEvents()
})
}
/**
* 授予单月豁免
*/
async grantMonthlyBypass(command: GrantMonthlyBypassCommand): Promise<void> {
return await this.unitOfWork.execute(async () => {
const assessment = await this.assessmentRepository.findByAuthorizationAndMonth(
AuthorizationId.create(command.authorizationId),
Month.create(command.month)
)
if (!assessment) {
throw new ApplicationError('考核记录不存在')
}
assessment.grantBypass(AdminUserId.create(command.adminId))
await this.assessmentRepository.save(assessment)
await this.eventBus.publishAll(assessment.domainEvents)
assessment.clearDomainEvents()
})
}
/**
* 豁免占比考核
*/
async exemptLocalPercentageCheck(
command: ExemptLocalPercentageCheckCommand
): Promise<void> {
return await this.unitOfWork.execute(async () => {
const authorization = await this.authorizationRepository.findById(
AuthorizationId.create(command.authorizationId)
)
if (!authorization) {
throw new ApplicationError('授权不存在')
}
authorization.exemptLocalPercentageCheck(
AdminUserId.create(command.adminId)
)
await this.authorizationRepository.save(authorization)
await this.eventBus.publishAll(authorization.domainEvents)
authorization.clearDomainEvents()
})
}
/**
* 查询用户授权列表
*/
async getUserAuthorizations(query: GetUserAuthorizationsQuery): Promise<AuthorizationDTO[]> {
const authorizations = await this.authorizationRepository.findByUserId(
UserId.create(query.userId)
)
return authorizations.map(auth => this.toAuthorizationDTO(auth))
}
/**
* 查询火柴人排名数据
*/
async getStickmanRanking(query: GetStickmanRankingQuery): Promise<StickmanRankingDTO[]> {
const assessments = await this.assessmentRepository.findRankingsByMonthAndRegion(
Month.create(query.month),
query.roleType,
RegionCode.create(query.regionCode)
)
// 获取用户信息并组装火柴人数据
const rankings: StickmanRankingDTO[] = []
for (const assessment of assessments) {
const userInfo = await this.getUserInfo(assessment.userId)
const finalTarget = LadderTargetRule.getFinalTarget(assessment.roleType)
rankings.push({
userId: assessment.userId.value,
nickname: userInfo.nickname,
avatarUrl: userInfo.avatarUrl,
ranking: assessment.rankingInRegion!,
isFirstPlace: assessment.isFirstPlace,
cumulativeCompleted: assessment.cumulativeCompleted,
cumulativeTarget: assessment.cumulativeTarget,
finalTarget,
progressPercentage: (assessment.cumulativeCompleted / finalTarget) * 100,
exceedRatio: assessment.exceedRatio,
monthlyRewardUsdt: await this.calculateMonthlyReward(assessment),
monthlyRewardRwad: 0 // 需要从其他服务获取
})
}
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.eventBus.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): AuthorizationDTO {
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
}
}
}
定时任务
// application/schedulers/monthly-assessment.scheduler.ts
@Injectable()
export class MonthlyAssessmentScheduler {
constructor(
private readonly authorizationRepository: IAuthorizationRoleRepository,
private readonly assessmentRepository: IMonthlyAssessmentRepository,
private readonly statsRepository: ITeamStatisticsRepository,
private readonly calculatorService: AssessmentCalculatorService,
private readonly eventBus: EventBus,
private readonly unitOfWork: UnitOfWork,
private readonly logger: Logger
) {}
/**
* 每月1号凌晨2点执行月度考核
*/
@Cron('0 2 1 * *')
async executeMonthlyAssessment(): Promise<void> {
this.logger.log('开始执行月度考核...')
const previousMonth = Month.current().previous()
try {
await this.unitOfWork.execute(async () => {
// 1. 获取所有激活的授权
const activeAuths = await this.authorizationRepository.findAllActive()
// 2. 按角色类型和区域分组处理
const groupedByRoleAndRegion = this.groupByRoleAndRegion(activeAuths)
for (const [key, auths] of groupedByRoleAndRegion) {
const [roleType, regionCode] = key.split('|')
// 跳过正式省市公司(无月度考核)
if (roleType === RoleType.PROVINCE_COMPANY ||
roleType === RoleType.CITY_COMPANY) {
continue
}
// 执行考核并排名
const assessments = await this.calculatorService.assessAndRankRegion(
roleType as RoleType,
RegionCode.create(regionCode),
previousMonth,
this.authorizationRepository,
this.statsRepository,
this.assessmentRepository
)
// 保存考核结果
await this.assessmentRepository.saveAll(assessments)
// 处理不达标的授权
for (const assessment of assessments) {
if (assessment.result === AssessmentResult.FAIL) {
const auth = auths.find(a =>
a.authorizationId.equals(assessment.authorizationId)
)
if (auth) {
// 权益失效
auth.deactivateBenefit('月度考核不达标')
await this.authorizationRepository.save(auth)
await this.eventBus.publishAll(auth.domainEvents)
auth.clearDomainEvents()
}
} else if (assessment.isPassed()) {
// 达标,递增月份索引
const auth = auths.find(a =>
a.authorizationId.equals(assessment.authorizationId)
)
if (auth) {
auth.incrementMonthIndex()
await this.authorizationRepository.save(auth)
}
}
await this.eventBus.publishAll(assessment.domainEvents)
assessment.clearDomainEvents()
}
}
})
this.logger.log('月度考核执行完成')
} catch (error) {
this.logger.error('月度考核执行失败', error)
throw error
}
}
/**
* 每天凌晨1点更新火柴人排名数据
*/
@Cron('0 1 * * *')
async updateStickmanRankings(): Promise<void> {
this.logger.log('开始更新火柴人排名数据...')
const currentMonth = Month.current()
try {
// 获取所有激活的授权省/市公司
const activeAuths = await this.authorizationRepository.findAllActive()
const provinceAuths = activeAuths.filter(
a => a.roleType === RoleType.AUTH_PROVINCE_COMPANY
)
const cityAuths = activeAuths.filter(
a => a.roleType === RoleType.AUTH_CITY_COMPANY
)
// 按区域分组并更新排名
// ... 实现排名更新逻辑
this.logger.log('火柴人排名数据更新完成')
} catch (error) {
this.logger.error('火柴人排名数据更新失败', error)
}
}
private groupByRoleAndRegion(
authorizations: AuthorizationRole[]
): Map<string, AuthorizationRole[]> {
const map = new Map<string, AuthorizationRole[]>()
for (const auth of authorizations) {
const key = `${auth.roleType}|${auth.regionCode.value}`
if (!map.has(key)) {
map.set(key, [])
}
map.get(key)!.push(auth)
}
return map
}
}
API端点设计
用户端 API
// presentation/controllers/authorization.controller.ts
@Controller('api/v1/authorizations')
@UseGuards(JwtAuthGuard)
export class AuthorizationController {
constructor(
private readonly authorizationService: AuthorizationApplicationService
) {}
/**
* 申请社区授权
*/
@Post('community')
async applyCommunityAuth(
@CurrentUser() user: UserPayload,
@Body() dto: ApplyCommunityAuthDTO
): Promise<ApiResponse<ApplyCommunityAuthResult>> {
const result = await this.authorizationService.applyCommunityAuth({
userId: user.userId,
communityName: dto.communityName
})
return ApiResponse.success(result)
}
/**
* 申请授权省公司
*/
@Post('auth-province-company')
async applyAuthProvinceCompany(
@CurrentUser() user: UserPayload,
@Body() dto: ApplyAuthProvinceCompanyDTO
): Promise<ApiResponse<ApplyAuthProvinceCompanyResult>> {
const result = await this.authorizationService.applyAuthProvinceCompany({
userId: user.userId,
provinceCode: dto.provinceCode,
provinceName: dto.provinceName
})
return ApiResponse.success(result)
}
/**
* 申请授权市公司
*/
@Post('auth-city-company')
async applyAuthCityCompany(
@CurrentUser() user: UserPayload,
@Body() dto: ApplyAuthCityCompanyDTO
): Promise<ApiResponse<ApplyAuthCityCompanyResult>> {
const result = await this.authorizationService.applyAuthCityCompany({
userId: user.userId,
cityCode: dto.cityCode,
cityName: dto.cityName
})
return ApiResponse.success(result)
}
/**
* 获取我的授权列表
*/
@Get('my')
async getMyAuthorizations(
@CurrentUser() user: UserPayload
): Promise<ApiResponse<AuthorizationDTO[]>> {
const result = await this.authorizationService.getUserAuthorizations({
userId: user.userId
})
return ApiResponse.success(result)
}
/**
* 获取我的考核记录
*/
@Get('my/assessments')
async getMyAssessments(
@CurrentUser() user: UserPayload,
@Query() query: GetAssessmentsQueryDTO
): Promise<ApiResponse<AssessmentDTO[]>> {
const result = await this.authorizationService.getUserAssessments({
userId: user.userId,
page: query.page || 1,
pageSize: query.pageSize || 20
})
return ApiResponse.success(result)
}
/**
* 获取火柴人排名(省公司)
*/
@Get('stickman-ranking/province/:provinceCode')
async getProvinceStickmanRanking(
@Param('provinceCode') provinceCode: string,
@Query('month') month?: string
): Promise<ApiResponse<StickmanRankingDTO[]>> {
const result = await this.authorizationService.getStickmanRanking({
roleType: RoleType.AUTH_PROVINCE_COMPANY,
regionCode: provinceCode,
month: month || Month.current().value
})
return ApiResponse.success(result)
}
/**
* 获取火柴人排名(市公司)
*/
@Get('stickman-ranking/city/:cityCode')
async getCityStickmanRanking(
@Param('cityCode') cityCode: string,
@Query('month') month?: string
): Promise<ApiResponse<StickmanRankingDTO[]>> {
const result = await this.authorizationService.getStickmanRanking({
roleType: RoleType.AUTH_CITY_COMPANY,
regionCode: cityCode,
month: month || Month.current().value
})
return ApiResponse.success(result)
}
/**
* 获取省市认种热度(可选是否允许查看)
*/
@Get('region-heat')
async getRegionHeat(
@Query() query: GetRegionHeatQueryDTO
): Promise<ApiResponse<RegionHeatDTO[]>> {
const result = await this.authorizationService.getRegionHeat({
regionType: query.regionType,
parentCode: query.parentCode
})
return ApiResponse.success(result)
}
}
管理端 API
// presentation/controllers/admin-authorization.controller.ts
@Controller('api/v1/admin/authorizations')
@UseGuards(JwtAuthGuard, AdminGuard)
export class AdminAuthorizationController {
constructor(
private readonly authorizationService: AuthorizationApplicationService,
private readonly approvalService: AdminApprovalService
) {}
/**
* 授权正式省公司
*/
@Post('province-company')
@RequireApproval(3) // 需要3人审批
async grantProvinceCompany(
@CurrentAdmin() admin: AdminPayload,
@Body() dto: GrantProvinceCompanyDTO
): Promise<ApiResponse<void>> {
await this.authorizationService.grantProvinceCompany({
userId: dto.userId,
provinceCode: dto.provinceCode,
provinceName: dto.provinceName,
adminId: admin.adminId
})
return ApiResponse.success()
}
/**
* 授权正式市公司
*/
@Post('city-company')
@RequireApproval(3)
async grantCityCompany(
@CurrentAdmin() admin: AdminPayload,
@Body() dto: GrantCityCompanyDTO
): Promise<ApiResponse<void>> {
await this.authorizationService.grantCityCompany({
userId: dto.userId,
cityCode: dto.cityCode,
cityName: dto.cityName,
adminId: admin.adminId
})
return ApiResponse.success()
}
/**
* 撤销授权
*/
@Delete(':authorizationId')
@RequireApproval(3)
async revokeAuthorization(
@CurrentAdmin() admin: AdminPayload,
@Param('authorizationId') authorizationId: string,
@Body() dto: RevokeAuthorizationDTO
): Promise<ApiResponse<void>> {
await this.authorizationService.revokeAuthorization({
authorizationId,
adminId: admin.adminId,
reason: dto.reason
})
return ApiResponse.success()
}
/**
* 授予单月豁免
*/
@Post(':authorizationId/bypass')
@RequireApproval(3)
async grantMonthlyBypass(
@CurrentAdmin() admin: AdminPayload,
@Param('authorizationId') authorizationId: string,
@Body() dto: GrantBypassDTO
): Promise<ApiResponse<void>> {
await this.authorizationService.grantMonthlyBypass({
authorizationId,
month: dto.month,
adminId: admin.adminId,
reason: dto.reason
})
return ApiResponse.success()
}
/**
* 豁免占比考核
*/
@Post(':authorizationId/exempt-percentage')
@RequireApproval(3)
async exemptLocalPercentageCheck(
@CurrentAdmin() admin: AdminPayload,
@Param('authorizationId') authorizationId: string
): Promise<ApiResponse<void>> {
await this.authorizationService.exemptLocalPercentageCheck({
authorizationId,
adminId: admin.adminId
})
return ApiResponse.success()
}
/**
* 获取授权列表
*/
@Get()
async getAuthorizations(
@Query() query: GetAuthorizationsQueryDTO
): Promise<ApiResponse<PaginatedResult<AuthorizationDTO>>> {
const result = await this.authorizationService.getAuthorizationsList(query)
return ApiResponse.success(result)
}
/**
* 获取考核列表
*/
@Get('assessments')
async getAssessments(
@Query() query: GetAssessmentsQueryDTO
): Promise<ApiResponse<PaginatedResult<AssessmentDTO>>> {
const result = await this.authorizationService.getAssessmentsList(query)
return ApiResponse.success(result)
}
/**
* 设置认种限制
*/
@Post('planting-restrictions')
@RequireApproval(3)
async setPlantingRestriction(
@CurrentAdmin() admin: AdminPayload,
@Body() dto: SetPlantingRestrictionDTO
): Promise<ApiResponse<void>> {
await this.authorizationService.setPlantingRestriction({
...dto,
adminId: admin.adminId
})
return ApiResponse.success()
}
/**
* 获取审批列表
*/
@Get('approvals')
async getApprovals(
@Query() query: GetApprovalsQueryDTO
): Promise<ApiResponse<PaginatedResult<ApprovalDTO>>> {
const result = await this.approvalService.getApprovalsList(query)
return ApiResponse.success(result)
}
/**
* 审批操作
*/
@Post('approvals/:approvalId/approve')
async approveOperation(
@CurrentAdmin() admin: AdminPayload,
@Param('approvalId') approvalId: string
): Promise<ApiResponse<void>> {
await this.approvalService.approve({
approvalId,
adminId: admin.adminId
})
return ApiResponse.success()
}
/**
* 拒绝操作
*/
@Post('approvals/:approvalId/reject')
async rejectOperation(
@CurrentAdmin() admin: AdminPayload,
@Param('approvalId') approvalId: string,
@Body() dto: RejectApprovalDTO
): Promise<ApiResponse<void>> {
await this.approvalService.reject({
approvalId,
adminId: admin.adminId,
reason: dto.reason
})
return ApiResponse.success()
}
}
领域事件
// domain/events/authorization.events.ts
// 社区授权申请事件
export class CommunityAuthRequestedEvent extends DomainEvent {
constructor(public readonly payload: {
authorizationId: string
userId: string
communityName: string
}) {
super()
}
}
// 授权省公司申请事件
export class AuthProvinceCompanyRequestedEvent extends DomainEvent {
constructor(public readonly payload: {
authorizationId: string
userId: string
provinceCode: string
provinceName: string
}) {
super()
}
}
// 授权市公司申请事件
export class AuthCityCompanyRequestedEvent extends DomainEvent {
constructor(public readonly payload: {
authorizationId: string
userId: string
cityCode: string
cityName: string
}) {
super()
}
}
// 正式省公司授权事件
export class ProvinceCompanyAuthorizedEvent extends DomainEvent {
constructor(public readonly payload: {
authorizationId: string
userId: string
provinceCode: string
provinceName: string
authorizedBy: string
}) {
super()
}
}
// 正式市公司授权事件
export class CityCompanyAuthorizedEvent extends DomainEvent {
constructor(public readonly payload: {
authorizationId: string
userId: string
cityCode: string
cityName: string
authorizedBy: string
}) {
super()
}
}
// 角色授权事件
export class RoleAuthorizedEvent extends DomainEvent {
constructor(public readonly payload: {
authorizationId: string
userId: string
roleType: RoleType
regionCode: string
authorizedBy: string
}) {
super()
}
}
// 角色撤销事件
export class RoleRevokedEvent extends DomainEvent {
constructor(public readonly payload: {
authorizationId: string
userId: string
roleType: RoleType
regionCode: string
reason: string
revokedBy: string
}) {
super()
}
}
// 权益激活事件
export class BenefitActivatedEvent extends DomainEvent {
constructor(public readonly payload: {
authorizationId: string
userId: string
roleType: RoleType
regionCode: string
}) {
super()
}
}
// 权益失效事件
export class BenefitDeactivatedEvent extends DomainEvent {
constructor(public readonly payload: {
authorizationId: string
userId: string
roleType: RoleType
reason: string
}) {
super()
}
}
// 月度考核通过事件
export class MonthlyAssessmentPassedEvent extends DomainEvent {
constructor(public readonly payload: {
assessmentId: string
userId: string
roleType: RoleType
month: string
cumulativeCompleted: number
cumulativeTarget: number
}) {
super()
}
}
// 月度考核失败事件
export class MonthlyAssessmentFailedEvent extends DomainEvent {
constructor(public readonly payload: {
assessmentId: string
userId: string
roleType: RoleType
month: string
cumulativeCompleted: number
cumulativeTarget: number
reason: string
}) {
super()
}
}
// 单月豁免授予事件
export class MonthlyBypassGrantedEvent extends DomainEvent {
constructor(public readonly payload: {
assessmentId: string
userId: string
roleType: RoleType
month: string
grantedBy: string
}) {
super()
}
}
// 占比考核豁免事件
export class PercentageCheckExemptedEvent extends DomainEvent {
constructor(public readonly payload: {
authorizationId: string
userId: string
exemptedBy: string
}) {
super()
}
}
// 第一名达成事件
export class FirstPlaceAchievedEvent extends DomainEvent {
constructor(public readonly payload: {
assessmentId: string
userId: string
roleType: RoleType
regionCode: string
month: string
}) {
super()
}
}
Kafka事件主题
// 发布的事件主题
export const AUTHORIZATION_TOPICS = {
// 授权相关
AUTHORIZATION_REQUESTED: 'authorization.requested',
AUTHORIZATION_GRANTED: 'authorization.granted',
AUTHORIZATION_REVOKED: 'authorization.revoked',
// 权益相关
BENEFIT_ACTIVATED: 'authorization.benefit.activated',
BENEFIT_DEACTIVATED: 'authorization.benefit.deactivated',
// 考核相关
ASSESSMENT_PASSED: 'authorization.assessment.passed',
ASSESSMENT_FAILED: 'authorization.assessment.failed',
ASSESSMENT_BYPASSED: 'authorization.assessment.bypassed',
// 排名相关
FIRST_PLACE_ACHIEVED: 'authorization.ranking.first-place'
}
// 订阅的事件主题
export const SUBSCRIBED_TOPICS = {
// 从Planting Context订阅
PLANTING_ORDER_PAID: 'planting.order.paid',
// 从Referral Context订阅
TEAM_STATS_UPDATED: 'referral.team-stats.updated'
}
跨上下文集成
与Referral Context集成
// 获取团队统计数据
interface TeamStatistics {
userId: string
totalTeamPlantingCount: number
directReferralCount: number
teamMemberCount: number
provinceTeamCounts: Map<string, number> // 各省团队认种数
cityTeamCounts: Map<string, number> // 各市团队认种数
getProvinceTeamCount(provinceCode: string): number
getCityTeamCount(cityCode: string): number
}
与Planting Context集成
// 监听认种事件,检查是否激活权益
@EventHandler(PlantingOrderPaidEvent)
async handlePlantingOrderPaid(event: PlantingOrderPaidEvent): Promise<void> {
// 检查用户是否有待激活的授权
const pendingAuths = await this.authorizationRepository.findPendingByUserId(
UserId.create(event.userId)
)
for (const auth of pendingAuths) {
// 检查是否达到初始考核目标
const teamStats = await this.statsRepository.findByUserId(auth.userId)
if (teamStats && teamStats.totalTeamPlantingCount >= auth.getInitialTarget()) {
auth.activateBenefit()
await this.authorizationRepository.save(auth)
// 发布权益激活事件
}
}
}
与Reward Context集成
// 发布权益激活/失效事件,通知Reward Context调整奖励计算
// BenefitActivatedEvent -> Reward Context开始计算该用户的权益收益
// BenefitDeactivatedEvent -> Reward Context停止计算该用户的权益收益
与Identity Context集成
// 获取用户信息用于显示
interface UserInfo {
userId: string
nickname: string
avatarUrl: string
provinceCode: string
cityCode: string
}
// 发布授权事件,更新用户头像显示标识
// RoleAuthorizedEvent -> Identity Context更新用户displayTitle
// RoleRevokedEvent -> Identity Context清除用户displayTitle
目录结构
authorization-service/
├── src/
│ ├── domain/
│ │ ├── aggregates/
│ │ │ ├── authorization-role.aggregate.ts
│ │ │ └── monthly-assessment.aggregate.ts
│ │ ├── entities/
│ │ │ ├── ladder-target-rule.entity.ts
│ │ │ ├── planting-restriction.entity.ts
│ │ │ └── admin-approval.entity.ts
│ │ ├── value-objects/
│ │ │ ├── authorization-id.vo.ts
│ │ │ ├── assessment-id.vo.ts
│ │ │ ├── region-code.vo.ts
│ │ │ ├── month.vo.ts
│ │ │ ├── assessment-config.vo.ts
│ │ │ ├── benefit-amount.vo.ts
│ │ │ ├── validation-result.vo.ts
│ │ │ └── restriction-check-result.vo.ts
│ │ ├── services/
│ │ │ ├── authorization-validator.service.ts
│ │ │ ├── assessment-calculator.service.ts
│ │ │ └── planting-restriction.service.ts
│ │ ├── repositories/
│ │ │ ├── authorization-role.repository.ts
│ │ │ ├── monthly-assessment.repository.ts
│ │ │ ├── planting-restriction.repository.ts
│ │ │ └── admin-approval.repository.ts
│ │ ├── events/
│ │ │ └── authorization.events.ts
│ │ └── errors/
│ │ └── domain-errors.ts
│ │
│ ├── application/
│ │ ├── services/
│ │ │ ├── authorization-application.service.ts
│ │ │ └── admin-approval.service.ts
│ │ ├── commands/
│ │ │ ├── apply-community-auth.command.ts
│ │ │ ├── apply-auth-province-company.command.ts
│ │ │ ├── apply-auth-city-company.command.ts
│ │ │ ├── grant-province-company.command.ts
│ │ │ ├── grant-city-company.command.ts
│ │ │ ├── revoke-authorization.command.ts
│ │ │ ├── grant-monthly-bypass.command.ts
│ │ │ └── exempt-percentage-check.command.ts
│ │ ├── queries/
│ │ │ ├── get-user-authorizations.query.ts
│ │ │ ├── get-stickman-ranking.query.ts
│ │ │ └── get-region-heat.query.ts
│ │ ├── dtos/
│ │ │ ├── authorization.dto.ts
│ │ │ ├── assessment.dto.ts
│ │ │ └── stickman-ranking.dto.ts
│ │ ├── schedulers/
│ │ │ └── monthly-assessment.scheduler.ts
│ │ └── event-handlers/
│ │ ├── planting-order-paid.handler.ts
│ │ └── team-stats-updated.handler.ts
│ │
│ ├── infrastructure/
│ │ ├── persistence/
│ │ │ ├── prisma/
│ │ │ │ └── schema.prisma
│ │ │ ├── repositories/
│ │ │ │ ├── prisma-authorization-role.repository.ts
│ │ │ │ ├── prisma-monthly-assessment.repository.ts
│ │ │ │ └── prisma-admin-approval.repository.ts
│ │ │ └── mappers/
│ │ │ ├── authorization-role.mapper.ts
│ │ │ └── monthly-assessment.mapper.ts
│ │ ├── messaging/
│ │ │ ├── kafka/
│ │ │ │ ├── kafka.module.ts
│ │ │ │ ├── kafka-producer.service.ts
│ │ │ │ └── kafka-consumer.service.ts
│ │ │ └── event-bus.ts
│ │ ├── cache/
│ │ │ └── redis-cache.service.ts
│ │ └── external/
│ │ ├── referral-context.client.ts
│ │ └── identity-context.client.ts
│ │
│ ├── presentation/
│ │ ├── controllers/
│ │ │ ├── authorization.controller.ts
│ │ │ └── admin-authorization.controller.ts
│ │ ├── guards/
│ │ │ ├── jwt-auth.guard.ts
│ │ │ └── admin.guard.ts
│ │ ├── decorators/
│ │ │ ├── current-user.decorator.ts
│ │ │ ├── current-admin.decorator.ts
│ │ │ └── require-approval.decorator.ts
│ │ └── interceptors/
│ │ └── approval.interceptor.ts
│ │
│ ├── app.module.ts
│ └── main.ts
│
├── test/
│ ├── unit/
│ │ ├── domain/
│ │ └── application/
│ └── integration/
│
├── prisma/
│ ├── schema.prisma
│ └── migrations/
│
├── Dockerfile
├── package.json
├── tsconfig.json
└── DEVELOPMENT_GUIDE.md
注意事项
- 阶梯考核逻辑:豁免当月后,下月继续考核上月的累计目标,月份索引不递增
- 团队唯一性:必须检查上下级整条推荐链,确保同一省/市授权在团队内唯一
- 占比考核:自有团队占比5%是参与评选第一名的前提条件,可豁免
- 排名规则:超越比例相同时,以达标时间先后排序
- 三人审批:涉及数据修改或授权的管理操作需要三个管理员审批通过
- 权益失效:月度考核不达标后权益失效,需重新完成初始考核才能重新激活
- 热度数据:可通过配置控制是否允许未认种用户查看各省市认种热度