diff --git a/config/views/merchant_links.py b/config/views/merchant_links.py index d28d241..cb67980 100644 --- a/config/views/merchant_links.py +++ b/config/views/merchant_links.py @@ -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) # 抢单要求:根据模板是否启用佣金 diff --git a/jituan/services/order_deal.py b/jituan/services/order_deal.py index 0158e30..e20b050 100644 --- a/jituan/services/order_deal.py +++ b/jituan/services/order_deal.py @@ -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), } diff --git a/merchant_ops/services/dispatch.py b/merchant_ops/services/dispatch.py index 75a63eb..6194f76 100644 --- a/merchant_ops/services/dispatch.py +++ b/merchant_ops/services/dispatch.py @@ -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: diff --git a/merchant_ops/services/link.py b/merchant_ops/services/link.py index c7befbf..1821f3b 100644 --- a/merchant_ops/services/link.py +++ b/merchant_ops/services/link.py @@ -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: diff --git a/merchant_ops/views_orders.py b/merchant_ops/views_orders.py index c1d66a1..7160c63 100644 --- a/merchant_ops/views_orders.py +++ b/merchant_ops/views_orders.py @@ -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: diff --git a/orders/services/merchant_order_list_enrich.py b/orders/services/merchant_order_list_enrich.py index 76d8363..d08174c 100644 --- a/orders/services/merchant_order_list_enrich.py +++ b/orders/services/merchant_order_list_enrich.py @@ -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 diff --git a/orders/views/merchant.py b/orders/views/merchant.py index cb1cb11..25e4a71 100644 --- a/orders/views/merchant.py +++ b/orders/views/merchant.py @@ -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