diff --git a/app/api/v1/wallet.py b/app/api/v1/wallet.py index 2dc8858..55a6f5e 100644 --- a/app/api/v1/wallet.py +++ b/app/api/v1/wallet.py @@ -155,8 +155,8 @@ def bind_wechat(req: BindWechatRequest, user: CurrentUser, db: DbSession) -> Bin def unbind_wechat( user: CurrentUser, db: DbSession, req: UnbindWechatRequest | None = None ) -> UnbindWechatResultOut: - # 有「待审核(reviewing)」提现单时,首次解绑拦下要求确认:这类单还没打款,解绑后审核时 - # 回读空 openid → 自动退回现金余额(钱不丢),但用户需知情。确认后客户端带 force=true 再调即放行。 + # 有「待审核(reviewing)」提现单时,首次解绑拦下要求确认:这类单还没打款,确认解绑(force) + # 时会被立即退回现金余额(refund_reviewing_withdraws_on_unbind),后台不再挂单,用户需先知情。 # (pending 单转账已在途、解绑影响不到,不拦;见 has_reviewing_withdraw。) force = req.force if req is not None else False if not force and crud_wallet.has_reviewing_withdraw(db, user.id): @@ -166,8 +166,10 @@ def unbind_wechat( needs_confirm=True, message="你有提现正在审核中,解绑微信后这笔会自动退回到现金余额。确定解绑吗?", ) + # 先把待审核提现立即退回现金(无则 no-op),再清 openid —— 让"解绑即退"名副其实 + refunded = crud_wallet.refund_reviewing_withdraws_on_unbind(db, user.id) crud_wallet.unbind_wechat_openid(db, user.id) - logger.info("unbind wechat ok user_id=%d force=%s", user.id, force) + logger.info("unbind wechat ok user_id=%d force=%s refunded_withdraws=%d", user.id, force, refunded) return UnbindWechatResultOut(bound=False) diff --git a/app/repositories/wallet.py b/app/repositories/wallet.py index cb5f38b..3438928 100644 --- a/app/repositories/wallet.py +++ b/app/repositories/wallet.py @@ -337,8 +337,8 @@ def bind_wechat_openid(db: Session, user_id: int, code: str) -> dict: def has_reviewing_withdraw(db: Session, user_id: int) -> bool: """用户是否有「待审核(reviewing)」的提现单。 - 只看 reviewing:这类单还没打款,解绑微信后审核打款时回读空 openid → 自动退回现金余额 - (见 execute_withdraw_transfer),解绑前据此提示用户知情确认。 + 只看 reviewing:这类单还没打款,用户确认解绑时会被立即退回现金 + (refund_reviewing_withdraws_on_unbind),解绑前据此提示用户知情确认。 pending 单转账已在途(execute 已过 openid 闸),解绑影响不到、照常打款,故不计入。 """ return ( @@ -366,6 +366,33 @@ def unbind_wechat_openid(db: Session, user_id: int) -> None: db.commit() +def refund_reviewing_withdraws_on_unbind(db: Session, user_id: int) -> int: + """解绑微信时,把该用户所有「待审核(reviewing)」提现单立即退回现金并置终态。 + + reviewing 单还没打款(钱在 create_withdraw 时已从现金扣进待审核单),解绑后这笔无法 + 打款到微信,直接退回现金最干净:用户即时拿回钱、后台不再挂死单(不必等管理员审核)。 + 复用 _refund_withdraw(退现金 + 流水 + 幂等)。返回退掉的单数。 + """ + orders = ( + db.execute( + select(WithdrawOrder).where( + WithdrawOrder.user_id == user_id, + WithdrawOrder.status == "reviewing", + ) + ) + .scalars() + .all() + ) + for order in orders: + _refund_withdraw( + db, + order, + reason="用户解绑微信,待审核提现自动退回", + remark="解绑微信,提现已退回现金余额", + ) + return len(orders) + + _OUT_BILL_NO_RE = re.compile(r"^[0-9A-Za-z_-]{8,32}$") @@ -401,7 +428,8 @@ def _add_cash(db: Session, user_id: int, amount_cents: int) -> int: def _refund_withdraw( - db: Session, order: WithdrawOrder, reason: str, *, final_status: str = "failed" + db: Session, order: WithdrawOrder, reason: str, *, final_status: str = "failed", + remark: str | None = None, ) -> None: """退回现金 + 写 withdraw_refund 流水 + 单子置终态。幂等:已是退款终态不重复退。 @@ -409,6 +437,7 @@ def _refund_withdraw( - "failed" (默认):微信转账失败/取消 → 自动退款 - "rejected":管理员审核拒绝 → 退款(走 reject_withdraw) 两种都已退款,用 in 判定防重复退(并发/对账/拒绝重复点)。 + remark:给用户可见的现金流水文案;省略则按 final_status 取默认(解绑退回等场景可自定义)。 """ if order.status in ("failed", "rejected"): return # 防重复退款(并发/对账与查单/重复拒绝同时触发) @@ -433,7 +462,10 @@ def _refund_withdraw( biz_type="withdraw_refund", ref_id=order.out_bill_no, # 用户可见文案区分"未成功(自动退)"vs"审核未通过";技术原因记在 order.fail_reason - remark="提现审核未通过,金额已退回" if final_status == "rejected" else "提现未成功,金额已退回", + remark=( + remark + or ("提现审核未通过,金额已退回" if final_status == "rejected" else "提现未成功,金额已退回") + ), created_at=datetime.now(rewards.CN_TZ).replace(tzinfo=None), ) ) diff --git a/docs/api/wallet-unbind-wechat.md b/docs/api/wallet-unbind-wechat.md index 9db534d..01211dc 100644 --- a/docs/api/wallet-unbind-wechat.md +++ b/docs/api/wallet-unbind-wechat.md @@ -25,4 +25,4 @@ **有待审核提现时的二次确认**:用户有 `reviewing`(待审核)状态的提现单且未带 `force=true` 时,**首次解绑被拦下**——不清 openid,返回 `bound`(真实绑定态)+ `needs_confirm=true` + `message`。客户端据此弹确认弹窗,用户确认后带 `force=true` 再调一次即放行解绑。 -> 只拦 `reviewing`:这类单还没打款,解绑后审核时回读到空 openid → 自动退回现金余额(钱不丢,见 [`execute_withdraw_transfer`](../../app/repositories/wallet.py)),用户需知情。`pending`(打款在途)单转账已发起、解绑影响不到、照常打款到微信,故**不拦**。 +> 只拦 `reviewing`:这类单还没打款,用户确认解绑(`force=true`)时**立即退回现金余额**(写 `withdraw_refund` 流水、单子置终态),后台不再挂单、不必等管理员审核。`pending`(打款在途)单转账已发起、解绑影响不到、照常打款到微信,故**不拦**。