支持自动提现多商户轮换:配置入库、打款单强制记 mch_id、收款失败自动换号。
- 新增 wechat_pay_mch_config 表,迁移时从 settings 种子默认商户 - TixianAutoRecord 增加 mch_id;查单/回调解密后按记录对齐 - 后台接口 wxmchlb/wxmchbj/wxmchscwj(权限 8080a)与证书上传 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,7 +12,6 @@ import string
|
||||
import time
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
from rest_framework import permissions
|
||||
@@ -63,6 +62,22 @@ 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=''):
|
||||
"""
|
||||
可立刻换下一商户重试的错误(本笔尚未进入 WAIT_USER_CONFIRM)。
|
||||
仅白名单,避免误伤导致重复开单。
|
||||
"""
|
||||
if _is_wx_balance_insufficient(err_code, err_msg):
|
||||
return True
|
||||
blob = f'{err_code} {err_msg}'.upper()
|
||||
keywords = (
|
||||
'FREQUENCY_LIMITED', 'RATE_LIMIT', 'QUOTA', 'LIMIT',
|
||||
'日限额', '超出限额', '超过限额', '限额', '次数超限',
|
||||
'ACCOUNT_ERROR', 'NO_AUTH', 'PERM', '商户号异常', '账户异常',
|
||||
)
|
||||
return any(k in blob for k in keywords)
|
||||
|
||||
|
||||
def _verify_collect_quota_or_response(user_main, audit, shenhe_danhao):
|
||||
"""收款限额校验;不通过时直接返回 Response"""
|
||||
ok, msg, limit_kind = check_collect_quota_limits(
|
||||
@@ -121,7 +136,7 @@ def _call_wechat_transfer(body, wx_cfg):
|
||||
|
||||
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)
|
||||
auth = build_authorization('POST', url, body_json, wx_cfg=wx_cfg)
|
||||
headers = {
|
||||
'Authorization': auth,
|
||||
'Content-Type': 'application/json',
|
||||
@@ -208,12 +223,14 @@ def process_audit_collect(request):
|
||||
)
|
||||
|
||||
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': '用户未绑定微信,无法收款'})
|
||||
|
||||
wx_cfg = settings.WECHAT_PAY_V3_CONFIG
|
||||
if not wx_cfg.get('APPID') or not wx_cfg.get('MCHID') or not wx_cfg.get('TRANSFER_SCENE_ID'):
|
||||
logger.error('微信打款配置不完整 APPID=%s MCHID=%s', wx_cfg.get('APPID'), wx_cfg.get('MCHID'))
|
||||
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': '系统配置异常,请联系客服'})
|
||||
|
||||
ctx, err = resolve_collect_context(
|
||||
@@ -304,33 +321,55 @@ def process_audit_collect(request):
|
||||
|
||||
quota_reserved = True
|
||||
|
||||
# 多商户轮换:仅在发起阶段未进入待确认时切换;成功后 mch_id 必须落库
|
||||
last_user_msg = '微信收款发起失败,请稍后重试'
|
||||
last_was_balance = False
|
||||
tixian_id = None
|
||||
|
||||
for mch_idx, wx_cfg in enumerate(mch_cfgs):
|
||||
tixian_id = generate_tixian_id()
|
||||
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 = wx_cfg['MCHID']
|
||||
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,
|
||||
)
|
||||
audit.tixian_auto_id = tixian_id
|
||||
audit.save(update_fields=['tixian_auto_id', 'update_time'])
|
||||
|
||||
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)
|
||||
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': '收款请求已提交,系统正在核对状态,请稍后再试,切勿重复点击',
|
||||
})
|
||||
|
||||
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.save(update_fields=['wechat_package'])
|
||||
auto_record.mch_id = mch_id
|
||||
auto_record.save(update_fields=['wechat_package', 'mch_id'])
|
||||
|
||||
return _build_collect_success_response({
|
||||
'tixian_id': tixian_id,
|
||||
@@ -338,7 +377,7 @@ def process_audit_collect(request):
|
||||
'shouxufei': str(shouxufei),
|
||||
'shijidaozhang': str(shijidaozhang),
|
||||
'current_rate': str(feilv),
|
||||
'mch_id': wx_cfg['MCHID'],
|
||||
'mch_id': mch_id,
|
||||
'shenhe_danhao': shenhe_danhao,
|
||||
'tixianjilu_id': tixianjilu_id_val,
|
||||
})
|
||||
@@ -346,27 +385,17 @@ def process_audit_collect(request):
|
||||
err_code = wx_result.get('code', '')
|
||||
err_msg = wx_result.get('message', wx_result.get('detail', '微信接口调用失败'))
|
||||
logger.error(
|
||||
'微信发起转账未进入待确认: bill=%s code=%s msg=%s HTTP%s',
|
||||
tixian_id, err_code, err_msg, resp.status_code,
|
||||
'微信发起转账未进入待确认: bill=%s mch=%s code=%s msg=%s HTTP%s',
|
||||
tixian_id, mch_id, err_code, err_msg, resp.status_code,
|
||||
)
|
||||
|
||||
is_balance_issue = _is_wx_balance_insufficient(err_code, err_msg)
|
||||
if is_balance_issue:
|
||||
_mark_auto_record_failed(tixian_id, err_msg)
|
||||
if quota_reserved:
|
||||
release_collect_quota_for_record(
|
||||
TixianAutoRecord.objects.get(tixian_id=tixian_id),
|
||||
)
|
||||
quota_reserved = False
|
||||
return Response({
|
||||
'code': 99,
|
||||
'msg': '运营账户资金不足,需等管理员充值后才能提现',
|
||||
})
|
||||
|
||||
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
|
||||
@@ -385,24 +414,42 @@ def process_audit_collect(request):
|
||||
)
|
||||
return Response({'code': 12, 'msg': pending_msg or '有收款处理中,请稍后再试'})
|
||||
|
||||
# 明确失败:标失败;可切换则试下一商户(不释放限额,直到全部失败)
|
||||
user_msg = err_msg or '微信收款发起失败,请稍后重试'
|
||||
_mark_auto_record_failed(tixian_id, user_msg)
|
||||
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 _is_wx_switchable_error(err_code, err_msg) and 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
|
||||
if last_was_balance and mch_idx >= len(mch_cfgs) - 1:
|
||||
return Response({
|
||||
'code': 99,
|
||||
'msg': '运营账户资金不足,需等管理员充值后才能提现',
|
||||
})
|
||||
return Response({'code': 99, 'msg': user_msg})
|
||||
|
||||
except requests.RequestException as e:
|
||||
logger.error(
|
||||
'微信打款网络异常(保持待查单): bill=%s err=%s',
|
||||
tixian_id, e, exc_info=True,
|
||||
if quota_reserved and tixian_id:
|
||||
release_collect_quota_for_record(
|
||||
TixianAutoRecord.objects.get(tixian_id=tixian_id),
|
||||
)
|
||||
quota_reserved = False
|
||||
if last_was_balance:
|
||||
return Response({
|
||||
'code': 12,
|
||||
'msg': '收款请求已提交,系统正在核对状态,请稍后再试,切勿重复点击',
|
||||
'code': 99,
|
||||
'msg': '运营账户资金不足,需等管理员充值后才能提现',
|
||||
})
|
||||
return Response({'code': 99, 'msg': last_user_msg})
|
||||
|
||||
finally:
|
||||
release_lock(lock_key)
|
||||
|
||||
Reference in New Issue
Block a user