# settlement-service 结算服务

## 服务定位

结算服务负责把游戏结果转成结算任务，并编排派彩、注单已结算事件、商户回调、数仓事件等后续动作。第一阶段使用内存仓储和内存 producer，接口与模型按正式链路设计，后续可以平滑替换为 PostgreSQL、Redis、Redpanda。

## 核心职责

- 接收游戏结果侧结算请求，固化 `bet_order_id`、`round_id`、`merchant_id`、`player_id`、`payout`、`result_hash`、`rtp_bucket`。
- 使用 `idempotency_key` 保证同一结算请求只生成一笔派彩请求。
- 创建结算任务后发布 `wallet.credit.requested` 预留事件字段。
- 钱包派彩确认后完成结算，发布 `bet.settled`、`merchant.callback.requested`、warehouse 预留事件。
- 处理失败和超时；如果超时发生在钱包已派彩之后，记录人工复核标记，不再次发起派彩。

## 当前实现范围

第一阶段已具备标准 Go 服务分层：

```text
cmd/internal/bootstrap/config/domain/handler/model/repository/service/api/openapi/configs
```

当前仓储为 `internal/repository/postgres.NewMemorySettlementRepository()`，producer 为 `service.NewMemoryProducer()`。两者都是内存实现，但模型已保留正式事件字段：

- `wallet_credit_event_id`
- `wallet_transaction_id`
- `bet_settled_event_id`
- `callback_event_id`
- `warehouse_event_id`
- `balance_before` / `balance_after`

## HTTP 接口

### 创建结算任务

```bash
curl -X POST http://127.0.0.1:8087/settlements \
  -H 'Content-Type: application/json' \
  -H 'X-Request-ID: trace_001' \
  -d '{
    "event_id": "game_result_001",
    "trace_id": "trace_001",
    "merchant_id": "DEMO",
    "player_id": "player_001",
    "round_id": "round_001",
    "bet_order_id": "bet_001",
    "idempotency_key": "settle_001",
    "payout": 180,
    "currency": "CNY",
    "result_hash": "sha256:result_001",
    "rtp_bucket": "demo-slots:96",
    "callback_url": "https://merchant.example.com/callback"
  }'
```

成功后状态为 `CREATED`，并发布一次 `wallet.credit.requested`。重复提交同一 `idempotency_key` 和同一业务参数返回 `IDEMPOTENT_HIT`，不会再次发布派彩请求；同 key 不同 `payout/result_hash/currency/rtp_bucket` 返回 `IDEMPOTENCY_CONFLICT`。

### 完成结算

```bash
curl -X POST http://127.0.0.1:8087/settlements/{settlement_id}/complete \
  -H 'Content-Type: application/json' \
  -d '{
    "wallet_transaction_id": "wallet_txn_001",
    "balance_before": 1000,
    "balance_after": 1180
  }'
```

成功后状态为 `COMPLETED`，并发布：

- `bet.settled`
- `merchant.callback.requested`
- warehouse 预留事件，当前使用 `game.result.generated` 事件类型承载 `warehouse_stream=settlement_completed`

重复完成同一结算返回 `IDEMPOTENT_HIT`，不会重复发布完成事件。

### 标记失败

```bash
curl -X POST http://127.0.0.1:8087/settlements/{settlement_id}/fail \
  -H 'Content-Type: application/json' \
  -d '{
    "reason": "wallet credit timeout"
  }'
```

如果失败请求带 `wallet_transaction_id`，表示超时前钱包已经实际派彩：

```bash
curl -X POST http://127.0.0.1:8087/settlements/{settlement_id}/fail \
  -H 'Content-Type: application/json' \
  -d '{
    "reason": "settlement timeout after wallet credit",
    "wallet_transaction_id": "wallet_txn_001"
  }'
```

服务会设置 `wallet_credit_applied=true`、`requires_manual_review=true`、状态 `FAILED`，但不会再次发布 `wallet.credit.requested`，避免重复派彩。

