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 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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",