diff --git a/backend/app/__pycache__/alipay_provider.cpython-313.pyc b/backend/app/__pycache__/alipay_provider.cpython-313.pyc index fad1e2e..e16d0d2 100644 Binary files a/backend/app/__pycache__/alipay_provider.cpython-313.pyc and b/backend/app/__pycache__/alipay_provider.cpython-313.pyc differ diff --git a/backend/app/__pycache__/main.cpython-313.pyc b/backend/app/__pycache__/main.cpython-313.pyc index d5f443e..b55e30e 100644 Binary files a/backend/app/__pycache__/main.cpython-313.pyc and b/backend/app/__pycache__/main.cpython-313.pyc differ 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 0b1de5b..72718e5 100644 Binary files a/backend/tests/__pycache__/test_alipay_provider.cpython-313-pytest-9.1.1.pyc and b/backend/tests/__pycache__/test_alipay_provider.cpython-313-pytest-9.1.1.pyc differ 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",