From 1b733d21cc44a8f861541530643f6073a8dbec8c Mon Sep 17 00:00:00 2001 From: XingQue Date: Mon, 27 Jul 2026 02:08:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=AE=A2=E5=8D=95=E5=88=97=E8=A1=A8/?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E8=BF=94=E5=9B=9E=E8=87=AA=E5=8A=A8=E7=BB=93?= =?UTF-8?q?=E7=AE=97=E5=80=92=E8=AE=A1=E6=97=B6=E5=AD=97=E6=AE=B5=EF=BC=8C?= =?UTF-8?q?=E5=B9=B6=E5=8A=A0=E5=B0=8F=E7=A8=8B=E5=BA=8F=E7=BB=93=E7=AE=97?= =?UTF-8?q?=E7=8E=87=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 仅 eligible 结算中订单下发预计时间;支持 auto_settle_pending 筛选;新增 /dingdan/wo-de-deal-stat。 Co-authored-by: Cursor --- jituan/services/deal_stats.py | 45 ++++++++++++++++ jituan/services/order_deal.py | 35 +++++++++++++ jituan/views_order_deal.py | 51 +++++++++++++++++++ orders/services/merchant_order_list_enrich.py | 7 ++- .../services/merchant_order_list_filters.py | 33 ++++++++++++ orders/urls.py | 2 + orders/views/dashou.py | 16 +++++- orders/views/merchant.py | 6 +++ 8 files changed, 192 insertions(+), 3 deletions(-) diff --git a/jituan/services/deal_stats.py b/jituan/services/deal_stats.py index 6b27cb5..579743a 100644 --- a/jituan/services/deal_stats.py +++ b/jituan/services/deal_stats.py @@ -287,3 +287,48 @@ def serialize_dashou_stat(row) -> dict: 'deal_rate': round(c / n, 6) if n else 0, 'fined_rate': round(f / n, 6) if n else 0, } + + +def miniapp_merchant_deal_payload(row) -> dict | None: + """小程序商家订单列表顶:有样本才返回;文案字段供前端标「结算率」。""" + if not row: + return None + n = int(row.order_count or 0) + if n <= 0: + return None + full = serialize_merchant_stat(row) + out = { + 'role': 'merchant', + 'order_count': full['order_count'], + 'deal_rate': full['deal_rate'], + 'fine_rate': full['fine_rate'], + 'deal_rate_text': f"{round(full['deal_rate'] * 100, 1)}%", + 'fine_rate_text': f"{round(full['fine_rate'] * 100, 1)}%", + } + sc = int(full.get('settle_duration_sample_count') or 0) + if sc > 0 and full.get('avg_deal_hours'): + h = float(full['avg_deal_hours']) + out['avg_deal_hours'] = h + if h < 1: + out['avg_deal_hours_text'] = f'{max(1, int(round(h * 60)))}分钟' + else: + out['avg_deal_hours_text'] = f'{round(h, 1)}小时' + return out + + +def miniapp_dashou_deal_payload(row) -> dict | None: + """小程序打手订单列表顶:有样本才返回。""" + if not row: + return None + n = int(row.order_count or 0) + if n <= 0: + return None + full = serialize_dashou_stat(row) + return { + 'role': 'dashou', + 'order_count': full['order_count'], + 'deal_rate': full['deal_rate'], + 'fined_rate': full['fined_rate'], + 'deal_rate_text': f"{round(full['deal_rate'] * 100, 1)}%", + 'fined_rate_text': f"{round(full['fined_rate'] * 100, 1)}%", + } diff --git a/jituan/services/order_deal.py b/jituan/services/order_deal.py index f345e66..12c1e11 100644 --- a/jituan/services/order_deal.py +++ b/jituan/services/order_deal.py @@ -27,6 +27,41 @@ def get_or_create_deal_config(club_id: str): return row +def order_auto_settle_payload(order) -> dict: + """ + 小程序列表/详情展示用。 + 仅 Status=8 且 AutoSettleEligible 且有 AutoExpireAt 时返回字段;否则空 dict。 + 前端有字段才展示,避免关开关/老单误报。 + """ + 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 {} + if not bool(getattr(order, 'AutoSettleEligible', False)): + return {} + expire_at = getattr(order, 'AutoExpireAt', None) + if not expire_at: + return {} + now = timezone.now() + # 与项目 USE_TZ 对齐,避免剩余秒数偏差 + if timezone.is_aware(expire_at) and timezone.is_naive(now): + now = timezone.make_aware(now, timezone.get_current_timezone()) + elif timezone.is_naive(expire_at) and timezone.is_aware(now): + expire_at = timezone.make_aware(expire_at, timezone.get_current_timezone()) + remain = int((expire_at - now).total_seconds()) + return { + 'auto_settle_eligible': True, + 'auto_expire_at': fmt_datetime(expire_at), + 'auto_settle_remain_seconds': max(0, remain), + } + + def config_to_dict(row) -> dict: if not row: return { diff --git a/jituan/views_order_deal.py b/jituan/views_order_deal.py index d828b8b..ffe0889 100644 --- a/jituan/views_order_deal.py +++ b/jituan/views_order_deal.py @@ -114,3 +114,54 @@ class DashouDealStatView(APIView): 'msg': 'ok', 'data': {'list': [stats_svc.serialize_dashou_stat(r) for r in qs]}, }) + + +class MiniappMyDealStatView(APIView): + """ + POST /dingdan/wo-de-deal-stat + 小程序订单列表顶:结算率/罚款率。 + body.role: merchant | dashou(默认按身份推断) + 商家客服看所属商家指标;无样本返回 data=null。 + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + club_id = (resolve_club_id_from_request(request) or '').strip() + if not club_id: + return Response({'code': 400, 'msg': '缺少俱乐部', 'data': None}) + user = request.user + uid = getattr(user, 'UserUID', '') or '' + role = (request.data.get('role') or '').strip().lower() + + if role not in ('merchant', 'dashou'): + # 推断:活跃子客服 → merchant;否则看商家认证 / 打手 + from merchant_ops.services.authz import get_active_staff_member, is_merchant_owner + if get_active_staff_member(user) or is_merchant_owner(user): + role = 'merchant' + else: + role = 'dashou' + + if role == 'merchant': + merchant_uid = uid + from merchant_ops.services.authz import get_active_staff_member, is_merchant_owner, member_has_perm + member = get_active_staff_member(user) + if member and not is_merchant_owner(user): + if not member_has_perm(member, 'order_view_all') and not member_has_perm(member, 'order_view_self'): + return Response({'code': 403, 'msg': '无订单查看权限', 'data': None}) + merchant_uid = member.merchant_id + from jituan.models import MerchantDealStat + row = MerchantDealStat.query.filter(club_id=club_id, merchant_uid=merchant_uid).first() + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': stats_svc.miniapp_merchant_deal_payload(row), + }) + + from jituan.models import DashouDealStat + row = DashouDealStat.query.filter(club_id=club_id, dashou_uid=uid).first() + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': stats_svc.miniapp_dashou_deal_payload(row), + }) diff --git a/orders/services/merchant_order_list_enrich.py b/orders/services/merchant_order_list_enrich.py index fcce274..76d8363 100644 --- a/orders/services/merchant_order_list_enrich.py +++ b/orders/services/merchant_order_list_enrich.py @@ -8,6 +8,7 @@ from rank.models import DingdanBiaoqian, YonghuChenghao from users.business_models import User from users.models import UserShangjia from backend.utils import fmt_datetime +from jituan.services.order_deal import order_auto_settle_payload logger = logging.getLogger(__name__) @@ -126,7 +127,7 @@ def enrich_merchant_order_list_items(ext_list, club_id=None): zid = o.AssignedID or '' zinfo = zhiding_info.get(zid, {}) - result.append({ + item = { 'dingdan_id': oid, 'zhuangtai': o.Status, 'jine': float(o.Amount) if o.Amount else 0.0, @@ -149,6 +150,8 @@ def enrich_merchant_order_list_items(ext_list, club_id=None): 'zhiding_uid': zid, 'zhiding_avatar': zinfo.get('avatar', ''), 'zhiding_nicheng': zinfo.get('nicheng', ''), - }) + } + item.update(order_auto_settle_payload(o)) + result.append(item) return result diff --git a/orders/services/merchant_order_list_filters.py b/orders/services/merchant_order_list_filters.py index e6ad254..a1a88aa 100644 --- a/orders/services/merchant_order_list_filters.py +++ b/orders/services/merchant_order_list_filters.py @@ -47,6 +47,38 @@ def parse_order_list_bound(value, is_end=False): return _db_dt(dt) +def wants_auto_settle_pending(data) -> bool: + """前端筛「待自动结算」:auto_settle_pending = 1/true。""" + if not data: + return False + v = data.get('auto_settle_pending') + if v is True or v == 1: + return True + if isinstance(v, str) and v.strip().lower() in ('1', 'true', 'yes'): + return True + return False + + +def apply_auto_settle_pending_filter(qs, data, *, through_order=True): + """ + 仅 Status=8 且 AutoSettleEligible 且有到期时间。 + through_order=True:MerchantOrderExt(Order__*);False:Order 本表。 + """ + if not wants_auto_settle_pending(data): + return qs + if through_order: + return qs.filter( + Order__Status=8, + Order__AutoSettleEligible=True, + Order__AutoExpireAt__isnull=False, + ) + return qs.filter( + Status=8, + AutoSettleEligible=True, + AutoExpireAt__isnull=False, + ) + + def apply_merchant_order_list_filters(qs, data): """在 MerchantOrderExt 查询集上应用类型/关键字/时间段筛选。""" leixing_id = pick_leixing_id(data) @@ -95,4 +127,5 @@ def apply_merchant_order_list_filters(qs, data): return qs.none() qs = qs.filter(Order__OrderID__in=order_ids) + qs = apply_auto_settle_pending_filter(qs, data, through_order=True) return qs diff --git a/orders/urls.py b/orders/urls.py index 50de523..c653274 100644 --- a/orders/urls.py +++ b/orders/urls.py @@ -13,6 +13,7 @@ from .views import CreateOrderView, WechatPayNotifyView, AlipayPayNotifyView, al LianTongPeiDuiDingDanLieBiaoView, LianTongDingDanZhuangTaiView, LianTongDingDanZhuangTaiPiLiangView, \ ZhiDingChaxunView, ZhiDingHuiFuView from jituan.services.fubei_notify import FubeiPayNotifyView +from jituan.views_order_deal import MiniappMyDealStatView urlpatterns = [ @@ -39,6 +40,7 @@ urlpatterns = [ path('sjchufa', ShangjiaChufaShenqingView.as_view(), name='商家处罚打手申请'), path('ddlxhq', ShangpinLeixingHuoquView.as_view(), name='打手抢单页商品类型获取'), path('dshqdingdan', DashouDingdanHuoquView1.as_view(), name='打手订单列表页面获取'), + path('wo-de-deal-stat', MiniappMyDealStatView.as_view(), name='小程序我的结算率罚款率'), # 获取订单列表 path('ddhq', DashouDingdanHuoquView.as_view(), name='打手抢单页获取订单'), diff --git a/orders/views/dashou.py b/orders/views/dashou.py index e8c18e9..d9476b4 100644 --- a/orders/views/dashou.py +++ b/orders/views/dashou.py @@ -679,6 +679,11 @@ class DashouDingdanHuoquView1(APIView): keyword_query = Q(OrderID__icontains=keyword) | Q(Description__icontains=keyword) query &= keyword_query + from orders.services.merchant_order_list_filters import wants_auto_settle_pending + # 待自动结算筛:强制落在结算中且 eligible + if wants_auto_settle_pending(data): + query &= Q(Status=8, AutoSettleEligible=True, AutoExpireAt__isnull=False) + # 5. 执行查询,按创建时间倒序 dingdan_query = Order.query.filter(query).order_by('-CreateTime') @@ -690,7 +695,9 @@ class DashouDingdanHuoquView1(APIView): 'Description', 'ImageURL', 'Nickname', - 'CreateTime' + 'CreateTime', + 'AutoSettleEligible', + 'AutoExpireAt', ) total_count = dingdan_query.count() @@ -702,6 +709,7 @@ class DashouDingdanHuoquView1(APIView): current_page_count = len(current_page_orders) # 7. 构建返回数据列表 + from jituan.services.order_deal import order_auto_settle_payload order_list = [] for order in current_page_orders: order_data = { @@ -713,6 +721,7 @@ class DashouDingdanHuoquView1(APIView): 'nicheng': order.Nickname or '', **datetime_aliases(order.CreateTime), } + order_data.update(order_auto_settle_payload(order)) order_list.append(order_data) # 8. 判断是否还有更多数据 @@ -1278,6 +1287,11 @@ class DashouDingdanXiangqingView(APIView): 'fuwudashou_id': dingdan.PlayerID or '', 'fadanpingtai': dingdan.Platform or '' } + try: + from jituan.services.order_deal import order_auto_settle_payload + dingdan_data.update(order_auto_settle_payload(dingdan)) + except Exception: + pass # 6. 查询打手提交的图片 dashou_tupian_list = PlayerDeliveryImage.query.filter( diff --git a/orders/views/merchant.py b/orders/views/merchant.py index 7bc7d35..46bdcb1 100644 --- a/orders/views/merchant.py +++ b/orders/views/merchant.py @@ -754,6 +754,12 @@ class ShangjiaDingdanXiangqingView(APIView): 'laoban_shouji': getattr(shangjia_kuozhan, 'BossPhone', '') or '', } + try: + from jituan.services.order_deal import order_auto_settle_payload + response_data.update(order_auto_settle_payload(order)) + except Exception as e: + logger.warning('自动结算展示字段附加失败: %s', e) + try: from merchant_ops.services.order_enrich import enrich_order_detail_data enrich_order_detail_data(response_data, dingdan_id)