- 划额度扣商家余额,客服派单/链接只扣个人额度 - 新增 wallet/revoke 收回未使用额度,权限 wallet_revoke - 客服额度单撤销/退款只释放额度,不再误退商家余额 - 链接待填单(13)可撤销;总管/财务可划收额度
588 lines
26 KiB
Python
588 lines
26 KiB
Python
"""商家老板:邀请码、成员、角色、额度、日志、排行。"""
|
|
import logging
|
|
import secrets
|
|
import string
|
|
from datetime import timedelta
|
|
from decimal import Decimal
|
|
|
|
from django.db.models import Sum, Q
|
|
from django.utils import timezone
|
|
from rest_framework.permissions import IsAuthenticated
|
|
from rest_framework.response import Response
|
|
from rest_framework.views import APIView
|
|
|
|
from merchant_ops.constants import (
|
|
INVITE_STATUS_REVOKED, INVITE_STATUS_UNUSED, INVITE_STATUS_USED,
|
|
MEMBER_STATUS_ACTIVE, MEMBER_STATUS_DISABLED, MEMBER_STATUS_REMOVED,
|
|
)
|
|
from merchant_ops.models import (
|
|
MerchantStaffInvite, MerchantStaffMember, MerchantStaffRole,
|
|
MerchantStaffRolePermission, MerchantStaffMemberPermission,
|
|
MerchantStaffWallet, MerchantStaffWalletLedger, MerchantStaffAuditLog,
|
|
MerchantStaffDailyStats, MerchantStaffPermission,
|
|
)
|
|
from merchant_ops.services.authz import (
|
|
StaffAuthError, ensure_owner_merchant, get_member_permissions,
|
|
is_merchant_owner, resolve_staff_context,
|
|
get_active_staff_member, member_has_perm as _member_has_perm,
|
|
)
|
|
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
|
|
from users.models import UserShangjia
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _err(e):
|
|
if isinstance(e, StaffAuthError):
|
|
return Response({'code': e.code, 'msg': e.msg, 'data': None})
|
|
logger.exception(e)
|
|
return Response({'code': 500, 'msg': '服务器错误', 'data': None})
|
|
|
|
|
|
def _role_prefix(role_code):
|
|
m = {'CHIEF': 'ZG', 'FINANCE': 'CW', 'DISPATCHER': 'PD'}
|
|
return m.get(role_code, 'KF')
|
|
|
|
|
|
def _role_hint(role_code, tpl_desc=None):
|
|
hints = {
|
|
'CHIEF': '总管:除老板专属外,订单/派单/客服/额度/罚单等均可操作',
|
|
'FINANCE': '财务:查看余额与额度、划额度、看全部订单与统计,不可派单结单',
|
|
'DISPATCHER': '派单员:派单发单、只看自己派的单,不可结单退款',
|
|
}
|
|
if role_code in hints:
|
|
return hints[role_code]
|
|
name = (tpl_desc or {}).get(role_code) or role_code
|
|
return f'自定义角色:{name}'
|
|
|
|
|
|
def _resolve_manage_context(request, need_staff_manage=False):
|
|
"""商家老板,或具备 staff_manage 的总管等子客服。"""
|
|
if is_merchant_owner(request.user):
|
|
ensure_owner_merchant(request.user.UserUID, request.user)
|
|
return request.user.UserUID, None
|
|
member = get_active_staff_member(request.user)
|
|
if not member:
|
|
raise StaffAuthError(403, '无权限')
|
|
if need_staff_manage and not _member_has_perm(member, 'staff_manage'):
|
|
raise StaffAuthError(403, '无客服管理权限')
|
|
ensure_merchant_roles(member.merchant_id)
|
|
return member.merchant_id, member
|
|
|
|
|
|
def _resolve_wallet_operator(request, perm_code):
|
|
"""商家老板,或具备 wallet_allocate / wallet_revoke 的子客服(总管/财务)。"""
|
|
if is_merchant_owner(request.user):
|
|
ensure_owner_merchant(request.user.UserUID, request.user)
|
|
return request.user.UserUID, None
|
|
member = get_active_staff_member(request.user)
|
|
if not member or not _member_has_perm(member, perm_code):
|
|
raise StaffAuthError(403, f'无{perm_code}权限')
|
|
ensure_merchant_roles(member.merchant_id)
|
|
return member.merchant_id, member
|
|
|
|
|
|
class StaffInviteCreateView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
try:
|
|
ensure_owner_merchant(request.user.UserUID, request.user)
|
|
role_id = request.data.get('role_id')
|
|
if not role_id:
|
|
return Response({'code': 400, 'msg': '请选择角色', 'data': None})
|
|
role = MerchantStaffRole.query.get(id=role_id, merchant_id=request.user.UserUID)
|
|
prefix = _role_prefix(role.role_code)
|
|
code = f"{prefix}-{secrets.token_hex(3).upper()}"
|
|
invite = MerchantStaffInvite.objects.create(
|
|
merchant_id=request.user.UserUID,
|
|
invite_code=code,
|
|
role=role,
|
|
status=INVITE_STATUS_UNUSED,
|
|
created_by=request.user.UserUID,
|
|
expire_at=timezone.now() + timedelta(days=30),
|
|
)
|
|
audit_owner(request.user.UserUID, request.user.UserUID, 'INVITE_CREATE', request,
|
|
resource_type='invite', resource_id=code)
|
|
return Response({'code': 0, 'msg': '成功', 'data': {'invite_code': code, 'id': invite.id}})
|
|
except Exception as e:
|
|
return _err(e)
|
|
|
|
|
|
class StaffInviteListView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
try:
|
|
ensure_owner_merchant(request.user.UserUID, request.user)
|
|
status_filter = request.data.get('status') # None=all, 0,1,2
|
|
qs = MerchantStaffInvite.query.filter(merchant_id=request.user.UserUID).select_related('role')
|
|
if status_filter is not None:
|
|
qs = qs.filter(status=int(status_filter))
|
|
items = []
|
|
for inv in qs.order_by('-CreateTime')[:200]:
|
|
item = {
|
|
'id': inv.id,
|
|
'code': inv.invite_code,
|
|
'typeName': inv.role.role_name if inv.role else '',
|
|
'status': inv.status,
|
|
'adddate': inv.CreateTime.strftime('%Y-%m-%d') if inv.CreateTime else '',
|
|
'usedate': inv.used_at.strftime('%Y-%m-%d') if inv.used_at else '',
|
|
'user': None,
|
|
}
|
|
if inv.used_by:
|
|
from users.business_models import User
|
|
u = User.query.filter(UserUID=inv.used_by).first()
|
|
item['user'] = {'nickname': getattr(u, 'Nickname', '') or inv.used_by, 'uid': inv.used_by}
|
|
items.append(item)
|
|
return Response({'code': 0, 'msg': '成功', 'data': {'list': items, 'gudingCode': 'XQKF2026'}})
|
|
except Exception as e:
|
|
return _err(e)
|
|
|
|
|
|
class StaffInviteRevokeView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
try:
|
|
ensure_owner_merchant(request.user.UserUID, request.user)
|
|
invite_id = request.data.get('id')
|
|
inv = MerchantStaffInvite.query.get(id=invite_id, merchant_id=request.user.UserUID, status=INVITE_STATUS_UNUSED)
|
|
inv.status = INVITE_STATUS_REVOKED
|
|
inv.save(update_fields=['status'])
|
|
audit_owner(request.user.UserUID, request.user.UserUID, 'INVITE_REVOKE', request, resource_id=inv.invite_code)
|
|
return Response({'code': 0, 'msg': '已作废', 'data': None})
|
|
except MerchantStaffInvite.DoesNotExist:
|
|
return Response({'code': 404, 'msg': '邀请码不存在或已使用', 'data': None})
|
|
except Exception as e:
|
|
return _err(e)
|
|
|
|
|
|
class StaffMemberListView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
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()
|
|
members = MerchantStaffMember.query.filter(
|
|
merchant_id=mid,
|
|
status__in=[MEMBER_STATUS_ACTIVE, MEMBER_STATUS_DISABLED],
|
|
).select_related('role')
|
|
result = []
|
|
for m in members:
|
|
total_stats = MerchantStaffDailyStats.query.filter(member_id=m.id).aggregate(
|
|
dispatch_count=Sum('dispatch_count'),
|
|
dispatch_amount=Sum('dispatch_amount'),
|
|
settle_count=Sum('settle_count'),
|
|
settle_amount=Sum('settle_amount'),
|
|
refund_count=Sum('refund_count'),
|
|
refund_amount=Sum('refund_amount'),
|
|
)
|
|
today_stat = MerchantStaffDailyStats.query.filter(
|
|
member_id=m.id, stat_date=today,
|
|
).first()
|
|
wallet = get_or_create_wallet(m)
|
|
u = User.query.filter(UserUID=m.staff_user_id).first()
|
|
result.append({
|
|
'id': m.id,
|
|
'uid': m.staff_user_id,
|
|
'nickname': m.display_name or m.staff_user_id,
|
|
'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,
|
|
'isFinance': m.role.role_code == 'FINANCE' if m.role else False,
|
|
'status': m.status,
|
|
'isDisabled': m.status == MEMBER_STATUS_DISABLED,
|
|
'todayDispatchCount': today_stat.dispatch_count if today_stat else int(m.today_dispatch_count or 0),
|
|
'todayDispatchAmount': str(today_stat.dispatch_amount if today_stat else (m.today_dispatch_amount or '0.00')),
|
|
'dispatchCount': total_stats['dispatch_count'] or 0,
|
|
'dispatchAmount': str(total_stats['dispatch_amount'] or '0.00'),
|
|
'orderCount': total_stats['dispatch_count'] or 0,
|
|
'settleCount': total_stats['settle_count'] or 0,
|
|
'settleAmount': str(total_stats['settle_amount'] or '0.00'),
|
|
'refundCount': total_stats['refund_count'] or 0,
|
|
'refundAmount': str(total_stats['refund_amount'] or '0.00'),
|
|
'quota_available': str(wallet.quota_available),
|
|
'quota_total': str(wallet.quota_total),
|
|
'quota_used': str(wallet.quota_used),
|
|
})
|
|
return Response({'code': 0, 'msg': '成功', 'data': {'list': result}})
|
|
except Exception as e:
|
|
return _err(e)
|
|
|
|
|
|
class StaffMemberUpdateRoleView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
try:
|
|
mid, _ = _resolve_manage_context(request, need_staff_manage=True)
|
|
member_id = request.data.get('member_id')
|
|
role_id = request.data.get('role_id')
|
|
m = MerchantStaffMember.query.get(
|
|
id=member_id, merchant_id=mid, status=MEMBER_STATUS_ACTIVE,
|
|
)
|
|
role = MerchantStaffRole.query.get(id=role_id, merchant_id=mid, status=1)
|
|
old = m.role_id
|
|
m.role = role
|
|
m.save(update_fields=['role'])
|
|
audit_owner(mid, request.user.UserUID, 'MEMBER_UPDATE_ROLE', request,
|
|
resource_type='member', resource_id=member_id,
|
|
value_before={'role_id': old}, value_after={'role_id': role_id})
|
|
perms = list(role.role_permissions.values_list('perm_code', flat=True))
|
|
return Response({'code': 0, 'msg': '角色已更新', 'data': {
|
|
'member_id': m.id,
|
|
'role_id': role.id,
|
|
'role_code': role.role_code,
|
|
'role_name': role.role_name,
|
|
'permissions': perms,
|
|
}})
|
|
except Exception as e:
|
|
return _err(e)
|
|
|
|
|
|
class StaffMemberRemoveView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
try:
|
|
ensure_owner_merchant(request.user.UserUID, request.user)
|
|
member_id = request.data.get('member_id')
|
|
m = MerchantStaffMember.query.get(id=member_id, merchant_id=request.user.UserUID)
|
|
m.status = MEMBER_STATUS_REMOVED
|
|
m.revoked_at = timezone.now()
|
|
m.save(update_fields=['status', 'revoked_at'])
|
|
audit_owner(request.user.UserUID, request.user.UserUID, 'MEMBER_REMOVE', request,
|
|
resource_type='member', resource_id=member_id)
|
|
return Response({'code': 0, 'msg': '已移除', 'data': None})
|
|
except Exception as e:
|
|
return _err(e)
|
|
|
|
|
|
class StaffMemberDisableView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
try:
|
|
mid, _ = _resolve_manage_context(request, need_staff_manage=True)
|
|
member_id = request.data.get('member_id')
|
|
disable = request.data.get('disable', True)
|
|
m = MerchantStaffMember.query.get(id=member_id, merchant_id=mid)
|
|
m.status = MEMBER_STATUS_DISABLED if disable else MEMBER_STATUS_ACTIVE
|
|
m.save(update_fields=['status'])
|
|
action = 'MEMBER_DISABLE' if disable else 'MEMBER_ENABLE'
|
|
audit_owner(mid, request.user.UserUID, action, request, resource_id=member_id)
|
|
return Response({'code': 0, 'msg': '成功', 'data': None})
|
|
except Exception as e:
|
|
return _err(e)
|
|
|
|
|
|
class StaffRoleListView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
try:
|
|
from merchant_ops.constants import PERMISSION_DEFINITIONS, SYSTEM_ROLE_TEMPLATES
|
|
ensure_owner_merchant(request.user.UserUID, request.user)
|
|
roles = MerchantStaffRole.query.filter(merchant_id=request.user.UserUID, status=1)
|
|
tpl_desc = {t['role_code']: t['role_name'] for t in SYSTEM_ROLE_TEMPLATES}
|
|
data = []
|
|
for r in roles:
|
|
perms = list(r.role_permissions.values_list('perm_code', flat=True))
|
|
perm_labels = [
|
|
dict(PERMISSION_DEFINITIONS).get(p, p) for p in perms
|
|
]
|
|
data.append({
|
|
'id': r.id, 'role_code': r.role_code, 'role_name': r.role_name,
|
|
'is_system': r.is_system, 'permissions': perms,
|
|
'permission_labels': perm_labels,
|
|
'role_hint': _role_hint(r.role_code, tpl_desc),
|
|
})
|
|
all_perms = list(MerchantStaffPermission.query.filter(status=1).order_by('sort_order').values(
|
|
'perm_code', 'perm_name'))
|
|
return Response({'code': 0, 'msg': '成功', 'data': {'roles': data, 'all_permissions': all_perms}})
|
|
except Exception as e:
|
|
return _err(e)
|
|
|
|
|
|
class StaffRoleSetPermsView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
try:
|
|
ensure_owner_merchant(request.user.UserUID, request.user)
|
|
role_id = request.data.get('role_id')
|
|
permissions = request.data.get('permissions', [])
|
|
role = MerchantStaffRole.query.get(id=role_id, merchant_id=request.user.UserUID)
|
|
MerchantStaffRolePermission.query.filter(role=role).delete()
|
|
for p in permissions:
|
|
MerchantStaffRolePermission.objects.create(role=role, perm_code=p)
|
|
audit_owner(request.user.UserUID, request.user.UserUID, 'ROLE_SET_PERMS', request, resource_id=role_id)
|
|
return Response({'code': 0, 'msg': '成功', 'data': None})
|
|
except Exception as e:
|
|
return _err(e)
|
|
|
|
|
|
class StaffRoleCreateView(APIView):
|
|
"""商家自定义角色(自由勾选权限)。"""
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
try:
|
|
mid = request.user.UserUID
|
|
ensure_owner_merchant(mid, request.user)
|
|
ensure_merchant_roles(mid)
|
|
role_name = (request.data.get('role_name') or '').strip()
|
|
if not role_name:
|
|
return Response({'code': 400, 'msg': '角色名称不能为空', 'data': None})
|
|
role_code = (request.data.get('role_code') or '').strip()
|
|
if not role_code:
|
|
role_code = 'CUSTOM_' + secrets.token_hex(3).upper()
|
|
if MerchantStaffRole.query.filter(merchant_id=mid, role_code=role_code).exists():
|
|
return Response({'code': 400, 'msg': '角色码已存在', 'data': None})
|
|
permissions = request.data.get('permissions', [])
|
|
role = MerchantStaffRole.objects.create(
|
|
club_id='xq',
|
|
merchant_id=mid,
|
|
role_code=role_code,
|
|
role_name=role_name,
|
|
is_system=False,
|
|
sort_order=50,
|
|
status=1,
|
|
)
|
|
valid = set(MerchantStaffPermission.query.filter(status=1).values_list('perm_code', flat=True))
|
|
for p in permissions:
|
|
if p in valid:
|
|
MerchantStaffRolePermission.objects.create(role=role, perm_code=p)
|
|
audit_owner(mid, mid, 'ROLE_CREATE', request, resource_id=role.id)
|
|
return Response({'code': 0, 'msg': '创建成功', 'data': {'id': role.id, 'role_code': role.role_code}})
|
|
except Exception as e:
|
|
return _err(e)
|
|
|
|
|
|
class StaffMemberUpdatePermsView(APIView):
|
|
"""成员级权限加减(覆盖角色默认权限)。"""
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
try:
|
|
mid = request.user.UserUID
|
|
ensure_owner_merchant(mid, request.user)
|
|
member_id = request.data.get('member_id')
|
|
overrides = request.data.get('overrides', [])
|
|
m = MerchantStaffMember.query.get(id=member_id, merchant_id=mid, status=MEMBER_STATUS_ACTIVE)
|
|
MerchantStaffMemberPermission.query.filter(member=m).delete()
|
|
valid = set(MerchantStaffPermission.query.filter(status=1).values_list('perm_code', flat=True))
|
|
for item in overrides:
|
|
if isinstance(item, dict):
|
|
code = item.get('perm_code')
|
|
granted = bool(item.get('granted', True))
|
|
else:
|
|
code = item
|
|
granted = True
|
|
if code not in valid:
|
|
continue
|
|
MerchantStaffMemberPermission.objects.create(member=m, perm_code=code, granted=granted)
|
|
audit_owner(mid, mid, 'MEMBER_SET_PERMS', request, resource_id=member_id)
|
|
return Response({'code': 0, 'msg': '成员权限已更新', 'data': {'permissions': list(get_member_permissions(m))}})
|
|
except Exception as e:
|
|
return _err(e)
|
|
|
|
|
|
class StaffWalletAllocateView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
try:
|
|
mid, _ = _resolve_wallet_operator(request, 'wallet_allocate')
|
|
member_id = request.data.get('member_id')
|
|
amount = request.data.get('amount')
|
|
m = MerchantStaffMember.query.get(
|
|
id=member_id, merchant_id=mid, status=MEMBER_STATUS_ACTIVE,
|
|
)
|
|
get_or_create_wallet(m)
|
|
wallet = allocate_quota(m, amount, request.user.UserUID, remark='划额度')
|
|
audit_owner(mid, request.user.UserUID, 'WALLET_ALLOCATE', request,
|
|
resource_type='wallet', resource_id=member_id, amount=amount)
|
|
shangjia = UserShangjia.query.filter(user__UserUID=mid).first()
|
|
return Response({'code': 0, 'msg': '划额度成功', 'data': {
|
|
'quota_available': str(wallet.quota_available),
|
|
'merchant_yue': str(shangjia.yue) if shangjia else '0',
|
|
}})
|
|
except ValueError as e:
|
|
return Response({'code': 400, 'msg': str(e), 'data': None})
|
|
except Exception as e:
|
|
return _err(e)
|
|
|
|
|
|
class StaffWalletRevokeView(APIView):
|
|
"""收回客服未使用额度,退回商家余额。"""
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
try:
|
|
mid, _ = _resolve_wallet_operator(request, 'wallet_revoke')
|
|
member_id = request.data.get('member_id')
|
|
amount = request.data.get('amount')
|
|
remark = (request.data.get('remark') or '').strip() or '收回未使用额度'
|
|
m = MerchantStaffMember.query.get(
|
|
id=member_id,
|
|
merchant_id=mid,
|
|
status__in=[MEMBER_STATUS_ACTIVE, MEMBER_STATUS_DISABLED],
|
|
)
|
|
wallet = revoke_quota(m, amount, request.user.UserUID, remark=remark)
|
|
audit_owner(mid, request.user.UserUID, 'WALLET_REVOKE', request,
|
|
resource_type='wallet', resource_id=member_id, amount=amount)
|
|
shangjia = UserShangjia.query.filter(user__UserUID=mid).first()
|
|
return Response({'code': 0, 'msg': '收回成功', 'data': {
|
|
'quota_available': str(wallet.quota_available),
|
|
'merchant_yue': str(shangjia.yue) if shangjia else '0',
|
|
}})
|
|
except ValueError as e:
|
|
return Response({'code': 400, 'msg': str(e), 'data': None})
|
|
except Exception as e:
|
|
return _err(e)
|
|
|
|
|
|
class StaffWalletLedgerView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
try:
|
|
mid = request.user.UserUID
|
|
if is_merchant_owner(request.user):
|
|
member_id = request.data.get('member_id')
|
|
qs = MerchantStaffWalletLedger.query.filter(merchant_id=mid)
|
|
if member_id:
|
|
m = MerchantStaffMember.query.get(id=member_id, merchant_id=mid)
|
|
qs = qs.filter(staff_user_id=m.staff_user_id)
|
|
else:
|
|
ctx = resolve_staff_context(request.user)
|
|
if not ctx:
|
|
return Response({'code': 403, 'msg': '无权限', 'data': None})
|
|
qs = MerchantStaffWalletLedger.query.filter(staff_user_id=request.user.UserUID)
|
|
items = [{
|
|
'change_type': x.change_type,
|
|
'amount': str(x.amount),
|
|
'balance_after': str(x.balance_after),
|
|
'related_order_id': x.related_order_id or '',
|
|
'remark': x.remark,
|
|
'time': x.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if x.CreateTime else '',
|
|
} for x in qs.order_by('-CreateTime')[:100]]
|
|
return Response({'code': 0, 'msg': '成功', 'data': {'list': items}})
|
|
except Exception as e:
|
|
return _err(e)
|
|
|
|
|
|
class StaffAuditListView(APIView):
|
|
"""POST /yonghu/merchant-staff/audit/list — 商家老板或有 audit_view 权限的客服可查看重要操作日志。"""
|
|
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
try:
|
|
from merchant_ops.constants import IMPORTANT_AUDIT_ACTIONS
|
|
from merchant_ops.services.audit import serialize_audit_row
|
|
|
|
mid = request.user.UserUID
|
|
member = None
|
|
if is_merchant_owner(request.user):
|
|
ensure_owner_merchant(mid, request.user)
|
|
else:
|
|
member = get_active_staff_member(request.user)
|
|
if not member or not _member_has_perm(member, 'audit_view'):
|
|
return Response({'code': 403, 'msg': '无权限', 'data': None})
|
|
mid = member.merchant_id
|
|
page = max(1, int(request.data.get('page', 1)))
|
|
page_size = min(50, int(request.data.get('page_size', 20)))
|
|
qs = MerchantStaffAuditLog.query.filter(
|
|
merchant_id=mid,
|
|
action__in=IMPORTANT_AUDIT_ACTIONS,
|
|
)
|
|
staff_filter = request.data.get('staff_user_id')
|
|
if staff_filter:
|
|
qs = qs.filter(operator_user_id=staff_filter)
|
|
total = qs.count()
|
|
rows = list(qs.order_by('-CreateTime')[(page - 1) * page_size: page * page_size])
|
|
member_ids = {r.staff_member_id for r in rows if r.staff_member_id}
|
|
member_map = {}
|
|
if member_ids:
|
|
for m in MerchantStaffMember.query.filter(id__in=member_ids).select_related('role'):
|
|
member_map[m.id] = m
|
|
items = [serialize_audit_row(r, member_map) for r in rows]
|
|
return Response({'code': 0, 'msg': '成功', 'data': {
|
|
'list': items, 'total': total, 'has_more': page * page_size < total,
|
|
}})
|
|
except Exception as e:
|
|
return _err(e)
|
|
|
|
|
|
class StaffRankView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
try:
|
|
from datetime import date, timedelta
|
|
from django.db.models import Sum
|
|
|
|
if is_merchant_owner(request.user):
|
|
mid = request.user.UserUID
|
|
else:
|
|
member = get_active_staff_member(request.user)
|
|
if not member:
|
|
return Response({'code': 403, 'msg': '无权限', 'data': None})
|
|
mid = member.merchant_id
|
|
period = request.data.get('period', 'today')
|
|
sort_by = request.data.get('sort_by', 'dispatch_count')
|
|
today = date.today()
|
|
if period == 'today':
|
|
start = today
|
|
elif period == 'yesterday':
|
|
start = today - timedelta(days=1)
|
|
elif period == 'week':
|
|
start = today - timedelta(days=7)
|
|
elif period == 'month':
|
|
start = today - timedelta(days=30)
|
|
else:
|
|
start = None
|
|
|
|
qs = MerchantStaffDailyStats.query.filter(merchant_id=mid)
|
|
if start:
|
|
if period == 'yesterday':
|
|
qs = qs.filter(stat_date=start)
|
|
else:
|
|
qs = qs.filter(stat_date__gte=start)
|
|
|
|
agg = qs.values('staff_user_id', 'member_id').annotate(
|
|
dispatch_count=Sum('dispatch_count'),
|
|
dispatch_amount=Sum('dispatch_amount'),
|
|
settle_count=Sum('settle_count'),
|
|
settle_amount=Sum('settle_amount'),
|
|
)
|
|
sort_field = sort_by if sort_by in ('dispatch_count', 'dispatch_amount', 'settle_count', 'settle_amount') else 'dispatch_count'
|
|
agg = sorted(agg, key=lambda x: x[sort_field] or 0, reverse=True)[:50]
|
|
|
|
result = []
|
|
for idx, row in enumerate(agg, 1):
|
|
m = MerchantStaffMember.query.filter(id=row['member_id']).first()
|
|
result.append({
|
|
'rank': idx,
|
|
'member_id': row['member_id'],
|
|
'staff_user_id': row['staff_user_id'],
|
|
'display_name': (m.display_name if m else '') or row['staff_user_id'],
|
|
'dispatch_count': row['dispatch_count'] or 0,
|
|
'dispatch_amount': str(row['dispatch_amount'] or 0),
|
|
'settle_count': row['settle_count'] or 0,
|
|
'settle_amount': str(row['settle_amount'] or 0),
|
|
})
|
|
return Response({'code': 0, 'msg': '成功', 'data': {'list': result, 'period': period}})
|
|
except Exception as e:
|
|
return _err(e)
|