# wallet-service 钱包余额服务

## 服务职责

wallet-service 负责玩家钱包余额视图和第一阶段资金变更入口，按 `merchant_id + player_id + currency` 维护可用余额、冻结余额预留字段和乐观版本号。

第一阶段提供同步 HTTP 能力：

```text
POST /wallet/debit
POST /wallet/credit
GET  /wallet/balance
```

当前 repository 是内存实现，用于验证接口、幂等、并发边界和测试结构；接口和模型按后续 PostgreSQL/Redis 落地设计。

## 当前代码目录

```text
平台服务/wallet-service
├── cmd/wallet-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/Redis
└── internal/service                扣款、加款、余额查询、幂等和版本逻辑
```

## 命令

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

项目级成熟度检查：

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

## 接口

### POST /wallet/credit

给指定钱包加款。重复 `idempotency_key` 返回第一次结果，不重复加款。

```bash
curl -sS http://127.0.0.1:8092/wallet/credit \
  -H 'Content-Type: application/json' \
  -H 'X-Request-ID: wallet-credit-local-001' \
  -d '{
    "merchant_id": "DEMO",
    "player_id": "player_001",
    "currency": "CNY",
    "idempotency_key": "credit_001",
    "amount": 1000
  }'
```

### POST /wallet/debit

扣减指定钱包可用余额。余额不足返回 `409 INSUFFICIENT_BALANCE`，重复 `idempotency_key` 返回第一次结果，不重复扣款。

```bash
curl -sS http://127.0.0.1:8092/wallet/debit \
  -H 'Content-Type: application/json' \
  -H 'X-Request-ID: wallet-debit-local-001' \
  -d '{
    "merchant_id": "DEMO",
    "player_id": "player_001",
    "currency": "CNY",
    "idempotency_key": "bet_001",
    "amount": 300
  }'
```

### GET /wallet/balance

查询指定商户、玩家、币种余额。

```bash
curl -sS 'http://127.0.0.1:8092/wallet/balance?merchant_id=DEMO&player_id=player_001&currency=CNY'
```

## 并发设计

- 余额维度固定为 `merchant_id + player_id + currency`，不同商户、玩家、币种互不串账。
- 幂等维度固定为 `merchant_id + player_id + currency + idempotency_key`，同一请求重复到达时返回第一次 `wallet_transaction_id` 和余额结果。
- 同一个 `idempotency_key` 如果被不同操作类型或不同金额复用，必须返回 `409 IDEMPOTENCY_CONFLICT`，不能把旧结果误当作新请求成功。
- 内存 repository 用单个临界区模拟数据库事务，事务内顺序执行：查幂等记录、读取余额、校验 version、校验余额、更新余额、写入幂等结果。
- `version` 每次成功加款或扣款递增；请求可传 `expected_version` 做乐观并发控制，不匹配返回 `409 VERSION_CONFLICT`。
- `available_balance` 当前参与扣款；`frozen_balance` 第一阶段保留为 0，后续冻结、解冻、结算可复用同一版本控制模型。
- 后续 PostgreSQL 落地建议使用 `SELECT ... FOR UPDATE` 锁定单行钱包，或用 `version = expected_version` 条件更新配合重试；Redis 落地建议用 Lua 脚本把幂等检查和余额更新放在一个原子脚本里。

## 数据库表建议

### wallet_balances

```sql
CREATE TABLE wallet_balances (
  merchant_id text NOT NULL,
  player_id text NOT NULL,
  currency text NOT NULL,
  available_balance bigint NOT NULL DEFAULT 0,
  frozen_balance bigint NOT NULL DEFAULT 0,
  version bigint NOT NULL DEFAULT 0,
  updated_at timestamptz NOT NULL DEFAULT now(),
  PRIMARY KEY (merchant_id, player_id, currency)
);
```

### wallet_idempotency

```sql
CREATE TABLE wallet_idempotency (
  merchant_id text NOT NULL,
  player_id text NOT NULL,
  currency text NOT NULL,
  idempotency_key text NOT NULL,
  wallet_transaction_id text NOT NULL,
  operation_type text NOT NULL,
  amount bigint NOT NULL,
  balance_before bigint NOT NULL,
  balance_after bigint NOT NULL,
  version bigint NOT NULL,
  response_json jsonb NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now(),
  PRIMARY KEY (merchant_id, player_id, currency, idempotency_key)
);
```

### wallet_transactions

```sql
CREATE TABLE wallet_transactions (
  wallet_transaction_id text PRIMARY KEY,
  merchant_id text NOT NULL,
  player_id text NOT NULL,
  currency text NOT NULL,
  operation_type text NOT NULL,
  idempotency_key text NOT NULL,
  amount bigint NOT NULL,
  balance_before bigint NOT NULL,
  balance_after bigint NOT NULL,
  frozen_balance bigint NOT NULL DEFAULT 0,
  version bigint NOT NULL,
  trace_id text,
  created_at timestamptz NOT NULL DEFAULT now()
);
```

## 超时但 Go 已扣款的处理建议

当客户端、网关或上游服务超时，但 wallet-service 内部已经完成扣款时，禁止用新幂等键重试扣款。

推荐处理：

- 上游必须携带稳定 `idempotency_key`，例如注单号、派奖单号或退款单号。
- 超时后优先用同一个 `idempotency_key` 重试原请求；服务返回 `IDEMPOTENT_HIT` 时以上一次结果为准。
- 若上游无法确认结果，先查 `GET /wallet/balance` 或后续 `wallet_transactions` 查询接口，再决定是否补偿。
- 对已经扣款但后续链路失败的场景，走反向交易或退款交易，生成新的幂等键和新的交易记录，不能直接回滚余额行。
- 后续接 ledger-service 后，应以账本交易状态和 wallet idempotency 表作为最终排查依据。

## 第一阶段已实现

- 标准 Go 服务分层和 Hertz 启动。
- `POST /wallet/debit`、`POST /wallet/credit`、`GET /wallet/balance`。
- merchant/player/currency 余额隔离。
- idempotency_key 幂等命中。
- idempotency_key 冲突检测。
- 余额不足错误。
- version 递增和 `expected_version` 乐观并发控制。
- `available_balance`、`frozen_balance` 字段预留。
- OpenAPI、config example、业务测试、架构测试。

## 下一阶段必须补齐

- PostgreSQL repository 和 migration。
- Redis 原子幂等/余额脚本或分布式锁策略。
- 与 wallet-shard-service 的事件消费衔接。
- 与 ledger-service 的交易/账本一致性衔接。
- 冻结、解冻、退款、冲正接口。
- Prometheus 指标、OpenTelemetry trace、压测脚本和报告。
