fix(withdraw): 新人档提现没成功也被判定已用(解绑微信退回后 0.1 永久锁死) #142

Merged
guke merged 1 commits from fix/newbie-tier-restore-on-refund into main 2026-07-17 16:34:58 +08:00
Member

现象

新人提现 0.1 元,提现审核中时解绑微信 —— 现金正常退回,但「新人领取的 0.1 元」被判定为已使用:该档位从提现页永久消失,重提报 400。

根因

新人档(0.1/0.3)「一次性」的判定口径只看有没有发起过,完全不看提现单的最终状态。withdraw_tier_states() 里的 used_newbie 查询没有任何 status 过滤:

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

## 现象 新人提现 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)
liujiahui added 1 commit 2026-07-17 16:10:17 +08:00
新人档(0.1/0.3)「一次性」判定原本只看「有没有发起过」、不看提现单最终状态:
withdraw_tier_states 的 used_newbie 查询没有任何 status 过滤。用户解绑微信时,
待审核单经 refund_reviewing_withdraws_on_unbind → _refund_withdraw 退回现金并
置 failed,但记录仍在库里,于是照样被算作「已用」→ 钱退了、资格没退,0.1 档从
档位列表永久消失且重提 400。

判定口径改为只认 reviewing/pending/success(进行中或真拿到钱);failed/rejected
(被拒、微信转账失败、解绑退回,钱都没到手)一律恢复可提。下发(withdraw-info)与
下单校验(create_withdraw)共用 withdraw_tier_states,改一处两处同时生效。复用现有
status 语义,无新字段、无 alembic 迁移;存量受害用户上线后自动恢复,已成功提现的
(success)仍正确锁死。

⚠️行为变更:7-9 定的「新人档发起就算(含被拒/失败)」按本次拍板放宽为「没成功打款
就恢复」。常规档(0.5/10/20)的「当天名额发起就算、被拒不退」规则未动。

⚠️测试按要求不进本 PR。但请注意:既有测试
tests/test_withdraw_tiers.py::test_newbie_tiers_independent_and_once_forever
锁死的正是被本次推翻的旧口径(断言被拒后 0.1 消失、重提 400),本改动会让它失败,
需要同步调整该测试。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
liujiahui force-pushed fix/newbie-tier-restore-on-refund from faa7039e7d to 1b799a146a 2026-07-17 16:10:17 +08:00 Compare
guke merged commit b395648b7c into main 2026-07-17 16:34:58 +08:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: WonderableAI/shaguabijia-app-server#142