rwadurian/backend/services/authorization-service/DEVELOPMENT_GUIDE.md

97 KiB
Raw Permalink Blame History

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

注意事项

  1. 阶梯考核逻辑:豁免当月后,下月继续考核上月的累计目标,月份索引不递增
  2. 团队唯一性:必须检查上下级整条推荐链,确保同一省/市授权在团队内唯一
  3. 占比考核自有团队占比5%是参与评选第一名的前提条件,可豁免
  4. 排名规则:超越比例相同时,以达标时间先后排序
  5. 三人审批:涉及数据修改或授权的管理操作需要三个管理员审批通过
  6. 权益失效:月度考核不达标后权益失效,需重新完成初始考核才能重新激活
  7. 热度数据:可通过配置控制是否允许未认种用户查看各省市认种热度