# 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 { 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 { 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 { // 优先从 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 ```