### 查询结算

```bash
curl http://127.0.0.1:8087/settlements/{settlement_id}
```

## 状态机

```text
CREATED -> COMPLETED
CREATED -> FAILED
WALLET_CREDITED -> COMPLETED
WALLET_CREDITED -> FAILED
COMPLETED -> COMPLETED(IDEMPOTENT_HIT)
FAILED -> FAILED(IDEMPOTENT_HIT)
```

`WALLET_CREDITED` 是模型预留状态，用于后续消费 `wallet.credit.succeeded` 后先落“已派彩、未完成所有下游动作”的中间状态。第一阶段 HTTP `complete` 会直接将任务完成，同时记录钱包交易信息。

## 与主链路关系

- `bet-order-service`：结算完成后接收或消费 `bet.settled`，更新注单结算状态。
- `wallet-service`：创建结算时通过 `wallet.credit.requested` 语义请求派彩，幂等 key 使用结算 `idempotency_key`，防止重复加款。
- `callback-service`：结算完成后通过 `merchant.callback.requested` 创建商户回调任务，回调内容包含 payout、result_hash、status。
- `warehouse-writer`：结算完成后写入 warehouse 预留事件，字段包含 `settlement_id`、`bet_order_id`、`round_id`、`payout`、`result_hash`、`rtp_bucket`、钱包交易号。

## 并发与幂等设计

- 内存仓储使用 `sync.Mutex` 保护 `settlements` 和 `byIdempotency`。
- 幂等范围：`merchant_id + player_id + round_id + bet_order_id + idempotency_key`。
- 创建接口只有第一次写入会发布 `wallet.credit.requested`，并发重复请求返回同一个 `settlement_id`。
- 完成接口对 `COMPLETED` 返回幂等命中，不重复发布 `bet.settled/callback/warehouse`。
- 失败接口对 `FAILED` 返回幂等命中；对 `COMPLETED` 拒绝，避免完成后被错误回退。

## 数据库表建议

后续 PostgreSQL 表可按以下字段落地：

```sql
CREATE TABLE settlement_tasks (
  settlement_id TEXT PRIMARY KEY,
  event_id 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,
  idempotency_key TEXT NOT NULL,
  payout BIGINT NOT NULL CHECK (payout >= 0),
  currency TEXT NOT NULL,
  result_hash TEXT NOT NULL,
  rtp_bucket TEXT,
  status TEXT NOT NULL,
  wallet_credit_event_id TEXT NOT NULL,
  wallet_transaction_id TEXT,
  wallet_credit_applied BOOLEAN NOT NULL DEFAULT FALSE,
  bet_settled_event_id TEXT NOT NULL,
  callback_event_id TEXT NOT NULL,
  callback_url TEXT,
  warehouse_event_id TEXT NOT NULL,
  balance_before BIGINT,
  balance_after BIGINT,
  failure_reason TEXT,
  requires_manual_review BOOLEAN NOT NULL DEFAULT FALSE,
  created_at TIMESTAMPTZ NOT NULL,
  updated_at TIMESTAMPTZ NOT NULL,
  completed_at TIMESTAMPTZ,
  failed_at TIMESTAMPTZ,
  UNIQUE (merchant_id, player_id, round_id, bet_order_id, idempotency_key)
);
```

建议配套 outbox 表保存 `wallet.credit.requested`、`bet.settled`、`merchant.callback.requested`、warehouse 事件，生产端以 `event_id` 做唯一键。

## 禁止事项

- 不能绕过 wallet-shard-service 增减余额。
- 不能覆盖历史账本。
- 不能在玩家未扣款成功时直接派彩。

## 本地命令

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

## 健康检查

```bash
curl http://127.0.0.1:8087/health
curl http://127.0.0.1:8087/ready
```

## OpenAPI 与配置

- OpenAPI：`api/openapi/settlement-service.openapi.yaml`
- 配置示例：`configs/config.example.yaml`
