diff --git a/yonghu/tixian_shenhe_services.py b/yonghu/tixian_shenhe_services.py index 846997a..3287190 100644 --- a/yonghu/tixian_shenhe_services.py +++ b/yonghu/tixian_shenhe_services.py @@ -1394,6 +1394,18 @@ def reconcile_shenhe_wechat_bills(shenhe_danhao): wx_data = query_wechat_transfer_bill(rec.tixian_id) if wx_data is None: + # IP 未放行时创建/查单都会失败。若从未拿到 package,说明没进待确认, + # 标失败放行换商户;否则会永远卡在「处理中」并反复打第一个商户。 + has_pkg = bool(_parse_package_info(rec.wechat_package)) + if rec.zhuangtai == 0 and not has_pkg: + rec.zhuangtai = 2 + rec.fail_reason = '查单不可用且未待确认(常见于IP白名单),已换其他商户重试' + rec.save(update_fields=['zhuangtai', 'fail_reason', 'update_time']) + logger.warning( + '对账查单不可用已放行换号 shenhe=%s bill=%s mch=%s', + shenhe_danhao, rec.tixian_id, rec.mch_id, + ) + continue logger.error( '对账查单不可用 shenhe_danhao=%s bill=%s', shenhe_danhao, rec.tixian_id, diff --git a/yonghu/tixian_shenhe_views.py b/yonghu/tixian_shenhe_views.py index 6cd40bf..67ba234 100644 --- a/yonghu/tixian_shenhe_views.py +++ b/yonghu/tixian_shenhe_views.py @@ -90,11 +90,25 @@ def _is_wx_unsafe_to_switch(err_code='', err_msg='', status_code=None): return any(k in blob for k in keys) +def _is_wx_ip_denied(err_code='', err_msg=''): + """商户未配置服务器出口 IP 白名单(创建/查单都会拦)。""" + blob = f'{err_code} {err_msg}'.upper().replace('\t', '') + if '268491067' in blob: + return True + if 'IP' in blob and '不允许' in blob: + return True + return any(k in blob for k in ( + 'IP地址不允许', 'IP 地址不允许', '不允许调用该接口', '不允许调用接口', + 'NOT_ALLOW_IP', 'IP_DENIED', + )) + + def _is_wx_clear_switchable_fail(err_code='', err_msg='', status_code=None): """ - 明确未建单的业务失败:可静默换下一商户(日额度/余额不足/无权限等)。 - 查单失败(None)时也允许换——避免首户卡死后永不试下一户。 + 明确未建单的业务失败:可静默换下一商户(日额度/余额不足/IP白名单/无权限等)。 """ + if _is_wx_ip_denied(err_code, err_msg): + return True if _is_wx_unsafe_to_switch(err_code, err_msg, status_code): return False if status_code in (400, 401, 403): @@ -267,6 +281,12 @@ def process_audit_collect(request): if not mch_cfgs or not wx_cfg_is_complete(mch_cfgs[0]): logger.error('微信打款配置不完整 enabled_count=%s', len(mch_cfgs)) return Response({'code': 11, 'msg': '系统配置异常,请联系客服'}) + if len(mch_cfgs) < 2: + logger.warning( + '启用且配置完整的转账商户仅 %s 个,无法轮换 mch_ids=%s ' + '(后台多个启用但私钥路径无效会被跳过)', + len(mch_cfgs), [c.get('MCHID') for c in mch_cfgs], + ) ctx, err = resolve_collect_context( yonghuid, @@ -479,13 +499,47 @@ def process_audit_collect(request): if resp.status_code == 200 and (not err_code or err_msg == '微信接口调用失败'): err_msg = f'微信状态:{state200 or "未知"}' logger.error( - '微信发起转账未进入待确认: bill=%s mch=%s code=%s msg=%s HTTP%s state=%s', + '微信发起转账未进入待确认: bill=%s mch=%s code=%s msg=%s HTTP%s state=%s body=%s', tixian_id, mch_id, err_code, err_msg, resp.status_code, state200, + json.dumps(wx_result, ensure_ascii=False)[:500], ) has_next = mch_idx < len(mch_cfgs) - 1 unsafe = _is_wx_unsafe_to_switch(err_code, err_msg, resp.status_code) clear_fail = _is_wx_clear_switchable_fail(err_code, err_msg, resp.status_code) + ip_denied = _is_wx_ip_denied(err_code, err_msg) + + # IP 白名单/明确 4xx:创建失败时查单也会被同一 IP 拦住,查了只会误卡「处理中」 + # 直接标失败并静默换下一户,绝不回前端中间错误 + if ip_denied or (clear_fail and resp.status_code in (400, 401, 403)): + 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}') + if has_next: + logger.warning( + '商户明确失败(含IP白名单),跳过查单静默换下一户 shenhe=%s ' + 'from=%s code=%s msg=%s http=%s idx=%s/%s', + shenhe_danhao, mch_id, err_code, err_msg, resp.status_code, + 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 ip_denied: + return Response({ + 'code': 99, + 'msg': '微信商户未配置服务器IP白名单或配置未生效,请联系管理员检查各商户号', + }) + if last_was_balance: + return Response({ + 'code': 99, + 'msg': '运营账户资金不足,需等管理员充值后才能提现', + }) + return Response({'code': 99, 'msg': user_msg}) wx_q = query_wechat_transfer_bill(tixian_id, wx_cfg=wx_cfg)