"""子客服:绑定、个人信息。""" import logging from django.db import transaction 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_UNUSED, INVITE_STATUS_USED, MEMBER_STATUS_ACTIVE from merchant_ops.models import MerchantStaffInvite, MerchantStaffMember from merchant_ops.services.authz import ( StaffAuthError, assert_can_bind_staff, get_active_staff_member, resolve_staff_context, ) from merchant_ops.services.audit import audit_staff from merchant_ops.services.stats import get_staff_stats_payload from merchant_ops.services.wallet import get_or_create_wallet from merchant_ops.services.bootstrap import ensure_merchant_roles from users.business_models import User 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}) class StaffBindView(APIView): """POST /yonghu/merchant-staff/bind""" permission_classes = [IsAuthenticated] def post(self, request): try: invite_code = str(request.data.get('inviteCode') or request.data.get('invite_code') or '').strip() if not invite_code or invite_code == 'XQKF2026': return Response({'code': 400, 'msg': '请使用商家提供的一次性邀请码', 'data': None}) with transaction.atomic(): inv = MerchantStaffInvite.objects.select_for_update().get( invite_code=invite_code, status=INVITE_STATUS_UNUSED, ) assert_can_bind_staff(request.user, invite_merchant_id=inv.merchant_id) if inv.expire_at and inv.expire_at < timezone.now(): inv.status = 3 inv.save(update_fields=['status']) return Response({'code': 400, 'msg': '邀请码已过期', 'data': None}) if not inv.role_id: return Response({'code': 400, 'msg': '邀请码未绑定角色', 'data': None}) display = getattr(request.user, 'Nickname', '') or request.user.UserUID member, created = MerchantStaffMember.objects.get_or_create( merchant_id=inv.merchant_id, staff_user_id=request.user.UserUID, defaults={ 'role_id': inv.role_id, 'status': MEMBER_STATUS_ACTIVE, 'display_name': display[:64], 'invited_by_code': inv, }, ) if not created: if member.status == MEMBER_STATUS_ACTIVE: return Response({'code': 400, 'msg': '您已是该商家客服', 'data': None}) member.status = MEMBER_STATUS_ACTIVE member.role_id = inv.role_id member.display_name = display[:64] member.invited_by_code = inv member.revoked_at = None member.save() inv.status = INVITE_STATUS_USED inv.used_by = request.user.UserUID inv.used_at = timezone.now() inv.save(update_fields=['status', 'used_by', 'used_at']) get_or_create_wallet(member) ctx = resolve_staff_context(request.user) shangjia = UserShangjia.query.filter(user__UserUID=inv.merchant_id).first() if ctx and shangjia: ctx['merchant_display'] = {'nicheng': shangjia.nicheng, 'merchant_id': inv.merchant_id} audit_staff(member, 'STAFF_BIND', request, resource_id=invite_code) return Response({'code': 0, 'msg': '绑定成功', 'data': {'staff_context': ctx}}) except MerchantStaffInvite.DoesNotExist: return Response({'code': 404, 'msg': '邀请码无效或已使用', 'data': None}) except Exception as e: return _err(e) class StaffMeView(APIView): """GET/POST /yonghu/merchant-staff/me""" permission_classes = [IsAuthenticated] def post(self, request): ctx = resolve_staff_context(request.user) if not ctx: return Response({'code': 404, 'msg': '非子客服身份', 'data': None}) member = get_active_staff_member(request.user) ensure_merchant_roles(member.merchant_id) member.refresh_from_db() ctx = resolve_staff_context(request.user) shangjia = UserShangjia.query.filter(user__UserUID=member.merchant_id).first() ctx['merchant_display'] = { 'nicheng': shangjia.nicheng if shangjia else '', 'sjyue': str(shangjia.yue) if shangjia else '0', } ctx['stats'] = get_staff_stats_payload(member) return Response({'code': 0, 'msg': '成功', 'data': ctx})