diff --git a/yonghu/tixian_shenhe_views.py b/yonghu/tixian_shenhe_views.py index ed09ba4..9b9f21f 100644 --- a/yonghu/tixian_shenhe_views.py +++ b/yonghu/tixian_shenhe_views.py @@ -301,760 +301,6 @@ class TixianZddkshApplyView(APIView): 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_enabled_wx_cfgs, wx_cfg_is_complete - - if not user_main.openid: - return Response({'code': 10, 'msg': '用户未绑定微信,无法收款'}) - - mch_cfgs = list_enabled_wx_cfgs() - 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.error( - '启用且配置完整的转账商户仅 %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 - - # 多商户轮换:仅在发起阶段未进入待确认时切换;成功后 mch_id 必须落库 - # 本审核单已失败过的商户本轮优先跳过,直接试下一户 - 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, - count=len(mch_cfgs), - mch_ids=','.join(c.get('MCHID') for c in mch_cfgs), - skip_failed=','.join(sorted(failed_mchs)) if failed_mchs else '', - ) - last_user_msg = '微信收款发起失败,请稍后重试' - last_was_balance = False - last_ip_denied = False - tixian_id = None - tried_details = [] - - for mch_idx, wx_cfg in enumerate(mch_cfgs): - tixian_id = generate_tixian_id() - mch_id = wx_cfg['MCHID'] - _log_wx_collect( - f'尝试第{mch_idx + 1}/{len(mch_cfgs)}个商户', - shenhe=shenhe_danhao, mch_id=mch_id, bill=tixian_id, - ) - 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: - _log_wx_collect('网络超时禁换号', mch_id=mch_id, bill=tixian_id, err=str(e)) - return Response({ - 'code': 12, - 'msg': '收款请求已提交,系统正在核对状态,请稍后再试,切勿重复点击', - }) - except (OSError, IOError, ValueError) as e: - err_code, err_msg = 'CERT_ERROR', f'商户证书/私钥异常: {e}' - last_user_msg = err_msg - tried_details.append({ - 'mch_id': mch_id, 'http': 0, 'code': err_code, 'msg': err_msg, - }) - _log_wx_collect('本地证书异常', mch_id=mch_id, err=str(e)) - _mark_auto_record_failed(tixian_id, f'[{mch_id}] {err_msg}') - if mch_idx < len(mch_cfgs) - 1: - _log_wx_collect('证书异常→换下一户', from_mch=mch_id, next_idx=mch_idx + 2) - continue - if quota_reserved: - release_collect_quota_for_record( - TixianAutoRecord.objects.get(tixian_id=tixian_id), - ) - quota_reserved = False - return _wx_fail_response( - tried_details, err_msg, single_mch=len(mch_cfgs) < 2, - ) - - 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( - '成功待确认', - shenhe=shenhe_danhao, mch_id=mch_id, bill=tixian_id, - tried_before=','.join(d['mch_id'] for d in tried_details), - ) - 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) - _log_wx_collect('直接成功到账', mch_id=mch_id, bill=tixian_id) - 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 = '微信状态:' + (state200 or '未知') - body_preview = json.dumps(wx_result, ensure_ascii=False)[:800] - _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=body_preview, - ) - tried_details.append({ - 'mch_id': mch_id, - 'http': resp.status_code, - 'code': err_code, - 'msg': err_msg, - }) - - has_next = mch_idx < len(mch_cfgs) - 1 - ip_denied = _is_wx_ip_denied(err_code, err_msg) - clear_fail = _is_wx_clear_switchable_fail(err_code, err_msg, resp.status_code) - skip_query = ( - ip_denied - or clear_fail - or any(k in err_msg for k in ('额度', '限额', '上限', '受限', '管控')) - or resp.status_code in (400, 401, 403) - ) - - if not skip_query: - 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, - }, - }) - if sync_action == RECONCILE_PENDING and not has_next: - return Response({ - 'code': 12, - 'msg': ( - sync_payload.get('msg') - if isinstance(sync_payload, dict) else sync_payload - ) or '有收款处理中,请稍后再试', - }) - - user_msg = err_msg or '微信收款发起失败,请稍后重试' - last_user_msg = user_msg - last_was_balance = _is_wx_balance_insufficient(err_code, err_msg) - last_ip_denied = last_ip_denied or ip_denied - _mark_auto_record_failed(tixian_id, f'[{mch_id}] {err_msg}') - - if has_next: - _log_wx_collect( - '本商户官方拒绝→静默换下一户', - from_mch=mch_id, - next_idx='%s/%s' % (mch_idx + 2, len(mch_cfgs)), - wx_code=err_code, - wx_msg=err_msg, - ) - continue - - if quota_reserved: - release_collect_quota_for_record( - TixianAutoRecord.objects.get(tixian_id=tixian_id), - ) - quota_reserved = False - return _wx_fail_response( - tried_details, - user_msg, - single_mch=len(mch_cfgs) < 2, - ip_denied=last_ip_denied, - no_balance=last_was_balance, - ) - - if quota_reserved and tixian_id: - release_collect_quota_for_record( - TixianAutoRecord.objects.get(tixian_id=tixian_id), - ) - quota_reserved = False - return _wx_fail_response( - tried_details, - last_user_msg, - single_mch=len(mch_cfgs) < 2, - ip_denied=last_ip_denied, - no_balance=last_was_balance, - ) - - 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_enabled_wx_cfgs, wx_cfg_is_complete - - if not user_main.openid: - return Response({'code': 10, 'msg': '用户未绑定微信,无法收款'}) - - mch_cfgs = list_enabled_wx_cfgs() - 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.error( - '启用且配置完整的转账商户仅 %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 - - # 多商户轮换:仅在发起阶段未进入待确认时切换;成功后 mch_id 必须落库 - # 本审核单已失败过的商户本轮优先跳过,直接试下一户 - 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, - count=len(mch_cfgs), - mch_ids=','.join(c.get('MCHID') for c in mch_cfgs), - skip_failed=','.join(sorted(failed_mchs)) if failed_mchs else '', - ) - last_user_msg = '微信收款发起失败,请稍后重试' - last_was_balance = False - last_ip_denied = False - tixian_id = None - tried_details = [] - - for mch_idx, wx_cfg in enumerate(mch_cfgs): - tixian_id = generate_tixian_id() - mch_id = wx_cfg['MCHID'] - _log_wx_collect( - f'尝试第{mch_idx + 1}/{len(mch_cfgs)}个商户', - shenhe=shenhe_danhao, mch_id=mch_id, bill=tixian_id, - ) - 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_code, err_msg = 'CERT_ERROR', f'商户证书/私钥异常: {e}' - last_user_msg = err_msg - last_was_balance = False - _mark_auto_record_failed(tixian_id, f'[{mch_id}] {err_msg}') - if mch_idx < len(mch_cfgs) - 1: - logger.warning( - '商户本地配置失败,切换下一商户 shenhe=%s from=%s idx=%s/%s', - shenhe_danhao, mch_id, 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 - 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) - 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) - - 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 "未知"}' - logger.error( - '微信发起转账未进入待确认: 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 - ip_denied = _is_wx_ip_denied(err_code, err_msg) - clear_fail = _is_wx_clear_switchable_fail(err_code, err_msg, resp.status_code) - # 限额/额度/IP/4xx:一律不先查单(查单常一起挂),直接换号 - skip_query = ( - ip_denied - or clear_fail - or ('额度' in err_msg) - or ('限额' in err_msg) - or ('上限' in err_msg) - or resp.status_code in (400, 401, 403) - ) - - if not skip_query: - 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, - }, - }) - # PENDING 有下一户:放弃本户继续;无下一户才告诉前端处理中 - if sync_action == RECONCILE_PENDING and not has_next: - return Response({ - 'code': 12, - 'msg': ( - sync_payload.get('msg') - if isinstance(sync_payload, dict) else sync_payload - ) or '有收款处理中,请稍后再试', - }) - - 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( - '商户不可用,静默换下一户 shenhe=%s from=%s code=%s msg=%s ' - 'http=%s skip_query=%s idx=%s/%s', - shenhe_danhao, mch_id, err_code, err_msg, resp.status_code, - skip_query, 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 - tried = '、'.join(tried_mchs) or mch_id - if len(mch_cfgs) < 2: - return Response({ - 'code': 99, - 'msg': ( - f'仅1个可用打款商户({tried}),无法轮换。' - f'失败原因:{user_msg}。请在后台再完善并启用第二个商户(私钥须存在)。' - ), - }) - if ip_denied: - return Response({ - 'code': 99, - 'msg': f'已尝试商户 {tried} 均失败(含IP白名单),请检查各商户配置', - }) - if last_was_balance: - return Response({ - 'code': 99, - 'msg': f'已尝试商户 {tried},运营账户资金不足,需等管理员充值后才能提现', - }) - return Response({ - 'code': 99, - 'msg': f'已尝试商户 {tried} 均失败:{user_msg}', - }) - - 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 '' - if last_was_balance: - return Response({ - 'code': 99, - 'msg': ( - f'已尝试商户 {tried},运营账户资金不足,需等管理员充值后才能提现' - if tried else '运营账户资金不足,需等管理员充值后才能提现' - ), - }) - return Response({ - 'code': 99, - 'msg': ( - f'已尝试商户 {tried} 均失败:{last_user_msg}' - if tried else last_user_msg - ), - }) - - finally: - release_lock(lock_key) - except Exception as e: logger.error( '收款接口未捕获异常 yonghuid=%s tixianjilu_id=%s shenhe_danhao=%s err=%s', @@ -1062,3 +308,4 @@ def process_audit_collect(request): exc_info=True, ) return Response({'code': 99, 'msg': '系统繁忙,请稍后重试'}) +