63 KiB
63 KiB
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`);
}
}
开发顺序建议
-
Phase 1: 项目初始化
- 创建NestJS项目
- 安装依赖
- 配置环境变量
-
Phase 2: 数据库层
- 创建Prisma Schema
- 运行迁移和种子数据
- 创建PrismaService
-
Phase 3: 领域层
- 实现所有值对象
- 实现聚合根 (RewardLedgerEntry, RewardSummary)
- 实现领域事件
- 实现领域服务 (RewardCalculationService, RewardExpirationService)
- 编写单元测试
-
Phase 4: 基础设施层
- 实现仓储 (Repository Implementations)
- 实现外部服务客户端 (ReferralServiceClient, AuthorizationServiceClient, WalletServiceClient)
- 实现Kafka消费者和发布者
-
Phase 5: 应用层
- 实现应用服务 (RewardApplicationService)
- 实现定时任务 (RewardExpirationScheduler)
- 实现Command/Query handlers
-
Phase 6: API层
- 实现DTO
- 实现Controllers
- 配置Swagger文档
- 配置JWT认证
-
Phase 7: 测试和部署
- 集成测试
- E2E测试
- Docker配置
与前端页面对应关系
我的收益页面
- 显示待领取收益(含24小时倒计时)
- 显示可结算收益
- 显示已结算总额
- 显示已过期总额
- 调用:
GET /rewards/summary
奖励明细页面
- 分页显示奖励流水
- 按状态/类型筛选
- 调用:
GET /rewards/details?status=PENDING&page=1&pageSize=20
结算页面
- 选择结算币种(BNB/OG/USDT/DST)
- 显示可结算金额
- 点击结算按钮
- 调用:
POST /rewards/settle
注意事项
- 24小时倒计时: 前端需要实时显示倒计时,后端返回
expireAt和remainingTimeMs - 结算币种选择: 结算时支持 BNB/OG/USDT/DST 四种币种
- 过期奖励转移: 过期奖励自动转入总部社区账户
- 外部服务依赖: 需要调用 referral-service 获取推荐链,调用 authorization-service 查找授权用户
- SWAP执行: 结算时调用 wallet-service 执行 SWAP 操作
- 事务一致性: 奖励分配需要保证事务一致性,使用数据库事务
- 幂等性: Kafka消费者需要实现幂等性,避免重复处理