测试转账通过,最后调整

This commit is contained in:
db_huang 2026-10-31 18:12:11 +08:00
parent b552ac56b0
commit a25b774521
6 changed files with 87 additions and 12 deletions

View File

@ -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

View File

@ -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="支付宝已确认本次转账失败;确认原因后可再次提交")

View File

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