834 lines
25 KiB
Markdown
834 lines
25 KiB
Markdown
# Trading Service (交易服务) 开发指导
|
||
|
||
## 1. 服务概述
|
||
|
||
### 1.1 核心职责
|
||
Trading Service 负责积分股的买卖交易、K线数据生成、以及维护币价上涨机制。
|
||
|
||
**主要功能:**
|
||
- 处理积分股买卖订单
|
||
- 计算卖出销毁量(确保卖出不降价)
|
||
- 管理流通池
|
||
- 生成K线数据(多周期)
|
||
- 处理交易手续费
|
||
- 维护交易明细账
|
||
|
||
### 1.2 技术栈
|
||
- **框架**: NestJS + TypeScript
|
||
- **数据库**: PostgreSQL (事务型)
|
||
- **ORM**: Prisma
|
||
- **消息队列**: Kafka
|
||
- **缓存**: Redis (K线缓存、价格缓存)
|
||
|
||
### 1.3 端口分配
|
||
- HTTP: 3022
|
||
- 数据库: rwa_trading
|
||
|
||
---
|
||
|
||
## 2. 架构设计
|
||
|
||
### 2.1 目录结构
|
||
|
||
```
|
||
trading-service/
|
||
├── src/
|
||
│ ├── api/
|
||
│ │ ├── controllers/
|
||
│ │ │ ├── trade.controller.ts # 买卖API
|
||
│ │ │ ├── order.controller.ts # 订单查询API
|
||
│ │ │ ├── kline.controller.ts # K线数据API
|
||
│ │ │ ├── price.controller.ts # 价格查询API
|
||
│ │ │ └── health.controller.ts
|
||
│ │ └── dto/
|
||
│ │ ├── request/
|
||
│ │ │ ├── buy-shares.request.ts
|
||
│ │ │ ├── sell-shares.request.ts
|
||
│ │ │ └── get-kline.request.ts
|
||
│ │ └── response/
|
||
│ │ ├── trade-result.response.ts
|
||
│ │ ├── order.response.ts
|
||
│ │ ├── kline.response.ts
|
||
│ │ └── price.response.ts
|
||
│ │
|
||
│ ├── application/
|
||
│ │ ├── commands/
|
||
│ │ │ ├── buy-shares.command.ts
|
||
│ │ │ ├── sell-shares.command.ts
|
||
│ │ │ ├── cancel-order.command.ts
|
||
│ │ │ └── aggregate-kline.command.ts
|
||
│ │ ├── queries/
|
||
│ │ │ ├── get-user-orders.query.ts
|
||
│ │ │ ├── get-kline-data.query.ts
|
||
│ │ │ ├── get-current-price.query.ts
|
||
│ │ │ └── get-trade-history.query.ts
|
||
│ │ ├── services/
|
||
│ │ │ ├── trade-execution.service.ts
|
||
│ │ │ ├── sell-burn-calculator.service.ts
|
||
│ │ │ └── kline-aggregator.service.ts
|
||
│ │ ├── event-handlers/
|
||
│ │ │ ├── price-updated.handler.ts
|
||
│ │ │ └── shares-burned.handler.ts
|
||
│ │ └── schedulers/
|
||
│ │ ├── kline-aggregation.scheduler.ts # K线聚合定时器
|
||
│ │ └── price-tick.scheduler.ts # 价格记录
|
||
│ │
|
||
│ ├── domain/
|
||
│ │ ├── aggregates/
|
||
│ │ │ ├── trade-order.aggregate.ts
|
||
│ │ │ ├── kline-bar.aggregate.ts
|
||
│ │ │ └── trade-transaction.aggregate.ts
|
||
│ │ ├── repositories/
|
||
│ │ │ ├── trade-order.repository.interface.ts
|
||
│ │ │ ├── trade-transaction.repository.interface.ts
|
||
│ │ │ ├── kline.repository.interface.ts
|
||
│ │ │ └── price-tick.repository.interface.ts
|
||
│ │ ├── value-objects/
|
||
│ │ │ ├── order-type.vo.ts
|
||
│ │ │ ├── kline-period.vo.ts
|
||
│ │ │ └── trade-amount.vo.ts
|
||
│ │ ├── events/
|
||
│ │ │ ├── trade-completed.event.ts
|
||
│ │ │ ├── order-created.event.ts
|
||
│ │ │ └── kline-updated.event.ts
|
||
│ │ └── services/
|
||
│ │ ├── sell-multiplier-calculator.domain-service.ts
|
||
│ │ └── fee-calculator.domain-service.ts
|
||
│ │
|
||
│ ├── infrastructure/
|
||
│ │ ├── persistence/
|
||
│ │ │ ├── prisma/
|
||
│ │ │ │ └── prisma.service.ts
|
||
│ │ │ ├── repositories/
|
||
│ │ │ │ ├── trade-order.repository.impl.ts
|
||
│ │ │ │ ├── trade-transaction.repository.impl.ts
|
||
│ │ │ │ ├── kline.repository.impl.ts
|
||
│ │ │ │ └── price-tick.repository.impl.ts
|
||
│ │ │ └── unit-of-work/
|
||
│ │ │ └── unit-of-work.service.ts
|
||
│ │ ├── kafka/
|
||
│ │ │ ├── mining-event-consumer.service.ts
|
||
│ │ │ ├── event-publisher.service.ts
|
||
│ │ │ └── kafka.module.ts
|
||
│ │ ├── redis/
|
||
│ │ │ ├── price-cache.service.ts
|
||
│ │ │ ├── kline-cache.service.ts
|
||
│ │ │ └── order-book-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 核心表结构
|
||
|
||
```sql
|
||
-- ============================================
|
||
-- 交易订单表
|
||
-- ============================================
|
||
|
||
CREATE TABLE trade_orders (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
order_no VARCHAR(32) NOT NULL UNIQUE, -- 订单号
|
||
account_sequence VARCHAR(20) NOT NULL,
|
||
|
||
order_type VARCHAR(10) NOT NULL, -- BUY / SELL
|
||
order_status VARCHAR(20) NOT NULL, -- PENDING / COMPLETED / FAILED / CANCELLED
|
||
|
||
-- 数量
|
||
share_amount DECIMAL(30,10) NOT NULL, -- 积分股数量
|
||
burn_amount DECIMAL(30,10) DEFAULT 0, -- 卖出销毁量
|
||
effective_amount DECIMAL(30,10), -- 有效数量(含销毁)
|
||
|
||
-- 价格
|
||
price_at_order DECIMAL(30,18) NOT NULL, -- 下单时价格
|
||
execution_price DECIMAL(30,18), -- 成交价格
|
||
|
||
-- 金额
|
||
green_points_amount DECIMAL(30,10) NOT NULL, -- 绿积分金额
|
||
fee_amount DECIMAL(30,10) DEFAULT 0, -- 手续费
|
||
fee_rate DECIMAL(10,6) DEFAULT 0.10, -- 手续费率 10%
|
||
net_amount DECIMAL(30,10), -- 净额(扣除手续费后)
|
||
|
||
-- 卖出倍数(卖出时使用)
|
||
sell_multiplier DECIMAL(20,10),
|
||
|
||
-- 计算参数快照(审计用)
|
||
black_hole_at_order DECIMAL(30,10),
|
||
circulation_pool_at_order DECIMAL(30,10),
|
||
share_pool_at_order DECIMAL(30,10),
|
||
|
||
-- 版本号
|
||
version INT DEFAULT 1,
|
||
|
||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||
executed_at TIMESTAMP WITH TIME ZONE,
|
||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||
);
|
||
CREATE INDEX idx_trade_orders_account ON trade_orders(account_sequence);
|
||
CREATE INDEX idx_trade_orders_status ON trade_orders(order_status);
|
||
CREATE INDEX idx_trade_orders_created ON trade_orders(created_at);
|
||
|
||
-- ============================================
|
||
-- 交易流水表(明细账)
|
||
-- ============================================
|
||
|
||
CREATE TABLE trade_transactions (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
order_id UUID NOT NULL REFERENCES trade_orders(id),
|
||
account_sequence VARCHAR(20) NOT NULL,
|
||
|
||
transaction_type VARCHAR(30) NOT NULL, -- SHARE_DEBIT / SHARE_CREDIT / GREEN_POINT_DEBIT / GREEN_POINT_CREDIT / FEE / BURN
|
||
asset_type VARCHAR(20) NOT NULL, -- SHARE / GREEN_POINT
|
||
|
||
amount DECIMAL(30,10) NOT NULL,
|
||
balance_before DECIMAL(30,10),
|
||
balance_after DECIMAL(30,10),
|
||
|
||
-- 关联的销毁记录(如果是BURN类型)
|
||
related_burn_id UUID,
|
||
|
||
memo VARCHAR(200),
|
||
|
||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||
);
|
||
CREATE INDEX idx_trade_transactions_order ON trade_transactions(order_id);
|
||
CREATE INDEX idx_trade_transactions_account ON trade_transactions(account_sequence);
|
||
|
||
-- ============================================
|
||
-- K线数据表
|
||
-- ============================================
|
||
|
||
CREATE TABLE kline_data (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
||
period_type VARCHAR(10) NOT NULL, -- 1m/5m/15m/30m/1h/4h/1d/1w/1M/1Q/1Y
|
||
period_start TIMESTAMP WITH TIME ZONE NOT NULL,
|
||
period_end TIMESTAMP WITH TIME ZONE NOT NULL,
|
||
|
||
-- OHLC
|
||
open_price DECIMAL(30,18) NOT NULL,
|
||
high_price DECIMAL(30,18) NOT NULL,
|
||
low_price DECIMAL(30,18) NOT NULL,
|
||
close_price DECIMAL(30,18) NOT NULL,
|
||
|
||
-- 成交量
|
||
volume DECIMAL(30,10) DEFAULT 0, -- 积分股成交量
|
||
green_points_volume DECIMAL(30,10) DEFAULT 0, -- 绿积分成交量
|
||
trade_count INT DEFAULT 0, -- 成交笔数
|
||
|
||
-- 买卖统计
|
||
buy_volume DECIMAL(30,10) DEFAULT 0,
|
||
sell_volume DECIMAL(30,10) DEFAULT 0,
|
||
|
||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||
|
||
UNIQUE(period_type, period_start)
|
||
);
|
||
CREATE INDEX idx_kline_period ON kline_data(period_type, period_start);
|
||
|
||
-- ============================================
|
||
-- 价格快照表(每分钟)
|
||
-- ============================================
|
||
|
||
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),
|
||
|
||
-- 该分钟内的交易统计
|
||
minute_volume DECIMAL(30,10) DEFAULT 0,
|
||
minute_trade_count INT DEFAULT 0,
|
||
|
||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||
);
|
||
CREATE INDEX idx_price_ticks_time ON price_ticks(tick_time);
|
||
|
||
-- ============================================
|
||
-- 手续费配置表
|
||
-- ============================================
|
||
|
||
CREATE TABLE fee_configs (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
||
fee_type VARCHAR(20) NOT NULL, -- BUY / SELL
|
||
fee_rate DECIMAL(10,6) NOT NULL DEFAULT 0.10, -- 10%
|
||
|
||
is_active BOOLEAN DEFAULT TRUE,
|
||
|
||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||
);
|
||
|
||
-- 初始化数据
|
||
INSERT INTO fee_configs (fee_type, fee_rate) VALUES
|
||
('BUY', 0.10),
|
||
('SELL', 0.10);
|
||
|
||
-- ============================================
|
||
-- 流通池状态表(本地缓存,与mining-service同步)
|
||
-- ============================================
|
||
|
||
CREATE TABLE circulation_pool_state (
|
||
id UUID PRIMARY KEY DEFAULT '00000000-0000-0000-0000-000000000001',
|
||
|
||
circulation_pool DECIMAL(30,10) DEFAULT 0,
|
||
last_synced_from_mining TIMESTAMP WITH TIME ZONE,
|
||
|
||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||
);
|
||
|
||
-- ============================================
|
||
-- 已处理事件(幂等性)
|
||
-- ============================================
|
||
|
||
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
|
||
/**
|
||
* 卖出积分股
|
||
* 核心机制:通过提前销毁未来的积分股,确保卖出不会导致价格下跌
|
||
*/
|
||
async executeSellOrder(command: SellSharesCommand): Promise<TradeResult> {
|
||
const { accountSequence, shareAmount } = command;
|
||
|
||
return await this.unitOfWork.runInTransaction(async (tx) => {
|
||
// 1. 获取当前状态
|
||
const state = await this.getMiningState();
|
||
const currentPrice = new Decimal(state.currentPrice);
|
||
|
||
// 2. 计算卖出倍数
|
||
// 倍数 = (100亿 - 销毁量) ÷ (200万 - 流通池量)
|
||
const multiplier = this.calculateSellMultiplier(state);
|
||
|
||
// 3. 计算卖出销毁量
|
||
// 卖出销毁量 = 卖出积分股 × 倍数
|
||
const burnAmount = shareAmount.multipliedBy(multiplier);
|
||
|
||
// 4. 计算有效数量(卖出量 + 销毁量)
|
||
const effectiveAmount = shareAmount.plus(burnAmount);
|
||
|
||
// 5. 计算交易额
|
||
// 卖出交易额 = 有效数量 × 积分股价
|
||
const grossAmount = effectiveAmount.multipliedBy(currentPrice);
|
||
|
||
// 6. 计算手续费(10%)
|
||
const feeRate = await this.getFeeRate('SELL');
|
||
const feeAmount = grossAmount.multipliedBy(feeRate);
|
||
|
||
// 7. 计算净额
|
||
const netAmount = grossAmount.minus(feeAmount);
|
||
|
||
// 8. 验证用户余额
|
||
const shareAccount = await this.shareAccountRepo.getByAccountSequence(tx, accountSequence);
|
||
if (shareAccount.availableBalance.lessThan(shareAmount)) {
|
||
throw new InsufficientBalanceException('积分股余额不足');
|
||
}
|
||
|
||
// 9. 创建订单
|
||
const order = await this.orderRepo.create(tx, {
|
||
orderNo: generateOrderNo(),
|
||
accountSequence,
|
||
orderType: 'SELL',
|
||
orderStatus: 'COMPLETED',
|
||
shareAmount,
|
||
burnAmount,
|
||
effectiveAmount,
|
||
priceAtOrder: currentPrice,
|
||
executionPrice: currentPrice,
|
||
greenPointsAmount: grossAmount,
|
||
feeAmount,
|
||
feeRate,
|
||
netAmount,
|
||
sellMultiplier: multiplier,
|
||
blackHoleAtOrder: state.blackHoleAmount,
|
||
circulationPoolAtOrder: state.circulationPool,
|
||
sharePoolAtOrder: state.sharePoolGreenPoints,
|
||
executedAt: new Date(),
|
||
});
|
||
|
||
// 10. 扣减用户积分股
|
||
await this.shareAccountRepo.deductBalance(tx, accountSequence, shareAmount);
|
||
|
||
// 11. 记录交易流水 - 积分股扣减
|
||
await this.transactionRepo.create(tx, {
|
||
orderId: order.id,
|
||
accountSequence,
|
||
transactionType: 'SHARE_DEBIT',
|
||
assetType: 'SHARE',
|
||
amount: shareAmount,
|
||
balanceBefore: shareAccount.availableBalance,
|
||
balanceAfter: shareAccount.availableBalance.minus(shareAmount),
|
||
});
|
||
|
||
// 12. 积分股进入流通池
|
||
await this.updateCirculationPool(tx, shareAmount);
|
||
|
||
// 13. 记录交易流水 - 销毁
|
||
await this.transactionRepo.create(tx, {
|
||
orderId: order.id,
|
||
accountSequence,
|
||
transactionType: 'BURN',
|
||
assetType: 'SHARE',
|
||
amount: burnAmount,
|
||
memo: `卖出触发销毁,倍数: ${multiplier.toString()}`,
|
||
});
|
||
|
||
// 14. 从积分股池扣减绿积分给用户
|
||
await this.deductFromSharePool(tx, netAmount);
|
||
|
||
// 15. 增加用户绿积分(调用 wallet-service 或发送事件)
|
||
await this.creditGreenPoints(accountSequence, netAmount);
|
||
|
||
// 16. 记录交易流水 - 绿积分增加
|
||
await this.transactionRepo.create(tx, {
|
||
orderId: order.id,
|
||
accountSequence,
|
||
transactionType: 'GREEN_POINT_CREDIT',
|
||
assetType: 'GREEN_POINT',
|
||
amount: netAmount,
|
||
});
|
||
|
||
// 17. 手续费注入积分股池
|
||
await this.injectFeeToSharePool(tx, feeAmount);
|
||
|
||
// 18. 记录交易流水 - 手续费
|
||
await this.transactionRepo.create(tx, {
|
||
orderId: order.id,
|
||
accountSequence,
|
||
transactionType: 'FEE',
|
||
assetType: 'GREEN_POINT',
|
||
amount: feeAmount,
|
||
memo: '卖出手续费,注入积分股池',
|
||
});
|
||
|
||
// 19. 发布事件 - 触发 mining-service 执行销毁
|
||
await this.eventPublisher.publish('trading.trade-completed', {
|
||
eventId: uuid(),
|
||
orderNo: order.orderNo,
|
||
orderType: 'SELL',
|
||
accountSequence,
|
||
shareAmount: shareAmount.toString(),
|
||
burnAmount: burnAmount.toString(),
|
||
greenPointsAmount: grossAmount.toString(),
|
||
feeAmount: feeAmount.toString(),
|
||
executedAt: new Date().toISOString(),
|
||
});
|
||
|
||
// 20. 更新K线数据
|
||
await this.updateKlineData(tx, currentPrice, shareAmount, 'SELL');
|
||
|
||
return {
|
||
orderId: order.id,
|
||
orderNo: order.orderNo,
|
||
shareAmount: shareAmount.toString(),
|
||
burnAmount: burnAmount.toString(),
|
||
effectiveAmount: effectiveAmount.toString(),
|
||
grossAmount: grossAmount.toString(),
|
||
feeAmount: feeAmount.toString(),
|
||
netAmount: netAmount.toString(),
|
||
multiplier: multiplier.toString(),
|
||
price: currentPrice.toString(),
|
||
};
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 计算卖出倍数
|
||
* 倍数 = (100亿 - 销毁量) ÷ (200万 - 流通池量)
|
||
*/
|
||
private calculateSellMultiplier(state: MiningState): Decimal {
|
||
const totalToBurn = new Decimal('10000000000'); // 100亿
|
||
const originalPool = new Decimal('2000000'); // 200万
|
||
|
||
const numerator = totalToBurn.minus(state.blackHoleAmount);
|
||
const denominator = originalPool.minus(state.circulationPool);
|
||
|
||
if (denominator.isZero() || denominator.isNegative()) {
|
||
throw new TradingException('流通池已满,无法卖出');
|
||
}
|
||
|
||
return numerator.dividedBy(denominator);
|
||
}
|
||
```
|
||
|
||
### 4.2 买入交易逻辑
|
||
|
||
```typescript
|
||
/**
|
||
* 买入积分股
|
||
*/
|
||
async executeBuyOrder(command: BuySharesCommand): Promise<TradeResult> {
|
||
const { accountSequence, greenPointsAmount } = command;
|
||
|
||
return await this.unitOfWork.runInTransaction(async (tx) => {
|
||
// 1. 获取当前状态
|
||
const state = await this.getMiningState();
|
||
const currentPrice = new Decimal(state.currentPrice);
|
||
|
||
// 2. 计算手续费(10%)
|
||
const feeRate = await this.getFeeRate('BUY');
|
||
const feeAmount = greenPointsAmount.multipliedBy(feeRate);
|
||
|
||
// 3. 计算净额(用于买入的金额)
|
||
const netAmount = greenPointsAmount.minus(feeAmount);
|
||
|
||
// 4. 计算可买入积分股数量
|
||
// 从流通池购买
|
||
const shareAmount = netAmount.dividedBy(currentPrice);
|
||
|
||
// 5. 验证流通池余额
|
||
const circulationPool = await this.getCirculationPool();
|
||
if (circulationPool.lessThan(shareAmount)) {
|
||
throw new InsufficientLiquidityException('流通池积分股不足');
|
||
}
|
||
|
||
// 6. 验证用户绿积分余额(调用 wallet-service)
|
||
const hasBalance = await this.checkGreenPointsBalance(accountSequence, greenPointsAmount);
|
||
if (!hasBalance) {
|
||
throw new InsufficientBalanceException('绿积分余额不足');
|
||
}
|
||
|
||
// 7. 创建订单
|
||
const order = await this.orderRepo.create(tx, {
|
||
orderNo: generateOrderNo(),
|
||
accountSequence,
|
||
orderType: 'BUY',
|
||
orderStatus: 'COMPLETED',
|
||
shareAmount,
|
||
priceAtOrder: currentPrice,
|
||
executionPrice: currentPrice,
|
||
greenPointsAmount,
|
||
feeAmount,
|
||
feeRate,
|
||
netAmount,
|
||
executedAt: new Date(),
|
||
});
|
||
|
||
// 8. 扣减用户绿积分
|
||
await this.deductGreenPoints(accountSequence, greenPointsAmount);
|
||
|
||
// 9. 从流通池扣减积分股
|
||
await this.updateCirculationPool(tx, shareAmount.negated());
|
||
|
||
// 10. 增加用户积分股余额
|
||
await this.shareAccountRepo.addBalance(tx, accountSequence, shareAmount);
|
||
|
||
// 11. 绿积分进入积分股池
|
||
await this.injectToSharePool(tx, netAmount);
|
||
|
||
// 12. 手续费也进入积分股池
|
||
await this.injectFeeToSharePool(tx, feeAmount);
|
||
|
||
// 13. 记录交易流水
|
||
await this.createBuyTransactions(tx, order, shareAmount, greenPointsAmount, feeAmount, netAmount);
|
||
|
||
// 14. 发布事件
|
||
await this.eventPublisher.publish('trading.trade-completed', {
|
||
eventId: uuid(),
|
||
orderNo: order.orderNo,
|
||
orderType: 'BUY',
|
||
accountSequence,
|
||
shareAmount: shareAmount.toString(),
|
||
greenPointsAmount: greenPointsAmount.toString(),
|
||
feeAmount: feeAmount.toString(),
|
||
executedAt: new Date().toISOString(),
|
||
});
|
||
|
||
// 15. 更新K线数据
|
||
await this.updateKlineData(tx, currentPrice, shareAmount, 'BUY');
|
||
|
||
return {
|
||
orderId: order.id,
|
||
orderNo: order.orderNo,
|
||
shareAmount: shareAmount.toString(),
|
||
grossAmount: greenPointsAmount.toString(),
|
||
feeAmount: feeAmount.toString(),
|
||
netAmount: netAmount.toString(),
|
||
price: currentPrice.toString(),
|
||
};
|
||
});
|
||
}
|
||
```
|
||
|
||
### 4.3 K线数据聚合
|
||
|
||
```typescript
|
||
/**
|
||
* K线聚合定时器
|
||
* 支持周期:1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w, 1M, 1Q, 1Y
|
||
*/
|
||
@Cron('* * * * *') // 每分钟
|
||
async aggregateKlineData(): Promise<void> {
|
||
const now = new Date();
|
||
|
||
// 聚合各周期K线
|
||
await this.aggregate1MinuteKline(now);
|
||
|
||
if (now.getMinutes() % 5 === 0) {
|
||
await this.aggregate5MinuteKline(now);
|
||
}
|
||
|
||
if (now.getMinutes() % 15 === 0) {
|
||
await this.aggregate15MinuteKline(now);
|
||
}
|
||
|
||
if (now.getMinutes() % 30 === 0) {
|
||
await this.aggregate30MinuteKline(now);
|
||
}
|
||
|
||
if (now.getMinutes() === 0) {
|
||
await this.aggregate1HourKline(now);
|
||
|
||
if (now.getHours() % 4 === 0) {
|
||
await this.aggregate4HourKline(now);
|
||
}
|
||
|
||
if (now.getHours() === 0) {
|
||
await this.aggregate1DayKline(now);
|
||
// 周、月、季、年在日线基础上聚合
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 聚合1分钟K线
|
||
*/
|
||
private async aggregate1MinuteKline(endTime: Date): Promise<void> {
|
||
const startTime = subMinutes(endTime, 1);
|
||
|
||
// 获取该分钟内的所有价格快照
|
||
const ticks = await this.priceTickRepo.findByTimeRange(startTime, endTime);
|
||
|
||
if (ticks.length === 0) {
|
||
// 无交易,使用上一个K线的收盘价
|
||
const lastKline = await this.klineRepo.getLastKline('1m');
|
||
if (lastKline) {
|
||
await this.klineRepo.create({
|
||
periodType: '1m',
|
||
periodStart: startTime,
|
||
periodEnd: endTime,
|
||
openPrice: lastKline.closePrice,
|
||
highPrice: lastKline.closePrice,
|
||
lowPrice: lastKline.closePrice,
|
||
closePrice: lastKline.closePrice,
|
||
volume: new Decimal(0),
|
||
tradeCount: 0,
|
||
});
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 计算OHLC
|
||
const openPrice = ticks[0].price;
|
||
const closePrice = ticks[ticks.length - 1].price;
|
||
const highPrice = Decimal.max(...ticks.map(t => t.price));
|
||
const lowPrice = Decimal.min(...ticks.map(t => t.price));
|
||
const volume = ticks.reduce((sum, t) => sum.plus(t.minuteVolume), new Decimal(0));
|
||
const tradeCount = ticks.reduce((sum, t) => sum + t.minuteTradeCount, 0);
|
||
|
||
await this.klineRepo.create({
|
||
periodType: '1m',
|
||
periodStart: startTime,
|
||
periodEnd: endTime,
|
||
openPrice,
|
||
highPrice,
|
||
lowPrice,
|
||
closePrice,
|
||
volume,
|
||
tradeCount,
|
||
});
|
||
|
||
// 更新缓存
|
||
await this.klineCache.updateLatest('1m', {
|
||
openPrice: openPrice.toString(),
|
||
highPrice: highPrice.toString(),
|
||
lowPrice: lowPrice.toString(),
|
||
closePrice: closePrice.toString(),
|
||
volume: volume.toString(),
|
||
});
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 服务间通信
|
||
|
||
### 5.1 订阅的事件
|
||
|
||
| Topic | 来源服务 | 数据内容 | 处理方式 |
|
||
|-------|---------|---------|---------|
|
||
| `mining.price-updated` | mining-service | 价格更新 | 更新本地价格缓存 |
|
||
| `mining.shares-burned` | mining-service | 销毁事件 | 记录销毁对交易的影响 |
|
||
|
||
### 5.2 发布的事件
|
||
|
||
| Topic | 事件类型 | 订阅者 |
|
||
|-------|---------|-------|
|
||
| `trading.trade-completed` | TradeCompleted | mining-service(更新流通池、触发销毁) |
|
||
| `trading.kline-updated` | KlineUpdated | mining-admin(实时图表) |
|
||
|
||
### 5.3 与 wallet-service 交互
|
||
|
||
```typescript
|
||
// 买入时:扣减绿积分
|
||
await this.walletClient.deductGreenPoints(accountSequence, amount, orderId);
|
||
|
||
// 卖出时:增加绿积分
|
||
await this.walletClient.creditGreenPoints(accountSequence, amount, orderId);
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Redis 缓存结构
|
||
|
||
```typescript
|
||
// 当前价格缓存
|
||
interface PriceCache {
|
||
key: 'trading:price:current';
|
||
ttl: 10; // 10秒
|
||
data: {
|
||
price: string;
|
||
updatedAt: string;
|
||
};
|
||
}
|
||
|
||
// K线缓存(各周期最新一根)
|
||
interface KlineCache {
|
||
key: `trading:kline:${periodType}:latest`;
|
||
ttl: 60; // 60秒
|
||
data: KlineBar;
|
||
}
|
||
|
||
// K线历史缓存(按需加载)
|
||
interface KlineHistoryCache {
|
||
key: `trading:kline:${periodType}:history`;
|
||
ttl: 300; // 5分钟
|
||
data: KlineBar[];
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 7. K线周期说明
|
||
|
||
| 周期代码 | 说明 | 聚合频率 |
|
||
|---------|------|---------|
|
||
| `1m` | 1分钟 | 每分钟 |
|
||
| `5m` | 5分钟 | 每5分钟 |
|
||
| `15m` | 15分钟 | 每15分钟 |
|
||
| `30m` | 30分钟 | 每30分钟 |
|
||
| `1h` | 1小时 | 每小时 |
|
||
| `4h` | 4小时 | 每4小时 |
|
||
| `1d` | 1天 | 每天0点 |
|
||
| `1w` | 1周 | 每周一0点 |
|
||
| `1M` | 1月 | 每月1号0点 |
|
||
| `1Q` | 1季度 | 每季度首日 |
|
||
| `1Y` | 1年 | 每年1月1日 |
|
||
|
||
---
|
||
|
||
## 8. 关键计算公式汇总
|
||
|
||
```
|
||
卖出倍数 = (100亿 - 销毁量) ÷ (200万 - 流通池量)
|
||
|
||
卖出销毁量 = 卖出积分股 × 倍数
|
||
|
||
卖出交易额 = (卖出量 + 卖出销毁量) × 积分股价
|
||
|
||
买入获得量 = (绿积分 - 手续费) ÷ 积分股价
|
||
|
||
手续费 = 交易额 × 10%
|
||
```
|
||
|
||
---
|
||
|
||
## 9. 关键注意事项
|
||
|
||
### 9.1 价格同步
|
||
- 从 mining-service 获取实时价格
|
||
- 本地缓存价格,TTL 10秒
|
||
- 下单时锁定当前价格
|
||
|
||
### 9.2 流通池管理
|
||
- 卖出:积分股进入流通池
|
||
- 买入:从流通池购买
|
||
- 与 mining-service 保持同步
|
||
|
||
### 9.3 原子性
|
||
- 交易流水与余额变更在同一事务
|
||
- 发布事件使用 Outbox Pattern
|
||
|
||
### 9.4 K线精度
|
||
- 价格使用 `DECIMAL(30,18)`
|
||
- 成交量使用 `DECIMAL(30,10)`
|
||
|
||
---
|
||
|
||
## 10. 开发检查清单
|
||
|
||
- [ ] 实现卖出交易逻辑(含销毁计算)
|
||
- [ ] 实现买入交易逻辑
|
||
- [ ] 实现交易明细账记录
|
||
- [ ] 实现K线聚合(所有周期)
|
||
- [ ] 实现价格查询API
|
||
- [ ] 实现K线查询API
|
||
- [ ] 配置与 wallet-service 交互
|
||
- [ ] 配置与 mining-service 事件同步
|
||
- [ ] 编写单元测试
|
||
- [ ] 性能测试K线查询
|
||
|
||
---
|
||
|
||
## 11. 启动命令
|
||
|
||
```bash
|
||
# 开发环境
|
||
npm run start:dev
|
||
|
||
# 生成 Prisma Client
|
||
npx prisma generate
|
||
|
||
# 运行迁移
|
||
npx prisma migrate dev
|
||
|
||
# 生产环境
|
||
npm run build && npm run start:prod
|
||
```
|