# withdraw_order — 提现单(现金 → 微信零钱)

> 模型 `app/models/wallet.py` · 仓库 `app/repositories/wallet.py` · 接口 [wallet-withdraw](../api/wallet/wallet-withdraw.md) / [wallet-withdraw-status](../api/wallet/wallet-withdraw-status.md) / [wallet-withdraw-orders](../api/wallet/wallet-withdraw-orders.md) · admin [admin-withdraws-list](../api/admin/withdraws/admin-withdraws-list.md) / [admin-withdraw-refresh](../api/admin/withdraws/admin-withdraw-refresh.md) · [← 索引](./README.md) · [总览](./OVERVIEW.md)

用户把现金余额提到微信零钱的工单。**含人工审核**(2026-06 起):发起即扣现金、进 `reviewing` 待审核、**不打款**;管理员后台审核通过才真正发起微信商家转账,拒绝则退款。

**状态机**:
```
发起 ──扣现金+建单──▶ reviewing
reviewing ──admin 审核通过──▶ pending(微信转账在途) ──▶ success / failed(失败自动退款)
reviewing ──admin 审核拒绝──▶ rejected(已退款)
```

## 用在哪 / 增删改查
- **C(插入)**:`POST /wallet/withdraw`(`create_withdraw`)→ 校验额度([10 分, 5,000,000 分])+ 必须已绑微信 → 原子扣现金 + 写 `cash_transaction(withdraw,−)` + 建单 `status='reviewing'`,同事务。`out_bill_no` 客户端可传作幂等键(同号重发返回现状,不重复扣款建单)。
- **U(更新,改 `status` / 微信回执)**:
  - admin 审核通过 `approve_withdraw` → `pending` → 调微信转账 → `success`(到账)/ 失败 `_settle_after_ambiguous`(查单后定夺)。
  - admin 审核拒绝 `reject_withdraw` → 退款 + `rejected`。
  - 用户/系统查单 `refresh_withdraw_status`(`GET /wallet/withdraw/status`)、对账 `reconcile_pending_withdraws`(定时任务扫 >15min 的 pending)→ 按微信侧状态归一化 `success`/`failed`。
  - 落微信回执:`wechat_state` / `transfer_bill_no` / `package_info`。
- **D**:无。
- **R**:`GET /wallet/withdraw/orders`(用户提现单列表);admin 提现管理列表/查单。

## 字段
| 列 | 类型 | 约束 / 默认 | 说明(取值 / join) |
|---|---|---|---|
| `id` | Integer | PK, autoincrement | |
| `user_id` | Integer | FK→user.id, index, NOT NULL | 归属用户 |
| `out_bill_no` | String(64) | UNIQUE, index, NOT NULL | 商户单号(幂等键 + 微信查单键)。客户端可传(`[0-9A-Za-z_-]{8,32}`),不传则服务端 `uuid4().hex`。**被 `cash_transaction.ref_id` / `invite_cash_transaction.ref_id` 引用**(按 `source` 落对应账本) |
| `source` | String(16) | NOT NULL, default `coin_cash` | 提现账户来源(#121 分账):`coin_cash`(金币兑换的现金,扣 `coin_account.cash_balance_cents`、流水落 `cash_transaction`)/ `invite_cash`(邀请奖励金,扣 `invite_cash_balance_cents`、流水落 [`invite_cash_transaction`](./invite_cash_transaction.md)) |
| `amount_cents` | Integer | NOT NULL | 提现金额(分) |
| `user_name` | String(64) | nullable | 提现实名;微信**达额转账要求实名**,发起时存下、审核打款时传给微信 |
| `status` | String(16) | NOT NULL, default `reviewing` | 归一化状态:`reviewing`(待审核,已扣款未打款)/ `pending`(打款在途)/ `success` / `failed`(打款失败已退)/ `rejected`(审核拒绝已退) |
| `wechat_state` | String(32) | nullable | 微信侧原始状态:`WAIT_USER_CONFIRM` / `SUCCESS` / `FAIL` / `CANCELLED` / `CLOSED` … |
| `transfer_bill_no` | String(64) | nullable | 微信转账单号(转账成功后回填) |
| `package_info` | String(512) | nullable | 待用户确认时返回 App 拉起微信确认页用(确认模式有,免确认转账无) |
| `fail_reason` | String(256) | nullable | 失败/拒绝技术原因(不外露,用户只看流水 `remark`) |
| `created_at` | DateTime(tz) | server_default now(), index | 发起时间 |
| `updated_at` | DateTime(tz) | server_default now(), onupdate now() | 更新时间 |

## 关系 / Join Key
- `user_id` → `user.id`(多对一)。
- `out_bill_no` ← 被流水 `ref_id` 引用,**账本按 `source` 分**:`coin_cash` 单 → `cash_transaction`(`withdraw` − / `withdraw_refund` +);`invite_cash` 单 → `invite_cash_transaction`(`invite_withdraw` − / `invite_withdraw_refund` +)。admin `GET /admin/api/withdraws/ledger-check` 按 source 分账做「单 ↔ 流水」对账(#121)。
- 打款方式依赖 `wechat_transfer_authorization`(用户有生效授权 → 免确认转账,否则确认模式)。

## 索引与约束
- PK `id`;UNIQUE+index `out_bill_no`;index `user_id`、`created_at`。
- 部分唯一索引 `ux_withdraw_order_user_active`(`user_id`),条件 `status IN ('reviewing', 'pending')`:每个用户同时只能有一笔在途(待审核 / 打款中)提现单,DB 层挡并发重复提现。

## 注意
- **资金安全**:原子扣款(`WHERE cash_balance_cents >= amount`)+ `out_bill_no` 幂等 + 结果不明时**先查单再决定,绝不盲目退款**(防退款后又到账)+ 孤儿 pending 单 `reconcile_pending_withdraws` 对账兜底。
- `WITHDRAW_MIN_CENTS=10`(0.1 元,微信商家转账地板价),可经 `app_config.withdraw_min_cents` 后台调。
- "待审核期间钱已扣减",防用户拿同一笔余额重复发起多笔提现。
