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

63 KiB
Raw Blame History

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 项目

cd backend/services/reward-service
npx @nestjs/cli new . --skip-git --package-manager npm

1.2 安装依赖

# 核心依赖
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:

# 应用配置
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

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 初始化数据库和种子数据

# 生成 Prisma Client
npx prisma generate

# 创建并运行迁移
npx prisma migrate dev --name init

创建 prisma/seed.ts:

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

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, { usdt: number; hashpowerPercent: number }> = {
  [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

export enum RewardStatus {
  PENDING = 'PENDING',          // 待领取(24h倒计时)
  SETTLEABLE = 'SETTLEABLE',    // 可结算
  SETTLED = 'SETTLED',          // 已结算
  EXPIRED = 'EXPIRED',          // 已过期(进总部社区)
}

3.1.3 src/domain/value-objects/money.vo.ts

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

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

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

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

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<string, any>;
}

3.2.2 src/domain/events/reward-created.event.ts

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

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

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

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

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

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

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<void>;
  saveAll(entries: RewardLedgerEntry[]): Promise<void>;
  findById(entryId: bigint): Promise<RewardLedgerEntry | null>;
  findByUserId(
    userId: bigint,
    filters?: {
      status?: RewardStatus;
      rightType?: RightType;
      startDate?: Date;
      endDate?: Date;
    },
    pagination?: { page: number; pageSize: number },
  ): Promise<RewardLedgerEntry[]>;
  findPendingByUserId(userId: bigint): Promise<RewardLedgerEntry[]>;
  findSettleableByUserId(userId: bigint): Promise<RewardLedgerEntry[]>;
  findExpiredPending(beforeDate: Date): Promise<RewardLedgerEntry[]>;
  findBySourceOrderId(sourceOrderId: bigint): Promise<RewardLedgerEntry[]>;
  countByUserId(userId: bigint, status?: RewardStatus): Promise<number>;
}

export const REWARD_LEDGER_ENTRY_REPOSITORY = Symbol('IRewardLedgerEntryRepository');

3.4.2 src/domain/repositories/reward-summary.repository.interface.ts

import { RewardSummary } from '../aggregates/reward-summary/reward-summary.aggregate';

export interface IRewardSummaryRepository {
  save(summary: RewardSummary): Promise<void>;
  findByUserId(userId: bigint): Promise<RewardSummary | null>;
  getOrCreate(userId: bigint): Promise<RewardSummary>;
  findByUserIds(userIds: bigint[]): Promise<Map<string, RewardSummary>>;
  findTopSettleableUsers(limit: number): Promise<RewardSummary[]>;
}

export const REWARD_SUMMARY_REPOSITORY = Symbol('IRewardSummaryRepository');

3.5 领域服务 (Domain Services)

3.5.1 src/domain/services/reward-calculation.service.ts

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<bigint | null>;
  findNearestAuthorizedCity(userId: bigint, cityCode: string): Promise<bigint | null>;
  findNearestCommunity(userId: bigint): Promise<bigint | null>;
}

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<RewardLedgerEntry[]> {
    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<RewardLedgerEntry[]> {
    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<RewardLedgerEntry> {
    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<RewardLedgerEntry> {
    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<RewardLedgerEntry> {
    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

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)

// 文档参考
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 奖励结算成功

定时任务

过期奖励检查

// 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小时倒计时: 前端需要实时显示倒计时,后端返回 expireAtremainingTimeMs
  2. 结算币种选择: 结算时支持 BNB/OG/USDT/DST 四种币种
  3. 过期奖励转移: 过期奖励自动转入总部社区账户
  4. 外部服务依赖: 需要调用 referral-service 获取推荐链,调用 authorization-service 查找授权用户
  5. SWAP执行: 结算时调用 wallet-service 执行 SWAP 操作
  6. 事务一致性: 奖励分配需要保证事务一致性,使用数据库事务
  7. 幂等性: Kafka消费者需要实现幂等性避免重复处理