"""商家老板:邀请码、成员、角色、额度、日志、排行。""" 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 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): permission_classes = [IsAuthenticated] def post(self, request): try: 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) staff_filter = request.data.get('staff_user_id') if staff_filter: qs = qs.filter(operator_user_id=staff_filter) total = qs.count() rows = qs.order_by('-CreateTime')[(page - 1) * page_size: page * page_size] items = [{ 'id': r.id, 'operator_user_id': r.operator_user_id, 'operator_type': r.operator_type, 'action': r.action, 'resource_type': r.resource_type, 'resource_id': r.resource_id, 'amount': str(r.amount) if r.amount is not None else '', 'remark': r.remark, 'time': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '', } 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)