# 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
```
