Files
Django/merchant_ops/views_staff.py
XingQue 26dcbc1dc9 fix: 商家老板可绑定为其他店子客服
允许已是商家老板的用户担任其他商家子客服,禁止绑自己的店。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-04 20:48:12 +08:00

114 lines
5.0 KiB
Python

"""子客服:绑定、个人信息。"""
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})