# 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 { // 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 { // 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 { // 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 { 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 findById(authorizationId: AuthorizationId): Promise findByUserIdAndRoleType(userId: UserId, roleType: RoleType): Promise findByUserIdRoleTypeAndRegion(userId: UserId, roleType: RoleType, regionCode: RegionCode): Promise findByUserId(userId: UserId): Promise findActiveByRoleTypeAndRegion(roleType: RoleType, regionCode: RegionCode): Promise findAllActive(roleType?: RoleType): Promise findPendingByUserId(userId: UserId): Promise } // domain/repositories/monthly-assessment.repository.ts export interface IMonthlyAssessmentRepository { save(assessment: MonthlyAssessment): Promise saveAll(assessments: MonthlyAssessment[]): Promise findById(assessmentId: AssessmentId): Promise findByAuthorizationAndMonth(authorizationId: AuthorizationId, month: Month): Promise findByUserAndMonth(userId: UserId, month: Month): Promise findFirstByAuthorization(authorizationId: AuthorizationId): Promise findByMonthAndRegion(month: Month, roleType: RoleType, regionCode: RegionCode): Promise findRankingsByMonthAndRegion(month: Month, roleType: RoleType, regionCode: RegionCode): Promise } // domain/repositories/planting-restriction.repository.ts export interface IPlantingRestrictionRepository { save(restriction: PlantingRestriction): Promise findActiveAccountRestriction(): Promise findActiveTotalRestriction(): Promise incrementTotalCount(restrictionId: string, count: number): Promise } ``` --- ## 应用层设计 ### 应用服务 ```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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { const authorizations = await this.authorizationRepository.findByUserId( UserId.create(query.userId) ) return authorizations.map(auth => this.toAuthorizationDTO(auth)) } /** * 查询火柴人排名数据 */ async getStickmanRanking(query: GetStickmanRankingQuery): Promise { 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 { 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 { 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 { 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 { const map = new Map() 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> { 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> { 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> { 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> { 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> { 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> { 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> { 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> { 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> { 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> { 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> { 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> { 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> { await this.authorizationService.exemptLocalPercentageCheck({ authorizationId, adminId: admin.adminId }) return ApiResponse.success() } /** * 获取授权列表 */ @Get() async getAuthorizations( @Query() query: GetAuthorizationsQueryDTO ): Promise>> { const result = await this.authorizationService.getAuthorizationsList(query) return ApiResponse.success(result) } /** * 获取考核列表 */ @Get('assessments') async getAssessments( @Query() query: GetAssessmentsQueryDTO ): Promise>> { 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> { await this.authorizationService.setPlantingRestriction({ ...dto, adminId: admin.adminId }) return ApiResponse.success() } /** * 获取审批列表 */ @Get('approvals') async getApprovals( @Query() query: GetApprovalsQueryDTO ): Promise>> { 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> { 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> { 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 // 各省团队认种数 cityTeamCounts: Map // 各市团队认种数 getProvinceTeamCount(provinceCode: string): number getCityTeamCount(cityCode: string): number } ``` ### 与Planting Context集成 ```typescript // 监听认种事件,检查是否激活权益 @EventHandler(PlantingOrderPaidEvent) async handlePlantingOrderPaid(event: PlantingOrderPaidEvent): Promise { // 检查用户是否有待激活的授权 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. **热度数据**:可通过配置控制是否允许未认种用户查看各省市认种热度