From bcf304dd37c8a448261a574d26a69607bc9cab0f Mon Sep 17 00:00:00 2001 From: XingQue Date: Tue, 14 Jul 2026 13:39:47 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E6=AC=BE=E5=A4=B1=E8=B4=A5=E9=9D=99?= =?UTF-8?q?=E9=BB=98=E6=8D=A2=E4=B8=8B=E4=B8=80=E5=95=86=E6=88=B7=EF=BC=8C?= =?UTF-8?q?=E5=85=A8=E9=83=A8=E5=A4=B1=E8=B4=A5=E6=89=8D=E5=9B=9E=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 日额度等 INVALID_REQUEST 不再因白名单漏匹配提前返回;查无单默认可换号,仅频率/系统错误禁换。 Co-authored-by: Cursor --- yonghu/tixian_shenhe_views.py | 160 ++++++++++++++++------------------ 1 file changed, 76 insertions(+), 84 deletions(-) diff --git a/yonghu/tixian_shenhe_views.py b/yonghu/tixian_shenhe_views.py index 7f23bb4..3e8e48f 100644 --- a/yonghu/tixian_shenhe_views.py +++ b/yonghu/tixian_shenhe_views.py @@ -28,7 +28,6 @@ from .tixian_shenhe_services import ( create_audit_application, ensure_shenhe_collect_completed, get_audit_for_collect, - handle_post_transfer_failure, query_wechat_transfer_bill, reconcile_shenhe_wechat_bills, 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', '余额不足', '资金不足')) -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 blob = f'{err_code} {err_msg}'.upper() - keywords = ( - 'FREQUENCY_LIMITED', 'RATE_LIMIT', 'QUOTA_EXCEEDED', 'QUOTA', - '日限额', '超出限额', '超过限额', '限额', '次数超限', - 'ACCOUNT_ERROR', 'NO_AUTH', 'PERMISSION', '商户号异常', '账户异常', - 'CERT_ERROR', 'PRIVATE_KEY', 'SIGN_ERROR', '商户证书', '私钥', + keys = ( + 'FREQUENCY_LIMIT', 'FREQUENCY_LIMITED', 'FREQUENCY_LIMIT_EXCEED', + 'RATELIMIT', 'RATE_LIMIT', 'SYSTEM_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): @@ -393,6 +403,7 @@ def process_audit_collect(request): quota_reserved = False return Response({'code': 99, 'msg': '微信商户配置异常,请联系管理员'}) + # 成功进待确认 → 立刻回前端(这是唯一「中途成功回包」的路径) if resp.status_code == 200 and wx_result.get('state') == 'WAIT_USER_CONFIRM': package_info = wx_result.get('package_info') auto_record = TixianAutoRecord.objects.get(tixian_id=tixian_id) @@ -411,98 +422,79 @@ def process_audit_collect(request): 'tixianjilu_id': tixianjilu_id_val, }) - err_code = wx_result.get('code', '') - err_msg = wx_result.get('message', wx_result.get('detail', '微信接口调用失败')) + err_code, err_msg = _extract_wx_err(wx_result) logger.error( '微信发起转账未进入待确认: bill=%s mch=%s code=%s msg=%s HTTP%s', 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, - ) - if post_action == RECONCILE_WAIT_CONFIRM: - if isinstance(post_payload, dict): - post_payload.setdefault('mch_id', mch_id) - post_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(post_payload) - if post_action == RECONCILE_COMPLETED: - ensure_shenhe_collect_completed(shenhe_danhao) - return Response({ - 'code': 0, - 'msg': '提现已成功到账', - 'data': {'already_completed': True, 'tixianjilu_id': tixianjilu_id_val}, - }) - if post_action == RECONCILE_PENDING: - # 关键漏洞修复:微信已返回明确可切换业务错误时,查单宽限会得到 PENDING(not_found), - # 若不处理会死卡「请稍后再试」永不换号。确认查单仍 not_found 后才换;查单不可用则仍禁换。 - can_try_switch = ( - _is_wx_switchable_error(err_code, err_msg) - and mch_idx < len(mch_cfgs) - 1 - ) - if can_try_switch: - wx_q = query_wechat_transfer_bill(tixian_id, wx_cfg=wx_cfg) - if wx_q is None: - return Response({ - 'code': 12, - 'msg': '收款请求已提交,系统正在核对状态,请稍后再试,切勿重复点击', - }) - if wx_q.get('_not_found'): - user_msg = err_msg or '微信收款发起失败,请稍后重试' - last_user_msg = user_msg - last_was_balance = _is_wx_balance_insufficient(err_code, err_msg) - _mark_auto_record_failed(tixian_id, f'[{mch_id}] {user_msg}') - logger.warning( - '商户转账失败(查单无单),切换下一商户 shenhe=%s from=%s idx=%s/%s', - shenhe_danhao, mch_id, mch_idx + 1, len(mch_cfgs), - ) - continue - # 微信侧其实已有单:按查单结果走,禁止盲换 - ar = TixianAutoRecord.objects.get(tixian_id=tixian_id) - 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 '有收款处理中,请稍后再试'}) + # 查单决定:已成功/待确认 → 回前端;无单可换 → 静默试下一户;中间失败不回前端 + has_next = mch_idx < len(mch_cfgs) - 1 + unsafe = _is_wx_unsafe_to_switch(err_code, err_msg, resp.status_code) + + wx_q = query_wechat_transfer_bill(tixian_id, wx_cfg=wx_cfg) + if wx_q is None: + return Response({ + 'code': 12, + 'msg': '收款请求已提交,系统正在核对状态,请稍后再试,切勿重复点击', + }) + + if not wx_q.get('_not_found'): + ar = TixianAutoRecord.objects.get(tixian_id=tixian_id) + 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}, + }) + if sync_action == RECONCILE_PENDING: + return Response({ + 'code': 12, + 'msg': ( + sync_payload.get('msg') + if isinstance(sync_payload, dict) else sync_payload + ) or '有收款处理中,请稍后再试', + }) + # ALLOW_NEW:微信侧该单已明确失败,下面可换号 + + if unsafe: + # 频率/5xx:即便暂查无单也不换号(结果不确定) + return Response({ + 'code': 12, + 'msg': '收款请求已提交,系统正在核对状态,请稍后再试,切勿重复点击', + }) - # 明确失败:标失败;可切换则试下一商户(不释放限额,直到全部失败) user_msg = err_msg or '微信收款发起失败,请稍后重试' last_user_msg = user_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( - '商户转账失败,切换下一商户 shenhe=%s from=%s idx=%s/%s', - shenhe_danhao, mch_id, mch_idx + 1, len(mch_cfgs), + '商户转账失败,静默切换下一商户(不回前端) shenhe=%s from=%s ' + 'code=%s msg=%s idx=%s/%s', + shenhe_danhao, mch_id, err_code, err_msg, + mch_idx + 1, len(mch_cfgs), ) continue + # 全部商户都失败了才回前端 if quota_reserved: release_collect_quota_for_record( TixianAutoRecord.objects.get(tixian_id=tixian_id), ) quota_reserved = False - if last_was_balance and mch_idx >= len(mch_cfgs) - 1: + if last_was_balance: return Response({ 'code': 99, 'msg': '运营账户资金不足,需等管理员充值后才能提现',