From 00ee4b0333d716b069a83bd07bb65bc4badc2d30 Mon Sep 17 00:00:00 2001 From: Pine Date: Sun, 13 Sep 2026 17:14:44 +0800 Subject: [PATCH] =?UTF-8?q?fix(compute):=20=E4=B8=AA=E4=BA=BA=E4=BD=99?= =?UTF-8?q?=E9=A2=9D=E8=BD=AC=E4=BC=81=E4=B8=9A=E4=BB=A5=E5=BC=95=E6=93=8E?= =?UTF-8?q?=E5=8F=AF=E7=94=A8=E9=A2=9D=E5=BA=A6=E4=B8=BA=E6=9D=83=E5=A8=81?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E5=A4=8D=E8=B4=A6=E6=9C=AC=E4=B8=8E=E5=BC=95?= =?UTF-8?q?=E6=93=8E=E4=B8=8D=E4=B8=80=E8=87=B4=E5=AF=BC=E8=87=B4=E6=97=A0?= =?UTF-8?q?=E6=B3=95=E8=BD=AC=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:个人可用算力有三套口径——引擎 quota(实际可消费)、平台账本 compute_personal_balance(充值/企业转入记录)、充值订单。三者可能不一致 (赠送/历史调整后引擎额度 > 账本),而 from-personal 转账硬校验账本 >= 金额,导致「算力/我的余额显示有余额,但企业管理个人余额 0、无法转企业」。 修复:转账校验与扣减改为引擎可用额度(quota - used_quota,个人实际可消费 的算力)为权威;账本同步扣减、不足扣到 0 不转负(账本仅记录充值/转入部分); 错误信息中文友好(不再外泄引擎异常细节)。 --- app/api/routers/rbac_compute_pricing.py | 38 ++++++++++++------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/app/api/routers/rbac_compute_pricing.py b/app/api/routers/rbac_compute_pricing.py index 0af898a..485ce15 100644 --- a/app/api/routers/rbac_compute_pricing.py +++ b/app/api/routers/rbac_compute_pricing.py @@ -1020,7 +1020,9 @@ async def transfer_from_personal( raise HTTPException(status_code=500, detail="个人算力账号未就绪") total_micro = amount_fen * MICRO_PER_FEN - # ★ 服务端严格校验个人可用余额(以平台账本为权威,引擎余额为参考) + # ★ 个人可转余额以引擎可用额度为权威(quota - used_quota,即个人实际可消费的算力)。 + # 平台账本 compute_personal_balance 仅记录「个人充值/企业转入」的累计,可能与引擎额度 + # 不一致(赠送/历史调整等),转账时同步扣减、不足扣到 0、不转负。 from ..infrastructure.models import User as _User from sqlalchemy import update as _sa_update user_row = await db.session.get(_User, user["id"]) @@ -1030,34 +1032,30 @@ async def transfer_from_personal( quota_micro = int(user_bal.get("quota", 0) or 0) used_micro = int(user_bal.get("used_quota", 0) or 0) available_micro = max(0, quota_micro - used_micro) - except Exception as exc: - raise HTTPException(status_code=502, detail=f"查询个人余额失败: {exc}") from exc - if ledger_balance < amount_fen: - raise HTTPException( - status_code=400, - detail=f"个人可转余额不足:账本可用 ¥{ledger_balance / 100:.2f},尝试转出 ¥{body.amount_yuan:.2f}", - ) + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=502, detail="查询个人余额失败,请稍后重试") from exc if available_micro < total_micro: raise HTTPException( status_code=400, - detail=f"个人可用余额不足:可用 ¥{available_micro / 1000000:.2f},尝试转出 ¥{body.amount_yuan:.2f}", + detail=f"个人可用余额不足:可用 ¥{available_micro / 1000000:.2f},本次需转出 ¥{body.amount_yuan:.2f}", ) - # 先扣账本(幂等条件更新,防止并发超扣) - upd = await db.session.execute( - _sa_update(_User) - .where(_User.id == user["id"], _User.compute_personal_balance >= amount_fen) - .values(compute_personal_balance=_User.compute_personal_balance - amount_fen) - ) - if upd.rowcount == 0: - raise HTTPException(status_code=400, detail="个人可转余额不足(账本校验失败)") - + # 先扣引擎额度(引擎为权威),成功后再落库 try: await compute_client.adjust_user_quota(engine_user_id, total_micro, "subtract") - except Exception as exc: - raise HTTPException(status_code=400, detail=f"个人余额扣减失败: {exc}") from exc + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail="个人余额扣减失败,请稍后重试") from exc await compute_client.sync_user_mirror(db, engine_user_id) + # 账本同步扣减(不足扣到 0,不转负) + new_ledger = max(0, ledger_balance - amount_fen) + if new_ledger != ledger_balance: + await db.session.execute( + _sa_update(_User) + .where(_User.id == user["id"]) + .values(compute_personal_balance=new_ledger) + ) + # 增加企业余额 company.compute_balance = (company.compute_balance or 0) + amount_fen