729 lines
23 KiB
Markdown
729 lines
23 KiB
Markdown
# 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
|
||
```
|