feat: 商家超时待结算统计与全入口禁派
This commit is contained in:
1 parent
6e885dd41d
commit
73fd62c142
7 files changed
+293
-39
No files matched your search
@@ -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
@@ -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),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user