From a25b774521ff53a65c82459e3d0b3412e251ba76 Mon Sep 17 00:00:00 2001
From: db_huang <18006480240@163.com>
Date: Sat, 31 Oct 2026 18:12:11 +0800
Subject: [PATCH] =?UTF-8?q?=E6=B5=8B=E8=AF=95=E8=BD=AC=E8=B4=A6=E9=80=9A?=
=?UTF-8?q?=E8=BF=87=EF=BC=8C=E6=9C=80=E5=90=8E=E8=B0=83=E6=95=B4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../alipay_provider.cpython-313.pyc | Bin 22969 -> 24407 bytes
backend/app/__pycache__/main.cpython-313.pyc | Bin 359250 -> 359789 bytes
backend/app/alipay_provider.py | 53 ++++++++++++++----
backend/app/main.py | 10 ++++
...ipay_provider.cpython-313-pytest-9.1.1.pyc | Bin 26029 -> 33096 bytes
backend/tests/test_alipay_provider.py | 36 ++++++++++++
6 files changed, 87 insertions(+), 12 deletions(-)
diff --git a/backend/app/__pycache__/alipay_provider.cpython-313.pyc b/backend/app/__pycache__/alipay_provider.cpython-313.pyc
index fad1e2e3fa4f9a1617f6c46ceb6dcc0b5ec1a3f6..e16d0d203da1d73684c87d921c4615a2b5053a59 100644
GIT binary patch
delta 2998
zcmbVOdu-Fk9l!I>ulNz$aZD1&j(>TP7!s0b2~DyDX5u_?OyVErm294IoGu9L^lVo}
zpar(Bqm+!GqY@jeKt
Dlo2$4U-C^Re+OLne+XmjF;L
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
diff --git a/backend/app/__pycache__/main.cpython-313.pyc b/backend/app/__pycache__/main.cpython-313.pyc
index d5f443e1e49d127dfd195121b00ee8397b396c9b..b55e30e13b3e1668cc164b627a196e00505432f2 100644
GIT binary patch
delta 6100
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
V-yj_+tGMc=U7Dc>l*oZ0{{;ZCY5)KL
delta 5562
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
JIci|+{{!Izeue-5
diff --git a/backend/app/alipay_provider.py b/backend/app/alipay_provider.py
index 85758ae..fddede3 100644
--- a/backend/app/alipay_provider.py
+++ b/backend/app/alipay_provider.py
@@ -235,6 +235,26 @@ def _parse_response(raw, response_cls):
return response
+def _provider_response_from_exception(exc: Exception) -> dict | None:
+ message = str(exc)
+ json_start = message.find("{")
+ if json_start < 0:
+ return None
+ try:
+ payload = json.loads(message[json_start:])
+ except (TypeError, ValueError):
+ return None
+ return next((
+ value for key, value in payload.items()
+ if key.endswith("_response") and isinstance(value, dict)
+ ), None)
+
+
+def _unverified_success_response(exc: Exception) -> bool:
+ response = _provider_response_from_exception(exc)
+ return bool(response and str(response.get("code")) == "10000" and not response.get("sub_code"))
+
+
def _provider_error_from_exception(exc: Exception) -> AlipayTransferError | None:
"""Recover Alipay's structured business error from old SDK exceptions.
@@ -244,20 +264,14 @@ def _provider_error_from_exception(exc: Exception) -> AlipayTransferError | None
sub-code/message instead of treating a deterministic rejection as an
ambiguous network failure.
"""
- message = str(exc)
- json_start = message.find("{")
- if json_start < 0:
- return None
- try:
- payload = json.loads(message[json_start:])
- except (TypeError, ValueError):
- return None
- response = next((
- value for key, value in payload.items()
- if key.endswith("_response") and isinstance(value, dict)
- ), None)
+ response = _provider_response_from_exception(exc)
if not response or not response.get("code"):
return None
+ # A code=10000 envelope is not a provider rejection. The SDK may still
+ # raise because the response signature could not be verified; in that
+ # case the transfer result is uncertain and must remain pending.
+ if str(response.get("code")) == "10000" and not response.get("sub_code"):
+ return None
return AlipayTransferError(
response.get("sub_msg") or response.get("msg") or "支付宝请求失败",
response.get("sub_code") or response.get("code") or "ALIPAY_ERROR",
@@ -309,6 +323,15 @@ def transfer(*, out_biz_no: str, amount_fen: int, account: str, payee_name: str,
except Exception as exc:
if isinstance(exc, AlipayTransferError):
raise
+ if _unverified_success_response(exc):
+ logger.error(
+ "Alipay returned a success-code envelope, but the SDK could not verify/parse it; transfer outcome is unconfirmed"
+ )
+ raise AlipayTransferError(
+ "支付宝返回成功格式响应,但本地未能验证响应;转账结果未确认,请查询支付宝流水,勿重复发起",
+ "ALIPAY_RESPONSE_UNVERIFIED",
+ ambiguous=True,
+ ) from exc
provider_error = _provider_error_from_exception(exc)
if provider_error:
logger.error(
@@ -342,6 +365,12 @@ def query(*, out_biz_no: str) -> dict:
except Exception as exc:
if isinstance(exc, AlipayTransferError):
raise
+ if _unverified_success_response(exc):
+ raise AlipayTransferError(
+ "支付宝查询返回成功格式响应,但本地未能验证响应;请稍后再次查询",
+ "ALIPAY_RESPONSE_UNVERIFIED",
+ ambiguous=True,
+ ) from exc
provider_error = _provider_error_from_exception(exc)
if provider_error:
raise provider_error from exc
diff --git a/backend/app/main.py b/backend/app/main.py
index 38f5b58..412fc76 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -3661,6 +3661,8 @@ def _alipay_error_detail(exc: AlipayTransferError, *, querying: bool = False) ->
return "支付宝根证书序列号无效,请检查证书模式配置后重试"
if exc.code == "TRANSFER_SCENE_REPORT_INFO_ILLEGAL":
return "支付宝转账失败(TRANSFER_SCENE_REPORT_INFO_ILLEGAL):佣金报酬场景必须上报“佣金报酬说明”,请确认服务器已部署最新代码并重启后端"
+ if exc.code == "ALIPAY_RESPONSE_UNVERIFIED":
+ return "支付宝已返回成功格式响应,但本地未能验证转账结果;请先查询支付宝流水,勿重复发起转账"
if exc.ambiguous:
return "支付宝请求超时,结果暂不明确;请稍后查询订单状态,勿重复创建转账"
action = "查询" if querying else "转账"
@@ -3696,6 +3698,13 @@ def _perform_alipay_refund(db: Session, record, kind: str, user: AdminUser, paye
if recipient:
recipient.amount_fen = order.amount_fen
return order.alipay_trade_no
+ # Older builds incorrectly treated a valid Alipay code=10000 response as
+ # a deterministic failure. Reconcile those rows before allowing any new
+ # out_biz_no, otherwise a retry could pay the recipient twice.
+ if order and order.status == "failed" and order.error_code == "10000":
+ order.status = "pending"
+ order.error_message = "旧版本未能确认支付宝响应验签结果,正在查询原转账单据;请勿重复发起"
+ db.commit()
if order and order.status == "pending":
try:
from .alipay_provider import query
@@ -3718,6 +3727,7 @@ def _perform_alipay_refund(db: Session, record, kind: str, user: AdminUser, paye
return trade_no
if provider_status in {"FAIL", "REFUND"}:
order.status = "failed"
+ order.error_code = ""
order.error_message = str(result.get("fail_reason") or "支付宝确认转账失败")[:500]
db.commit()
raise HTTPException(status_code=409, detail="支付宝已确认本次转账失败;确认原因后可再次提交")
diff --git a/backend/tests/__pycache__/test_alipay_provider.cpython-313-pytest-9.1.1.pyc b/backend/tests/__pycache__/test_alipay_provider.cpython-313-pytest-9.1.1.pyc
index 0b1de5bdce8db1808477c7dacebf0e8309f02f64..72718e50825e8b4aea616a4aa808c48fe5f9bde1 100644
GIT binary patch
delta 6893
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
diff --git a/backend/tests/test_alipay_provider.py b/backend/tests/test_alipay_provider.py
index b20ce82..21b847e 100644
--- a/backend/tests/test_alipay_provider.py
+++ b/backend/tests/test_alipay_provider.py
@@ -78,6 +78,42 @@ def test_old_sdk_error_envelope_is_recovered_from_response_exception():
assert error.ambiguous is False
+def test_success_code_envelope_is_not_misreported_as_provider_rejection():
+ raw = (
+ '[request-id]response sign verify failed. '
+ '{"alipay_fund_trans_uni_transfer_response":{'
+ '"code":"10000","msg":"Success","out_biz_no":"RF004",'
+ '"order_id":"ALI004","status":"SUCCESS"},"sign":"signature"}'
+ )
+
+ assert alipay_provider._provider_error_from_exception(RuntimeError(raw)) is None
+ assert alipay_provider._unverified_success_response(RuntimeError(raw)) is True
+
+
+def test_transfer_keeps_unverified_success_envelope_ambiguous(monkeypatch):
+ class RaisingClient:
+ def execute(self, _request):
+ raise RuntimeError(
+ '[request-id]response sign verify failed. '
+ '{"alipay_fund_trans_uni_transfer_response":{'
+ '"code":"10000","msg":"Success","out_biz_no":"RF005",'
+ '"order_id":"ALI005","status":"SUCCESS"},"sign":"signature"}'
+ )
+
+ monkeypatch.setattr(alipay_provider, "_client", lambda: RaisingClient())
+ monkeypatch.setattr(alipay_provider.settings, "alipay_transfer_scene_name", "佣金报酬")
+
+ with pytest.raises(alipay_provider.AlipayTransferError) as error:
+ alipay_provider.transfer(
+ out_biz_no="RF005", amount_fen=300, account="buyer@example.com",
+ payee_name="测试收款人", order_title="测试返款",
+ )
+
+ assert error.value.code == "ALIPAY_RESPONSE_UNVERIFIED"
+ assert error.value.ambiguous is True
+ assert "勿重复发起" in str(error.value)
+
+
def test_query_parses_processing_status(monkeypatch):
client = CapturingClient({
"code": "10000",