收款失败静默换下一商户,全部失败才回前端。
日额度等 INVALID_REQUEST 不再因白名单漏匹配提前返回;查无单默认可换号,仅频率/系统错误禁换。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -28,7 +28,6 @@ from .tixian_shenhe_services import (
|
|||||||
create_audit_application,
|
create_audit_application,
|
||||||
ensure_shenhe_collect_completed,
|
ensure_shenhe_collect_completed,
|
||||||
get_audit_for_collect,
|
get_audit_for_collect,
|
||||||
handle_post_transfer_failure,
|
|
||||||
query_wechat_transfer_bill,
|
query_wechat_transfer_bill,
|
||||||
reconcile_shenhe_wechat_bills,
|
reconcile_shenhe_wechat_bills,
|
||||||
reject_audit_and_refund,
|
reject_audit_and_refund,
|
||||||
@@ -64,21 +63,32 @@ def _is_wx_balance_insufficient(err_code='', err_msg=''):
|
|||||||
return any(k in blob for k in ('NOT_ENOUGH', 'INSUFFICIENT', '余额不足', '资金不足'))
|
return any(k in blob for k in ('NOT_ENOUGH', 'INSUFFICIENT', '余额不足', '资金不足'))
|
||||||
|
|
||||||
|
|
||||||
def _is_wx_switchable_error(err_code='', err_msg=''):
|
def _extract_wx_err(wx_result):
|
||||||
|
"""解析微信错误码/文案(兼容 detail 为对象)。"""
|
||||||
|
err_code = str((wx_result or {}).get('code') or '')
|
||||||
|
raw = (wx_result or {}).get('message')
|
||||||
|
if raw is None:
|
||||||
|
raw = (wx_result or {}).get('detail')
|
||||||
|
if isinstance(raw, dict):
|
||||||
|
raw = raw.get('message') or raw.get('detail') or json.dumps(raw, ensure_ascii=False)
|
||||||
|
err_msg = str(raw or '微信接口调用失败')
|
||||||
|
return err_code, err_msg
|
||||||
|
|
||||||
|
|
||||||
|
def _is_wx_unsafe_to_switch(err_code='', err_msg='', status_code=None):
|
||||||
"""
|
"""
|
||||||
可立刻换下一商户重试的错误(本笔尚未进入 WAIT_USER_CONFIRM)。
|
结果可能已受理:换号有双笔风险,禁止立刻换商户,只能查单/待确认。
|
||||||
仅白名单,避免误伤导致重复开单。不含裸词 LIMIT,防误匹配。
|
其余明确业务失败(日额度、余额不足、INVALID_REQUEST 等)默认可换号。
|
||||||
"""
|
"""
|
||||||
if _is_wx_balance_insufficient(err_code, err_msg):
|
if status_code in (429, 500, 502, 503):
|
||||||
return True
|
return True
|
||||||
blob = f'{err_code} {err_msg}'.upper()
|
blob = f'{err_code} {err_msg}'.upper()
|
||||||
keywords = (
|
keys = (
|
||||||
'FREQUENCY_LIMITED', 'RATE_LIMIT', 'QUOTA_EXCEEDED', 'QUOTA',
|
'FREQUENCY_LIMIT', 'FREQUENCY_LIMITED', 'FREQUENCY_LIMIT_EXCEED',
|
||||||
'日限额', '超出限额', '超过限额', '限额', '次数超限',
|
'RATELIMIT', 'RATE_LIMIT', 'SYSTEM_ERROR',
|
||||||
'ACCOUNT_ERROR', 'NO_AUTH', 'PERMISSION', '商户号异常', '账户异常',
|
'频率超限', '系统错误', '结果不明确', '结果未明确',
|
||||||
'CERT_ERROR', 'PRIVATE_KEY', 'SIGN_ERROR', '商户证书', '私钥',
|
|
||||||
)
|
)
|
||||||
return any(k in blob for k in keywords)
|
return any(k in blob for k in keys)
|
||||||
|
|
||||||
|
|
||||||
def _verify_collect_quota_or_response(user_main, audit, shenhe_danhao):
|
def _verify_collect_quota_or_response(user_main, audit, shenhe_danhao):
|
||||||
@@ -393,6 +403,7 @@ def process_audit_collect(request):
|
|||||||
quota_reserved = False
|
quota_reserved = False
|
||||||
return Response({'code': 99, 'msg': '微信商户配置异常,请联系管理员'})
|
return Response({'code': 99, 'msg': '微信商户配置异常,请联系管理员'})
|
||||||
|
|
||||||
|
# 成功进待确认 → 立刻回前端(这是唯一「中途成功回包」的路径)
|
||||||
if resp.status_code == 200 and wx_result.get('state') == 'WAIT_USER_CONFIRM':
|
if resp.status_code == 200 and wx_result.get('state') == 'WAIT_USER_CONFIRM':
|
||||||
package_info = wx_result.get('package_info')
|
package_info = wx_result.get('package_info')
|
||||||
auto_record = TixianAutoRecord.objects.get(tixian_id=tixian_id)
|
auto_record = TixianAutoRecord.objects.get(tixian_id=tixian_id)
|
||||||
@@ -411,98 +422,79 @@ def process_audit_collect(request):
|
|||||||
'tixianjilu_id': tixianjilu_id_val,
|
'tixianjilu_id': tixianjilu_id_val,
|
||||||
})
|
})
|
||||||
|
|
||||||
err_code = wx_result.get('code', '')
|
err_code, err_msg = _extract_wx_err(wx_result)
|
||||||
err_msg = wx_result.get('message', wx_result.get('detail', '微信接口调用失败'))
|
|
||||||
logger.error(
|
logger.error(
|
||||||
'微信发起转账未进入待确认: bill=%s mch=%s code=%s msg=%s HTTP%s',
|
'微信发起转账未进入待确认: bill=%s mch=%s code=%s msg=%s HTTP%s',
|
||||||
tixian_id, mch_id, err_code, err_msg, resp.status_code,
|
tixian_id, mch_id, err_code, err_msg, resp.status_code,
|
||||||
)
|
)
|
||||||
|
|
||||||
post_action, post_payload = handle_post_transfer_failure(
|
# 查单决定:已成功/待确认 → 回前端;无单可换 → 静默试下一户;中间失败不回前端
|
||||||
tixian_id, shenhe_danhao, err_code, err_msg,
|
has_next = mch_idx < len(mch_cfgs) - 1
|
||||||
)
|
unsafe = _is_wx_unsafe_to_switch(err_code, err_msg, resp.status_code)
|
||||||
if post_action == RECONCILE_WAIT_CONFIRM:
|
|
||||||
if isinstance(post_payload, dict):
|
wx_q = query_wechat_transfer_bill(tixian_id, wx_cfg=wx_cfg)
|
||||||
post_payload.setdefault('mch_id', mch_id)
|
if wx_q is None:
|
||||||
post_payload.setdefault('tixianjilu_id', tixianjilu_id_val)
|
return Response({
|
||||||
quota_resp = _verify_collect_quota_or_response(user_main, audit, shenhe_danhao)
|
'code': 12,
|
||||||
if quota_resp:
|
'msg': '收款请求已提交,系统正在核对状态,请稍后再试,切勿重复点击',
|
||||||
return quota_resp
|
})
|
||||||
return _build_collect_success_response(post_payload)
|
|
||||||
if post_action == RECONCILE_COMPLETED:
|
if not wx_q.get('_not_found'):
|
||||||
ensure_shenhe_collect_completed(shenhe_danhao)
|
ar = TixianAutoRecord.objects.get(tixian_id=tixian_id)
|
||||||
return Response({
|
sync_action, sync_payload = _sync_record_from_wx_query(ar, wx_q)
|
||||||
'code': 0,
|
if sync_action == RECONCILE_WAIT_CONFIRM:
|
||||||
'msg': '提现已成功到账',
|
if isinstance(sync_payload, dict):
|
||||||
'data': {'already_completed': True, 'tixianjilu_id': tixianjilu_id_val},
|
sync_payload.setdefault('mch_id', mch_id)
|
||||||
})
|
sync_payload.setdefault('tixianjilu_id', tixianjilu_id_val)
|
||||||
if post_action == RECONCILE_PENDING:
|
quota_resp = _verify_collect_quota_or_response(user_main, audit, shenhe_danhao)
|
||||||
# 关键漏洞修复:微信已返回明确可切换业务错误时,查单宽限会得到 PENDING(not_found),
|
if quota_resp:
|
||||||
# 若不处理会死卡「请稍后再试」永不换号。确认查单仍 not_found 后才换;查单不可用则仍禁换。
|
return quota_resp
|
||||||
can_try_switch = (
|
return _build_collect_success_response(sync_payload)
|
||||||
_is_wx_switchable_error(err_code, err_msg)
|
if sync_action == RECONCILE_COMPLETED:
|
||||||
and mch_idx < len(mch_cfgs) - 1
|
ensure_shenhe_collect_completed(shenhe_danhao)
|
||||||
)
|
return Response({
|
||||||
if can_try_switch:
|
'code': 0,
|
||||||
wx_q = query_wechat_transfer_bill(tixian_id, wx_cfg=wx_cfg)
|
'msg': '提现已成功到账',
|
||||||
if wx_q is None:
|
'data': {'already_completed': True, 'tixianjilu_id': tixianjilu_id_val},
|
||||||
return Response({
|
})
|
||||||
'code': 12,
|
if sync_action == RECONCILE_PENDING:
|
||||||
'msg': '收款请求已提交,系统正在核对状态,请稍后再试,切勿重复点击',
|
return Response({
|
||||||
})
|
'code': 12,
|
||||||
if wx_q.get('_not_found'):
|
'msg': (
|
||||||
user_msg = err_msg or '微信收款发起失败,请稍后重试'
|
sync_payload.get('msg')
|
||||||
last_user_msg = user_msg
|
if isinstance(sync_payload, dict) else sync_payload
|
||||||
last_was_balance = _is_wx_balance_insufficient(err_code, err_msg)
|
) or '有收款处理中,请稍后再试',
|
||||||
_mark_auto_record_failed(tixian_id, f'[{mch_id}] {user_msg}')
|
})
|
||||||
logger.warning(
|
# ALLOW_NEW:微信侧该单已明确失败,下面可换号
|
||||||
'商户转账失败(查单无单),切换下一商户 shenhe=%s from=%s idx=%s/%s',
|
|
||||||
shenhe_danhao, mch_id, mch_idx + 1, len(mch_cfgs),
|
if unsafe:
|
||||||
)
|
# 频率/5xx:即便暂查无单也不换号(结果不确定)
|
||||||
continue
|
return Response({
|
||||||
# 微信侧其实已有单:按查单结果走,禁止盲换
|
'code': 12,
|
||||||
ar = TixianAutoRecord.objects.get(tixian_id=tixian_id)
|
'msg': '收款请求已提交,系统正在核对状态,请稍后再试,切勿重复点击',
|
||||||
sync_action, sync_payload = _sync_record_from_wx_query(ar, wx_q)
|
})
|
||||||
if sync_action == RECONCILE_WAIT_CONFIRM:
|
|
||||||
if isinstance(sync_payload, dict):
|
|
||||||
sync_payload.setdefault('mch_id', mch_id)
|
|
||||||
sync_payload.setdefault('tixianjilu_id', tixianjilu_id_val)
|
|
||||||
quota_resp = _verify_collect_quota_or_response(user_main, audit, shenhe_danhao)
|
|
||||||
if quota_resp:
|
|
||||||
return quota_resp
|
|
||||||
return _build_collect_success_response(sync_payload)
|
|
||||||
if sync_action == RECONCILE_COMPLETED:
|
|
||||||
ensure_shenhe_collect_completed(shenhe_danhao)
|
|
||||||
return Response({
|
|
||||||
'code': 0,
|
|
||||||
'msg': '提现已成功到账',
|
|
||||||
'data': {'already_completed': True, 'tixianjilu_id': tixianjilu_id_val},
|
|
||||||
})
|
|
||||||
pending_msg = (
|
|
||||||
post_payload.get('msg', post_payload)
|
|
||||||
if isinstance(post_payload, dict) else post_payload
|
|
||||||
)
|
|
||||||
return Response({'code': 12, 'msg': pending_msg or '有收款处理中,请稍后再试'})
|
|
||||||
|
|
||||||
# 明确失败:标失败;可切换则试下一商户(不释放限额,直到全部失败)
|
|
||||||
user_msg = err_msg or '微信收款发起失败,请稍后重试'
|
user_msg = err_msg or '微信收款发起失败,请稍后重试'
|
||||||
last_user_msg = user_msg
|
last_user_msg = user_msg
|
||||||
last_was_balance = _is_wx_balance_insufficient(err_code, err_msg)
|
last_was_balance = _is_wx_balance_insufficient(err_code, err_msg)
|
||||||
_mark_auto_record_failed(tixian_id, f'[{mch_id}] {user_msg}')
|
_mark_auto_record_failed(tixian_id, f'[{mch_id}] {err_msg}')
|
||||||
|
|
||||||
if _is_wx_switchable_error(err_code, err_msg) and mch_idx < len(mch_cfgs) - 1:
|
if has_next:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
'商户转账失败,切换下一商户 shenhe=%s from=%s idx=%s/%s',
|
'商户转账失败,静默切换下一商户(不回前端) shenhe=%s from=%s '
|
||||||
shenhe_danhao, mch_id, mch_idx + 1, len(mch_cfgs),
|
'code=%s msg=%s idx=%s/%s',
|
||||||
|
shenhe_danhao, mch_id, err_code, err_msg,
|
||||||
|
mch_idx + 1, len(mch_cfgs),
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# 全部商户都失败了才回前端
|
||||||
if quota_reserved:
|
if quota_reserved:
|
||||||
release_collect_quota_for_record(
|
release_collect_quota_for_record(
|
||||||
TixianAutoRecord.objects.get(tixian_id=tixian_id),
|
TixianAutoRecord.objects.get(tixian_id=tixian_id),
|
||||||
)
|
)
|
||||||
quota_reserved = False
|
quota_reserved = False
|
||||||
if last_was_balance and mch_idx >= len(mch_cfgs) - 1:
|
if last_was_balance:
|
||||||
return Response({
|
return Response({
|
||||||
'code': 99,
|
'code': 99,
|
||||||
'msg': '运营账户资金不足,需等管理员充值后才能提现',
|
'msg': '运营账户资金不足,需等管理员充值后才能提现',
|
||||||
|
|||||||
Reference in New Issue
Block a user