# ledger-writer-service 账本写入服务

## 服务职责

ledger-writer-service 负责把已经确认成功的账本事件幂等写入持久化存储。它不重新计算余额，也不修改历史账本，只做事件收件、去重、append-only 落地和写入状态返回。

第一阶段链路：

```text
wallet.debit.succeeded 事件
        ↓
POST /ledger-writer/events
        ↓
event_id 幂等去重
        ↓
PostgreSQL ledger_event_inbox + ClickHouse ledger_entries 写入模拟
        ↓
APPENDED / IDEMPOTENT_HIT 批量结果
```

## 当前代码目录

```text
平台服务/ledger-writer-service
├── cmd/ledger-writer-service       进程入口
├── api/openapi                     HTTP API 契约
├── configs                         配置模板
├── internal/architecture           标准目录约束测试
├── internal/bootstrap              服务启动组装
├── internal/config                 环境变量读取
├── internal/domain                 领域错误
├── internal/handler/http           Hertz 路由和请求响应适配
├── internal/middleware             中间件预留
├── internal/model                  事件、写入记录、批量结果模型
├── internal/repository/postgres    第一阶段内存仓储；模拟 PostgreSQL/ClickHouse 写入
└── internal/service                批量校验、幂等、append-only 写入编排
```

## HTTP 接口

### POST /ledger-writer/events

接收账本事件批次。当前第一阶段只支持：

```text
wallet.debit.succeeded
```

请求示例：

```bash
curl -sS http://127.0.0.1:8094/ledger-writer/events \
  -H 'Content-Type: application/json' \
  -H 'X-Request-ID: ledger-writer-local-001' \
  -d '{
    "events": [
      {
        "event_id": "wallet_debit_succeeded_001",
        "event_type": "wallet.debit.succeeded",
        "trace_id": "trace_001",
        "merchant_id": "DEMO",
        "player_id": "player_001",
        "round_id": "round_001",
        "bet_order_id": "bet_001",
        "schema_version": "v1",
        "occurred_at": "2026-06-06T10:00:00Z",
        "payload": {
          "source_event_id": "wallet_evt_001",
          "ledger_transaction_id": "ledger_txn_001",
          "idempotency_key": "bet_001",
          "entry_type": "DEBIT",
          "amount": 100,
          "currency": "CNY",
          "balance_before": 0,
          "balance_after": -100,
          "shard_id": 7
        }
      }
    ]
  }'
```

返回示例：

```json
{
  "code": "OK",
  "message": "success",
  "request_id": "ledger-writer-local-001",
  "data": {
    "total": 1,
    "accepted": 1,
    "duplicated": 0,
    "results": [
      {
        "event_id": "wallet_debit_succeeded_001",
        "status": "APPENDED",
        "record_id": "ledger_write_xxx",
        "ledger_transaction_id": "ledger_txn_001",
        "idempotency_key": "bet_001",
        "stored_sinks": [
          "postgres.ledger_event_inbox",
          "clickhouse.ledger_entries"
        ],
        "message": "append-only record persisted"
      }
    ]
  }
}
```

重复提交同一个 `event_id` 时返回 `IDEMPOTENT_HIT`，不会再次追加记录。

## 幂等与 append-only 约束

- 幂等主键：`event_id`。
- 第一阶段 repository 用内存 map 模拟 PostgreSQL 唯一约束。
- 同一批次内重复事件也按同一个 `event_id` 去重。
- `Records()` 返回副本，测试覆盖外部修改不能影响历史记录。
- 当前只支持 `wallet.debit.succeeded` + `entry_type=DEBIT`。
- 模型预留 `credit_amount`、`settle_amount`、`settle_order_id`、`external_reference_id`，后续可扩展 `wallet.credit.succeeded` 和 `bet.settled`。

## 数据库表建议

### PostgreSQL: ledger_event_inbox

```sql
CREATE TABLE ledger_event_inbox (
  record_id TEXT PRIMARY KEY,
  event_id TEXT NOT NULL UNIQUE,
  event_type TEXT NOT NULL,
  trace_id TEXT NOT NULL,
  merchant_id TEXT NOT NULL,
  player_id TEXT NOT NULL,
  round_id TEXT NOT NULL,
  bet_order_id TEXT NOT NULL,
  ledger_transaction_id TEXT NOT NULL,
  idempotency_key TEXT NOT NULL,
  schema_version TEXT NOT NULL DEFAULT 'v1',
  payload JSONB NOT NULL,
  occurred_at TIMESTAMPTZ NOT NULL,
  received_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_ledger_event_inbox_bet_order
  ON ledger_event_inbox (merchant_id, bet_order_id);
```

### ClickHouse: ledger_entries

```sql
CREATE TABLE ledger_entries (
  event_id String,
  event_type String,
  trace_id String,
  merchant_id String,
  player_id String,
  round_id String,
  bet_order_id String,
  ledger_transaction_id String,
  idempotency_key String,
  entry_type String,
  amount Int64,
  currency String,
  balance_before Int64,
  balance_after Int64,
  shard_id Int32,
  occurred_at DateTime64(3, 'UTC'),
  received_at DateTime64(3, 'UTC')
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(received_at)
ORDER BY (merchant_id, bet_order_id, event_id);
```

## 并发设计

第一阶段内存仓储用 mutex 包住 `event_id` 索引和 append-only 列表，模拟真实库里的唯一索引与事务边界。生产化后建议：

- PostgreSQL 使用 `INSERT ... ON CONFLICT (event_id) DO NOTHING/RETURNING` 保证幂等。
- ClickHouse 写入由 PostgreSQL inbox 成功后异步或同事务外可靠投递，失败时保留待补偿状态。
- 批量上限由 `LEDGER_WRITER_MAX_BATCH_SIZE` 控制，默认 500。
- 上游消费 Redpanda 时按 `merchant_id/player_id` 或 `bet_order_id` 分区，避免同玩家资金事件乱序。
- 写入结果必须可重放：消费者崩溃后重新消费同一事件，只能命中幂等，不能产生新账。

## 命令

```bash
cd /Users/amumu/Desktop/beifen/golang新架构/平台服务/ledger-writer-service
PORT=8094 go run ./cmd/ledger-writer-service
go test ./...
go build ./cmd/ledger-writer-service
```

成熟度检查：

```bash
cd /Users/amumu/Desktop/beifen/golang新架构
./scripts/check-go-service-maturity.sh
```

## 后续生产化改造

- 接入 Redpanda consumer，消费 `wallet.debit.succeeded` topic。
- 用 PostgreSQL repository 替换内存仓储，补 migration 和唯一约束。
- 用 ClickHouse batch writer 替换内存模拟 sink。
- 增加 inbox 状态：`RECEIVED`、`POSTGRES_WRITTEN`、`CLICKHOUSE_WRITTEN`、`FAILED_RETRYABLE`。
- 增加重试、死信队列、Prometheus 指标、OpenTelemetry trace。
- 扩展 `wallet.credit.succeeded`、`bet.settled`、冲正事件，并为每种事件补独立测试。
