feat: 商家超时待结算统计与全入口禁派

This commit is contained in:
XingQue committed 2026-08-09 13:41:42 +08:00
1 parent 6e885dd41d
commit 73fd62c142
7 files changed
+293 -39

No files matched your search

+19
View File
@@ -107,6 +107,25 @@ class ShangjiaGenerateLinkView(APIView):
# 打手+管事分成(与普通派单 sjpaifa 同一套算法,打手优先)
club_id = resolve_club_id_from_request(request)
try:
from jituan.services.order_deal import check_merchant_dispatch_block
block = check_merchant_dispatch_block(club_id, user.UserUID)
if not block.get('ok', True):
return Response({
'code': 400,
'msg': block.get('msg') or '存在超时未结算订单,暂不可生成派单链接',
'data': {
'overdue_order_ids': block.get('overdue_order_ids') or [],
'overdue_count': block.get('overdue_count') or 0,
'hours': block.get('hours'),
},
}, status=400)
except Exception:
return Response({
'code': 400,
'msg': '派单前检查失败,请稍后重试',
'data': {},
}, status=400)
dashou_fencheng, guanshi_fencheng = calc_shangjia_order_fencheng(jiage, club_id=club_id)
# 抢单要求:根据模板是否启用佣金
+184 -24
View File
@@ -252,15 +252,183 @@ def _clear_merchant_auto_settle_eligible(club_id: str, merchant_uid: str):
)
def _merchant_status8_qs(club_id: str, merchant_uid: str):
from orders.models import Order
return Order.objects.filter(
ClubID=club_id,
Platform=2,
Status=8,
shangjia_kuozhan__MerchantID=merchant_uid,
)
def _dispatch_block_overdue_q(cutoff):
from django.db.models import Q
# SettlementTime 超时;无 SettlementTime 的脏数据也按超时
return Q(SettlementTime__isnull=True) | Q(SettlementTime__lte=cutoff)
def merchant_overdue_pending_q(club_id: str, merchant_uid: str, cfg=None):
"""
「超时待结算」= 禁派单开关开启且非白名单时,
Status=8 且已超过 dispatch_block_hours(与 check_merchant_dispatch_block 同源)。
不含自动结算:到点会自动结掉,不应再统计进超时待结算。
"""
cid = (club_id or '').strip()
uid = (merchant_uid or '').strip()
if not cid or not uid:
return None, None
cfg = cfg or get_or_create_deal_config(cid)
if not cfg:
return None, None
if not bool(getattr(cfg, 'dispatch_block_enabled', False)):
return None, cfg
if merchant_skips_dispatch_block(cid, uid):
return None, cfg
hours = max(1, min(int(getattr(cfg, 'dispatch_block_hours', None) or 24), 720))
now = _db_datetime(timezone.now())
cutoff = now - timedelta(hours=hours)
return _dispatch_block_overdue_q(cutoff), cfg
def count_merchant_overdue_pending(club_id: str, merchant_uid: str) -> dict:
"""
商家维度「超时待结算」统计(单数+金额,全量不截断)。
与禁派单检查同源;开关关闭时 count/amount 为 0。
"""
from django.db.models import Count, Sum
cid = (club_id or '').strip()
uid = (merchant_uid or '').strip()
empty = {
'overdue_pending_count': 0,
'overdue_pending_amount': '0.00',
'overdue_pending_enabled': False,
'dispatch_block_enabled': False,
'dispatch_block_hours': 24,
'auto_settle_enabled': False,
}
if not cid or not uid:
return empty
overdue_q, cfg = merchant_overdue_pending_q(cid, uid)
if cfg is None:
return empty
db_on = bool(getattr(cfg, 'dispatch_block_enabled', False))
auto_on = bool(getattr(cfg, 'auto_settle_enabled', False))
hours = max(1, min(int(getattr(cfg, 'dispatch_block_hours', None) or 24), 720))
base = {
'overdue_pending_enabled': db_on and not merchant_skips_dispatch_block(cid, uid),
'dispatch_block_enabled': db_on,
'dispatch_block_hours': hours,
'auto_settle_enabled': auto_on,
'overdue_pending_amount': '0.00',
}
if overdue_q is None:
return {**base, 'overdue_pending_count': 0}
row = _merchant_status8_qs(cid, uid).filter(overdue_q).aggregate(
cnt=Count('OrderID'),
amt=Sum('Amount'),
)
amt = row.get('amt')
return {
**base,
'overdue_pending_count': int(row.get('cnt') or 0),
'overdue_pending_amount': str(amt) if amt is not None else '0.00',
}
def apply_overdue_pending_to_ext_qs(qs, data, club_id: str, merchant_uid: str):
"""列表筛 overdue_pending=1:只保留统一超时待结算单。"""
if not data:
return qs
v = data.get('overdue_pending')
wanted = v is True or v == 1 or (
isinstance(v, str) and v.strip().lower() in ('1', 'true', 'yes')
)
if not wanted:
return qs
overdue_q, _cfg = merchant_overdue_pending_q(club_id, merchant_uid)
if overdue_q is None:
return qs.none()
overdue_ids = list(
_merchant_status8_qs(club_id, merchant_uid).filter(overdue_q).values_list('OrderID', flat=True)[:2000]
)
if not overdue_ids:
return qs.none()
return qs.filter(Order__OrderID__in=overdue_ids, Order__Status=8)
def order_settle_timeout_payload(order, club_id: str = '', merchant_uid: str = '') -> dict:
"""
列表/详情:禁派超时倒计时(SettlementTime + dispatch_block_hours)。
自动结算倒计时仍走 order_auto_settle_payload,两套字段可并存。
"""
from backend.utils import fmt_datetime
if not order:
return {}
try:
status = int(getattr(order, 'Status', 0) or 0)
except (TypeError, ValueError):
status = 0
if status != 8:
return {}
cid = (club_id or getattr(order, 'ClubID', None) or '').strip()
mid = (merchant_uid or merchant_uid_of_order(order) or '').strip()
if not cid:
return {}
cfg = get_or_create_deal_config(cid)
if not cfg or not bool(getattr(cfg, 'dispatch_block_enabled', False)):
return {}
if mid and merchant_skips_dispatch_block(cid, mid):
return {}
hours = max(1, min(int(getattr(cfg, 'dispatch_block_hours', None) or 24), 720))
now = _db_datetime(timezone.now())
st = getattr(order, 'SettlementTime', None)
if st is None:
deadline_at = now - timedelta(seconds=1)
else:
deadline_at = _db_datetime(st) + timedelta(hours=hours)
remain = int((deadline_at - now).total_seconds())
overdue = remain <= 0
try:
deadline_ts = int(deadline_at.timestamp())
except Exception:
deadline_ts = 0
tip = (
f'已超时待结算(超{hours}小时未结,禁派单)'
if overdue
else f'超时禁派倒计时(规定{hours}小时内须结算)'
)
return {
'settle_timeout_enabled': True,
'settle_timeout_overdue': overdue,
'settle_timeout_at': fmt_datetime(deadline_at),
'settle_timeout_at_ts': deadline_ts,
'settle_timeout_remain_seconds': max(0, remain),
'settle_timeout_tip': tip,
'settle_timeout_label': '结算超时',
'settle_timeout_hours': hours,
}
def check_merchant_dispatch_block(club_id: str, merchant_uid: str) -> dict:
"""
派单前检查:功能开且非白名单时,该商家任意 Status=8 且结算停留超时 → 禁止派单。
不改订单状态、不打款。关闭开关立即放行。
返回 {ok: True} 或 {ok: False, msg, overdue_order_ids, hours}
返回 {ok: True} 或 {ok: False, msg, overdue_order_ids, hours, overdue_count}
"""
from orders.models import Order
from django.db.models import Q
cid = (club_id or '').strip()
uid = (merchant_uid or '').strip()
if not cid or not uid:
@@ -276,35 +444,27 @@ def check_merchant_dispatch_block(club_id: str, merchant_uid: str) -> dict:
now = _db_datetime(timezone.now())
cutoff = now - timedelta(hours=hours)
# 全部结算中订单:SettlementTime 超时;无 SettlementTime 的脏数据也按超时拦(逼商家处理)
qs = (
Order.objects.filter(
ClubID=cid,
Platform=2,
Status=8,
shangjia_kuozhan__MerchantID=uid,
)
.filter(Q(SettlementTime__isnull=True) | Q(SettlementTime__lte=cutoff))
.order_by('SettlementTime', 'CreateTime')
.values_list('OrderID', flat=True)[:20]
)
overdue_ids = [str(x) for x in qs if x]
if not overdue_ids:
base = _merchant_status8_qs(cid, uid).filter(_dispatch_block_overdue_q(cutoff))
total = base.count()
if total <= 0:
return {'ok': True}
show = overdue_ids[:5]
more = len(overdue_ids) - len(show)
id_text = '、'.join(show) + (f' 等{more}单' if more > 0 else '')
sample_ids = [
str(x) for x in base.order_by('SettlementTime', 'CreateTime').values_list('OrderID', flat=True)[:5]
if x
]
more = total - len(sample_ids)
id_text = '、'.join(sample_ids) + (f' 等{more}单' if more > 0 else '')
msg = (
f'您有结算中订单已超过{hours}小时未处理(如 {id_text}),'
f'您有超时未结算订单已超过{hours}小时未处理(如 {id_text}),'
f'请先完成结算或退款后再派单'
)
return {
'ok': False,
'msg': msg,
'hours': hours,
'overdue_order_ids': overdue_ids,
'overdue_count': len(overdue_ids),
'overdue_order_ids': sample_ids,
'overdue_count': int(total),
}
+17
View File
@@ -102,6 +102,23 @@ def staff_dispatch_order(member, staff_user, data):
if shangjia.zhuangtai != 1:
return False, {'code': 403, 'msg': '商家状态异常', 'data': None}
try:
from jituan.services.order_deal import check_merchant_dispatch_block
club_id = getattr(member, 'club_id', None) or ''
block = check_merchant_dispatch_block(club_id, merchant_id)
if not block.get('ok', True):
return False, {
'code': 400,
'msg': block.get('msg') or '存在超时未结算订单,暂不可派单',
'data': {
'overdue_order_ids': block.get('overdue_order_ids') or [],
'overdue_count': block.get('overdue_count') or 0,
'hours': block.get('hours'),
},
}
except Exception:
return False, {'code': 400, 'msg': '派单前检查失败,请稍后重试', 'data': None}
try:
check_wallet_available(member, jiage)
except ValueError as e:
+17
View File
@@ -84,6 +84,23 @@ def staff_generate_link(member, staff_user, data):
if shangjia.zhuangtai != 1:
return False, {'code': 403, 'msg': '商家状态异常', 'data': {}}
try:
from jituan.services.order_deal import check_merchant_dispatch_block
club_id = getattr(member, 'club_id', None) or ''
block = check_merchant_dispatch_block(club_id, merchant_id)
if not block.get('ok', True):
return False, {
'code': 400,
'msg': block.get('msg') or '存在超时未结算订单,暂不可生成派单链接',
'data': {
'overdue_order_ids': block.get('overdue_order_ids') or [],
'overdue_count': block.get('overdue_count') or 0,
'hours': block.get('hours'),
},
}
except Exception:
return False, {'code': 400, 'msg': '派单前检查失败,请稍后重试', 'data': {}}
try:
shangpin_leixing = ShangpinLeixing.query.get(id=shangpin_type_id, shenhezhuangtai=1)
except ShangpinLeixing.DoesNotExist:
+21 -5
View File
@@ -82,15 +82,21 @@ class StaffOrderListView(APIView):
if k.endswith('_amount'):
order_stats[k] = '0.00'
from jituan.services.club_context import resolve_club_id_from_request
from orders.services.merchant_order_list_enrich import enrich_merchant_order_list_items
from jituan.services.order_deal import (
apply_overdue_pending_to_ext_qs,
count_merchant_overdue_pending,
)
club_id = resolve_club_id_from_request(request)
base_qs = apply_overdue_pending_to_ext_qs(base_qs, data, club_id, member.merchant_id)
total_count = base_qs.count()
start = (page - 1) * page_size
page_qs = list(base_qs.order_by('-Order__CreateTime')[start:start + page_size])
from jituan.services.club_context import resolve_club_id_from_request
from orders.services.merchant_order_list_enrich import enrich_merchant_order_list_items
club_id = resolve_club_id_from_request(request)
order_list = enrich_merchant_order_list_items(page_qs, club_id=club_id)
overdue_stats = count_merchant_overdue_pending(club_id, member.merchant_id)
order_stats.update(overdue_stats)
return Response({
'code': 0,
@@ -104,6 +110,7 @@ class StaffOrderListView(APIView):
'order_stats': order_stats,
'can_view_finance': can_finance,
'view_scope': 'all' if staff_can_view_all_orders(member) else 'self',
**overdue_stats,
},
})
except Exception as e:
@@ -148,12 +155,21 @@ class StaffOrderStatsView(APIView):
if k.endswith('_amount'):
order_stats[k] = '0.00'
from jituan.services.club_context import resolve_club_id_from_request
from jituan.services.order_deal import count_merchant_overdue_pending
overdue_stats = count_merchant_overdue_pending(
resolve_club_id_from_request(request),
member.merchant_id,
)
order_stats.update(overdue_stats)
return Response({
'code': 0,
'msg': '成功',
'data': {
'order': order_stats,
'can_view_finance': can_finance,
**overdue_stats,
},
})
except Exception as e:
@@ -152,6 +152,11 @@ def enrich_merchant_order_list_items(ext_list, club_id=None):
'zhiding_nicheng': zinfo.get('nicheng', ''),
}
item.update(order_auto_settle_payload(o))
try:
from jituan.services.order_deal import order_settle_timeout_payload
item.update(order_settle_timeout_payload(o, club_id=club_id or '', merchant_uid=sj_uid))
except Exception:
pass
result.append(item)
return result
+30 -10
View File
@@ -474,24 +474,31 @@ class ShangjiaDingdanHuoquView(APIView):
# 6–7. 类型 / 关键字 / 时间段 / 客服筛选
base_qs = apply_merchant_order_list_filters(base_qs, filter_payload)
# 8. 待结算统计(全量聚合,非分页求和)
from orders.services.merchant_order_stats import (
build_merchant_order_stats, merchant_ext_qs, filter_by_staff_member,
from jituan.services.club_context import resolve_club_id_from_request
from jituan.services.order_deal import (
apply_overdue_pending_to_ext_qs,
count_merchant_overdue_pending,
)
from orders.services.merchant_order_list_enrich import enrich_merchant_order_list_items
from orders.services.merchant_order_stats import (
build_merchant_order_stats, merchant_ext_qs,
)
club_id = resolve_club_id_from_request(request)
# 超时待结算筛选(统一口径,须在分页前)
base_qs = apply_overdue_pending_to_ext_qs(base_qs, filter_payload, club_id, yonghuid)
# 8. 待结算统计(全量聚合,非分页求和;不受 overdue 列表筛影响)
stats_qs = merchant_ext_qs(yonghuid)
stats_qs = apply_merchant_order_list_filters(stats_qs, filter_payload)
order_stats = build_merchant_order_stats(stats_qs)
overdue_stats = count_merchant_overdue_pending(club_id, yonghuid)
# 9. 排序、分页
total_count = base_qs.count()
start_index = (page - 1) * page_size
end_index = start_index + page_size
current_page_orders = list(base_qs.order_by('-Order__CreateTime')[start_index:end_index])
from jituan.services.club_context import resolve_club_id_from_request
from orders.services.merchant_order_list_enrich import enrich_merchant_order_list_items
club_id = resolve_club_id_from_request(request)
order_list = enrich_merchant_order_list_items(current_page_orders, club_id=club_id)
# 11. 判断是否还有更多
@@ -507,6 +514,7 @@ class ShangjiaDingdanHuoquView(APIView):
'total': total_count,
'pending_count': order_stats['pending_count'],
'pending_amount': order_stats['pending_amount'],
**overdue_stats,
}
}, status=status.HTTP_200_OK)
@@ -543,12 +551,17 @@ class ShangjiaShujuTongjiView(APIView):
data = request.data or {}
from orders.services.merchant_order_list_filters import apply_merchant_order_list_filters
from orders.services.merchant_order_stats import merchant_ext_qs, build_merchant_order_stats
from jituan.services.club_context import resolve_club_id_from_request
from jituan.services.order_deal import count_merchant_overdue_pending
filter_payload = dict(data)
filter_payload['_merchant_id_filter'] = yonghuid
qs = merchant_ext_qs(yonghuid)
qs = apply_merchant_order_list_filters(qs, filter_payload)
order_stats = build_merchant_order_stats(qs)
club_id = resolve_club_id_from_request(request)
overdue_stats = count_merchant_overdue_pending(club_id, yonghuid)
order_stats.update(overdue_stats)
penalty_stats = None
try:
@@ -582,6 +595,7 @@ class ShangjiaShujuTongjiView(APIView):
'order': order_stats,
'penalty': penalty_stats,
'can_view_finance': True,
**overdue_stats,
},
})
@@ -810,10 +824,16 @@ class ShangjiaDingdanXiangqingView(APIView):
response_data['tuikuan_tupian'] = []
try:
from jituan.services.order_deal import order_auto_settle_payload
from jituan.services.order_deal import order_auto_settle_payload, order_settle_timeout_payload
from jituan.services.club_context import resolve_club_id_from_request
response_data.update(order_auto_settle_payload(order))
response_data.update(order_settle_timeout_payload(
order,
club_id=resolve_club_id_from_request(request),
merchant_uid=yonghuid,
))
except Exception as e:
logger.warning('自动结算展示字段附加失败: %s', e)
logger.warning('结算超时/自动结算展示字段附加失败: %s', e)
try:
from orders.services.partial_settle import build_refund_settle_payload