From 0290b3c48023861649f1bcdb81a3ad144e0a1a9c Mon Sep 17 00:00:00 2001 From: OuYingJun1024 <1034284404@qq.com> Date: Fri, 12 Jun 2026 11:43:31 +0800 Subject: [PATCH] =?UTF-8?q?feat(wallet):=20=E8=A7=A3=E7=BB=91=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1=E5=89=8D=E5=AF=B9=E5=BE=85=E5=AE=A1=E6=A0=B8=E6=8F=90?= =?UTF-8?q?=E7=8E=B0=E4=BA=8C=E6=AC=A1=E7=A1=AE=E8=AE=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 有待审核(reviewing)提现时,首次解绑拦下返回 needs_confirm + 提示文案,用户确认后带 force=true 再调才真解绑。reviewing 单解绑后审核回读空 openid → 自动退回现金(钱不丢、 只为知情);pending 已在途、解绑影响不到,不拦。unbind-wechat 接受可选 body 兼容旧 客户端,bound 返回真实绑定态。同步 docs/api。 Co-Authored-By: Claude Opus 4.8 (1M context) --- app/api/v1/wallet.py | 18 ++++++++++++++++-- app/repositories/wallet.py | 20 ++++++++++++++++++++ app/schemas/welfare.py | 8 ++++++++ docs/api/wallet-unbind-wechat.md | 14 ++++++++++++-- 4 files changed, 56 insertions(+), 4 deletions(-) diff --git a/app/api/v1/wallet.py b/app/api/v1/wallet.py index 8b64c23..2dc8858 100644 --- a/app/api/v1/wallet.py +++ b/app/api/v1/wallet.py @@ -35,6 +35,7 @@ from app.schemas.welfare import ( ExchangeResultOut, TransferAuthResultOut, TransferAuthStatusOut, + UnbindWechatRequest, UnbindWechatResultOut, WithdrawInfoOut, WithdrawOrderOut, @@ -151,9 +152,22 @@ def bind_wechat(req: BindWechatRequest, user: CurrentUser, db: DbSession) -> Bin @router.post("/unbind-wechat", response_model=UnbindWechatResultOut, summary="解绑微信(清空 openid)") -def unbind_wechat(user: CurrentUser, db: DbSession) -> UnbindWechatResultOut: +def unbind_wechat( + user: CurrentUser, db: DbSession, req: UnbindWechatRequest | None = None +) -> UnbindWechatResultOut: + # 有「待审核(reviewing)」提现单时,首次解绑拦下要求确认:这类单还没打款,解绑后审核时 + # 回读空 openid → 自动退回现金余额(钱不丢),但用户需知情。确认后客户端带 force=true 再调即放行。 + # (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): + logger.info("unbind wechat needs_confirm(有待审核提现) user_id=%d", user.id) + return UnbindWechatResultOut( + bound=bool(user.wechat_openid), + needs_confirm=True, + message="你有提现正在审核中,解绑微信后这笔会自动退回到现金余额。确定解绑吗?", + ) crud_wallet.unbind_wechat_openid(db, user.id) - logger.info("unbind wechat ok user_id=%d", user.id) + logger.info("unbind wechat ok user_id=%d force=%s", user.id, force) return UnbindWechatResultOut(bound=False) diff --git a/app/repositories/wallet.py b/app/repositories/wallet.py index 7be86b4..cb5f38b 100644 --- a/app/repositories/wallet.py +++ b/app/repositories/wallet.py @@ -334,6 +334,26 @@ def bind_wechat_openid(db: Session, user_id: int, code: str) -> dict: return info +def has_reviewing_withdraw(db: Session, user_id: int) -> bool: + """用户是否有「待审核(reviewing)」的提现单。 + + 只看 reviewing:这类单还没打款,解绑微信后审核打款时回读空 openid → 自动退回现金余额 + (见 execute_withdraw_transfer),解绑前据此提示用户知情确认。 + pending 单转账已在途(execute 已过 openid 闸),解绑影响不到、照常打款,故不计入。 + """ + return ( + db.execute( + select(WithdrawOrder.id) + .where( + WithdrawOrder.user_id == user_id, + WithdrawOrder.status == "reviewing", + ) + .limit(1) + ).first() + is not None + ) + + def unbind_wechat_openid(db: Session, user_id: int) -> None: """解绑微信:清空 openid。已绑定才清,未绑定为幂等空操作。""" user = db.get(User, user_id) diff --git a/app/schemas/welfare.py b/app/schemas/welfare.py index 8977faf..2cb6027 100644 --- a/app/schemas/welfare.py +++ b/app/schemas/welfare.py @@ -109,8 +109,16 @@ class BindWechatResultOut(BaseModel): wechat_avatar_url: str | None = None +class UnbindWechatRequest(BaseModel): + # 有进行中提现单时,首次解绑会被拦(needs_confirm),用户确认后带 force=true 再调一次才真解绑。 + force: bool = Field(False, description="跳过「进行中提现」二次确认,强制解绑") + + class UnbindWechatResultOut(BaseModel): bound: bool = False + # 有进行中提现单且未 force → True:本次未解绑(bound 仍 True),客户端弹确认弹窗。 + needs_confirm: bool = False + message: str | None = None class WithdrawRequest(BaseModel): diff --git a/docs/api/wallet-unbind-wechat.md b/docs/api/wallet-unbind-wechat.md index daba1da..9db534d 100644 --- a/docs/api/wallet-unbind-wechat.md +++ b/docs/api/wallet-unbind-wechat.md @@ -5,14 +5,24 @@ > 集成实现:见 [integrations/wxpay](../integrations/wxpay.md)。 ## 入参 -无(用户由 token 确定)。 +请求体 `UnbindWechatRequest`(**可选**:旧客户端可不带 body,等价 `force=false`): + +| 字段 | 类型 | 默认 | 说明 | +|---|---|---|---| +| `force` | bool | `false` | 跳过「有待审核提现」二次确认,强制解绑。首次解绑传 `false`;命中 `needs_confirm` 后用户确认,再带 `true` 调一次才真解绑 | ## 出参 响应 `200`:`UnbindWechatResultOut` | 字段 | 类型 | 说明 | |---|---|---| -| `bound` | bool | 固定 `false` | +| `bound` | bool | 解绑成功为 `false`;命中二次确认(本次未解绑)时为当前真实绑定态(通常 `true`) | +| `needs_confirm` | bool | `true` = 有待审核提现且未 `force`,**本次未解绑**,客户端应弹确认弹窗 | +| `message` | string \| null | `needs_confirm=true` 时的提示文案,直接展示给用户 | ## 说明 清空当前用户的 `wechat_openid` / `wechat_nickname` / `wechat_avatar_url`。幂等:未绑定时调用也返回 `bound=false`。解绑后该微信可被其他账号绑定。 + +**有待审核提现时的二次确认**:用户有 `reviewing`(待审核)状态的提现单且未带 `force=true` 时,**首次解绑被拦下**——不清 openid,返回 `bound`(真实绑定态)+ `needs_confirm=true` + `message`。客户端据此弹确认弹窗,用户确认后带 `force=true` 再调一次即放行解绑。 + +> 只拦 `reviewing`:这类单还没打款,解绑后审核时回读到空 openid → 自动退回现金余额(钱不丢,见 [`execute_withdraw_transfer`](../../app/repositories/wallet.py)),用户需知情。`pending`(打款在途)单转账已发起、解绑影响不到、照常打款到微信,故**不拦**。