From fdecf2fe3cea42ed22b8e13d6f93e42478cbdc5f Mon Sep 17 00:00:00 2001 From: XingQue Date: Sat, 27 Jun 2026 05:17:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=95=86=E5=AE=B6=E8=AE=A2=E5=8D=95?= =?UTF-8?q?=E5=87=86=E7=A1=AE=E7=BB=9F=E8=AE=A1=E3=80=81=E5=AE=A2=E6=9C=8D?= =?UTF-8?q?=E5=A4=87=E6=B3=A8=E4=B8=8E=E6=8C=89=E5=AE=A2=E6=9C=8D=E7=AD=9B?= =?UTF-8?q?=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...002_merchantstaffmember_merchant_remark.py | 16 ++++ merchant_ops/models.py | 1 + merchant_ops/services/order_enrich.py | 17 +++- merchant_ops/urls.py | 6 +- merchant_ops/views_manage.py | 34 +++++++- merchant_ops/views_orders.py | 67 +++++++++++++- .../services/merchant_order_list_filters.py | 17 ++++ orders/services/merchant_order_stats.py | 71 +++++++++++++++ orders/urls.py | 3 +- orders/views.py | 87 +++++++++++++++++-- 10 files changed, 304 insertions(+), 15 deletions(-) create mode 100644 merchant_ops/migrations/0002_merchantstaffmember_merchant_remark.py create mode 100644 orders/services/merchant_order_stats.py diff --git a/merchant_ops/migrations/0002_merchantstaffmember_merchant_remark.py b/merchant_ops/migrations/0002_merchantstaffmember_merchant_remark.py new file mode 100644 index 0000000..ab8c710 --- /dev/null +++ b/merchant_ops/migrations/0002_merchantstaffmember_merchant_remark.py @@ -0,0 +1,16 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('merchant_ops', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='merchantstaffmember', + name='merchant_remark', + field=models.CharField(blank=True, default='', max_length=64, verbose_name='商家备注'), + ), + ] diff --git a/merchant_ops/models.py b/merchant_ops/models.py index f618c3c..dfe5f40 100644 --- a/merchant_ops/models.py +++ b/merchant_ops/models.py @@ -79,6 +79,7 @@ class MerchantStaffMember(QModel): role = models.ForeignKey(MerchantStaffRole, on_delete=models.PROTECT) status = models.IntegerField(default=1, verbose_name='1正常0停用2已移除') display_name = models.CharField(max_length=64, blank=True, default='') + merchant_remark = models.CharField(max_length=64, blank=True, default='', verbose_name='商家备注') invited_by_code = models.ForeignKey( MerchantStaffInvite, null=True, blank=True, on_delete=models.SET_NULL ) diff --git a/merchant_ops/services/order_enrich.py b/merchant_ops/services/order_enrich.py index fc9c63c..14008ae 100644 --- a/merchant_ops/services/order_enrich.py +++ b/merchant_ops/services/order_enrich.py @@ -23,6 +23,21 @@ def get_user_avatar_by_uid(uid): return user_avatar_relative(u) +def staff_display_label(member): + """商家侧展示名:商家备注 > 显示名 > 角色名 > UID""" + if not member: + return '' + remark = (getattr(member, 'merchant_remark', None) or '').strip() + if remark: + return remark + name = (member.display_name or '').strip() + if name: + return name + if member.role: + return member.role.role_name or member.staff_user_id + return member.staff_user_id + + def get_dispatch_staff_payload(order_id): """订单派单归属;无记录则 None(商家老板派单可能无 dispatch 记录)。""" if not order_id: @@ -48,7 +63,7 @@ def get_dispatch_staff_payload(order_id): return { 'dispatch_staff_type': ACTOR_STAFF, 'dispatch_staff_uid': m.staff_user_id, - 'dispatch_staff_name': m.display_name or (m.role.role_name if m.role else m.staff_user_id), + 'dispatch_staff_name': staff_display_label(m), 'dispatch_staff_avatar': user_avatar_relative(u), 'dispatch_role_code': m.role.role_code if m.role else '', 'dispatch_role_name': m.role.role_name if m.role else '', diff --git a/merchant_ops/urls.py b/merchant_ops/urls.py index c73fbca..44a111b 100644 --- a/merchant_ops/urls.py +++ b/merchant_ops/urls.py @@ -2,7 +2,7 @@ from django.urls import path from merchant_ops.views_manage import ( StaffInviteCreateView, StaffInviteListView, StaffInviteRevokeView, - StaffMemberListView, StaffMemberUpdateRoleView, StaffMemberRemoveView, + StaffMemberListView, StaffMemberUpdateRoleView, StaffMemberRemarkUpdateView, StaffMemberRemoveView, StaffMemberDisableView, StaffMemberUpdatePermsView, StaffRoleListView, StaffRoleCreateView, StaffRoleSetPermsView, StaffWalletAllocateView, StaffWalletRevokeView, StaffWalletLedgerView, StaffAuditListView, @@ -10,7 +10,7 @@ from merchant_ops.views_manage import ( ) from merchant_ops.views_staff import StaffBindView, StaffMeView from merchant_ops.views_orders import ( - StaffOrderListView, StaffOrderDetailView, StaffDispatchView, + StaffOrderListView, StaffOrderDetailView, StaffOrderStatsView, StaffDispatchView, StaffProductTypesView, StaffSettleView, StaffCancelView, StaffRefundView, StaffPenaltyApplyView, StaffPenaltyManageView, StaffChangePlayerView, ) @@ -28,6 +28,7 @@ urlpatterns = [ path('invite/list', StaffInviteListView.as_view()), path('invite/revoke', StaffInviteRevokeView.as_view()), path('member/list', StaffMemberListView.as_view()), + path('member/update-remark', StaffMemberRemarkUpdateView.as_view()), path('member/update-role', StaffMemberUpdateRoleView.as_view()), path('member/update-perms', StaffMemberUpdatePermsView.as_view()), path('member/remove', StaffMemberRemoveView.as_view()), @@ -42,6 +43,7 @@ urlpatterns = [ path('rank', StaffRankView.as_view()), # 子客服订单(全新接口) path('order/list', StaffOrderListView.as_view()), + path('order/stats', StaffOrderStatsView.as_view()), path('order/detail', StaffOrderDetailView.as_view()), path('order/dispatch', StaffDispatchView.as_view()), path('order/product-types', StaffProductTypesView.as_view()), diff --git a/merchant_ops/views_manage.py b/merchant_ops/views_manage.py index 4a41239..8e3c244 100644 --- a/merchant_ops/views_manage.py +++ b/merchant_ops/views_manage.py @@ -26,6 +26,7 @@ from merchant_ops.services.authz import ( is_merchant_owner, resolve_staff_context, get_active_staff_member, member_has_perm as _member_has_perm, ) +from merchant_ops.services.order_enrich import staff_display_label, user_avatar_relative from merchant_ops.services.audit import audit_owner from merchant_ops.services.bootstrap import ensure_merchant_roles from merchant_ops.services.wallet import allocate_quota, get_or_create_wallet, revoke_quota @@ -173,7 +174,6 @@ class StaffMemberListView(APIView): try: from datetime import date from users.business_models import User - from merchant_ops.services.order_enrich import user_avatar_relative mid, _ = _resolve_manage_context(request, need_staff_manage=True) today = date.today() @@ -199,7 +199,9 @@ class StaffMemberListView(APIView): result.append({ 'id': m.id, 'uid': m.staff_user_id, - 'nickname': m.display_name or m.staff_user_id, + 'nickname': staff_display_label(m), + 'merchant_remark': m.merchant_remark or '', + 'display_name': m.display_name or '', 'avatar': user_avatar_relative(u), 'role_name': m.role.role_name if m.role else '', 'isChief': m.role.role_code == 'CHIEF' if m.role else False, @@ -225,6 +227,34 @@ class StaffMemberListView(APIView): return _err(e) +class StaffMemberRemarkUpdateView(APIView): + """商家老板/总管:设置客服备注""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + mid, operator = _resolve_manage_context(request, need_staff_manage=True) + member_id = request.data.get('member_id') + remark = str(request.data.get('merchant_remark', '')).strip()[:64] + if not member_id: + return Response({'code': 400, 'msg': '缺少 member_id', 'data': None}) + m = MerchantStaffMember.query.get(id=member_id, merchant_id=mid) + m.merchant_remark = remark + m.save(update_fields=['merchant_remark']) + audit_owner(mid, request.user.UserUID, 'MEMBER_UPDATE_REMARK', request, + resource_type='member', resource_id=member_id, + value_after={'merchant_remark': remark}) + return Response({ + 'code': 0, + 'msg': '备注已保存', + 'data': {'member_id': m.id, 'merchant_remark': remark, 'nickname': staff_display_label(m)}, + }) + except MerchantStaffMember.DoesNotExist: + return Response({'code': 404, 'msg': '客服不存在', 'data': None}) + except Exception as e: + return _err(e) + + class StaffMemberUpdateRoleView(APIView): permission_classes = [IsAuthenticated] diff --git a/merchant_ops/views_orders.py b/merchant_ops/views_orders.py index d14bdee..c1d66a1 100644 --- a/merchant_ops/views_orders.py +++ b/merchant_ops/views_orders.py @@ -19,6 +19,7 @@ from merchant_ops.services.order_query import ( from merchant_ops.services.audit import audit_staff from merchant_ops.services.dispatch import staff_dispatch_order from merchant_ops.services.stats import bump_cancel_count, bump_penalty_count +from merchant_ops.models import MerchantOrderDispatch from orders.models import MerchantOrderExt from orders.views import ( ShangjiaDingdanXiangqingView, ShangjiaJiesuanView, ShangjiaChexiaoView, @@ -69,6 +70,17 @@ class StaffOrderListView(APIView): pending_qs = staff_order_ext_qs(member, [8]) pending_count = pending_qs.count() + from orders.services.merchant_order_stats import build_merchant_order_stats + + stats_qs = staff_order_ext_qs(member, None) + stats_qs = staff_apply_order_filters(stats_qs, data) + order_stats = build_merchant_order_stats(stats_qs) + + can_finance = member_has_perm(member, 'finance_view') + if not can_finance: + for k in list(order_stats.keys()): + if k.endswith('_amount'): + order_stats[k] = '0.00' total_count = base_qs.count() start = (page - 1) * page_size @@ -87,7 +99,10 @@ class StaffOrderListView(APIView): 'list': order_list, 'has_more': start + len(page_qs) < total_count, 'total': total_count, - 'pending_count': pending_count, + 'pending_count': order_stats['pending_count'], + 'pending_amount': order_stats['pending_amount'] if can_finance else '0.00', + 'order_stats': order_stats, + 'can_view_finance': can_finance, 'view_scope': 'all' if staff_can_view_all_orders(member) else 'self', }, }) @@ -95,6 +110,56 @@ class StaffOrderListView(APIView): return _err(e) +class StaffOrderStatsView(APIView): + """子客服/老板视角订单统计 POST /yonghu/merchant-staff/order/stats""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + member = _require_staff(request) + if not member_has_perm(member, 'order_view_all') and not member_has_perm(member, 'order_view_self'): + raise StaffAuthError(403, '无订单查看权限') + + data = request.data or {} + from orders.services.merchant_order_stats import build_merchant_order_stats, filter_by_staff_member + from orders.models import MerchantOrderExt + + filter_payload = dict(data) + filter_payload['_merchant_id_filter'] = member.merchant_id + + qs = MerchantOrderExt.query.filter( + MerchantID=member.merchant_id, + ).select_related('Order') + + if not staff_can_view_all_orders(member): + own_ids = list( + MerchantOrderDispatch.query.filter(staff_member_id=member.id).values_list('order_id', flat=True) + ) + qs = qs.filter(Order__OrderID__in=own_ids) + elif data.get('staff_member_id'): + qs = filter_by_staff_member(qs, member.merchant_id, data.get('staff_member_id')) + + qs = staff_apply_order_filters(qs, filter_payload) + order_stats = build_merchant_order_stats(qs) + + can_finance = member_has_perm(member, 'finance_view') + if not can_finance: + for k in list(order_stats.keys()): + if k.endswith('_amount'): + order_stats[k] = '0.00' + + return Response({ + 'code': 0, + 'msg': '成功', + 'data': { + 'order': order_stats, + 'can_view_finance': can_finance, + }, + }) + except Exception as e: + return _err(e) + + class StaffOrderDetailView(APIView): permission_classes = [IsAuthenticated] diff --git a/orders/services/merchant_order_list_filters.py b/orders/services/merchant_order_list_filters.py index fb93298..e6ad254 100644 --- a/orders/services/merchant_order_list_filters.py +++ b/orders/services/merchant_order_list_filters.py @@ -78,4 +78,21 @@ def apply_merchant_order_list_filters(qs, data): | Q(Order__PlayerID__exact=keyword) | Q(Order__ExternalOrderID__exact=keyword) ) + + staff_member_id = data.get('staff_member_id') + if staff_member_id: + from merchant_ops.models import MerchantOrderDispatch + try: + smid = int(staff_member_id) + except (TypeError, ValueError): + return qs.none() + merchant_id = data.get('_merchant_id_filter') + disp_q = MerchantOrderDispatch.objects.filter(staff_member_id=smid) + if merchant_id: + disp_q = disp_q.filter(merchant_id=merchant_id) + order_ids = list(disp_q.values_list('order_id', flat=True)) + if not order_ids: + return qs.none() + qs = qs.filter(Order__OrderID__in=order_ids) + return qs diff --git a/orders/services/merchant_order_stats.py b/orders/services/merchant_order_stats.py new file mode 100644 index 0000000..fc3542a --- /dev/null +++ b/orders/services/merchant_order_stats.py @@ -0,0 +1,71 @@ +"""商家订单维度统计(从订单表聚合,保证金额准确)。""" +from decimal import Decimal + +from django.db.models import Count, Sum + +from merchant_ops.models import MerchantOrderDispatch +from orders.models import MerchantOrderExt + +STATUS_PENDING = 8 +STATUS_COMPLETED = 3 +STATUS_REFUNDED = 5 + + +def _money_str(val): + if val is None: + return '0.00' + return str(val) + + +def _agg_amount(qs, status=None): + if status is not None: + qs = qs.filter(Order__Status=status) + row = qs.aggregate(cnt=Count('id'), amt=Sum('Order__Amount')) + return int(row['cnt'] or 0), _money_str(row['amt']) + + +def build_merchant_order_stats(base_qs): + """ + base_qs: MerchantOrderExt queryset(已含商家/客服范围与时间等筛选) + 返回订单统计字典。 + """ + pending_cnt, pending_amt = _agg_amount(base_qs, STATUS_PENDING) + completed_cnt, completed_amt = _agg_amount(base_qs, STATUS_COMPLETED) + refund_cnt, refund_amt = _agg_amount(base_qs, STATUS_REFUNDED) + dispatch_cnt, dispatch_amt = _agg_amount(base_qs, None) + + return { + 'pending_count': pending_cnt, + 'pending_amount': pending_amt, + 'completed_count': completed_cnt, + 'completed_amount': completed_amt, + 'refund_count': refund_cnt, + 'refund_amount': refund_amt, + 'dispatch_count': dispatch_cnt, + 'dispatch_amount': dispatch_amt, + } + + +def merchant_ext_qs(merchant_id, zhuangtai_list=None): + qs = MerchantOrderExt.query.filter(MerchantID=merchant_id).select_related('Order') + if zhuangtai_list is not None: + qs = qs.filter(Order__Status__in=zhuangtai_list) + return qs + + +def filter_by_staff_member(qs, merchant_id, staff_member_id): + if not staff_member_id: + return qs + try: + mid = int(staff_member_id) + except (TypeError, ValueError): + return qs.none() + order_ids = list( + MerchantOrderDispatch.query.filter( + merchant_id=merchant_id, + staff_member_id=mid, + ).values_list('order_id', flat=True) + ) + if not order_ids: + return qs.none() + return qs.filter(Order__OrderID__in=order_ids) diff --git a/orders/urls.py b/orders/urls.py index 8122260..f315c21 100644 --- a/orders/urls.py +++ b/orders/urls.py @@ -9,7 +9,7 @@ from .views import CreateOrderView, WechatPayNotifyView, \ AdTongYiTuiKuanPingTai, AdTongYiTuiKuanShangJia, AdQiangZhiJieDan, AdJuJueTuiKuan,\ AdGengHuanDaShou,ShangjiaCOSZhengshuView, DashouHuoquLeixingView,AdJuJueJieSuan, AdZhuanYiDaTing, AdQuXiaoZhiDing,DashouXiugaiView,DaiLiQunLiaoXiaoXiView,\ ShangjiaLianjieLiuYanZhuanFaView, DingdanXiangqingView2, JiedanView2, ZxsjghdsView, \ - ShangjiaFakuanApplyView, ShangjiaFakuaiXiugaiView + ShangjiaFakuanApplyView, ShangjiaFakuaiXiugaiView, ShangjiaShujuTongjiView urlpatterns = [ @@ -24,6 +24,7 @@ urlpatterns = [ path('sjspleixing', ShangpinLeixingView.as_view(), name='派单获取商品类型'), path('sjpaifa', ShangjiaPaifaView.as_view(), name='商家发单'), path('sjdingdanhq', ShangjiaDingdanHuoquView.as_view(), name='商家订单获取'), + path('sjshujutj', ShangjiaShujuTongjiView.as_view(), name='商家经营统计'), path('sjddxq', ShangjiaDingdanXiangqingView.as_view(), name='商家订单详情获取'), path('sjjiesuan', ShangjiaJiesuanView.as_view(), name='商家结单'), path('sjchexiao', ShangjiaChexiaoView.as_view(), name='商家撤销订单'), diff --git a/orders/views.py b/orders/views.py index d370792..b37ccf0 100644 --- a/orders/views.py +++ b/orders/views.py @@ -2082,17 +2082,22 @@ class ShangjiaDingdanHuoquView(APIView): MerchantID=yonghuid ).select_related('Order') + filter_payload = dict(data) + filter_payload['_merchant_id_filter'] = yonghuid + # 5. 状态过滤 base_qs = base_qs.filter(Order__Status__in=zhuangtai_list) - # 6–7. 类型 / 关键字 / 时间段 - base_qs = apply_merchant_order_list_filters(base_qs, data) + # 6–7. 类型 / 关键字 / 时间段 / 客服筛选 + base_qs = apply_merchant_order_list_filters(base_qs, filter_payload) - # 8. 计算待结算订单数量(状态为8) - pending_count = MerchantOrderExt.query.filter( - MerchantID=yonghuid, - Order__Status=8 - ).count() + # 8. 待结算统计(全量聚合,非分页求和) + from orders.services.merchant_order_stats import ( + build_merchant_order_stats, merchant_ext_qs, filter_by_staff_member, + ) + 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) # 9. 排序、分页 total_count = base_qs.count() @@ -2117,7 +2122,8 @@ class ShangjiaDingdanHuoquView(APIView): 'list': order_list, 'has_more': has_more, 'total': total_count, - 'pending_count': pending_count + 'pending_count': order_stats['pending_count'], + 'pending_amount': order_stats['pending_amount'], } }, status=status.HTTP_200_OK) @@ -2130,6 +2136,71 @@ class ShangjiaDingdanHuoquView(APIView): }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) +class ShangjiaShujuTongjiView(APIView): + """ + 商家经营数据统计(订单表聚合) + POST /dingdan/sjshujutj + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + current_user = request.user + yonghuid = current_user.UserUID + except AttributeError: + return Response({'code': 1, 'msg': '用户认证失败', 'data': None}, status=401) + + try: + shangjia = current_user.ShopProfile + if shangjia.zhuangtai != 1: + return Response({'code': 403, 'msg': '商家状态异常', 'data': None}, status=403) + except Exception: + return Response({'code': 403, 'msg': '非商家用户', 'data': None}, status=403) + + 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 + + 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) + + penalty_stats = None + try: + from orders.models import Penalty, PenaltyRecord + from django.db.models import Q, Sum + fakuan_pending = Penalty.query.filter(ApplicantID=yonghuid, Status=1).count() + fakuan_pending_amt = Penalty.query.filter(ApplicantID=yonghuid, Status=1).aggregate( + amt=Sum('FineAmount') + )['amt'] + jifen_pending = PenaltyRecord.query.filter( + ApplicantID=yonghuid, + ).filter(Q(ApplyStatus=0) | Q(ApplyStatus=3)).count() + penalty_stats = { + 'fakuan_pending': fakuan_pending, + 'fakuan_pending_amount': str(fakuan_pending_amt or '0.00'), + 'jifen_pending': jifen_pending, + } + except Exception: + penalty_stats = { + 'fakuan_pending': 0, + 'fakuan_pending_amount': '0.00', + 'jifen_pending': 0, + } + + return Response({ + 'code': 0, + 'msg': '成功', + 'data': { + 'order': order_stats, + 'penalty': penalty_stats, + 'can_view_finance': True, + }, + }) + + class ShangjiaDingdanXiangqingView(APIView):