rwadurian/backend/services/trading-service/DEVELOPMENT_GUIDE.md

25 KiB
Raw Blame History

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