Files
Django/merchant_ops/views_manage.py

475 lines
21 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
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')
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:
mid = request.user.UserUID
ensure_owner_merchant(mid, request.user)
members = MerchantStaffMember.query.filter(
merchant_id=mid, status=MEMBER_STATUS_ACTIVE,
).select_related('role')
result = []
for m in members:
stats = MerchantStaffDailyStats.query.filter(member_id=m.id).aggregate(
dispatch_count=Sum('dispatch_count'),
settle_count=Sum('settle_count'),
settle_amount=Sum('settle_amount'),
)
wallet = m.wallets.first()
result.append({
'id': m.id,
'uid': m.staff_user_id,
'nickname': m.display_name or m.staff_user_id,
'isChief': m.role.role_code == 'CHIEF',
'isFinance': m.role.role_code == 'FINANCE',
'role_name': m.role.role_name,
'orderCount': stats['dispatch_count'] or 0,
'settleCount': stats['settle_count'] or 0,
'settleAmount': str(stats['settle_amount'] or '0.00'),
'quota_available': str(wallet.quota_available if wallet else 0),
})
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:
ensure_owner_merchant(request.user.UserUID, request.user)
member_id = request.data.get('member_id')
role_id = request.data.get('role_id')
m = MerchantStaffMember.query.get(id=member_id, merchant_id=request.user.UserUID, status=MEMBER_STATUS_ACTIVE)
role = MerchantStaffRole.query.get(id=role_id, merchant_id=request.user.UserUID)
old = m.role_id
m.role = role
m.save(update_fields=['role'])
audit_owner(request.user.UserUID, 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})
return Response({'code': 0, 'msg': '成功', 'data': None})
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:
ensure_owner_merchant(request.user.UserUID, request.user)
member_id = request.data.get('member_id')
disable = request.data.get('disable', True)
m = MerchantStaffMember.query.get(id=member_id, merchant_id=request.user.UserUID)
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(request.user.UserUID, 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:
ensure_owner_merchant(request.user.UserUID, request.user)
roles = MerchantStaffRole.query.filter(merchant_id=request.user.UserUID, status=1)
data = []
for r in roles:
perms = list(r.role_permissions.values_list('perm_code', flat=True))
data.append({
'id': r.id, 'role_code': r.role_code, 'role_name': r.role_name,
'is_system': r.is_system, 'permissions': perms,
})
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:
ensure_owner_merchant(request.user.UserUID, request.user)
member_id = request.data.get('member_id')
amount = request.data.get('amount')
m = MerchantStaffMember.query.get(id=member_id, merchant_id=request.user.UserUID, status=MEMBER_STATUS_ACTIVE)
get_or_create_wallet(m)
allocate_quota(m, amount, request.user.UserUID, remark='老板划额度')
audit_owner(request.user.UserUID, request.user.UserUID, 'WALLET_ALLOCATE', request,
resource_type='wallet', resource_id=member_id, amount=amount)
return Response({'code': 0, 'msg': '划额度成功', 'data': None})
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)