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

729 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Mining Service (挖矿服务) 开发指导
## 1. 服务概述
### 1.1 核心职责
Mining Service 负责积分股的挖矿分配和全局状态管理,是代币经济的核心引擎。
**主要功能:**
- 管理积分股全局状态(总量、黑洞、流通池、价格)
- 执行每分钟销毁(维护币价上涨)
- 计算并分配每日/每小时/每分钟/每秒的积分股
- 维护挖矿明细账(每个用户获得的积分股来源)
- 管理分配阶段2年减半规则
### 1.2 技术栈
- **框架**: NestJS + TypeScript
- **数据库**: PostgreSQL (事务型) + Redis (实时状态)
- **ORM**: Prisma
- **消息队列**: Kafka
### 1.3 端口分配
- HTTP: 3021
- 数据库: rwa_mining
---
## 2. 架构设计
### 2.1 目录结构
```
mining-service/
├── src/
│ ├── api/
│ │ ├── controllers/
│ │ │ ├── share-account.controller.ts # 用户积分股账户API
│ │ │ ├── global-state.controller.ts # 全局状态API
│ │ │ ├── mining-record.controller.ts # 挖矿记录API
│ │ │ └── health.controller.ts
│ │ └── dto/
│ │ ├── request/
│ │ └── response/
│ │ ├── share-account.response.ts
│ │ ├── global-state.response.ts
│ │ ├── mining-record.response.ts
│ │ └── realtime-earning.response.ts
│ │
│ ├── application/
│ │ ├── commands/
│ │ │ ├── execute-minute-burn.command.ts # 每分钟销毁
│ │ │ ├── distribute-daily-shares.command.ts # 每日分配
│ │ │ ├── calculate-user-earning.command.ts # 计算用户收益
│ │ │ ├── advance-distribution-phase.command.ts # 阶段推进
│ │ │ └── initialize-global-state.command.ts # 初始化
│ │ ├── queries/
│ │ │ ├── get-user-share-account.query.ts
│ │ │ ├── get-global-state.query.ts
│ │ │ ├── get-user-mining-records.query.ts
│ │ │ └── get-realtime-earning.query.ts
│ │ ├── services/
│ │ │ ├── mining-distribution.service.ts
│ │ │ ├── burn-executor.service.ts
│ │ │ └── price-calculator.service.ts
│ │ ├── event-handlers/
│ │ │ ├── contribution-snapshot-created.handler.ts
│ │ │ └── trade-completed.handler.ts
│ │ └── schedulers/
│ │ ├── minute-burn.scheduler.ts # 每分钟销毁定时器
│ │ ├── daily-distribution.scheduler.ts # 每日分配定时器
│ │ └── phase-check.scheduler.ts # 阶段检查
│ │
│ ├── domain/
│ │ ├── aggregates/
│ │ │ ├── share-global-state.aggregate.ts
│ │ │ ├── share-account.aggregate.ts
│ │ │ └── mining-record.aggregate.ts
│ │ ├── repositories/
│ │ │ ├── share-global-state.repository.interface.ts
│ │ │ ├── share-account.repository.interface.ts
│ │ │ ├── mining-record.repository.interface.ts
│ │ │ └── burn-record.repository.interface.ts
│ │ ├── value-objects/
│ │ │ ├── share-amount.vo.ts
│ │ │ ├── share-price.vo.ts
│ │ │ └── distribution-phase.vo.ts
│ │ ├── events/
│ │ │ ├── shares-distributed.event.ts
│ │ │ ├── shares-burned.event.ts
│ │ │ ├── price-updated.event.ts
│ │ │ └── phase-advanced.event.ts
│ │ └── services/
│ │ ├── burn-calculator.domain-service.ts
│ │ └── distribution-calculator.domain-service.ts
│ │
│ ├── infrastructure/
│ │ ├── persistence/
│ │ │ ├── prisma/
│ │ │ │ └── prisma.service.ts
│ │ │ ├── repositories/
│ │ │ │ ├── share-global-state.repository.impl.ts
│ │ │ │ ├── share-account.repository.impl.ts
│ │ │ │ ├── mining-record.repository.impl.ts
│ │ │ │ └── burn-record.repository.impl.ts
│ │ │ └── unit-of-work/
│ │ │ └── unit-of-work.service.ts
│ │ ├── kafka/
│ │ │ ├── contribution-event-consumer.service.ts
│ │ │ ├── trade-event-consumer.service.ts
│ │ │ ├── event-publisher.service.ts
│ │ │ └── kafka.module.ts
│ │ ├── redis/
│ │ │ ├── global-state-cache.service.ts # 实时状态缓存
│ │ │ ├── price-cache.service.ts # 价格缓存
│ │ │ └── earning-cache.service.ts # 收益缓存
│ │ └── infrastructure.module.ts
│ │
│ ├── shared/
│ ├── config/
│ ├── app.module.ts
│ └── main.ts
├── prisma/
│ ├── schema.prisma
│ └── migrations/
├── package.json
├── tsconfig.json
├── Dockerfile
└── docker-compose.yml
```
---
## 3. 数据库设计
### 3.1 数据库类型选择
| 表类型 | 数据库类型 | 原因 |
|--------|-----------|------|
| 全局状态表 | 事务型 + Redis | 需要强一致性 + 实时读取 |
| 用户账户表 | 事务型 | 余额变更需要事务 |
| 挖矿明细表 | 事务型 | 明细账需要完整性 |
| 销毁记录表 | 事务型 | 审计追踪 |
### 3.2 核心表结构
```sql
-- ============================================
-- 全局状态表(单例)
-- ============================================
CREATE TABLE share_global_state (
id UUID PRIMARY KEY DEFAULT '00000000-0000-0000-0000-000000000001',
-- 积分股总量与池子
total_supply DECIMAL(30,10) DEFAULT 10002000000, -- 100.02亿
original_pool DECIMAL(30,10) DEFAULT 2000000, -- 200万用于分配
black_hole_amount DECIMAL(30,10) DEFAULT 0, -- 黑洞(已销毁)
circulation_pool DECIMAL(30,10) DEFAULT 0, -- 流通池(卖出的)
-- 积分股池(决定价格)
share_pool_green_points DECIMAL(30,10) DEFAULT 0, -- 池中绿积分
-- 价格
current_price DECIMAL(30,18) DEFAULT 0,
-- 销毁相关
minute_burn_rate DECIMAL(30,18), -- 每分钟销毁量
total_minutes_remaining BIGINT, -- 剩余分钟数
last_burn_at TIMESTAMP WITH TIME ZONE,
-- 分配相关
distribution_phase INT DEFAULT 1, -- 当前阶段 (1,2,3...)
daily_distribution DECIMAL(30,18), -- 每日分配量
total_distributed DECIMAL(30,10) DEFAULT 0, -- 累计已分配
-- 系统控制
transfer_enabled BOOLEAN DEFAULT FALSE,
system_start_at TIMESTAMP WITH TIME ZONE,
is_initialized BOOLEAN DEFAULT FALSE,
-- 乐观锁
version INT DEFAULT 1,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- ============================================
-- 用户积分股账户
-- ============================================
CREATE TABLE share_accounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
account_sequence VARCHAR(20) NOT NULL UNIQUE,
-- 余额
available_balance DECIMAL(30,10) DEFAULT 0, -- 可用余额
frozen_balance DECIMAL(30,10) DEFAULT 0, -- 冻结余额
-- 统计
total_mined DECIMAL(30,10) DEFAULT 0, -- 累计挖矿获得
total_sold DECIMAL(30,10) DEFAULT 0, -- 累计卖出
total_bought DECIMAL(30,10) DEFAULT 0, -- 累计买入
-- 实时收益计算参数(缓存)
last_contribution_ratio DECIMAL(30,18) DEFAULT 0, -- 最近算力占比
per_second_earning DECIMAL(30,18) DEFAULT 0, -- 每秒收益
-- 乐观锁
version INT DEFAULT 1,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- ============================================
-- 挖矿明细账
-- ============================================
-- 每日挖矿汇总记录
CREATE TABLE daily_mining_records (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
account_sequence VARCHAR(20) NOT NULL,
mining_date DATE NOT NULL,
-- 算力快照
effective_contribution DECIMAL(30,10) NOT NULL,
network_total_contribution DECIMAL(30,10) NOT NULL,
contribution_ratio DECIMAL(30,18) NOT NULL,
-- 分配结果
daily_network_distribution DECIMAL(30,18) NOT NULL, -- 当日全网分配量
mined_amount DECIMAL(30,18) NOT NULL, -- 用户获得量
-- 分配阶段
distribution_phase INT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(account_sequence, mining_date)
);
CREATE INDEX idx_daily_mining_account ON daily_mining_records(account_sequence);
CREATE INDEX idx_daily_mining_date ON daily_mining_records(mining_date);
-- 小时级挖矿记录(更细粒度追踪)
CREATE TABLE hourly_mining_records (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
account_sequence VARCHAR(20) NOT NULL,
mining_hour TIMESTAMP WITH TIME ZONE NOT NULL,
contribution_ratio DECIMAL(30,18) NOT NULL,
hourly_distribution DECIMAL(30,18) NOT NULL,
mined_amount DECIMAL(30,18) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(account_sequence, mining_hour)
);
-- ============================================
-- 销毁记录(明细账)
-- ============================================
CREATE TABLE burn_records (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
burn_type VARCHAR(20) NOT NULL, -- SCHEDULED / SELL_TRIGGERED
burn_amount DECIMAL(30,10) NOT NULL,
-- 触发信息
trigger_trade_id UUID, -- 卖出触发时的交易ID
trigger_account_sequence VARCHAR(20), -- 触发用户
-- 状态快照
before_black_hole DECIMAL(30,10),
after_black_hole DECIMAL(30,10),
before_price DECIMAL(30,18),
after_price DECIMAL(30,18),
before_minute_rate DECIMAL(30,18),
after_minute_rate DECIMAL(30,18),
-- 计算参数(审计用)
remaining_minutes_at_burn BIGINT,
total_to_burn_at_time DECIMAL(30,10),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_burn_records_type ON burn_records(burn_type);
CREATE INDEX idx_burn_records_time ON burn_records(created_at);
-- ============================================
-- 分配阶段配置
-- ============================================
CREATE TABLE distribution_phase_configs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
phase_number INT NOT NULL UNIQUE,
duration_years INT DEFAULT 2,
total_shares DECIMAL(30,10) NOT NULL, -- 该阶段总分配量
daily_shares DECIMAL(30,18) NOT NULL, -- 每日分配量
start_date DATE,
end_date DATE,
is_active BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 初始化数据
INSERT INTO distribution_phase_configs (phase_number, total_shares, daily_shares) VALUES
(1, 1000000, 1369.86301369863), -- 第一个两年100万
(2, 500000, 684.9315068493151), -- 第二个两年50万
(3, 250000, 342.46575342465753), -- 第三个两年25万
(4, 125000, 171.23287671232876); -- 第四个两年12.5万
-- ============================================
-- 价格历史(每分钟记录)
-- ============================================
CREATE TABLE price_ticks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tick_time TIMESTAMP WITH TIME ZONE NOT NULL,
price DECIMAL(30,18) NOT NULL,
-- 状态快照
share_pool_green_points DECIMAL(30,10),
black_hole_amount DECIMAL(30,10),
circulation_pool DECIMAL(30,10),
effective_supply DECIMAL(30,10), -- 有效供应量
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_price_ticks_time ON price_ticks(tick_time);
-- ============================================
-- 同步的算力快照(从 contribution-service
-- ============================================
CREATE TABLE synced_contribution_snapshots (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
snapshot_date DATE NOT NULL,
account_sequence VARCHAR(20) NOT NULL,
effective_contribution DECIMAL(30,10) NOT NULL,
network_total_contribution DECIMAL(30,10) NOT NULL,
contribution_ratio DECIMAL(30,18) NOT NULL,
source_sequence_num BIGINT NOT NULL,
synced_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
-- 是否已用于分配
distribution_processed BOOLEAN DEFAULT FALSE,
distribution_processed_at TIMESTAMP WITH TIME ZONE,
UNIQUE(snapshot_date, account_sequence)
);
-- ============================================
-- 已处理事件(幂等性)
-- ============================================
CREATE TABLE processed_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
event_id VARCHAR(100) NOT NULL UNIQUE,
event_type VARCHAR(50) NOT NULL,
source_service VARCHAR(50),
processed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
```
---
## 4. 核心业务逻辑
### 4.1 价格计算公式
```typescript
/**
* 积分股价格 = 积分股池绿积分 ÷ (100.02亿 - 黑洞量 - 流通池量)
*/
function calculatePrice(state: GlobalState): Decimal {
const effectiveSupply = state.totalSupply
.minus(state.blackHoleAmount)
.minus(state.circulationPool);
if (effectiveSupply.isZero()) {
return new Decimal(0);
}
return state.sharePoolGreenPoints.dividedBy(effectiveSupply);
}
```
### 4.2 每分钟销毁逻辑
```typescript
/**
* 定时销毁任务 - 每分钟执行
* 目的:让代币价格持续上涨
*/
@Cron('* * * * *') // 每分钟
async executeMinuteBurn(): Promise<void> {
await this.unitOfWork.runInTransaction(async (tx) => {
const state = await this.globalStateRepo.getForUpdate(tx);
// 计算当前每分钟销毁量
// 每分钟销毁量 = (100亿 - 黑洞总量) ÷ 剩余分钟数
const remainingToBurn = new Decimal('10000000000').minus(state.blackHoleAmount);
const remainingMinutes = this.calculateRemainingMinutes(state);
const burnAmount = remainingToBurn.dividedBy(remainingMinutes);
// 执行销毁
const newBlackHole = state.blackHoleAmount.plus(burnAmount);
// 重新计算价格
const newPrice = this.calculatePrice({
...state,
blackHoleAmount: newBlackHole
});
// 更新全局状态
await this.globalStateRepo.update(tx, {
blackHoleAmount: newBlackHole,
currentPrice: newPrice,
minuteBurnRate: burnAmount,
lastBurnAt: new Date(),
version: state.version + 1
});
// 记录销毁明细
await this.burnRecordRepo.create(tx, {
burnType: 'SCHEDULED',
burnAmount,
beforeBlackHole: state.blackHoleAmount,
afterBlackHole: newBlackHole,
beforePrice: state.currentPrice,
afterPrice: newPrice,
remainingMinutesAtBurn: remainingMinutes,
});
// 记录价格
await this.priceTickRepo.create(tx, {
tickTime: new Date(),
price: newPrice,
sharePoolGreenPoints: state.sharePoolGreenPoints,
blackHoleAmount: newBlackHole,
circulationPool: state.circulationPool,
});
// 更新 Redis 缓存
await this.globalStateCache.update({
price: newPrice,
blackHoleAmount: newBlackHole,
minuteBurnRate: burnAmount,
});
});
}
/**
* 计算剩余分钟数
* 4年 = 365 × 4 × 24 × 60 = 2,102,400 分钟
*/
private calculateRemainingMinutes(state: GlobalState): number {
const totalMinutes = 365 * 4 * 24 * 60; // 2,102,400
const elapsedMinutes = this.getElapsedMinutes(state.systemStartAt);
return Math.max(totalMinutes - elapsedMinutes, 1); // 至少1分钟避免除零
}
```
### 4.3 每日积分股分配
```typescript
/**
* 每日分配任务 - 凌晨执行
* 根据用户算力占比分配当日积分股
*/
@Cron('0 0 * * *') // 每天0点
async distributeDailyShares(): Promise<void> {
const today = new Date();
const yesterday = subDays(today, 1);
// 获取昨日算力快照
const snapshots = await this.syncedSnapshotRepo.findByDate(yesterday);
if (snapshots.length === 0) {
this.logger.warn('No contribution snapshots for distribution');
return;
}
const state = await this.globalStateRepo.get();
const dailyDistribution = state.dailyDistribution;
// 批量处理用户分配
for (const snapshot of snapshots) {
await this.unitOfWork.runInTransaction(async (tx) => {
// 计算用户应得份额
const userShare = dailyDistribution.multipliedBy(snapshot.contributionRatio);
// 更新用户账户
const account = await this.shareAccountRepo.getByAccountSequence(
tx,
snapshot.accountSequence
);
await this.shareAccountRepo.update(tx, {
id: account.id,
availableBalance: account.availableBalance.plus(userShare),
totalMined: account.totalMined.plus(userShare),
lastContributionRatio: snapshot.contributionRatio,
perSecondEarning: this.calculatePerSecondEarning(snapshot.contributionRatio, state),
version: account.version + 1,
});
// 记录挖矿明细
await this.dailyMiningRecordRepo.create(tx, {
accountSequence: snapshot.accountSequence,
miningDate: yesterday,
effectiveContribution: snapshot.effectiveContribution,
networkTotalContribution: snapshot.networkTotalContribution,
contributionRatio: snapshot.contributionRatio,
dailyNetworkDistribution: dailyDistribution,
minedAmount: userShare,
distributionPhase: state.distributionPhase,
});
// 标记快照已处理
await this.syncedSnapshotRepo.markProcessed(tx, snapshot.id);
});
}
// 更新全局分配统计
await this.globalStateRepo.addDistributed(dailyDistribution);
}
/**
* 计算每秒收益(用于前端实时显示)
* 每秒收益 = 每日分配量 × 用户占比 ÷ 86400
*/
private calculatePerSecondEarning(
contributionRatio: Decimal,
state: GlobalState
): Decimal {
return state.dailyDistribution
.multipliedBy(contributionRatio)
.dividedBy(86400);
}
```
### 4.4 实时收益查询(前端显示)
```typescript
/**
* 获取用户实时收益(用于前端每秒更新显示)
*/
async getRealtimeEarning(accountSequence: string): Promise<RealtimeEarningDto> {
// 优先从 Redis 获取
const cached = await this.earningCache.get(accountSequence);
if (cached) {
return cached;
}
const account = await this.shareAccountRepo.getByAccountSequence(accountSequence);
const state = await this.globalStateCache.get();
// 计算显示资产
// 资产显示 = (账户积分股 + 账户积分股 × 倍数) × 积分股价
const multiplier = this.calculateMultiplier(state);
const effectiveShares = account.availableBalance
.multipliedBy(new Decimal(1).plus(multiplier));
const displayAssetValue = effectiveShares.multipliedBy(state.currentPrice);
const result = {
accountSequence,
availableBalance: account.availableBalance.toString(),
perSecondEarning: account.perSecondEarning.toString(),
displayAssetValue: displayAssetValue.toString(),
currentPrice: state.currentPrice.toString(),
multiplier: multiplier.toString(),
};
// 缓存 10 秒
await this.earningCache.set(accountSequence, result, 10);
return result;
}
/**
* 计算倍数
* 倍数 = (100亿 - 销毁量) ÷ (200万 - 流通池量)
*/
private calculateMultiplier(state: GlobalState): Decimal {
const numerator = new Decimal('10000000000').minus(state.blackHoleAmount);
const denominator = new Decimal('2000000').minus(state.circulationPool);
if (denominator.isZero() || denominator.isNegative()) {
return new Decimal(0);
}
return numerator.dividedBy(denominator);
}
```
---
## 5. 服务间通信
### 5.1 订阅的事件
| Topic | 来源服务 | 数据内容 | 处理方式 |
|-------|---------|---------|---------|
| `contribution.daily-snapshot-created` | contribution-service | 每日算力快照 | 用于挖矿分配 |
| `trading.trade-completed` | trading-service | 交易完成事件 | 更新流通池、触发卖出销毁 |
### 5.2 发布的事件
| Topic | 事件类型 | 订阅者 |
|-------|---------|-------|
| `mining.shares-distributed` | SharesDistributed | trading-service |
| `mining.price-updated` | PriceUpdated | trading-service, mining-admin |
| `mining.shares-burned` | SharesBurned | mining-admin |
### 5.3 Redis 缓存结构
```typescript
// 全局状态缓存 - 高频读取
interface GlobalStateCache {
key: 'mining:global-state';
ttl: 60; // 60秒
data: {
currentPrice: string;
blackHoleAmount: string;
circulationPool: string;
minuteBurnRate: string;
dailyDistribution: string;
};
}
// 用户收益缓存 - 实时查询
interface UserEarningCache {
key: `mining:earning:${accountSequence}`;
ttl: 10; // 10秒
data: RealtimeEarningDto;
}
```
---
## 6. 关键计算公式汇总
### 6.1 价格相关
```
积分股价格 = 积分股池绿积分 ÷ (100.02亿 - 黑洞量 - 流通池量)
每分钟销毁量 = (100亿 - 黑洞总量) ÷ 剩余分钟数
倍数 = (100亿 - 销毁量) ÷ (200万 - 流通池量)
资产显示 = (账户积分股 + 账户积分股 × 倍数) × 积分股价
```
### 6.2 分配相关
```
第一个两年:每日分配 = 1000000 ÷ 730 = 1369.86301369863
第二个两年:每日分配 = 500000 ÷ 730 = 684.9315068493151
第N个两年每日分配 = 前一阶段 ÷ 2
用户每日获得 = 每日全网分配 × 用户算力占比
用户每秒获得 = 用户每日获得 ÷ 86400
```
---
## 7. 关键注意事项
### 7.1 精度处理
- 价格使用 `DECIMAL(30,18)` - 18位小数
- 积分股数量使用 `DECIMAL(30,10)` - 10位小数
- 使用 `Decimal.js` 库处理所有数学运算,避免浮点数精度问题
### 7.2 并发控制
- 全局状态更新使用乐观锁 (`version` 字段)
- 定时任务使用分布式锁Redis避免多实例重复执行
### 7.3 性能优化
- 全局状态缓存到 Redis避免频繁查库
- 用户收益预计算 `per_second_earning`,前端直接使用
- 批量处理每日分配,避免单条事务
### 7.4 数据一致性
- 销毁操作必须在事务中同时更新:黑洞量、价格、销毁记录
- 分配操作必须在事务中同时更新:用户余额、挖矿记录
---
## 8. 开发检查清单
- [ ] 实现全局状态管理
- [ ] 实现每分钟销毁定时任务
- [ ] 实现每日积分股分配
- [ ] 实现价格计算服务
- [ ] 实现挖矿明细账记录
- [ ] 实现销毁明细账记录
- [ ] 实现实时收益查询 API
- [ ] 实现分配阶段管理
- [ ] 配置 Redis 缓存
- [ ] 编写单元测试
- [ ] 配置 Kafka Consumer
---
## 9. 启动命令
```bash
# 开发环境
npm run start:dev
# 初始化全局状态
npm run cli -- init-global-state
# 生产环境
npm run build && npm run start:prod
```