b395648b7c
## 现象
新人提现 0.1 元,提现审核中时解绑微信 —— 现金正常退回,但「新人领取的 0.1 元」被判定为已使用:该档位从提现页永久消失,重提报 400。
## 根因
新人档(0.1/0.3)「一次性」的判定口径只看**有没有发起过**,完全不看提现单的最终状态。`withdraw_tier_states()` 里的 `used_newbie` 查询没有任何 `status` 过滤:
```python
select(WithdrawOrder.amount_cents).distinct().where(
WithdrawOrder.user_id == user_id,
WithdrawOrder.source == "coin_cash",
WithdrawOrder.amount_cents.in_(newbie_amounts),
# ← 没有 status 条件:只要「存在这条单」就算用过
)
```
而解绑微信时,`refund_reviewing_withdraws_on_unbind` → `_refund_withdraw` 把待审核单**退回现金 + 置 `failed`**,但记录仍在库里。两者相撞:那条 `failed` 的单照样落进 `used_newbie` → **钱退了,资格没退**。
## 改法
判定口径从「发起过没有」改为「钱是不是真到手 / 正在路上」——只认 `reviewing`/`pending`/`success`:
| status | 含义 | 钱在哪 | 新人档 |
|---|---|---|---|
| `reviewing` / `pending` | 待审核 / 打款在途 | 已扣,挂在单上 | 占用(防重复发起) |
| `success` | 打款成功 | **进了用户微信** | 用掉,永久锁死 |
| `failed` | 转账失败 / **解绑退回** | 已退回余额 | **恢复可提** |
| `rejected` | 审核拒绝 | 已退回余额 | **恢复可提** |
核心即查询加一行 `WithdrawOrder.status.in_(_NEWBIE_TIER_HELD_STATUSES)`。
- 下发(`withdraw-info`)与下单校验(`create_withdraw`)共用 `withdraw_tier_states`,**改一处两处同时生效**,不会出现「显示能提、点了 400」。
- 复用现有 `status` 语义,**无新字段、无 alembic 迁移**。
- **存量受害用户上线后自动恢复**(他们那条单是 `failed`,新查询天然排除它),无需数据修复脚本;已成功提现过的(`success`)仍正确锁死,不会误恢复。
## ⚠️ 行为变更
7-9 定的「新人档发起就算(含被拒/失败)」按本次拍板**放宽为「没成功打款就恢复」**。
常规档(0.5 / 10 / 20)的「当天名额发起就算、被拒不退」规则**一行未动**。
## 🔴 测试按要求不进本 PR —— 但既有测试会失败,合入前需同步处理
本 PR 只含源码改动。**既有测试 `tests/test_withdraw_tiers.py::test_newbie_tiers_independent_and_once_forever` 锁死的正是本次推翻的旧口径**(它断言「被拒后 0.1 消失、重提 400」)。
已实测(main 上的原版测试 + 本 PR 的新代码):
```
FAILED tests/test_withdraw_tiers.py::test_newbie_tiers_independent_and_once_forever
1 failed, 5 passed
```
**本 PR 合入前需同步调整该测试,否则 main 会红。** 本地已备好可用的测试改动(反转该断言,另加「解绑退回后恢复」回归 + 「成功打款后永久锁死」),需要即可单独提供。
## 待办
- 上述既有测试需同步调整(否则合入后 main 红)
- 纯后端逻辑改动,**未真机验证**
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: no_gen_mu <liujianhishen@gmail.com>
Reviewed-on: #142
Co-authored-by: liujiahui <liujiahui@wonderable.ai>
Co-committed-by: liujiahui <liujiahui@wonderable.ai>