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

3152 lines
97 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# 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
```prisma
// 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
```typescript
// 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
```typescript
// 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
}
}
```
```typescript
// 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
```typescript
// 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
```typescript
// 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)
}
}
```
```typescript
// 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
}
}
```
```typescript
// 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
```typescript
// 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>
}
```
---
## 应用层设计
### 应用服务
```typescript
// 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
}
}
}
```
### 定时任务
```typescript
// 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
```typescript
// 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
```typescript
// 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()
}
}
```
---
## 领域事件
```typescript
// 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事件主题
```typescript
// 发布的事件主题
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集成
```typescript
// 获取团队统计数据
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集成
```typescript
// 监听认种事件,检查是否激活权益
@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集成
```typescript
// 发布权益激活/失效事件通知Reward Context调整奖励计算
// BenefitActivatedEvent -> Reward Context开始计算该用户的权益收益
// BenefitDeactivatedEvent -> Reward Context停止计算该用户的权益收益
```
### 与Identity Context集成
```typescript
// 获取用户信息用于显示
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. **热度数据**:可通过配置控制是否允许未认种用户查看各省市认种热度