25 KiB
25 KiB
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 核心表结构
-- ============================================
-- 交易订单表
-- ============================================
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 卖出交易逻辑(核心)
/**
* 卖出积分股
* 核心机制:通过提前销毁未来的积分股,确保卖出不会导致价格下跌
*/
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 买入交易逻辑
/**
* 买入积分股
*/
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线数据聚合
/**
* 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 交互
// 买入时:扣减绿积分
await this.walletClient.deductGreenPoints(accountSequence, amount, orderId);
// 卖出时:增加绿积分
await this.walletClient.creditGreenPoints(accountSequence, amount, orderId);
6. Redis 缓存结构
// 当前价格缓存
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. 启动命令
# 开发环境
npm run start:dev
# 生成 Prisma Client
npx prisma generate
# 运行迁移
npx prisma migrate dev
# 生产环境
npm run build && npm run start:prod