P0防双笔:查不清绝不新建第二笔;新增提现抽成利润统计接口。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
328
houtai/tixian_profit_views.py
Normal file
328
houtai/tixian_profit_views.py
Normal file
@@ -0,0 +1,328 @@
|
||||
"""
|
||||
提现抽成利润统计(真实抽成 = 申请扣款额 − 实际到账额 = shouxufei)
|
||||
|
||||
口径说明(务必与分账一致):
|
||||
- 仅统计「提现成功」记录:Tixianjilu.zhuangtai = 2
|
||||
- 利润 = COALESCE(shouxufei, max(shenqing_jine - jine, 0))
|
||||
- 手动/自动一并计入(dakuan_mode 可筛)
|
||||
- 日期维度默认按 update_time(成功落账日);可按 create_time(申请日)
|
||||
- 不含会员/押金充值/商家充值等,那些不是抽成利润
|
||||
"""
|
||||
import logging
|
||||
from datetime import datetime, time
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db.models import (
|
||||
Count, DecimalField, F, Q, Sum, Value,
|
||||
)
|
||||
from django.db.models.functions import Coalesce, Greatest, TruncDate, TruncMonth
|
||||
from django.utils import timezone
|
||||
from rest_framework.parsers import JSONParser
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from yonghu.models import Tixianjilu
|
||||
|
||||
from .view import verify_kefu_permission
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_LEIXING_LABEL = {
|
||||
1: '打手佣金',
|
||||
2: '管事分红',
|
||||
3: '组长分红',
|
||||
4: '审核官分佣',
|
||||
5: '打手押金',
|
||||
6: '商家余额',
|
||||
}
|
||||
|
||||
_FEE_EXPR = Coalesce(
|
||||
F('shouxufei'),
|
||||
Greatest(
|
||||
Coalesce(F('shenqing_jine'), Value(Decimal('0.00')))
|
||||
- Coalesce(F('jine'), Value(Decimal('0.00'))),
|
||||
Value(Decimal('0.00')),
|
||||
),
|
||||
Value(Decimal('0.00')),
|
||||
output_field=DecimalField(max_digits=12, decimal_places=2),
|
||||
)
|
||||
|
||||
|
||||
def _parse_date(s):
|
||||
if not s:
|
||||
return None
|
||||
try:
|
||||
return datetime.strptime(str(s).strip()[:10], '%Y-%m-%d').date()
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _day_bounds(d):
|
||||
"""本地日历日 → aware datetime 区间 [start, end)"""
|
||||
start = datetime.combine(d, time.min)
|
||||
end = datetime.combine(d, time.max)
|
||||
if timezone.is_naive(start):
|
||||
tz = timezone.get_current_timezone()
|
||||
start = timezone.make_aware(start, tz)
|
||||
end = timezone.make_aware(end, tz)
|
||||
return start, end
|
||||
|
||||
|
||||
class TixianChouchengProfitView(APIView):
|
||||
"""
|
||||
POST /houtai/txchouchengtj
|
||||
提现抽成利润统计(分账专用口径)
|
||||
|
||||
参数:
|
||||
phone: 客服手机号
|
||||
date_from / date_to: YYYY-MM-DD(闭区间,按成功日或申请日)
|
||||
date_field: update_time(默认,成功日) | create_time(申请日)
|
||||
leixing: 1~6 可选;不传=全部身份
|
||||
dakuan_mode: 1手动 2自动 可选;不传=全部
|
||||
group_by: day | month | leixing | none(默认 day)
|
||||
page / page_size: 明细分页(可选,仅 group_by=none 时返回 list)
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
phone = (request.data.get('phone') or request.data.get('username') or '').strip()
|
||||
kefu, permissions = verify_kefu_permission(request, phone)
|
||||
if kefu is None:
|
||||
return permissions
|
||||
if 'caiwu' not in permissions and not any(
|
||||
p in permissions for p in ('5500a', '5500b', '5500c')
|
||||
):
|
||||
return Response({'code': 403, 'msg': '无提现抽成利润查看权限'})
|
||||
|
||||
date_from = _parse_date(request.data.get('date_from') or request.data.get('start_date'))
|
||||
date_to = _parse_date(request.data.get('date_to') or request.data.get('end_date'))
|
||||
if not date_from or not date_to:
|
||||
today = timezone.localdate()
|
||||
date_from = date_from or today
|
||||
date_to = date_to or today
|
||||
if date_from > date_to:
|
||||
return Response({'code': 400, 'msg': 'date_from 不能晚于 date_to'})
|
||||
|
||||
date_field = (request.data.get('date_field') or 'update_time').strip()
|
||||
if date_field not in ('update_time', 'create_time'):
|
||||
return Response({'code': 400, 'msg': 'date_field 仅支持 update_time / create_time'})
|
||||
|
||||
group_by = (request.data.get('group_by') or 'day').strip()
|
||||
if group_by not in ('day', 'month', 'leixing', 'none'):
|
||||
return Response({'code': 400, 'msg': 'group_by 仅支持 day/month/leixing/none'})
|
||||
|
||||
leixing_val = None
|
||||
raw_leixing = request.data.get('leixing')
|
||||
if raw_leixing is not None and str(raw_leixing).strip() != '':
|
||||
try:
|
||||
leixing_val = int(raw_leixing)
|
||||
except (TypeError, ValueError):
|
||||
return Response({'code': 400, 'msg': 'leixing 无效'})
|
||||
if leixing_val not in (1, 2, 3, 4, 5, 6):
|
||||
return Response({'code': 400, 'msg': 'leixing 仅支持 1~6'})
|
||||
|
||||
dakuan_mode_val = None
|
||||
raw_mode = request.data.get('dakuan_mode')
|
||||
if raw_mode is not None and str(raw_mode).strip() != '':
|
||||
try:
|
||||
dakuan_mode_val = int(raw_mode)
|
||||
except (TypeError, ValueError):
|
||||
return Response({'code': 400, 'msg': 'dakuan_mode 无效'})
|
||||
if dakuan_mode_val not in (1, 2):
|
||||
return Response({'code': 400, 'msg': 'dakuan_mode 仅支持 1/2'})
|
||||
|
||||
start_dt, _ = _day_bounds(date_from)
|
||||
_, end_dt = _day_bounds(date_to)
|
||||
|
||||
# 仅提现成功;手动+自动
|
||||
q = Q(zhuangtai=2)
|
||||
if date_field == 'create_time':
|
||||
q &= Q(create_time__gte=start_dt, create_time__lte=end_dt)
|
||||
else:
|
||||
q &= Q(update_time__gte=start_dt, update_time__lte=end_dt)
|
||||
if leixing_val is not None:
|
||||
q &= Q(leixing=leixing_val)
|
||||
if dakuan_mode_val is not None:
|
||||
q &= Q(dakuan_mode=dakuan_mode_val)
|
||||
|
||||
qs = Tixianjilu.objects.filter(q)
|
||||
annotated = qs.annotate(fee=_FEE_EXPR)
|
||||
|
||||
total_agg = annotated.aggregate(
|
||||
fee_sum=Sum('fee'),
|
||||
apply_sum=Sum(Coalesce(F('shenqing_jine'), F('jine'), Value(Decimal('0.00')))),
|
||||
payout_sum=Sum(Coalesce(F('jine'), Value(Decimal('0.00')))),
|
||||
cnt=Count('id'),
|
||||
)
|
||||
fee_total = total_agg.get('fee_sum') or Decimal('0.00')
|
||||
apply_total = total_agg.get('apply_sum') or Decimal('0.00')
|
||||
payout_total = total_agg.get('payout_sum') or Decimal('0.00')
|
||||
# 交叉校验:申请 − 到账 应约等于抽成(允许历史脏数据偏差)
|
||||
cross_check = (apply_total - payout_total).quantize(Decimal('0.01'))
|
||||
|
||||
summary = {
|
||||
'profit_total': str(fee_total.quantize(Decimal('0.01'))),
|
||||
'apply_total': str(apply_total.quantize(Decimal('0.01'))),
|
||||
'payout_total': str(payout_total.quantize(Decimal('0.01'))),
|
||||
'apply_minus_payout': str(cross_check),
|
||||
'success_count': int(total_agg.get('cnt') or 0),
|
||||
'date_from': date_from.isoformat(),
|
||||
'date_to': date_to.isoformat(),
|
||||
'date_field': date_field,
|
||||
'leixing': leixing_val,
|
||||
'leixing_label': _LEIXING_LABEL.get(leixing_val, '全部身份'),
|
||||
'dakuan_mode': dakuan_mode_val,
|
||||
'formula': '利润=申请扣款额(shenqing_jine)−实际到账(jine)=手续费(shouxufei);仅统计提现成功(zhuangtai=2)',
|
||||
}
|
||||
|
||||
rows = []
|
||||
if group_by == 'day':
|
||||
trunc = TruncDate(date_field)
|
||||
grouped = (
|
||||
annotated.annotate(period=trunc)
|
||||
.values('period')
|
||||
.annotate(
|
||||
profit=Sum('fee'),
|
||||
apply_amount=Sum(Coalesce(F('shenqing_jine'), F('jine'), Value(Decimal('0.00')))),
|
||||
payout_amount=Sum(Coalesce(F('jine'), Value(Decimal('0.00')))),
|
||||
count=Count('id'),
|
||||
)
|
||||
.order_by('period')
|
||||
)
|
||||
for g in grouped:
|
||||
period = g['period']
|
||||
rows.append({
|
||||
'period': period.isoformat() if period else '',
|
||||
'profit': str((g['profit'] or Decimal('0')).quantize(Decimal('0.01'))),
|
||||
'apply_amount': str((g['apply_amount'] or Decimal('0')).quantize(Decimal('0.01'))),
|
||||
'payout_amount': str((g['payout_amount'] or Decimal('0')).quantize(Decimal('0.01'))),
|
||||
'count': g['count'] or 0,
|
||||
})
|
||||
elif group_by == 'month':
|
||||
trunc = TruncMonth(date_field)
|
||||
grouped = (
|
||||
annotated.annotate(period=trunc)
|
||||
.values('period')
|
||||
.annotate(
|
||||
profit=Sum('fee'),
|
||||
apply_amount=Sum(Coalesce(F('shenqing_jine'), F('jine'), Value(Decimal('0.00')))),
|
||||
payout_amount=Sum(Coalesce(F('jine'), Value(Decimal('0.00')))),
|
||||
count=Count('id'),
|
||||
)
|
||||
.order_by('period')
|
||||
)
|
||||
for g in grouped:
|
||||
period = g['period']
|
||||
rows.append({
|
||||
'period': period.strftime('%Y-%m') if period else '',
|
||||
'profit': str((g['profit'] or Decimal('0')).quantize(Decimal('0.01'))),
|
||||
'apply_amount': str((g['apply_amount'] or Decimal('0')).quantize(Decimal('0.01'))),
|
||||
'payout_amount': str((g['payout_amount'] or Decimal('0')).quantize(Decimal('0.01'))),
|
||||
'count': g['count'] or 0,
|
||||
})
|
||||
elif group_by == 'leixing':
|
||||
grouped = (
|
||||
annotated.values('leixing')
|
||||
.annotate(
|
||||
profit=Sum('fee'),
|
||||
apply_amount=Sum(Coalesce(F('shenqing_jine'), F('jine'), Value(Decimal('0.00')))),
|
||||
payout_amount=Sum(Coalesce(F('jine'), Value(Decimal('0.00')))),
|
||||
count=Count('id'),
|
||||
)
|
||||
.order_by('leixing')
|
||||
)
|
||||
for g in grouped:
|
||||
lx = g['leixing']
|
||||
rows.append({
|
||||
'leixing': lx,
|
||||
'leixing_label': _LEIXING_LABEL.get(lx, f'类型{lx}'),
|
||||
'profit': str((g['profit'] or Decimal('0')).quantize(Decimal('0.01'))),
|
||||
'apply_amount': str((g['apply_amount'] or Decimal('0')).quantize(Decimal('0.01'))),
|
||||
'payout_amount': str((g['payout_amount'] or Decimal('0')).quantize(Decimal('0.01'))),
|
||||
'count': g['count'] or 0,
|
||||
})
|
||||
else:
|
||||
# 明细
|
||||
try:
|
||||
page = max(1, int(request.data.get('page') or 1))
|
||||
page_size = min(200, max(1, int(request.data.get('page_size') or 50)))
|
||||
except (TypeError, ValueError):
|
||||
page, page_size = 1, 50
|
||||
total = annotated.count()
|
||||
items = list(
|
||||
annotated.order_by(f'-{date_field}', '-id')
|
||||
[(page - 1) * page_size: page * page_size]
|
||||
.values(
|
||||
'id', 'yonghuid', 'leixing', 'dakuan_mode',
|
||||
'shenqing_jine', 'jine', 'shouxufei', 'fee',
|
||||
'create_time', 'update_time', 'shenhe_danhao',
|
||||
)
|
||||
)
|
||||
for it in items:
|
||||
fee = it.get('fee') or Decimal('0')
|
||||
rows.append({
|
||||
'id': it['id'],
|
||||
'yonghuid': it['yonghuid'],
|
||||
'leixing': it['leixing'],
|
||||
'leixing_label': _LEIXING_LABEL.get(it['leixing'], ''),
|
||||
'dakuan_mode': it['dakuan_mode'],
|
||||
'dakuan_mode_label': '自动' if it['dakuan_mode'] == 2 else '手动',
|
||||
'shenqing_jine': str(it['shenqing_jine'] or '0.00'),
|
||||
'shijidaozhang': str(it['jine'] or '0.00'),
|
||||
'shouxufei': str(it['shouxufei'] if it['shouxufei'] is not None else fee),
|
||||
'profit': str(Decimal(fee).quantize(Decimal('0.01'))),
|
||||
'shenhe_danhao': it.get('shenhe_danhao') or '',
|
||||
'create_time': it['create_time'].strftime('%Y-%m-%d %H:%M:%S') if it.get('create_time') else '',
|
||||
'update_time': it['update_time'].strftime('%Y-%m-%d %H:%M:%S') if it.get('update_time') else '',
|
||||
})
|
||||
return Response({
|
||||
'code': 0,
|
||||
'data': {
|
||||
'summary': summary,
|
||||
'group_by': group_by,
|
||||
'list': rows,
|
||||
'total': total,
|
||||
'page': page,
|
||||
'page_size': page_size,
|
||||
},
|
||||
})
|
||||
|
||||
# 按身份拆分汇总(便于分账核对,始终附带)
|
||||
by_leixing = []
|
||||
for g in (
|
||||
annotated.values('leixing')
|
||||
.annotate(profit=Sum('fee'), count=Count('id'))
|
||||
.order_by('leixing')
|
||||
):
|
||||
by_leixing.append({
|
||||
'leixing': g['leixing'],
|
||||
'leixing_label': _LEIXING_LABEL.get(g['leixing'], ''),
|
||||
'profit': str((g['profit'] or Decimal('0')).quantize(Decimal('0.01'))),
|
||||
'count': g['count'] or 0,
|
||||
})
|
||||
|
||||
by_mode = []
|
||||
for g in (
|
||||
annotated.values('dakuan_mode')
|
||||
.annotate(profit=Sum('fee'), count=Count('id'))
|
||||
.order_by('dakuan_mode')
|
||||
):
|
||||
by_mode.append({
|
||||
'dakuan_mode': g['dakuan_mode'],
|
||||
'dakuan_mode_label': '自动' if g['dakuan_mode'] == 2 else '手动',
|
||||
'profit': str((g['profit'] or Decimal('0')).quantize(Decimal('0.01'))),
|
||||
'count': g['count'] or 0,
|
||||
})
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'data': {
|
||||
'summary': summary,
|
||||
'group_by': group_by,
|
||||
'rows': rows,
|
||||
'by_leixing': by_leixing,
|
||||
'by_dakuan_mode': by_mode,
|
||||
},
|
||||
})
|
||||
@@ -18,6 +18,7 @@ from .view import GetClubConfigView, GetCrossOrderListView, PartnerGetOrderDataV
|
||||
CaiwuView, CwhybkhqView, HybkjtsjView, SzxxView, CwddhqlxView, CwhqjtddsjView, CwqtczhqView, KhpzhqView, ChzsgcView, \
|
||||
KhgglView, ShgxgsjView, ZxkfghdsView, KptxwztjbView
|
||||
from .dongjie_views import DongjieUserConfigView
|
||||
from .tixian_profit_views import TixianChouchengProfitView
|
||||
from .huashu_views import HuashuListAPIView, HuashuModifyAPIView
|
||||
from .xieyi_views import (
|
||||
XieyiListAPIView, XieyiModifyAPIView, XieyiImageUploadAPIView,
|
||||
@@ -120,6 +121,7 @@ urlpatterns = [
|
||||
path('bkxg', BkxgView.as_view(), name='板块增删改查'), # 板块增删改
|
||||
|
||||
path('caiwu', CaiwuView.as_view(), name='财务页面基础数据'),
|
||||
path('txchouchengtj', TixianChouchengProfitView.as_view(), name='提现抽成利润统计'),
|
||||
|
||||
path('cwhybkhq', CwhybkhqView.as_view(), name='财务会员模块'),
|
||||
path('hybkjtsj', HybkjtsjView.as_view(), name='财务会员统计'),
|
||||
|
||||
@@ -78,7 +78,7 @@ RECONCILE_WAIT_CONFIRM = 'wait_confirm'
|
||||
RECONCILE_ALLOW_NEW = 'allow_new'
|
||||
RECONCILE_PENDING = 'pending_query'
|
||||
|
||||
# 查单失败且本地 zhuangtai=0 超过该分钟数,才允许新建打款记录
|
||||
# 历史兼容:宽限分钟数常量仍保留(对账已改为「有 package 查无单也不新建」)
|
||||
PENDING_QUERY_GRACE_MINUTES = 10
|
||||
|
||||
|
||||
@@ -1280,17 +1280,11 @@ def mark_transfer_success(auto_record, *, wechat_transfer_no=None, with_accounti
|
||||
def _sync_record_from_wx_query(auto_record, wx_data):
|
||||
"""根据微信查单结果修正本地打款记录,返回 (action, payload)"""
|
||||
if wx_data.get('_not_found'):
|
||||
# 从未拿到 package = 未真正进入待确认,查无单应立刻放行换号/重发,
|
||||
# 不要卡 10 分钟宽限期(否则首户日额度失败后重试永远进不了多商户轮换)。
|
||||
# 微信明确无此单。若本地曾有 package,可能查单串户/短暂异常 → 禁止新建第二笔
|
||||
has_package = bool(_parse_package_info(auto_record.wechat_package))
|
||||
age = timezone.now() - auto_record.create_time
|
||||
if (
|
||||
auto_record.zhuangtai == 0
|
||||
and has_package
|
||||
and age < timedelta(minutes=PENDING_QUERY_GRACE_MINUTES)
|
||||
):
|
||||
if has_package:
|
||||
return RECONCILE_PENDING, {
|
||||
'msg': '有收款处理中,请稍后再试',
|
||||
'msg': '本地已有收款凭证但微信暂查无单,请稍后再试,切勿重复点击',
|
||||
'tixian_id': auto_record.tixian_id,
|
||||
}
|
||||
if auto_record.zhuangtai == 0:
|
||||
@@ -1358,12 +1352,11 @@ def _sync_record_from_wx_query(auto_record, wx_data):
|
||||
release_collect_quota_for_record(auto_record)
|
||||
return RECONCILE_ALLOW_NEW, {}
|
||||
|
||||
if auto_record.zhuangtai == 0:
|
||||
return RECONCILE_PENDING, {
|
||||
'msg': f'收款处理中(微信状态:{state or "未知"}),请稍后再试',
|
||||
'tixian_id': auto_record.tixian_id,
|
||||
}
|
||||
return RECONCILE_ALLOW_NEW, {}
|
||||
# 未知状态:宁可卡住,绝不放行新建第二笔
|
||||
return RECONCILE_PENDING, {
|
||||
'msg': f'收款处理中(微信状态:{state or "未知"}),请稍后再试',
|
||||
'tixian_id': auto_record.tixian_id,
|
||||
}
|
||||
|
||||
|
||||
def get_auto_record_for_collect(shenhe_danhao):
|
||||
@@ -1394,10 +1387,11 @@ def _mark_auto_record_failed_local(rec, reason):
|
||||
|
||||
def reconcile_shenhe_wechat_bills(shenhe_danhao):
|
||||
"""
|
||||
P0 防双笔:同一 shenhe_danhao 下所有打款记录逐条问微信
|
||||
P0 防双笔(宁可提不出也不开第二笔):
|
||||
- SUCCESS → completed
|
||||
- 真正待确认(有 package) → wait_confirm(禁止换户)
|
||||
- 其余(无 package / 限额失败 / IP / 查单失败)→ 标失败,ALLOW_NEW 以便换商户
|
||||
- WAIT_USER_CONFIRM(有 package)→ wait_confirm
|
||||
- ACCEPTED / PROCESSING / CANCELING / 查单失败(None) → PENDING,禁止新建
|
||||
- 仅微信明确 FAIL / 查无单(_not_found) → 可 ALLOW_NEW
|
||||
"""
|
||||
records = list(
|
||||
TixianAutoRecord.objects.filter(shenhe_danhao=shenhe_danhao).order_by('-create_time')
|
||||
@@ -1410,7 +1404,6 @@ def reconcile_shenhe_wechat_bills(shenhe_danhao):
|
||||
if not records:
|
||||
return RECONCILE_ALLOW_NEW, {}
|
||||
|
||||
pending_with_package = None
|
||||
for rec in records:
|
||||
if rec.zhuangtai == 1:
|
||||
ok, msg = ensure_shenhe_collect_completed(shenhe_danhao)
|
||||
@@ -1418,64 +1411,48 @@ def reconcile_shenhe_wechat_bills(shenhe_danhao):
|
||||
|
||||
local_pkg = _parse_package_info(rec.wechat_package)
|
||||
|
||||
# 本地已有收款包:必须先对齐数字,禁止为换号新建第二笔待确认
|
||||
if rec.zhuangtai == 0 and local_pkg:
|
||||
# 进行中记录:必须先对齐微信,查不清绝不新建
|
||||
if rec.zhuangtai == 0:
|
||||
wx_data = query_wechat_transfer_bill(rec.tixian_id)
|
||||
if wx_data is None:
|
||||
logger.error(
|
||||
'【防双笔】查单失败禁止新建 shenhe=%s bill=%s mch=%s has_pkg=%s',
|
||||
shenhe_danhao, rec.tixian_id, rec.mch_id, bool(local_pkg),
|
||||
)
|
||||
return RECONCILE_PENDING, {
|
||||
'msg': '有收款处理中,系统正在核对微信状态,请稍后再试',
|
||||
'msg': '有收款处理中,系统正在核对微信状态,请稍后再试,切勿重复点击',
|
||||
'tixian_id': rec.tixian_id,
|
||||
}
|
||||
action, payload = _sync_record_from_wx_query(rec, wx_data)
|
||||
if action in (RECONCILE_COMPLETED, RECONCILE_WAIT_CONFIRM, RECONCILE_PENDING):
|
||||
return action, payload
|
||||
# ALLOW_NEW:本条已明确失败(查无单或 FAIL),继续看其它记录
|
||||
continue
|
||||
|
||||
# 无 package:商户侧基本未进入待确认。限额/IP/卡单都不能挡住换号。
|
||||
if rec.zhuangtai == 0 and not local_pkg:
|
||||
wx_data = query_wechat_transfer_bill(rec.tixian_id)
|
||||
if wx_data is None or wx_data.get('_not_found'):
|
||||
_mark_auto_record_failed_local(
|
||||
rec, '未进入待确认(查无单或查单失败),已换商户重试',
|
||||
)
|
||||
continue
|
||||
action, payload = _sync_record_from_wx_query(rec, wx_data)
|
||||
if action == RECONCILE_COMPLETED:
|
||||
return action, payload
|
||||
if action == RECONCILE_WAIT_CONFIRM:
|
||||
return action, payload
|
||||
# PENDING / ALLOW_NEW:若仍无 package,标失败继续换号
|
||||
if action == RECONCILE_PENDING:
|
||||
pkg = (
|
||||
(payload or {}).get('package_info')
|
||||
if isinstance(payload, dict) else None
|
||||
) or wx_data.get('package_info')
|
||||
if pkg:
|
||||
if pending_with_package is None:
|
||||
pending_with_package = (action, payload)
|
||||
continue
|
||||
_mark_auto_record_failed_local(
|
||||
rec,
|
||||
f'微信不可用状态:{_wx_transfer_state(wx_data) or "未知"},已换商户重试',
|
||||
)
|
||||
continue
|
||||
|
||||
# 已失败记录:仍问微信。若仍待确认必须回旧 package,禁止新建第二笔(防双收)
|
||||
# 本地已失败:微信若仍有效/处理中,禁止新建第二笔
|
||||
if rec.zhuangtai == 2:
|
||||
wx_data = query_wechat_transfer_bill(rec.tixian_id)
|
||||
if wx_data is None or wx_data.get('_not_found'):
|
||||
if wx_data is None:
|
||||
logger.error(
|
||||
'【防双笔】失败单查单不明禁止新建 shenhe=%s bill=%s mch=%s',
|
||||
shenhe_danhao, rec.tixian_id, rec.mch_id,
|
||||
)
|
||||
return RECONCILE_PENDING, {
|
||||
'msg': '有历史收款单状态核对中,请稍后再试,切勿重复点击',
|
||||
'tixian_id': rec.tixian_id,
|
||||
}
|
||||
if wx_data.get('_not_found'):
|
||||
continue
|
||||
action, payload = _sync_record_from_wx_query(rec, wx_data)
|
||||
if action in (RECONCILE_COMPLETED, RECONCILE_WAIT_CONFIRM):
|
||||
if action in (RECONCILE_COMPLETED, RECONCILE_WAIT_CONFIRM, RECONCILE_PENDING):
|
||||
logger.warning(
|
||||
'本地失败单微信仍有效 shenhe=%s bill=%s mch=%s action=%s(禁止新建第二笔)',
|
||||
shenhe_danhao, rec.tixian_id, rec.mch_id, action,
|
||||
)
|
||||
return action, payload
|
||||
# ALLOW_NEW:微信确认终态失败,可继续
|
||||
continue
|
||||
|
||||
if pending_with_package:
|
||||
return pending_with_package
|
||||
return RECONCILE_ALLOW_NEW, {}
|
||||
|
||||
|
||||
|
||||
@@ -421,17 +421,19 @@ def process_audit_collect(request):
|
||||
|
||||
# 选户:绑定用户仅 1 户;未绑定按 priority。
|
||||
# 仅「商户整户日/月额度」+ 查单确认未建单 才静默换号;其它失败立刻回前端。
|
||||
# 跨次仅跳过「整户额度」失败过的商户,个人限额/IP/余额失败不跨次换号。
|
||||
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]
|
||||
failed_quota_mchs = set()
|
||||
for fr, mid in TixianAutoRecord.objects.filter(
|
||||
shenhe_danhao=shenhe_danhao, zhuangtai=2,
|
||||
).exclude(mch_id='').values_list('fail_reason', 'mch_id'):
|
||||
if mid and _is_wx_mch_total_quota_exhausted('', fr or ''):
|
||||
failed_quota_mchs.add(mid)
|
||||
prefer_cfgs = [c for c in mch_cfgs if c.get('MCHID') not in failed_quota_mchs]
|
||||
if prefer_cfgs and len(prefer_cfgs) < len(mch_cfgs):
|
||||
logger.warning(
|
||||
'收款优先未失败商户 shenhe=%s skip=%s try=%s',
|
||||
shenhe_danhao, sorted(failed_mchs),
|
||||
'收款优先未触整户额度商户 shenhe=%s skip=%s try=%s',
|
||||
shenhe_danhao, sorted(failed_quota_mchs),
|
||||
[c.get('MCHID') for c in prefer_cfgs],
|
||||
)
|
||||
mch_cfgs = prefer_cfgs
|
||||
|
||||
Reference in New Issue
Block a user