From 401c24d15d19447fa5946e23f38342fba8012694 Mon Sep 17 00:00:00 2001 From: OuYingJun1024 <1034284404@qq.com> Date: Fri, 12 Jun 2026 14:29:50 +0800 Subject: [PATCH] =?UTF-8?q?feat(wallet):=20=E8=A7=A3=E7=BB=91=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1=E6=97=B6=E5=BE=85=E5=AE=A1=E6=A0=B8=E6=8F=90=E7=8E=B0?= =?UTF-8?q?=E3=80=8C=E7=AB=8B=E5=8D=B3=E9=80=80=E5=9B=9E=E7=8E=B0=E9=87=91?= =?UTF-8?q?=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原实现只清 openid,reviewing 单的退款推迟到管理员审核打款(execute_withdraw_transfer 回读空 openid)才触发——用户解绑后后台仍挂单、现金不即时退回。改为 force 解绑时直接对 所有 reviewing 单走 _refund_withdraw 立即退回现金 + 置终态(新增 refund_reviewing_ withdraws_on_unbind),让提示文案名副其实、后台不再挂死单。_refund_withdraw 加可选 remark 自定义流水文案。同步 docs/api。 Co-Authored-By: Claude Opus 4.8 (1M context) --- app/api/v1/wallet.py | 8 ++++--- app/repositories/wallet.py | 40 ++++++++++++++++++++++++++++---- docs/api/wallet-unbind-wechat.md | 2 +- 3 files changed, 42 insertions(+), 8 deletions(-) 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`(打款在途)单转账已发起、解绑影响不到、照常打款到微信,故**不拦**。