23 KiB
23 KiB
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 核心表结构
-- ============================================
-- 全局状态表(单例)
-- ============================================
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 价格计算公式
/**
* 积分股价格 = 积分股池绿积分 ÷ (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 每分钟销毁逻辑
/**
* 定时销毁任务 - 每分钟执行
* 目的:让代币价格持续上涨
*/
@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 每日积分股分配
/**
* 每日分配任务 - 凌晨执行
* 根据用户算力占比分配当日积分股
*/
@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 实时收益查询(前端显示)
/**
* 获取用户实时收益(用于前端每秒更新显示)
*/
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 缓存结构
// 全局状态缓存 - 高频读取
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. 启动命令
# 开发环境
npm run start:dev
# 初始化全局状态
npm run cli -- init-global-state
# 生产环境
npm run build && npm run start:prod