# Reward Service 开发指导 ## 项目概述 Reward Service 是 RWA 榴莲皇后平台的权益奖励微服务,负责管理奖励计算与分配、24小时倒计时管理、收益状态流转、结算处理等功能。 ### 核心职责 ✅ - 权益规则定义(6种权益类型) - 奖励计算与分配 - 24小时倒计时管理(待领取→可结算/过期) - 收益状态流转(待领取→可结算→已结算/过期) - 结算处理(选择币种,触发SWAP) - 过期奖励转移至总部社区 ### 不负责 ❌ - 推荐关系查询(Referral Context) - 授权资格判定(Authorization Context) - 钱包余额管理(Wallet Context) - 区块链SWAP执行(Wallet Context) --- ## 技术栈 | 组件 | 技术选型 | |------|----------| | **框架** | NestJS 10.x | | **数据库** | PostgreSQL + Prisma ORM | | **架构** | DDD + Hexagonal Architecture (六边形架构) | | **语言** | TypeScript 5.x | | **消息队列** | Kafka (kafkajs) | | **缓存** | Redis (ioredis) | | **定时任务** | @nestjs/schedule | | **API文档** | Swagger (@nestjs/swagger) | --- ## 核心业务规则 ### 1. 奖励分配规则(每棵树 2199 USDT) | 权益类型 | 金额 | 算力 | 分配目标 | 说明 | |---------|------|------|---------|------| | 成本账户 | 400 USDT | - | 系统指定账户 | 固定分配 | | 运营账户 | 300 USDT | - | 系统指定账户 | 固定分配 | | 总部社区 | 9 USDT | - | 总部社区账户 | 固定分配 | | **分享权益** | 500 USDT | - | 推荐人账户 | 进入待结算或可结算 | | 省区域权益 | 15 USDT | 1% | 系统省公司账户 | 固定分配 | | **省团队权益** | 20 USDT | - | 最近授权省公司 | 无则进总部社区 | | 市区域权益 | 35 USDT | 2% | 系统市公司账户 | 固定分配 | | **市团队权益** | 40 USDT | - | 最近授权市公司 | 无则进总部社区 | | **社区权益** | 80 USDT | - | 最近社区 | 无则进总部社区 | | RWAD底池 | 800 USDT | - | 底池注入 | 5天批次注入 | ### 2. 24小时待领取规则 ``` 用户A推荐用户B → B认种 → A获得分享权益 情况1: A已认种 → 奖励直接进入A的"可结算收益" 情况2: A未认种 → 奖励进入A的"待领取收益"(24小时倒计时) → 24小时内A认种 → 待领取转为可结算 → 24小时后A仍未认种 → 待领取过期,转入总部社区 ``` ### 3. 收益状态流转 ``` [PENDING] 待领取 │ ├── 用户认种 ──→ [SETTLEABLE] 可结算 │ │ │ └── 用户结算 ──→ [SETTLED] 已结算 │ └── 24小时后未认种 ──→ [EXPIRED] 已过期 ──→ 转入总部社区 ``` ### 4. 结算规则 - 用户点击结算按钮 - 可结算收益通过 DST 公链 SWAP 兑换为选定币种(BNB/OG/USDT/DST) - DST 可在 APP 交易功能中卖出转换为 USDT --- ## 架构设计 ``` reward-service/ ├── prisma/ │ ├── schema.prisma # 数据库模型定义 │ └── migrations/ # 数据库迁移文件 │ ├── src/ │ ├── api/ # 🔵 Presentation Layer (表现层) │ │ ├── controllers/ │ │ │ ├── health.controller.ts │ │ │ ├── reward.controller.ts │ │ │ └── settlement.controller.ts │ │ ├── dto/ │ │ │ ├── request/ │ │ │ │ └── settle-rewards.dto.ts │ │ │ └── response/ │ │ │ ├── reward-summary.dto.ts │ │ │ ├── reward-entry.dto.ts │ │ │ └── settlement-result.dto.ts │ │ └── api.module.ts │ │ │ ├── application/ # 🟢 Application Layer (应用层) │ │ ├── commands/ │ │ │ ├── distribute-rewards/ │ │ │ │ ├── distribute-rewards.command.ts │ │ │ │ └── distribute-rewards.handler.ts │ │ │ ├── claim-pending-rewards/ │ │ │ │ ├── claim-pending-rewards.command.ts │ │ │ │ └── claim-pending-rewards.handler.ts │ │ │ ├── settle-rewards/ │ │ │ │ ├── settle-rewards.command.ts │ │ │ │ └── settle-rewards.handler.ts │ │ │ ├── expire-pending-rewards/ │ │ │ │ ├── expire-pending-rewards.command.ts │ │ │ │ └── expire-pending-rewards.handler.ts │ │ │ └── index.ts │ │ ├── queries/ │ │ │ ├── get-reward-summary/ │ │ │ │ ├── get-reward-summary.query.ts │ │ │ │ └── get-reward-summary.handler.ts │ │ │ ├── get-reward-details/ │ │ │ │ ├── get-reward-details.query.ts │ │ │ │ └── get-reward-details.handler.ts │ │ │ └── index.ts │ │ ├── services/ │ │ │ └── reward-application.service.ts │ │ ├── schedulers/ │ │ │ └── reward-expiration.scheduler.ts │ │ └── application.module.ts │ │ │ ├── domain/ # 🟡 Domain Layer (领域层) │ │ ├── aggregates/ │ │ │ ├── reward-ledger-entry/ │ │ │ │ ├── reward-ledger-entry.aggregate.ts │ │ │ │ ├── reward-ledger-entry.spec.ts │ │ │ │ └── index.ts │ │ │ └── reward-summary/ │ │ │ ├── reward-summary.aggregate.ts │ │ │ ├── reward-summary.spec.ts │ │ │ └── index.ts │ │ ├── value-objects/ │ │ │ ├── entry-id.vo.ts │ │ │ ├── reward-source.vo.ts │ │ │ ├── right-type.enum.ts │ │ │ ├── reward-status.enum.ts │ │ │ ├── money.vo.ts │ │ │ ├── hashpower.vo.ts │ │ │ ├── pending-rewards.vo.ts │ │ │ ├── settleable-rewards.vo.ts │ │ │ ├── settled-rewards.vo.ts │ │ │ ├── expired-rewards.vo.ts │ │ │ └── index.ts │ │ ├── entities/ │ │ │ └── right-definition.entity.ts │ │ ├── events/ │ │ │ ├── domain-event.base.ts │ │ │ ├── reward-created.event.ts │ │ │ ├── reward-claimed.event.ts │ │ │ ├── reward-settled.event.ts │ │ │ ├── reward-expired.event.ts │ │ │ └── index.ts │ │ ├── repositories/ │ │ │ ├── reward-ledger-entry.repository.interface.ts │ │ │ ├── reward-summary.repository.interface.ts │ │ │ └── index.ts │ │ ├── services/ │ │ │ ├── reward-calculation.service.ts │ │ │ ├── reward-expiration.service.ts │ │ │ └── index.ts │ │ └── domain.module.ts │ │ │ ├── infrastructure/ # 🔴 Infrastructure Layer (基础设施层) │ │ ├── persistence/ │ │ │ ├── prisma/ │ │ │ │ └── prisma.service.ts │ │ │ ├── mappers/ │ │ │ │ ├── reward-ledger-entry.mapper.ts │ │ │ │ └── reward-summary.mapper.ts │ │ │ └── repositories/ │ │ │ ├── reward-ledger-entry.repository.impl.ts │ │ │ └── reward-summary.repository.impl.ts │ │ ├── external/ │ │ │ ├── referral-service/ │ │ │ │ └── referral-service.client.ts │ │ │ ├── authorization-service/ │ │ │ │ └── authorization-service.client.ts │ │ │ └── wallet-service/ │ │ │ └── wallet-service.client.ts │ │ ├── kafka/ │ │ │ ├── event-consumer.controller.ts │ │ │ ├── event-publisher.service.ts │ │ │ └── kafka.module.ts │ │ ├── redis/ │ │ │ ├── redis.service.ts │ │ │ └── redis.module.ts │ │ └── infrastructure.module.ts │ │ │ ├── shared/ # 共享模块 │ │ ├── decorators/ │ │ ├── exceptions/ │ │ ├── filters/ │ │ ├── guards/ │ │ ├── interceptors/ │ │ └── strategies/ │ │ │ ├── config/ │ │ ├── app.config.ts │ │ ├── database.config.ts │ │ ├── jwt.config.ts │ │ ├── redis.config.ts │ │ ├── kafka.config.ts │ │ └── index.ts │ │ │ ├── app.module.ts │ └── main.ts │ ├── test/ ├── .env.example ├── .env.development ├── package.json ├── tsconfig.json └── Dockerfile ``` --- ## 第一阶段:项目初始化 ### 1.1 创建 NestJS 项目 ```bash cd backend/services/reward-service npx @nestjs/cli new . --skip-git --package-manager npm ``` ### 1.2 安装依赖 ```bash # 核心依赖 npm install @nestjs/config @nestjs/swagger @nestjs/jwt @nestjs/passport @nestjs/microservices @nestjs/schedule npm install @prisma/client class-validator class-transformer uuid ioredis kafkajs npm install passport passport-jwt # 开发依赖 npm install -D prisma @types/uuid @types/passport-jwt npm install -D @nestjs/testing jest ts-jest @types/jest supertest @types/supertest ``` ### 1.3 环境变量配置 创建 `.env.development`: ```env # 应用配置 NODE_ENV=development PORT=3005 APP_NAME=reward-service # 数据库 DATABASE_URL="postgresql://postgres:postgres@localhost:5432/rwadurian_reward?schema=public" # JWT (与 identity-service 共享密钥) JWT_SECRET=your-super-secret-jwt-key-change-in-production JWT_ACCESS_EXPIRES_IN=2h # Redis REDIS_HOST=localhost REDIS_PORT=6379 REDIS_PASSWORD= # Kafka KAFKA_BROKERS=localhost:9092 KAFKA_GROUP_ID=reward-service-group KAFKA_CLIENT_ID=reward-service # 外部服务 IDENTITY_SERVICE_URL=http://localhost:3001 REFERRAL_SERVICE_URL=http://localhost:3004 AUTHORIZATION_SERVICE_URL=http://localhost:3006 WALLET_SERVICE_URL=http://localhost:3002 PLANTING_SERVICE_URL=http://localhost:3003 # 奖励过期检查间隔(毫秒) REWARD_EXPIRATION_CHECK_INTERVAL=3600000 # 总部社区账户ID HEADQUARTERS_COMMUNITY_USER_ID=1 ``` --- ## 第二阶段:数据库设计 (Prisma Schema) ### 2.1 创建 prisma/schema.prisma ```prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // ============================================ // 奖励流水表 (聚合根1 - 行为表, append-only) // 记录每一笔奖励的创建、领取、结算、过期 // ============================================ model RewardLedgerEntry { id BigInt @id @default(autoincrement()) @map("entry_id") userId BigInt @map("user_id") // 接收奖励的用户ID // === 奖励来源 === sourceOrderId BigInt @map("source_order_id") // 来源认种订单ID sourceUserId BigInt @map("source_user_id") // 触发奖励的用户ID(认种者) rightType String @map("right_type") @db.VarChar(50) // 权益类型 // === 奖励金额 === usdtAmount Decimal @map("usdt_amount") @db.Decimal(20, 8) hashpowerAmount Decimal @default(0) @map("hashpower_amount") @db.Decimal(20, 8) // === 奖励状态 === rewardStatus String @default("PENDING") @map("reward_status") @db.VarChar(20) // === 时间戳 === createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6) expireAt DateTime? @map("expire_at") // 过期时间(24h后) claimedAt DateTime? @map("claimed_at") // 领取时间(用户认种) settledAt DateTime? @map("settled_at") // 结算时间 expiredAt DateTime? @map("expired_at") // 实际过期时间 // === 备注 === memo String? @map("memo") @db.VarChar(500) @@map("reward_ledger_entries") @@index([userId, rewardStatus], name: "idx_user_status") @@index([userId, createdAt(sort: Desc)], name: "idx_user_created") @@index([sourceOrderId], name: "idx_source_order") @@index([sourceUserId], name: "idx_source_user") @@index([rightType], name: "idx_right_type") @@index([rewardStatus], name: "idx_status") @@index([expireAt], name: "idx_expire") @@index([createdAt], name: "idx_created") } // ============================================ // 奖励汇总表 (聚合根2 - 状态表) // 每个用户的收益汇总,从流水表聚合 // ============================================ model RewardSummary { id BigInt @id @default(autoincrement()) @map("summary_id") userId BigInt @unique @map("user_id") // === 待领取收益 (24h倒计时) === pendingUsdt Decimal @default(0) @map("pending_usdt") @db.Decimal(20, 8) pendingHashpower Decimal @default(0) @map("pending_hashpower") @db.Decimal(20, 8) pendingExpireAt DateTime? @map("pending_expire_at") // 最早过期时间 // === 可结算收益 === settleableUsdt Decimal @default(0) @map("settleable_usdt") @db.Decimal(20, 8) settleableHashpower Decimal @default(0) @map("settleable_hashpower") @db.Decimal(20, 8) // === 已结算收益 (累计) === settledTotalUsdt Decimal @default(0) @map("settled_total_usdt") @db.Decimal(20, 8) settledTotalHashpower Decimal @default(0) @map("settled_total_hashpower") @db.Decimal(20, 8) // === 已过期收益 (累计) === expiredTotalUsdt Decimal @default(0) @map("expired_total_usdt") @db.Decimal(20, 8) expiredTotalHashpower Decimal @default(0) @map("expired_total_hashpower") @db.Decimal(20, 8) // === 时间戳 === lastUpdateAt DateTime @default(now()) @updatedAt @map("last_update_at") createdAt DateTime @default(now()) @map("created_at") @@map("reward_summaries") @@index([userId], name: "idx_summary_user") @@index([settleableUsdt(sort: Desc)], name: "idx_settleable_desc") @@index([pendingExpireAt], name: "idx_pending_expire") } // ============================================ // 权益定义表 (配置表) // 定义每种权益的奖励规则 // ============================================ model RightDefinition { id BigInt @id @default(autoincrement()) @map("definition_id") rightType String @unique @map("right_type") @db.VarChar(50) // === 奖励规则 === usdtPerTree Decimal @map("usdt_per_tree") @db.Decimal(20, 8) hashpowerPercent Decimal @default(0) @map("hashpower_percent") @db.Decimal(5, 2) // === 分配目标 === payableTo String @map("payable_to") @db.VarChar(50) // USER_ACCOUNT/SYSTEM_ACCOUNT/HEADQUARTERS // === 规则描述 === ruleDescription String? @map("rule_description") @db.Text // === 启用状态 === isEnabled Boolean @default(true) @map("is_enabled") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@map("right_definitions") @@index([rightType], name: "idx_def_right_type") @@index([isEnabled], name: "idx_def_enabled") } // ============================================ // 结算记录表 (行为表) // 记录每次结算的详情 // ============================================ model SettlementRecord { id BigInt @id @default(autoincrement()) @map("settlement_id") userId BigInt @map("user_id") // === 结算金额 === usdtAmount Decimal @map("usdt_amount") @db.Decimal(20, 8) hashpowerAmount Decimal @map("hashpower_amount") @db.Decimal(20, 8) // === 结算币种 === settleCurrency String @map("settle_currency") @db.VarChar(10) // BNB/OG/USDT/DST receivedAmount Decimal @map("received_amount") @db.Decimal(20, 8) // 实际收到的币种数量 // === 交易信息 === swapTxHash String? @map("swap_tx_hash") @db.VarChar(100) swapRate Decimal? @map("swap_rate") @db.Decimal(20, 8) // SWAP汇率 // === 状态 === status String @default("PENDING") @map("status") @db.VarChar(20) // PENDING/SUCCESS/FAILED // === 时间戳 === createdAt DateTime @default(now()) @map("created_at") completedAt DateTime? @map("completed_at") // === 关联的奖励条目ID列表 === rewardEntryIds BigInt[] @map("reward_entry_ids") @@map("settlement_records") @@index([userId], name: "idx_settlement_user") @@index([status], name: "idx_settlement_status") @@index([createdAt], name: "idx_settlement_created") } // ============================================ // 奖励事件表 (行为表, append-only) // 用于事件溯源和审计 // ============================================ model RewardEvent { id BigInt @id @default(autoincrement()) @map("event_id") eventType String @map("event_type") @db.VarChar(50) // 聚合根信息 aggregateId String @map("aggregate_id") @db.VarChar(100) aggregateType String @map("aggregate_type") @db.VarChar(50) // 事件数据 eventData Json @map("event_data") // 元数据 userId BigInt? @map("user_id") occurredAt DateTime @default(now()) @map("occurred_at") @db.Timestamp(6) version Int @default(1) @map("version") @@map("reward_events") @@index([aggregateType, aggregateId], name: "idx_reward_event_aggregate") @@index([eventType], name: "idx_reward_event_type") @@index([userId], name: "idx_reward_event_user") @@index([occurredAt], name: "idx_reward_event_occurred") } ``` ### 2.2 初始化数据库和种子数据 ```bash # 生成 Prisma Client npx prisma generate # 创建并运行迁移 npx prisma migrate dev --name init ``` 创建 `prisma/seed.ts`: ```typescript import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); async function main() { // 初始化权益定义 const rightDefinitions = [ { rightType: 'SHARE_RIGHT', usdtPerTree: 500, hashpowerPercent: 0, payableTo: 'USER_ACCOUNT', ruleDescription: '分享权益:每棵树500 USDT,分配给推荐链', }, { rightType: 'PROVINCE_AREA_RIGHT', usdtPerTree: 15, hashpowerPercent: 1, payableTo: 'SYSTEM_ACCOUNT', ruleDescription: '省区域权益:每棵树15 USDT + 1%算力,进系统省公司账户', }, { rightType: 'PROVINCE_TEAM_RIGHT', usdtPerTree: 20, hashpowerPercent: 0, payableTo: 'USER_ACCOUNT', ruleDescription: '省团队权益:每棵树20 USDT,给最近的授权省公司', }, { rightType: 'CITY_AREA_RIGHT', usdtPerTree: 35, hashpowerPercent: 2, payableTo: 'SYSTEM_ACCOUNT', ruleDescription: '市区域权益:每棵树35 USDT + 2%算力,进系统市公司账户', }, { rightType: 'CITY_TEAM_RIGHT', usdtPerTree: 40, hashpowerPercent: 0, payableTo: 'USER_ACCOUNT', ruleDescription: '市团队权益:每棵树40 USDT,给最近的授权市公司', }, { rightType: 'COMMUNITY_RIGHT', usdtPerTree: 80, hashpowerPercent: 0, payableTo: 'USER_ACCOUNT', ruleDescription: '社区权益:每棵树80 USDT,给最近的社区', }, ]; for (const def of rightDefinitions) { await prisma.rightDefinition.upsert({ where: { rightType: def.rightType }, update: def, create: def, }); } console.log('Seed completed: Right definitions initialized'); } main() .catch((e) => { console.error(e); process.exit(1); }) .finally(async () => { await prisma.$disconnect(); }); ``` --- ## 第三阶段:领域层实现 (Domain Layer) ### 3.1 值对象 (Value Objects) #### 3.1.1 src/domain/value-objects/right-type.enum.ts ```typescript export enum RightType { SHARE_RIGHT = 'SHARE_RIGHT', // 分享权益 500U PROVINCE_AREA_RIGHT = 'PROVINCE_AREA_RIGHT', // 省区域权益 15U + 1%算力 PROVINCE_TEAM_RIGHT = 'PROVINCE_TEAM_RIGHT', // 省团队权益 20U CITY_AREA_RIGHT = 'CITY_AREA_RIGHT', // 市区域权益 35U + 2%算力 CITY_TEAM_RIGHT = 'CITY_TEAM_RIGHT', // 市团队权益 40U COMMUNITY_RIGHT = 'COMMUNITY_RIGHT', // 社区权益 80U } // 权益金额配置 export const RIGHT_AMOUNTS: Record = { [RightType.SHARE_RIGHT]: { usdt: 500, hashpowerPercent: 0 }, [RightType.PROVINCE_AREA_RIGHT]: { usdt: 15, hashpowerPercent: 1 }, [RightType.PROVINCE_TEAM_RIGHT]: { usdt: 20, hashpowerPercent: 0 }, [RightType.CITY_AREA_RIGHT]: { usdt: 35, hashpowerPercent: 2 }, [RightType.CITY_TEAM_RIGHT]: { usdt: 40, hashpowerPercent: 0 }, [RightType.COMMUNITY_RIGHT]: { usdt: 80, hashpowerPercent: 0 }, }; ``` #### 3.1.2 src/domain/value-objects/reward-status.enum.ts ```typescript export enum RewardStatus { PENDING = 'PENDING', // 待领取(24h倒计时) SETTLEABLE = 'SETTLEABLE', // 可结算 SETTLED = 'SETTLED', // 已结算 EXPIRED = 'EXPIRED', // 已过期(进总部社区) } ``` #### 3.1.3 src/domain/value-objects/money.vo.ts ```typescript export class Money { private constructor( public readonly amount: number, public readonly currency: string = 'USDT', ) { if (amount < 0) { throw new Error('金额不能为负数'); } } static USDT(amount: number): Money { return new Money(amount, 'USDT'); } static zero(): Money { return new Money(0, 'USDT'); } add(other: Money): Money { if (this.currency !== other.currency) { throw new Error('货币类型不匹配'); } return new Money(this.amount + other.amount, this.currency); } subtract(other: Money): Money { if (this.currency !== other.currency) { throw new Error('货币类型不匹配'); } return new Money(Math.max(0, this.amount - other.amount), this.currency); } multiply(factor: number): Money { return new Money(this.amount * factor, this.currency); } equals(other: Money): boolean { return this.amount === other.amount && this.currency === other.currency; } isZero(): boolean { return this.amount === 0; } isGreaterThan(other: Money): boolean { return this.amount > other.amount; } } ``` #### 3.1.4 src/domain/value-objects/hashpower.vo.ts ```typescript export class Hashpower { private constructor(public readonly value: number) { if (value < 0) { throw new Error('算力不能为负数'); } } static create(value: number): Hashpower { return new Hashpower(value); } static zero(): Hashpower { return new Hashpower(0); } /** * 根据树数量和百分比计算算力 * @param treeCount 树数量 * @param percent 算力百分比 (1 = 1%) */ static fromTreeCount(treeCount: number, percent: number): Hashpower { return new Hashpower(treeCount * percent); } add(other: Hashpower): Hashpower { return new Hashpower(this.value + other.value); } subtract(other: Hashpower): Hashpower { return new Hashpower(Math.max(0, this.value - other.value)); } equals(other: Hashpower): boolean { return this.value === other.value; } isZero(): boolean { return this.value === 0; } } ``` #### 3.1.5 src/domain/value-objects/reward-source.vo.ts ```typescript import { RightType } from './right-type.enum'; export class RewardSource { private constructor( public readonly rightType: RightType, public readonly sourceOrderId: bigint, public readonly sourceUserId: bigint, ) {} static create( rightType: RightType, sourceOrderId: bigint, sourceUserId: bigint, ): RewardSource { return new RewardSource(rightType, sourceOrderId, sourceUserId); } equals(other: RewardSource): boolean { return ( this.rightType === other.rightType && this.sourceOrderId === other.sourceOrderId && this.sourceUserId === other.sourceUserId ); } } ``` #### 3.1.6 src/domain/value-objects/index.ts ```typescript export * from './right-type.enum'; export * from './reward-status.enum'; export * from './money.vo'; export * from './hashpower.vo'; export * from './reward-source.vo'; ``` ### 3.2 领域事件 (Domain Events) #### 3.2.1 src/domain/events/domain-event.base.ts ```typescript import { v4 as uuidv4 } from 'uuid'; export abstract class DomainEvent { public readonly eventId: string; public readonly occurredAt: Date; public readonly version: number; protected constructor(version: number = 1) { this.eventId = uuidv4(); this.occurredAt = new Date(); this.version = version; } abstract get eventType(): string; abstract get aggregateId(): string; abstract get aggregateType(): string; abstract toPayload(): Record; } ``` #### 3.2.2 src/domain/events/reward-created.event.ts ```typescript import { DomainEvent } from './domain-event.base'; import { RightType } from '../value-objects/right-type.enum'; import { RewardStatus } from '../value-objects/reward-status.enum'; export interface RewardCreatedPayload { entryId: string; userId: string; sourceOrderId: string; sourceUserId: string; rightType: RightType; usdtAmount: number; hashpowerAmount: number; rewardStatus: RewardStatus; expireAt: Date | null; } export class RewardCreatedEvent extends DomainEvent { constructor(private readonly payload: RewardCreatedPayload) { super(); } get eventType(): string { return 'RewardCreated'; } get aggregateId(): string { return this.payload.entryId; } get aggregateType(): string { return 'RewardLedgerEntry'; } toPayload(): RewardCreatedPayload { return { ...this.payload }; } } ``` #### 3.2.3 src/domain/events/reward-claimed.event.ts ```typescript import { DomainEvent } from './domain-event.base'; export interface RewardClaimedPayload { entryId: string; userId: string; usdtAmount: number; hashpowerAmount: number; } export class RewardClaimedEvent extends DomainEvent { constructor(private readonly payload: RewardClaimedPayload) { super(); } get eventType(): string { return 'RewardClaimed'; } get aggregateId(): string { return this.payload.entryId; } get aggregateType(): string { return 'RewardLedgerEntry'; } toPayload(): RewardClaimedPayload { return { ...this.payload }; } } ``` #### 3.2.4 src/domain/events/reward-expired.event.ts ```typescript import { DomainEvent } from './domain-event.base'; export interface RewardExpiredPayload { entryId: string; userId: string; usdtAmount: number; hashpowerAmount: number; transferredTo: string; // 转移到的目标账户 (总部社区) } export class RewardExpiredEvent extends DomainEvent { constructor(private readonly payload: RewardExpiredPayload) { super(); } get eventType(): string { return 'RewardExpired'; } get aggregateId(): string { return this.payload.entryId; } get aggregateType(): string { return 'RewardLedgerEntry'; } toPayload(): RewardExpiredPayload { return { ...this.payload }; } } ``` #### 3.2.5 src/domain/events/reward-settled.event.ts ```typescript import { DomainEvent } from './domain-event.base'; export interface RewardSettledPayload { entryId: string; userId: string; usdtAmount: number; hashpowerAmount: number; settleCurrency: string; receivedAmount: number; } export class RewardSettledEvent extends DomainEvent { constructor(private readonly payload: RewardSettledPayload) { super(); } get eventType(): string { return 'RewardSettled'; } get aggregateId(): string { return this.payload.entryId; } get aggregateType(): string { return 'RewardLedgerEntry'; } toPayload(): RewardSettledPayload { return { ...this.payload }; } } ``` ### 3.3 聚合根 (Aggregates) #### 3.3.1 src/domain/aggregates/reward-ledger-entry/reward-ledger-entry.aggregate.ts ```typescript import { DomainEvent } from '../../events/domain-event.base'; import { RewardCreatedEvent } from '../../events/reward-created.event'; import { RewardClaimedEvent } from '../../events/reward-claimed.event'; import { RewardExpiredEvent } from '../../events/reward-expired.event'; import { RewardSettledEvent } from '../../events/reward-settled.event'; import { RewardSource } from '../../value-objects/reward-source.vo'; import { RewardStatus } from '../../value-objects/reward-status.enum'; import { Money } from '../../value-objects/money.vo'; import { Hashpower } from '../../value-objects/hashpower.vo'; /** * 奖励流水聚合根 * * 不变式: * 1. 待领取奖励必须在24小时内认种,否则过期 * 2. 只有待领取状态才能领取(claim) * 3. 只有可结算状态才能结算(settle) * 4. 已结算/已过期的奖励状态不可变更 */ export class RewardLedgerEntry { private _id: bigint | null = null; private readonly _userId: bigint; private readonly _rewardSource: RewardSource; private readonly _usdtAmount: Money; private readonly _hashpowerAmount: Hashpower; private _rewardStatus: RewardStatus; private readonly _createdAt: Date; private _expireAt: Date | null; private _claimedAt: Date | null; private _settledAt: Date | null; private _expiredAt: Date | null; private readonly _memo: string; private _domainEvents: DomainEvent[] = []; private constructor( userId: bigint, rewardSource: RewardSource, usdtAmount: Money, hashpowerAmount: Hashpower, rewardStatus: RewardStatus, createdAt: Date, expireAt: Date | null, memo: string, ) { this._userId = userId; this._rewardSource = rewardSource; this._usdtAmount = usdtAmount; this._hashpowerAmount = hashpowerAmount; this._rewardStatus = rewardStatus; this._createdAt = createdAt; this._expireAt = expireAt; this._claimedAt = null; this._settledAt = null; this._expiredAt = null; this._memo = memo; } // ============ Getters ============ get id(): bigint | null { return this._id; } get userId(): bigint { return this._userId; } get rewardSource(): RewardSource { return this._rewardSource; } get usdtAmount(): Money { return this._usdtAmount; } get hashpowerAmount(): Hashpower { return this._hashpowerAmount; } get rewardStatus(): RewardStatus { return this._rewardStatus; } get createdAt(): Date { return this._createdAt; } get expireAt(): Date | null { return this._expireAt; } get claimedAt(): Date | null { return this._claimedAt; } get settledAt(): Date | null { return this._settledAt; } get expiredAt(): Date | null { return this._expiredAt; } get memo(): string { return this._memo; } get domainEvents(): DomainEvent[] { return [...this._domainEvents]; } get isPending(): boolean { return this._rewardStatus === RewardStatus.PENDING; } get isSettleable(): boolean { return this._rewardStatus === RewardStatus.SETTLEABLE; } get isSettled(): boolean { return this._rewardStatus === RewardStatus.SETTLED; } get isExpired(): boolean { return this._rewardStatus === RewardStatus.EXPIRED; } // ============ 工厂方法 ============ /** * 创建待领取奖励 (24小时倒计时) * 用于推荐人未认种的情况 */ static createPending(params: { userId: bigint; rewardSource: RewardSource; usdtAmount: Money; hashpowerAmount: Hashpower; memo?: string; }): RewardLedgerEntry { const now = new Date(); const expireAt = new Date(now.getTime() + 24 * 60 * 60 * 1000); // 24小时后 const entry = new RewardLedgerEntry( params.userId, params.rewardSource, params.usdtAmount, params.hashpowerAmount, RewardStatus.PENDING, now, expireAt, params.memo || '', ); entry._domainEvents.push(new RewardCreatedEvent({ entryId: entry._id?.toString() || 'temp', userId: entry._userId.toString(), sourceOrderId: entry._rewardSource.sourceOrderId.toString(), sourceUserId: entry._rewardSource.sourceUserId.toString(), rightType: entry._rewardSource.rightType, usdtAmount: entry._usdtAmount.amount, hashpowerAmount: entry._hashpowerAmount.value, rewardStatus: entry._rewardStatus, expireAt: entry._expireAt, })); return entry; } /** * 创建直接可结算奖励 (无需24小时等待) * 用于推荐人已认种的情况 */ static createSettleable(params: { userId: bigint; rewardSource: RewardSource; usdtAmount: Money; hashpowerAmount: Hashpower; memo?: string; }): RewardLedgerEntry { const entry = new RewardLedgerEntry( params.userId, params.rewardSource, params.usdtAmount, params.hashpowerAmount, RewardStatus.SETTLEABLE, new Date(), null, params.memo || '', ); entry._domainEvents.push(new RewardCreatedEvent({ entryId: entry._id?.toString() || 'temp', userId: entry._userId.toString(), sourceOrderId: entry._rewardSource.sourceOrderId.toString(), sourceUserId: entry._rewardSource.sourceUserId.toString(), rightType: entry._rewardSource.rightType, usdtAmount: entry._usdtAmount.amount, hashpowerAmount: entry._hashpowerAmount.value, rewardStatus: entry._rewardStatus, expireAt: null, })); return entry; } // ============ 领域行为 ============ /** * 领取奖励 (用户认种后,待领取 → 可结算) */ claim(): void { if (this._rewardStatus !== RewardStatus.PENDING) { throw new Error('只有待领取状态才能领取'); } if (this.isExpiredNow()) { throw new Error('奖励已过期,无法领取'); } this._rewardStatus = RewardStatus.SETTLEABLE; this._claimedAt = new Date(); this._expireAt = null; this._domainEvents.push(new RewardClaimedEvent({ entryId: this._id?.toString() || '', userId: this._userId.toString(), usdtAmount: this._usdtAmount.amount, hashpowerAmount: this._hashpowerAmount.value, })); } /** * 过期 (24小时后未认种) */ expire(): void { if (this._rewardStatus !== RewardStatus.PENDING) { throw new Error('只有待领取状态才能过期'); } this._rewardStatus = RewardStatus.EXPIRED; this._expiredAt = new Date(); this._domainEvents.push(new RewardExpiredEvent({ entryId: this._id?.toString() || '', userId: this._userId.toString(), usdtAmount: this._usdtAmount.amount, hashpowerAmount: this._hashpowerAmount.value, transferredTo: 'HEADQUARTERS_COMMUNITY', })); } /** * 结算 (可结算 → 已结算) */ settle(settleCurrency: string, receivedAmount: number): void { if (this._rewardStatus !== RewardStatus.SETTLEABLE) { throw new Error('只有可结算状态才能结算'); } this._rewardStatus = RewardStatus.SETTLED; this._settledAt = new Date(); this._domainEvents.push(new RewardSettledEvent({ entryId: this._id?.toString() || '', userId: this._userId.toString(), usdtAmount: this._usdtAmount.amount, hashpowerAmount: this._hashpowerAmount.value, settleCurrency, receivedAmount, })); } /** * 检查是否已过期 */ isExpiredNow(): boolean { if (!this._expireAt) return false; return new Date() > this._expireAt; } /** * 获取剩余过期时间 (毫秒) */ getRemainingTimeMs(): number { if (!this._expireAt) return 0; const remaining = this._expireAt.getTime() - Date.now(); return Math.max(0, remaining); } setId(id: bigint): void { this._id = id; } clearDomainEvents(): void { this._domainEvents = []; } // ============ 重建 ============ static reconstitute(data: { id: bigint; userId: bigint; rewardSource: RewardSource; usdtAmount: number; hashpowerAmount: number; rewardStatus: RewardStatus; createdAt: Date; expireAt: Date | null; claimedAt: Date | null; settledAt: Date | null; expiredAt: Date | null; memo: string; }): RewardLedgerEntry { const entry = new RewardLedgerEntry( data.userId, data.rewardSource, Money.USDT(data.usdtAmount), Hashpower.create(data.hashpowerAmount), data.rewardStatus, data.createdAt, data.expireAt, data.memo, ); entry._id = data.id; entry._claimedAt = data.claimedAt; entry._settledAt = data.settledAt; entry._expiredAt = data.expiredAt; return entry; } } ``` #### 3.3.2 src/domain/aggregates/reward-summary/reward-summary.aggregate.ts ```typescript import { Money } from '../../value-objects/money.vo'; import { Hashpower } from '../../value-objects/hashpower.vo'; /** * 奖励汇总聚合根 * 维护用户的各类收益汇总数据 */ export class RewardSummary { private _id: bigint | null = null; private readonly _userId: bigint; // 待领取收益 private _pendingUsdt: Money; private _pendingHashpower: Hashpower; private _pendingExpireAt: Date | null; // 可结算收益 private _settleableUsdt: Money; private _settleableHashpower: Hashpower; // 已结算收益 (累计) private _settledTotalUsdt: Money; private _settledTotalHashpower: Hashpower; // 已过期收益 (累计) private _expiredTotalUsdt: Money; private _expiredTotalHashpower: Hashpower; private _lastUpdateAt: Date; private readonly _createdAt: Date; private constructor(userId: bigint) { this._userId = userId; this._pendingUsdt = Money.zero(); this._pendingHashpower = Hashpower.zero(); this._pendingExpireAt = null; this._settleableUsdt = Money.zero(); this._settleableHashpower = Hashpower.zero(); this._settledTotalUsdt = Money.zero(); this._settledTotalHashpower = Hashpower.zero(); this._expiredTotalUsdt = Money.zero(); this._expiredTotalHashpower = Hashpower.zero(); this._lastUpdateAt = new Date(); this._createdAt = new Date(); } // ============ Getters ============ get id(): bigint | null { return this._id; } get userId(): bigint { return this._userId; } get pendingUsdt(): Money { return this._pendingUsdt; } get pendingHashpower(): Hashpower { return this._pendingHashpower; } get pendingExpireAt(): Date | null { return this._pendingExpireAt; } get settleableUsdt(): Money { return this._settleableUsdt; } get settleableHashpower(): Hashpower { return this._settleableHashpower; } get settledTotalUsdt(): Money { return this._settledTotalUsdt; } get settledTotalHashpower(): Hashpower { return this._settledTotalHashpower; } get expiredTotalUsdt(): Money { return this._expiredTotalUsdt; } get expiredTotalHashpower(): Hashpower { return this._expiredTotalHashpower; } get lastUpdateAt(): Date { return this._lastUpdateAt; } get createdAt(): Date { return this._createdAt; } // ============ 工厂方法 ============ static create(userId: bigint): RewardSummary { return new RewardSummary(userId); } // ============ 领域行为 ============ /** * 增加待领取收益 */ addPending(usdt: Money, hashpower: Hashpower, expireAt: Date): void { this._pendingUsdt = this._pendingUsdt.add(usdt); this._pendingHashpower = this._pendingHashpower.add(hashpower); // 更新为最早的过期时间 if (!this._pendingExpireAt || expireAt < this._pendingExpireAt) { this._pendingExpireAt = expireAt; } this._lastUpdateAt = new Date(); } /** * 待领取 → 可结算 (用户认种) */ movePendingToSettleable(usdt: Money, hashpower: Hashpower): void { this._pendingUsdt = this._pendingUsdt.subtract(usdt); this._pendingHashpower = this._pendingHashpower.subtract(hashpower); this._settleableUsdt = this._settleableUsdt.add(usdt); this._settleableHashpower = this._settleableHashpower.add(hashpower); // 如果待领取清空了,清除过期时间 if (this._pendingUsdt.isZero()) { this._pendingExpireAt = null; } this._lastUpdateAt = new Date(); } /** * 待领取 → 已过期 */ movePendingToExpired(usdt: Money, hashpower: Hashpower): void { this._pendingUsdt = this._pendingUsdt.subtract(usdt); this._pendingHashpower = this._pendingHashpower.subtract(hashpower); this._expiredTotalUsdt = this._expiredTotalUsdt.add(usdt); this._expiredTotalHashpower = this._expiredTotalHashpower.add(hashpower); if (this._pendingUsdt.isZero()) { this._pendingExpireAt = null; } this._lastUpdateAt = new Date(); } /** * 增加可结算收益 (直接可结算的奖励) */ addSettleable(usdt: Money, hashpower: Hashpower): void { this._settleableUsdt = this._settleableUsdt.add(usdt); this._settleableHashpower = this._settleableHashpower.add(hashpower); this._lastUpdateAt = new Date(); } /** * 可结算 → 已结算 */ settle(usdt: Money, hashpower: Hashpower): void { this._settleableUsdt = this._settleableUsdt.subtract(usdt); this._settleableHashpower = this._settleableHashpower.subtract(hashpower); this._settledTotalUsdt = this._settledTotalUsdt.add(usdt); this._settledTotalHashpower = this._settledTotalHashpower.add(hashpower); this._lastUpdateAt = new Date(); } setId(id: bigint): void { this._id = id; } // ============ 重建 ============ static reconstitute(data: { id: bigint; userId: bigint; pendingUsdt: number; pendingHashpower: number; pendingExpireAt: Date | null; settleableUsdt: number; settleableHashpower: number; settledTotalUsdt: number; settledTotalHashpower: number; expiredTotalUsdt: number; expiredTotalHashpower: number; lastUpdateAt: Date; createdAt: Date; }): RewardSummary { const summary = new RewardSummary(data.userId); summary._id = data.id; summary._pendingUsdt = Money.USDT(data.pendingUsdt); summary._pendingHashpower = Hashpower.create(data.pendingHashpower); summary._pendingExpireAt = data.pendingExpireAt; summary._settleableUsdt = Money.USDT(data.settleableUsdt); summary._settleableHashpower = Hashpower.create(data.settleableHashpower); summary._settledTotalUsdt = Money.USDT(data.settledTotalUsdt); summary._settledTotalHashpower = Hashpower.create(data.settledTotalHashpower); summary._expiredTotalUsdt = Money.USDT(data.expiredTotalUsdt); summary._expiredTotalHashpower = Hashpower.create(data.expiredTotalHashpower); return summary; } } ``` ### 3.4 仓储接口 (Repository Interfaces) #### 3.4.1 src/domain/repositories/reward-ledger-entry.repository.interface.ts ```typescript import { RewardLedgerEntry } from '../aggregates/reward-ledger-entry/reward-ledger-entry.aggregate'; import { RewardStatus } from '../value-objects/reward-status.enum'; import { RightType } from '../value-objects/right-type.enum'; export interface IRewardLedgerEntryRepository { save(entry: RewardLedgerEntry): Promise; saveAll(entries: RewardLedgerEntry[]): Promise; findById(entryId: bigint): Promise; findByUserId( userId: bigint, filters?: { status?: RewardStatus; rightType?: RightType; startDate?: Date; endDate?: Date; }, pagination?: { page: number; pageSize: number }, ): Promise; findPendingByUserId(userId: bigint): Promise; findSettleableByUserId(userId: bigint): Promise; findExpiredPending(beforeDate: Date): Promise; findBySourceOrderId(sourceOrderId: bigint): Promise; countByUserId(userId: bigint, status?: RewardStatus): Promise; } export const REWARD_LEDGER_ENTRY_REPOSITORY = Symbol('IRewardLedgerEntryRepository'); ``` #### 3.4.2 src/domain/repositories/reward-summary.repository.interface.ts ```typescript import { RewardSummary } from '../aggregates/reward-summary/reward-summary.aggregate'; export interface IRewardSummaryRepository { save(summary: RewardSummary): Promise; findByUserId(userId: bigint): Promise; getOrCreate(userId: bigint): Promise; findByUserIds(userIds: bigint[]): Promise>; findTopSettleableUsers(limit: number): Promise; } export const REWARD_SUMMARY_REPOSITORY = Symbol('IRewardSummaryRepository'); ``` ### 3.5 领域服务 (Domain Services) #### 3.5.1 src/domain/services/reward-calculation.service.ts ```typescript import { Injectable, Inject } from '@nestjs/common'; import { RewardLedgerEntry } from '../aggregates/reward-ledger-entry/reward-ledger-entry.aggregate'; import { RewardSource } from '../value-objects/reward-source.vo'; import { RightType, RIGHT_AMOUNTS } from '../value-objects/right-type.enum'; import { Money } from '../value-objects/money.vo'; import { Hashpower } from '../value-objects/hashpower.vo'; // 外部服务接口 (防腐层) export interface IReferralServiceClient { getReferralChain(userId: bigint): Promise<{ ancestors: Array<{ userId: bigint; hasPlanted: boolean }>; }>; } export interface IAuthorizationServiceClient { findNearestAuthorizedProvince(userId: bigint, provinceCode: string): Promise; findNearestAuthorizedCity(userId: bigint, cityCode: string): Promise; findNearestCommunity(userId: bigint): Promise; } export const REFERRAL_SERVICE_CLIENT = Symbol('IReferralServiceClient'); export const AUTHORIZATION_SERVICE_CLIENT = Symbol('IAuthorizationServiceClient'); // 总部社区账户ID const HEADQUARTERS_COMMUNITY_USER_ID = BigInt(1); @Injectable() export class RewardCalculationService { constructor( @Inject(REFERRAL_SERVICE_CLIENT) private readonly referralService: IReferralServiceClient, @Inject(AUTHORIZATION_SERVICE_CLIENT) private readonly authorizationService: IAuthorizationServiceClient, ) {} /** * 计算认种订单产生的所有奖励 */ async calculateRewards(params: { sourceOrderId: bigint; sourceUserId: bigint; treeCount: number; provinceCode: string; cityCode: string; }): Promise { const rewards: RewardLedgerEntry[] = []; // 1. 分享权益 (500 USDT) const shareRewards = await this.calculateShareRights( params.sourceOrderId, params.sourceUserId, params.treeCount, ); rewards.push(...shareRewards); // 2. 省团队权益 (20 USDT) const provinceTeamReward = await this.calculateProvinceTeamRight( params.sourceOrderId, params.sourceUserId, params.provinceCode, params.treeCount, ); rewards.push(provinceTeamReward); // 3. 省区域权益 (15 USDT + 1%算力) const provinceAreaReward = this.calculateProvinceAreaRight( params.sourceOrderId, params.sourceUserId, params.provinceCode, params.treeCount, ); rewards.push(provinceAreaReward); // 4. 市团队权益 (40 USDT) const cityTeamReward = await this.calculateCityTeamRight( params.sourceOrderId, params.sourceUserId, params.cityCode, params.treeCount, ); rewards.push(cityTeamReward); // 5. 市区域权益 (35 USDT + 2%算力) const cityAreaReward = this.calculateCityAreaRight( params.sourceOrderId, params.sourceUserId, params.cityCode, params.treeCount, ); rewards.push(cityAreaReward); // 6. 社区权益 (80 USDT) const communityReward = await this.calculateCommunityRight( params.sourceOrderId, params.sourceUserId, params.treeCount, ); rewards.push(communityReward); return rewards; } /** * 计算分享权益 (500 USDT) */ private async calculateShareRights( sourceOrderId: bigint, sourceUserId: bigint, treeCount: number, ): Promise { const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.SHARE_RIGHT]; const usdtAmount = Money.USDT(usdt * treeCount); const hashpower = Hashpower.fromTreeCount(treeCount, hashpowerPercent); const rewardSource = RewardSource.create( RightType.SHARE_RIGHT, sourceOrderId, sourceUserId, ); // 获取推荐链 const referralChain = await this.referralService.getReferralChain(sourceUserId); if (referralChain.ancestors.length > 0) { const directReferrer = referralChain.ancestors[0]; if (directReferrer.hasPlanted) { // 推荐人已认种,直接可结算 return [RewardLedgerEntry.createSettleable({ userId: directReferrer.userId, rewardSource, usdtAmount, hashpowerAmount: hashpower, memo: `分享权益:来自用户${sourceUserId}的认种`, })]; } else { // 推荐人未认种,进入待领取(24h倒计时) return [RewardLedgerEntry.createPending({ userId: directReferrer.userId, rewardSource, usdtAmount, hashpowerAmount: hashpower, memo: `分享权益:来自用户${sourceUserId}的认种(24h内认种可领取)`, })]; } } else { // 无推荐人,进总部社区 return [RewardLedgerEntry.createSettleable({ userId: HEADQUARTERS_COMMUNITY_USER_ID, rewardSource, usdtAmount, hashpowerAmount: hashpower, memo: '分享权益:无推荐人,进总部社区', })]; } } /** * 计算省团队权益 (20 USDT) */ private async calculateProvinceTeamRight( sourceOrderId: bigint, sourceUserId: bigint, provinceCode: string, treeCount: number, ): Promise { const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.PROVINCE_TEAM_RIGHT]; const usdtAmount = Money.USDT(usdt * treeCount); const hashpower = Hashpower.fromTreeCount(treeCount, hashpowerPercent); const rewardSource = RewardSource.create( RightType.PROVINCE_TEAM_RIGHT, sourceOrderId, sourceUserId, ); // 查找最近的授权省公司 const nearestProvince = await this.authorizationService.findNearestAuthorizedProvince( sourceUserId, provinceCode, ); if (nearestProvince) { return RewardLedgerEntry.createSettleable({ userId: nearestProvince, rewardSource, usdtAmount, hashpowerAmount: hashpower, memo: `省团队权益:来自${provinceCode}省的认种`, }); } else { return RewardLedgerEntry.createSettleable({ userId: HEADQUARTERS_COMMUNITY_USER_ID, rewardSource, usdtAmount, hashpowerAmount: hashpower, memo: '省团队权益:无达标授权省公司,进总部社区', }); } } /** * 计算省区域权益 (15 USDT + 1%算力) */ private calculateProvinceAreaRight( sourceOrderId: bigint, sourceUserId: bigint, provinceCode: string, treeCount: number, ): RewardLedgerEntry { const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.PROVINCE_AREA_RIGHT]; const usdtAmount = Money.USDT(usdt * treeCount); const hashpower = Hashpower.fromTreeCount(treeCount, hashpowerPercent); const rewardSource = RewardSource.create( RightType.PROVINCE_AREA_RIGHT, sourceOrderId, sourceUserId, ); // 进系统省公司账户 (使用特殊账户ID格式) const systemProvinceAccountId = BigInt(`9${provinceCode.padStart(6, '0')}`); return RewardLedgerEntry.createSettleable({ userId: systemProvinceAccountId, rewardSource, usdtAmount, hashpowerAmount: hashpower, memo: `省区域权益:${provinceCode}省,15U + 1%算力`, }); } /** * 计算市团队权益 (40 USDT) */ private async calculateCityTeamRight( sourceOrderId: bigint, sourceUserId: bigint, cityCode: string, treeCount: number, ): Promise { const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.CITY_TEAM_RIGHT]; const usdtAmount = Money.USDT(usdt * treeCount); const hashpower = Hashpower.fromTreeCount(treeCount, hashpowerPercent); const rewardSource = RewardSource.create( RightType.CITY_TEAM_RIGHT, sourceOrderId, sourceUserId, ); // 查找最近的授权市公司 const nearestCity = await this.authorizationService.findNearestAuthorizedCity( sourceUserId, cityCode, ); if (nearestCity) { return RewardLedgerEntry.createSettleable({ userId: nearestCity, rewardSource, usdtAmount, hashpowerAmount: hashpower, memo: `市团队权益:来自${cityCode}市的认种`, }); } else { return RewardLedgerEntry.createSettleable({ userId: HEADQUARTERS_COMMUNITY_USER_ID, rewardSource, usdtAmount, hashpowerAmount: hashpower, memo: '市团队权益:无达标授权市公司,进总部社区', }); } } /** * 计算市区域权益 (35 USDT + 2%算力) */ private calculateCityAreaRight( sourceOrderId: bigint, sourceUserId: bigint, cityCode: string, treeCount: number, ): RewardLedgerEntry { const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.CITY_AREA_RIGHT]; const usdtAmount = Money.USDT(usdt * treeCount); const hashpower = Hashpower.fromTreeCount(treeCount, hashpowerPercent); const rewardSource = RewardSource.create( RightType.CITY_AREA_RIGHT, sourceOrderId, sourceUserId, ); // 进系统市公司账户 const systemCityAccountId = BigInt(`8${cityCode.padStart(6, '0')}`); return RewardLedgerEntry.createSettleable({ userId: systemCityAccountId, rewardSource, usdtAmount, hashpowerAmount: hashpower, memo: `市区域权益:${cityCode}市,35U + 2%算力`, }); } /** * 计算社区权益 (80 USDT) */ private async calculateCommunityRight( sourceOrderId: bigint, sourceUserId: bigint, treeCount: number, ): Promise { const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.COMMUNITY_RIGHT]; const usdtAmount = Money.USDT(usdt * treeCount); const hashpower = Hashpower.fromTreeCount(treeCount, hashpowerPercent); const rewardSource = RewardSource.create( RightType.COMMUNITY_RIGHT, sourceOrderId, sourceUserId, ); // 查找最近的社区 const nearestCommunity = await this.authorizationService.findNearestCommunity(sourceUserId); if (nearestCommunity) { return RewardLedgerEntry.createSettleable({ userId: nearestCommunity, rewardSource, usdtAmount, hashpowerAmount: hashpower, memo: '社区权益:来自社区成员的认种', }); } else { return RewardLedgerEntry.createSettleable({ userId: HEADQUARTERS_COMMUNITY_USER_ID, rewardSource, usdtAmount, hashpowerAmount: hashpower, memo: '社区权益:无归属社区,进总部社区', }); } } } ``` #### 3.5.2 src/domain/services/reward-expiration.service.ts ```typescript import { Injectable } from '@nestjs/common'; import { RewardLedgerEntry } from '../aggregates/reward-ledger-entry/reward-ledger-entry.aggregate'; @Injectable() export class RewardExpirationService { /** * 检查并过期所有到期的待领取奖励 */ expireOverdueRewards(pendingRewards: RewardLedgerEntry[]): RewardLedgerEntry[] { const expiredRewards: RewardLedgerEntry[] = []; for (const reward of pendingRewards) { if (reward.isExpiredNow()) { reward.expire(); expiredRewards.push(reward); } } return expiredRewards; } /** * 检查用户的待领取奖励状态 */ checkUserPendingRewards( pendingRewards: RewardLedgerEntry[], ): { expired: RewardLedgerEntry[]; stillPending: RewardLedgerEntry[]; } { const expired: RewardLedgerEntry[] = []; const stillPending: RewardLedgerEntry[] = []; for (const reward of pendingRewards) { if (reward.isExpiredNow()) { reward.expire(); expired.push(reward); } else { stillPending.push(reward); } } return { expired, stillPending }; } } ``` --- ## 领域不变式 (Domain Invariants) ```typescript // 文档参考 class RewardContextInvariants { // 1. 待领取奖励必须在24小时内认种,否则过期 static PENDING_REWARD_EXPIRES_IN_24H = "待领取奖励必须在24小时内用户认种,否则进入总部社区"; // 2. 只有可结算状态才能结算 static ONLY_SETTLEABLE_CAN_BE_SETTLED = "只有可结算状态的收益才能结算"; // 3. 已结算/已过期的奖励不可变更 static SETTLED_EXPIRED_IMMUTABLE = "已结算或已过期的奖励状态不可变更"; // 4. 奖励金额必须匹配权益定义 static REWARD_AMOUNT_MUST_MATCH_DEFINITION = "奖励金额必须符合权益定义规则"; // 5. 分享权益500 USDT必须分配给推荐链 static REFERRAL_RIGHTS_DISTRIBUTION = "分享权益500 USDT必须按推荐链分配"; // 6. 无上级授权时,权益进系统账户或总部社区 static FALLBACK_TO_SYSTEM_ACCOUNT = "无达标上级时,权益进对应系统账户或总部社区"; } ``` --- ## API 端点设计 | 方法 | 路径 | 描述 | 认证 | |------|------|------|------| | GET | `/health` | 健康检查 | 否 | | GET | `/rewards/summary` | 获取我的收益汇总 | JWT | | GET | `/rewards/details` | 获取我的奖励明细 | JWT | | GET | `/rewards/pending` | 获取待领取奖励(含倒计时) | JWT | | POST | `/rewards/settle` | 结算可结算收益 | JWT | | GET | `/rewards/settlement-history` | 获取结算历史 | JWT | --- ## 事件订阅 (Kafka Events) ### 订阅的事件 | Topic | 事件类型 | 触发条件 | 处理逻辑 | |-------|---------|---------|---------| | `planting.order.paid` | PlantingOrderPaidEvent | 认种订单支付成功 | 计算并分配所有奖励 | | `planting.order.paid` | PlantingOrderPaidEvent | 认种订单支付成功 | 检查该用户是否有待领取奖励,若有则转为可结算 | ### 发布的事件 | Topic | 事件类型 | 触发条件 | |-------|---------|---------| | `reward.created` | RewardCreatedEvent | 奖励创建成功 | | `reward.claimed` | RewardClaimedEvent | 待领取奖励被领取 | | `reward.expired` | RewardExpiredEvent | 待领取奖励过期 | | `reward.settled` | RewardSettledEvent | 奖励结算成功 | --- ## 定时任务 ### 过期奖励检查 ```typescript // application/schedulers/reward-expiration.scheduler.ts import { Injectable } from '@nestjs/common'; import { Cron, CronExpression } from '@nestjs/schedule'; import { RewardApplicationService } from '../services/reward-application.service'; @Injectable() export class RewardExpirationScheduler { constructor( private readonly rewardService: RewardApplicationService, ) {} /** * 每小时检查过期的待领取奖励 */ @Cron(CronExpression.EVERY_HOUR) async handleExpiredRewards() { console.log('开始检查过期奖励...'); const result = await this.rewardService.expireOverdueRewards(); console.log(`过期奖励处理完成:${result.expiredCount}笔,共${result.totalUsdtExpired} USDT`); } } ``` --- ## 开发顺序建议 1. **Phase 1: 项目初始化** - 创建NestJS项目 - 安装依赖 - 配置环境变量 2. **Phase 2: 数据库层** - 创建Prisma Schema - 运行迁移和种子数据 - 创建PrismaService 3. **Phase 3: 领域层** - 实现所有值对象 - 实现聚合根 (RewardLedgerEntry, RewardSummary) - 实现领域事件 - 实现领域服务 (RewardCalculationService, RewardExpirationService) - 编写单元测试 4. **Phase 4: 基础设施层** - 实现仓储 (Repository Implementations) - 实现外部服务客户端 (ReferralServiceClient, AuthorizationServiceClient, WalletServiceClient) - 实现Kafka消费者和发布者 5. **Phase 5: 应用层** - 实现应用服务 (RewardApplicationService) - 实现定时任务 (RewardExpirationScheduler) - 实现Command/Query handlers 6. **Phase 6: API层** - 实现DTO - 实现Controllers - 配置Swagger文档 - 配置JWT认证 7. **Phase 7: 测试和部署** - 集成测试 - E2E测试 - Docker配置 --- ## 与前端页面对应关系 ### 我的收益页面 - 显示待领取收益(含24小时倒计时) - 显示可结算收益 - 显示已结算总额 - 显示已过期总额 - 调用: `GET /rewards/summary` ### 奖励明细页面 - 分页显示奖励流水 - 按状态/类型筛选 - 调用: `GET /rewards/details?status=PENDING&page=1&pageSize=20` ### 结算页面 - 选择结算币种(BNB/OG/USDT/DST) - 显示可结算金额 - 点击结算按钮 - 调用: `POST /rewards/settle` --- ## 注意事项 1. **24小时倒计时**: 前端需要实时显示倒计时,后端返回 `expireAt` 和 `remainingTimeMs` 2. **结算币种选择**: 结算时支持 BNB/OG/USDT/DST 四种币种 3. **过期奖励转移**: 过期奖励自动转入总部社区账户 4. **外部服务依赖**: 需要调用 referral-service 获取推荐链,调用 authorization-service 查找授权用户 5. **SWAP执行**: 结算时调用 wallet-service 执行 SWAP 操作 6. **事务一致性**: 奖励分配需要保证事务一致性,使用数据库事务 7. **幂等性**: Kafka消费者需要实现幂等性,避免重复处理