修复误推的重复 process_audit_collect 语法错误。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -301,760 +301,6 @@ class TixianZddkshApplyView(APIView):
|
|||||||
finally:
|
finally:
|
||||||
release_lock(lock_key)
|
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:
|
except Exception as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
'收款接口未捕获异常 yonghuid=%s tixianjilu_id=%s shenhe_danhao=%s err=%s',
|
'收款接口未捕获异常 yonghuid=%s tixianjilu_id=%s shenhe_danhao=%s err=%s',
|
||||||
@@ -1062,3 +308,4 @@ def process_audit_collect(request):
|
|||||||
exc_info=True,
|
exc_info=True,
|
||||||
)
|
)
|
||||||
return Response({'code': 99, 'msg': '系统繁忙,请稍后重试'})
|
return Response({'code': 99, 'msg': '系统繁忙,请稍后重试'})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user