# yonghu/tixian_shenhe_collect_b.py """ 自动提现审核 — 用户侧接口: ① TixianZddkshApplyView → POST /yonghu/zddksh 提交审核(扣款) ② process_audit_collect → POST /yonghu/tixiansq 收款(P0:一单到底,先查微信再决定是否新建) """ import decimal import json import logging import random import string import time import requests from django.db import transaction from django.utils import timezone from rest_framework import permissions from rest_framework.response import Response from rest_framework.views import APIView from .models import TixianAutoRecord, TixianShenheJilu from .tixian_shenhe_services import ( RECONCILE_ALLOW_NEW, RECONCILE_COMPLETED, RECONCILE_PENDING, RECONCILE_WAIT_CONFIRM, check_collect_quota_limits, create_audit_application, ensure_shenhe_collect_completed, get_audit_for_collect, query_wechat_transfer_bill, reconcile_shenhe_wechat_bills, reject_audit_and_refund, release_collect_quota_for_record, reserve_collect_quota_limits, resolve_collect_context, shijidaozhang_exceeds_wechat_limit, validate_collect_eligibility, wechat_limit_collect_message, _sync_record_from_wx_query, ) logger = logging.getLogger('yonghu.tixian_shenhe') def generate_tixian_id(): """生成唯一微信商户打款单号:TX + 时间戳(13位) + 4位随机数""" timestamp = str(int(time.time() * 1000)) rand = ''.join(random.choices(string.digits, k=4)) return f'TX{timestamp}{rand}' def _build_collect_success_response(payload): return Response({ 'code': 0, 'msg': '请确认收款', 'data': payload, }) def _is_wx_balance_insufficient(err_code='', err_msg=''): blob = f'{err_code} {err_msg}'.upper() return any(k in blob for k in ('NOT_ENOUGH', 'INSUFFICIENT', '余额不足', '资金不足')) 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): """ 结果可能已受理:换号有双笔风险,禁止立刻换商户,只能查单/待确认。 """ if status_code in (429, 500, 502, 503): return True blob = f'{err_code} {err_msg}'.upper() keys = ( 'FREQUENCY_LIMIT', 'FREQUENCY_LIMITED', 'FREQUENCY_LIMIT_EXCEED', 'RATELIMIT', 'RATE_LIMIT', 'SYSTEM_ERROR', 'ALREADY_EXISTS', '频率超限', '系统错误', '结果不明确', '结果未明确', '订单已存在', ) 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_user_personal_quota_msg(err_msg=''): """单用户/个人侧额度:禁止当作商户整户日限换号。""" msg = str(err_msg or '') keys = ( '向同一用户', '单用户', '同一用户', '收款用户', '该用户', '用户收款', '收款受限', '付款受限', ) return any(k in msg for k in keys) def _is_wx_mch_total_quota_exhausted(err_code='', err_msg=''): """ 仅识别微信「商户整户」日/月转账额度耗尽(如超出商户单日转账额度)。 个人侧限额、笼统「受限/管控」、余额不足一律 False。 """ if _is_wx_unsafe_to_switch(err_code, err_msg): return False if _is_wx_user_personal_quota_msg(err_msg): return False msg = str(err_msg or '') # 官方文案:超出商户单日转账额度,请核实产品设置是否准确 allow_phrases = ( '超出商户单日转账额度', '超出商户单月转账额度', '商户单日转账额度', '商户单月转账额度', '商户日转账额度', '商户月转账额度', ) if any(p in msg for p in allow_phrases): return True # 宽松但仍要求「商户」+「日/月」+「额度」,避免「受限」误匹配 if '商户' in msg and ('额度' in msg or '限额' in msg): if any(k in msg for k in ('单日', '日转账', '单月', '月转账')): if not _is_wx_user_personal_quota_msg(msg): return True return False def _log_wx_collect(tag, **kwargs): """收款排障:日志搜【微信官方收款】即可定位。""" parts = [f'{k}={v}' for k, v in kwargs.items()] logger.error('【微信官方收款】%s | %s', tag, ' | '.join(parts)) def _verify_collect_quota_or_response(user_main, audit, shenhe_danhao): """收款限额校验;不通过时直接返回 Response""" ok, msg, limit_kind = check_collect_quota_limits( user_main, audit.leixing, audit.shenqing_jine, audit.shijidaozhang, shenhe_danhao, ) if not ok: limit_code = 33 if limit_kind == 'platform' else 400 logger.warning( '收款限额校验未通过 yonghuid=%s shenhe_danhao=%s leixing=%s kind=%s msg=%s', user_main.yonghuid, shenhe_danhao, audit.leixing, limit_kind, msg, ) return Response({'code': limit_code, 'msg': msg}) return None def _mark_auto_record_failed(tixian_id, fail_reason): """微信明确失败时标记打款记录终态,避免后续一直被当成「处理中」""" TixianAutoRecord.objects.filter(tixian_id=tixian_id, zhuangtai=0).update( zhuangtai=2, fail_reason=(fail_reason or '打款失败')[:500], update_time=timezone.now(), ) def _bind_collect_mch(audit, tixian_id, mch_id): """成功进入待确认时,审核单与打款单同时记下实际商户号,防串户。""" fields = { 'tixian_auto_id': tixian_id, 'update_time': timezone.now(), } if hasattr(audit, 'dakuan_mch_id'): fields['dakuan_mch_id'] = (mch_id or '')[:32] TixianShenheJilu.objects.filter(id=audit.id).update(**fields) def _build_wx_transfer_body(wx_cfg, tixian_id, openid, yonghuid, leixing, shijidaozhang): transfer_scene_id = wx_cfg['TRANSFER_SCENE_ID'] role_map = {1: '打手', 2: '管事', 3: '组长', 4: '考核官', 5: '打手', 6: '商家'} desc_map = {1: '佣金提现', 2: '分红提现', 3: '分红提现', 4: '分佣提现', 5: '押金提现', 6: '余额提现'} transfer_scene_report_infos = [] if transfer_scene_id == '1005': transfer_scene_report_infos = [ {'info_type': '岗位类型', 'info_content': role_map.get(leixing, '用户')}, {'info_type': '报酬说明', 'info_content': desc_map.get(leixing, '提现')}, ] elif transfer_scene_id == '1009': transfer_scene_report_infos = [ {'info_type': '采购商品名称', 'info_content': '平台服务'}, ] body = { 'appid': wx_cfg['APPID'], 'out_bill_no': tixian_id, 'transfer_scene_id': transfer_scene_id, 'openid': openid, 'transfer_amount': int(shijidaozhang * 100), 'transfer_remark': f'兄弟们 收款前记得录屏发快手', 'transfer_scene_report_infos': transfer_scene_report_infos, } notify_url = str(wx_cfg.get('NOTIFY_URL') or '').strip() if notify_url: body['notify_url'] = notify_url return body def _call_wechat_transfer(body, wx_cfg): from utils.wechat_v3 import build_authorization url = 'https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills' body_json = json.dumps(body, separators=(',', ':')) auth = build_authorization('POST', url, body_json, wx_cfg=wx_cfg) headers = { 'Authorization': auth, 'Content-Type': 'application/json', 'Accept': 'application/json', 'User-Agent': 'Mozilla/5.0', } resp = requests.post(url, data=body_json, headers=headers, timeout=15) try: wx_result = resp.json() except ValueError: wx_result = {} return resp, wx_result class TixianZddkshApplyView(APIView): """POST /yonghu/zddksh 提交审核申请(扣款,不创建 TixianAutoRecord)""" permission_classes = [permissions.IsAuthenticated] def post(self, request): from utils.redis_lock import acquire_lock, release_lock user_main = request.user lock_key = f'tixian_audit_apply:{user_main.yonghuid}' if not acquire_lock(lock_key, timeout=10): return Response({'code': 429, 'msg': '操作频繁,请稍后重试'}) try: leixing = request.data.get('leixing') jine_str = request.data.get('jine') if leixing is None or jine_str is None: return Response({'code': 1, 'msg': '缺少参数: leixing 或 jine'}) try: leixing = int(leixing) jine = decimal.Decimal(str(jine_str)) except (ValueError, decimal.InvalidOperation): return Response({'code': 2, 'msg': '参数格式错误'}) if leixing not in [1, 2, 3, 4, 5, 6]: return Response({'code': 3, 'msg': '提现类型无效'}) if jine <= decimal.Decimal('0.1'): return Response({'code': 4, 'msg': '提现金额必须大于0.1'}) try: data = create_audit_application(user_main, leixing, jine) except ValueError as e: return Response({'code': 400, 'msg': str(e)}) except Exception as e: logger.error(f'提现审核申请异常: {e}', exc_info=True) return Response({'code': 99, 'msg': '提现申请失败,请稍后重试'}) if data.get('freeze_only'): return Response({ 'code': 0, 'msg': data.get('msg') or '资金已划入冻结池', 'data': data, }) return Response({'code': 0, 'msg': '提现申请已提交,请等待审核', 'data': data}) finally: release_lock(lock_key) def process_audit_collect(request): """ POST /yonghu/tixiansq 用户收款(P0 防多提) 主路径:tixianjilu_id → Tixianjilu → TixianShenheJilu(状态6) 若 TixianAutoRecord 已有 tixian_id → 只问微信,非终态绝不新建 仅微信 FAIL/CANCELLED 终态后才允许新建打款记录 """ from utils.redis_lock import acquire_lock, release_lock user_main = request.user yonghuid = user_main.yonghuid tixianjilu_id = request.data.get('tixianjilu_id') or request.data.get('id') shenhe_danhao = (request.data.get('shenhe_danhao') or '').strip() shenhe_jilu_id = request.data.get('shenhe_jilu_id') logger.info( '收款请求 yonghuid=%s tixianjilu_id=%s shenhe_jilu_id=%s shenhe_danhao=%s', yonghuid, tixianjilu_id, shenhe_jilu_id, shenhe_danhao, ) try: from utils.wechat_mch_service import list_wx_cfgs_for_collect, wx_cfg_is_complete if not user_main.openid: return Response({'code': 10, 'msg': '用户未绑定微信,无法收款'}) mch_cfgs, bind_err, is_user_bound = list_wx_cfgs_for_collect(yonghuid) if bind_err: return Response({'code': 11, 'msg': bind_err}) if not mch_cfgs or not wx_cfg_is_complete(mch_cfgs[0]): logger.error('微信打款配置不完整 enabled_count=%s bound=%s', len(mch_cfgs), is_user_bound) return Response({'code': 11, 'msg': '系统配置异常,请联系客服'}) if not is_user_bound and 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, tixianjilu_id=tixianjilu_id, shenhe_danhao=shenhe_danhao, shenhe_jilu_id=shenhe_jilu_id, ) if not ctx: return Response({'code': 400, 'msg': err}) audit = ctx['audit'] shenhe_danhao = ctx['shenhe_danhao'] tixianjilu_id_val = ctx['jilu'].id if ctx.get('jilu') else audit.tixianjilu_id leixing = audit.leixing # 微信单笔到账限额:与申请阶段同一标准(shijidaozhang >= 200 驳回并退款) if shijidaozhang_exceeds_wechat_limit(audit.shijidaozhang): reason = wechat_limit_collect_message(audit.shijidaozhang) _, msg = reject_audit_and_refund(user_main, audit, reason) return Response({'code': 400, 'msg': msg}) # 二次资格校验:不通过仅返回错误,审核单保持待收款(6),不退款,用户补齐资质后可再点收款 collect_ok, collect_msg = validate_collect_eligibility(user_main, leixing) if not collect_ok: logger.warning( '收款校验未通过 yonghuid=%s shenhe_danhao=%s leixing=%s msg=%s', yonghuid, shenhe_danhao, leixing, collect_msg, ) return Response({'code': 400, 'msg': collect_msg}) quota_resp = _verify_collect_quota_or_response(user_main, audit, shenhe_danhao) if quota_resp: return quota_resp lock_key = f'tixian_collect:{shenhe_danhao}' if not acquire_lock(lock_key, timeout=15): return Response({'code': 429, 'msg': '操作频繁,请稍后重试'}) tixian_id = None quota_reserved = False try: action, payload = reconcile_shenhe_wechat_bills(shenhe_danhao) if action == RECONCILE_COMPLETED: ensure_shenhe_collect_completed(shenhe_danhao) msg = payload.get('msg', '提现已成功到账') if isinstance(payload, dict) else '提现已成功到账' return Response({ 'code': 0, 'msg': msg, 'data': {'already_completed': True, 'tixianjilu_id': tixianjilu_id_val}, }) if action == RECONCILE_WAIT_CONFIRM: quota_resp = _verify_collect_quota_or_response(user_main, audit, shenhe_danhao) if quota_resp: return quota_resp if isinstance(payload, dict): _bind_collect_mch( audit, payload.get('tixian_id') or '', payload.get('mch_id') or '', ) return _build_collect_success_response(payload) if action == RECONCILE_PENDING: msg = payload.get('msg') if isinstance(payload, dict) else payload return Response({'code': 12, 'msg': msg or '有收款处理中,请稍后再试'}) if action != RECONCILE_ALLOW_NEW: return Response({'code': 12, 'msg': '收款处理中,请稍后再试'}) with transaction.atomic(): audit, err = get_audit_for_collect( shenhe_danhao, yonghuid, audit.id, ) if not audit: return Response({'code': 400, 'msg': err}) # 金额一律用审核表已落库数据(申请时扣款+算费),收款不再重算 jine = audit.shenqing_jine shouxufei = audit.shouxufei shijidaozhang = audit.shijidaozhang feilv = audit.feilv if shijidaozhang_exceeds_wechat_limit(shijidaozhang): reason = wechat_limit_collect_message(shijidaozhang) _, msg = reject_audit_and_refund(user_main, audit, reason) return Response({'code': 400, 'msg': msg}) ok, msg, limit_kind = reserve_collect_quota_limits( user_main, leixing, jine, shijidaozhang, shenhe_danhao, ) if not ok: limit_code = 33 if limit_kind == 'platform' else 400 return Response({'code': limit_code, 'msg': msg}) quota_reserved = True # 选户:绑定用户仅 1 户;未绑定按 priority。 # 仅「商户整户日/月额度」+ 查单确认未建单 才静默换号;其它失败立刻回前端。 if not is_user_bound and len(mch_cfgs) > 1: failed_mchs = set( TixianAutoRecord.objects.filter( shenhe_danhao=shenhe_danhao, zhuangtai=2, ).exclude(mch_id='').values_list('mch_id', flat=True) ) prefer_cfgs = [c for c in mch_cfgs if c.get('MCHID') not in failed_mchs] if prefer_cfgs and len(prefer_cfgs) < len(mch_cfgs): logger.warning( '收款优先未失败商户 shenhe=%s skip=%s try=%s', shenhe_danhao, sorted(failed_mchs), [c.get('MCHID') for c in prefer_cfgs], ) mch_cfgs = prefer_cfgs _log_wx_collect( '开始选户发起', shenhe=shenhe_danhao, bound=is_user_bound, count=len(mch_cfgs), mch_ids=','.join(c.get('MCHID') for c in mch_cfgs), ) last_user_msg = '微信收款发起失败,请稍后重试' tixian_id = None tried_mchs = [] tried_details = [] def _fail_front(msg, code=99): nonlocal quota_reserved if quota_reserved and tixian_id: release_collect_quota_for_record( TixianAutoRecord.objects.get(tixian_id=tixian_id), ) quota_reserved = False return Response({ 'code': code, 'msg': msg[:800], 'data': { 'source': 'wechat_official', 'tried_mch_ids': tried_mchs, 'user_bound': is_user_bound, }, }) for mch_idx, wx_cfg in enumerate(mch_cfgs): tixian_id = generate_tixian_id() mch_id = wx_cfg['MCHID'] tried_mchs.append(mch_id) _log_wx_collect( '尝试商户', idx=f'{mch_idx + 1}/{len(mch_cfgs)}', mch_id=mch_id, bill=tixian_id, bound=is_user_bound, ) with transaction.atomic(): TixianAutoRecord.objects.create( tixian_id=tixian_id, yonghuid=yonghuid, leixing=leixing, jine=jine, shouxufei=shouxufei, shijidaozhang=shijidaozhang, feilv=feilv, zhuangtai=0, shenhe_danhao=shenhe_danhao, mch_id=mch_id, ) TixianShenheJilu.objects.filter(id=audit.id).update( tixian_auto_id=tixian_id, update_time=timezone.now(), ) body = _build_wx_transfer_body( wx_cfg, tixian_id, user_main.openid, yonghuid, leixing, shijidaozhang, ) try: resp, wx_result = _call_wechat_transfer(body, wx_cfg) except requests.RequestException as e: # 网络不确定是否已建单:禁止换号,保持待查单防双笔 logger.error( '微信打款网络异常(保持待查单): bill=%s mch=%s err=%s', tixian_id, mch_id, e, exc_info=True, ) return Response({ 'code': 12, 'msg': '收款请求已提交,系统正在核对状态,请稍后再试,切勿重复点击', }) except (OSError, IOError, ValueError) as e: # 证书/私钥本地问题:未发到微信;按计划不换号,直接报错 logger.error( '微信打款本地配置异常: bill=%s mch=%s err=%s', tixian_id, mch_id, e, exc_info=True, ) err_msg = f'商户证书/私钥异常: {e}' _mark_auto_record_failed(tixian_id, f'[{mch_id}] {err_msg}') return _fail_front(f'【微信官方】商户{mch_id}配置异常,请联系管理员') # 成功进待确认 → 立刻回前端 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) auto_record.wechat_package = json.dumps(package_info, ensure_ascii=False) auto_record.mch_id = mch_id auto_record.save(update_fields=['wechat_package', 'mch_id']) _bind_collect_mch(audit, tixian_id, mch_id) _log_wx_collect( '成功待确认', mch_id=mch_id, bill=tixian_id, shenhe=shenhe_danhao, tried_before=','.join(tried_mchs[:-1]) if len(tried_mchs) > 1 else '', ) return _build_collect_success_response({ 'tixian_id': tixian_id, 'package_info': package_info, 'shouxufei': str(shouxufei), 'shijidaozhang': str(shijidaozhang), 'current_rate': str(feilv), 'mch_id': mch_id, 'shenhe_danhao': shenhe_danhao, 'tixianjilu_id': tixianjilu_id_val, }) state200 = (wx_result.get('state') or '').upper() if resp.status_code == 200 else '' if resp.status_code == 200 and state200 == 'SUCCESS': ar = TixianAutoRecord.objects.get(tixian_id=tixian_id) from .tixian_shenhe_services import mark_transfer_success mark_transfer_success( ar, wechat_transfer_no=( wx_result.get('transfer_bill_no') or wx_result.get('transfer_no') ), ) _bind_collect_mch(audit, tixian_id, mch_id) ensure_shenhe_collect_completed(shenhe_danhao) return Response({ 'code': 0, 'msg': '提现已成功到账', 'data': {'already_completed': True, 'tixianjilu_id': tixianjilu_id_val}, }) err_code, err_msg = _extract_wx_err(wx_result) if resp.status_code == 200 and (not err_code or err_msg == '微信接口调用失败'): err_msg = f'微信状态:{state200 or "未知"}' _log_wx_collect( '微信官方拒绝本商户', shenhe=shenhe_danhao, mch_id=mch_id, bill=tixian_id, http=resp.status_code, code=err_code, msg=err_msg, state=state200, body=json.dumps(wx_result, ensure_ascii=False)[:800], ) tried_details.append( f'商户{mch_id}: HTTP{resp.status_code}/{err_code or "-"} {err_msg}' ) user_msg = err_msg or '微信收款发起失败,请稍后重试' last_user_msg = user_msg has_next = (not is_user_bound) and (mch_idx < len(mch_cfgs) - 1) # 结果可能已受理:查单,禁止换号 if _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 not None and 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) _bind_collect_mch(audit, tixian_id, mch_id) return _build_collect_success_response(sync_payload) if sync_action == RECONCILE_COMPLETED: _bind_collect_mch(audit, tixian_id, mch_id) ensure_shenhe_collect_completed(shenhe_danhao) return Response({ 'code': 0, 'msg': '提现已成功到账', 'data': { 'already_completed': True, 'tixianjilu_id': tixianjilu_id_val, }, }) return Response({ 'code': 12, 'msg': '收款请求结果不明确,系统正在核对,请稍后再试,切勿重复点击', }) # 唯一可静默换号:商户整户日/月额度 + 查单确认未建单 mch_total_quota = _is_wx_mch_total_quota_exhausted(err_code, err_msg) if mch_total_quota and has_next: wx_q = query_wechat_transfer_bill(tixian_id, wx_cfg=wx_cfg) if wx_q is None: # 查单失败:不确定是否已建单,禁止换号 _log_wx_collect( '整户额度但查单失败禁止换号', mch_id=mch_id, bill=tixian_id, ) 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) _bind_collect_mch(audit, tixian_id, mch_id) return _build_collect_success_response(sync_payload) if sync_action == RECONCILE_COMPLETED: _bind_collect_mch(audit, tixian_id, mch_id) 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 sync_action != RECONCILE_ALLOW_NEW: return Response({ 'code': 12, 'msg': '有收款处理中,请稍后再试', }) _mark_auto_record_failed(tixian_id, f'[{mch_id}] {user_msg}') _log_wx_collect( '商户整户额度耗尽→静默换下一户', from_mch=mch_id, next_idx=f'{mch_idx + 2}/{len(mch_cfgs)}', wx_code=err_code, wx_msg=err_msg, ) continue # 其它失败(含绑定用户整户额度、个人限额、余额、IP):不换号,回前端 _mark_auto_record_failed(tixian_id, f'[{mch_id}] {user_msg}') detail = ';'.join(tried_details) if tried_details else user_msg tried = '、'.join(tried_mchs) or mch_id if is_user_bound: _log_wx_collect('绑定商户失败回前端', tried=tried, detail=detail) return _fail_front( f'【微信官方】绑定商户 {tried} 发起失败:{detail}' ) if mch_total_quota: _log_wx_collect('整户额度无下一户回前端', tried=tried, detail=detail) return _fail_front( f'【微信官方】商户整户转账额度已满({tried}):{detail}' ) if _is_wx_balance_insufficient(err_code, err_msg): return _fail_front( f'【微信官方】商户资金不足({tried}):{detail}' ) if _is_wx_ip_denied(err_code, err_msg): return _fail_front( f'【微信官方】商户IP未放行({tried}):{detail}' ) _log_wx_collect('非整户额度失败不换号回前端', tried=tried, detail=detail) return _fail_front( f'【微信官方】商户 {tried} 发起失败:{detail}' ) if quota_reserved and tixian_id: release_collect_quota_for_record( TixianAutoRecord.objects.get(tixian_id=tixian_id), ) quota_reserved = False tried = '、'.join(tried_mchs) if tried_mchs else '' detail = ';'.join(tried_details) if tried_details else last_user_msg _log_wx_collect('选户结束失败回前端', tried=tried, detail=detail) return Response({ 'code': 99, 'msg': ( f'【微信官方】已尝试商户 {tried} 均失败:{detail}' if tried else f'【微信官方】{last_user_msg}' )[:800], 'data': { 'source': 'wechat_official', 'tried_mch_ids': tried_mchs, 'user_bound': is_user_bound, }, }) finally: release_lock(lock_key) except Exception as e: logger.error( '收款接口未捕获异常 yonghuid=%s tixianjilu_id=%s shenhe_danhao=%s err=%s', yonghuid, tixianjilu_id, shenhe_danhao, e, exc_info=True, ) return Response({'code': 99, 'msg': '系统繁忙,请稍后重试'})