feat: 商家订单准确统计、客服备注与按客服筛选
This commit is contained in:
@@ -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='商家备注'),
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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 '',
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
71
orders/services/merchant_order_stats.py
Normal file
71
orders/services/merchant_order_stats.py
Normal file
@@ -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)
|
||||
@@ -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='商家撤销订单'),
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user