3152 lines
97 KiB
Markdown
3152 lines
97 KiB
Markdown
# 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. **热度数据**:可通过配置控制是否允许未认种用户查看各省市认种热度
|