Files
Django/gongdan/views.py
XingQue 7c6e14db01 fix: 客服菜单增加投诉工单,列表按俱乐部过滤
seed_kefu_menu 后侧栏才能出现「投诉工单」;admin list 使用 club 上下文过滤。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 00:47:24 +08:00

337 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""投诉工单 API用户提交/列表/升级 + 图片上传 + 客服处理。"""
import logging
import uuid
from django.utils import timezone
from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import status
from gongdan.constants import (
MAX_IMAGES, OSS_PREFIX, PERM_VIEW, PERM_HANDLE, TYPE_LABELS, STATUS_LABELS,
)
from gongdan.models import Gongdan
from gongdan.services import (
GongdanError,
create_gongdan,
escalate_gongdan,
handle_gongdan,
serialize_gongdan,
user_mark_dissatisfied,
)
from utils.oss_utils import upload_to_oss, validate_image
logger = logging.getLogger(__name__)
def _ok(data=None, message='ok', code=200):
body = {'code': code, 'message': message, 'msg': message}
if data is not None:
body['data'] = data
return Response(body, status=status.HTTP_200_OK)
def _err(message, http=400, code=None):
c = code if code is not None else http
return Response(
{'code': c, 'message': message, 'msg': message},
status=http,
)
def _resolve_club(request):
try:
from jituan.services.club_write import resolve_club_id_for_write
return resolve_club_id_for_write(request, request.user)
except Exception:
return getattr(request.user, 'club_id', None) or None
class GongdanUploadImageView(APIView):
"""上传证据图,返回 URL。路径POST /gongdan/upload-image/"""
permission_classes = [IsAuthenticated]
parser_classes = (MultiPartParser, FormParser)
def post(self, request):
f = request.FILES.get('file') or request.FILES.get('image') or request.FILES.get('tupian')
if not f:
return _err('请选择图片')
ok, msg = validate_image(f)
if not ok:
return _err(msg)
ext = (f.name.rsplit('.', 1)[-1] if '.' in f.name else 'jpg').lower()
if ext not in ('jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'):
ext = 'jpg'
uid = getattr(request.user, 'UserUID', 'anon')
day = timezone.now().strftime('%Y%m%d')
path = f'{OSS_PREFIX}/{day}/{uid}/{uuid.uuid4().hex}.{ext}'
url = upload_to_oss(f, path)
if not url:
return _err('上传失败,请稍后重试', http=500)
return _ok({'url': url}, '上传成功')
class GongdanCreateView(APIView):
"""创建投诉工单。POST /gongdan/create/"""
permission_classes = [IsAuthenticated]
parser_classes = (JSONParser, FormParser, MultiPartParser)
def post(self, request):
d = request.data
images = d.get('images') or d.get('image_urls') or []
if isinstance(images, str):
images = [x.strip() for x in images.split(',') if x.strip()]
try:
gd = create_gongdan(
yonghuid=request.user.UserUID,
leixing=d.get('leixing'),
shuoming=d.get('shuoming') or d.get('content') or '',
club_id=_resolve_club(request),
order_id=d.get('order_id') or d.get('OrderID'),
chongzhi_id=d.get('chongzhi_id') or d.get('dingdanid') or d.get('dingdan_id'),
fadan_id=d.get('fadan_id') or d.get('penalty_id'),
guanshi_id=d.get('guanshi_id'),
image_urls=images,
)
return _ok(serialize_gongdan(gd, include_images=True, include_shengji=True), '提交成功')
except GongdanError as e:
return _err(str(e))
except Exception as e:
logger.error('创建工单失败: %s', e, exc_info=True)
return _err('提交失败,请稍后重试', http=500)
class GongdanMyListView(APIView):
"""我的投诉列表。POST/GET /gongdan/my-list/"""
permission_classes = [IsAuthenticated]
def get(self, request):
return self._list(request)
def post(self, request):
return self._list(request)
def _list(self, request):
page = int(request.data.get('page') or request.query_params.get('page') or 1)
page_size = min(int(request.data.get('page_size') or request.query_params.get('page_size') or 20), 50)
qs = Gongdan.query.filter(shenqingren_id=request.user.UserUID).order_by('-CreateTime')
total = qs.count()
start = max(page - 1, 0) * page_size
rows = qs[start:start + page_size]
return _ok({
'list': [serialize_gongdan(g, include_images=False) for g in rows],
'total': total,
'page': page,
'page_size': page_size,
})
class GongdanDetailView(APIView):
"""工单详情。POST /gongdan/detail/ {gongdan_id}"""
permission_classes = [IsAuthenticated]
def post(self, request):
gid = (request.data.get('gongdan_id') or '').strip()
if not gid:
return _err('工单号不能为空')
gd = Gongdan.query.filter(gongdan_id=gid).first()
if not gd:
return _err('工单不存在', http=404)
# 申请人或有客服查看权限
if gd.shenqingren_id != request.user.UserUID:
from backend.utils import verify_kefu_permission
from gongdan.constants import PERM_VIEW, PERM_HANDLE
kefu, perms_or_resp = verify_kefu_permission(
request, request.data.get('username') or getattr(request.user, 'Phone', None),
)
if not kefu:
return perms_or_resp
perms = perms_or_resp if isinstance(perms_or_resp, list) else []
if PERM_VIEW not in perms and PERM_HANDLE not in perms and '000001' not in perms:
# 超管占位is admin via kefu object
is_admin = getattr(request.user, 'UserType', '') == 'admin' or getattr(request.user, 'IsSuperuser', False)
if not is_admin:
return _err('无权查看', http=403)
return _ok(serialize_gongdan(gd, include_images=True, include_shengji=True))
class GongdanEscalateView(APIView):
"""继续投诉/升级。POST /gongdan/escalate/"""
permission_classes = [IsAuthenticated]
def post(self, request):
d = request.data
images = d.get('images')
if images is not None and isinstance(images, str):
images = [x.strip() for x in images.split(',') if x.strip()]
try:
gd = escalate_gongdan(
gongdan_id=(d.get('gongdan_id') or '').strip(),
yonghuid=request.user.UserUID,
shuoming=d.get('shuoming') or '',
image_urls=images,
)
return _ok(serialize_gongdan(gd, include_images=True, include_shengji=True), '已提交补充投诉')
except GongdanError as e:
return _err(str(e))
except Exception as e:
logger.error('升级工单失败: %s', e, exc_info=True)
return _err('提交失败', http=500)
class GongdanDissatisfiedView(APIView):
"""标记不满意。POST /gongdan/dissatisfied/"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
gd = user_mark_dissatisfied(
gongdan_id=(request.data.get('gongdan_id') or '').strip(),
yonghuid=request.user.UserUID,
)
return _ok(serialize_gongdan(gd), '已标记不满意,可继续投诉')
except GongdanError as e:
return _err(str(e))
class GongdanMetaView(APIView):
"""类型/状态枚举供前端展示。GET /gongdan/meta/"""
permission_classes = [IsAuthenticated]
def get(self, request):
return _ok({
'types': [{'value': k, 'label': v} for k, v in TYPE_LABELS.items()],
'statuses': [{'value': k, 'label': v} for k, v in STATUS_LABELS.items()],
'max_images': MAX_IMAGES,
})
class ChongzhiRecordListView(APIView):
"""当前用户充值记录Czjilu供投诉选 ID / 充值记录页。POST /gongdan/chongzhi-list/"""
permission_classes = [IsAuthenticated]
def post(self, request):
from products.models import Czjilu
from products.czjilu_types import (
CZJILU_LEIXING_HUIYUAN, CZJILU_LEIXING_YAJIN, CZJILU_LEIXING_JIFEN,
CZJILU_LEIXING_SHANGJIA,
)
page = int(request.data.get('page') or 1)
page_size = min(int(request.data.get('page_size') or 20), 50)
qs = Czjilu.query.filter(yonghuid=request.user.UserUID).order_by('-CreateTime')
leixing = request.data.get('leixing')
if leixing not in (None, ''):
qs = qs.filter(leixing=int(leixing))
total = qs.count()
start = max(page - 1, 0) * page_size
type_map = {
CZJILU_LEIXING_HUIYUAN: '会员',
CZJILU_LEIXING_YAJIN: '押金',
CZJILU_LEIXING_JIFEN: '积分',
CZJILU_LEIXING_SHANGJIA: '商家充值',
5: '考核',
6: '罚款',
}
status_map = {9: '待支付', 3: '已支付', 2: '已取消', 8: '履约失败', 10: '业务失败'}
rows = []
for o in qs[start:start + page_size]:
rows.append({
'dingdan_id': o.dingdan_id,
'leixing': o.leixing,
'leixing_label': type_map.get(o.leixing, str(o.leixing)),
'jine': str(o.jine),
'zhuangtai': o.zhuangtai,
'zhuangtai_label': status_map.get(o.zhuangtai, str(o.zhuangtai)),
'shuoming': o.shuoming or '',
'CreateTime': o.CreateTime.isoformat() if getattr(o, 'CreateTime', None) else '',
})
return _ok({'list': rows, 'total': total, 'page': page, 'page_size': page_size})
# ---------- 客服后台 ----------
def _require_kefu_perm(request, *need_codes):
from backend.utils import verify_kefu_permission
username = request.data.get('username') or request.query_params.get('username') or getattr(request.user, 'Phone', None)
kefu, perms_or_resp = verify_kefu_permission(request, username)
if not kefu:
return None, None, perms_or_resp
perms = perms_or_resp if isinstance(perms_or_resp, list) else []
is_admin = (
getattr(request.user, 'UserType', '') == 'admin'
or getattr(request.user, 'IsSuperuser', False)
or '000001' in perms
)
if is_admin:
return kefu, perms, None
if not any(c in perms for c in need_codes):
return None, None, _err('无工单权限', http=403)
return kefu, perms, None
class GongdanAdminListView(APIView):
"""客服工单列表。POST /gongdan/admin/list/"""
permission_classes = [IsAuthenticated]
def post(self, request):
kefu, perms, err = _require_kefu_perm(request, PERM_VIEW, PERM_HANDLE)
if err:
return err
page = int(request.data.get('page') or 1)
page_size = min(int(request.data.get('page_size') or 20), 100)
qs = Gongdan.query.all().order_by('-CreateTime')
try:
from jituan.services.club_context import filter_club_char_field
qs = filter_club_char_field(qs, request, field='club_id')
except Exception:
pass
zhuangtai = request.data.get('zhuangtai')
leixing = request.data.get('leixing')
keyword = (request.data.get('keyword') or '').strip()
if zhuangtai not in (None, ''):
qs = qs.filter(zhuangtai=int(zhuangtai))
if leixing not in (None, ''):
qs = qs.filter(leixing=int(leixing))
if keyword:
from django.db.models import Q
qs = qs.filter(
Q(gongdan_id__icontains=keyword)
| Q(shenqingren_id__icontains=keyword)
| Q(order_id__icontains=keyword)
| Q(chongzhi_id__icontains=keyword)
| Q(fadan_id__icontains=keyword)
)
total = qs.count()
start = max(page - 1, 0) * page_size
rows = [serialize_gongdan(g, include_images=False) for g in qs[start:start + page_size]]
return _ok({'list': rows, 'total': total, 'page': page, 'page_size': page_size})
class GongdanAdminHandleView(APIView):
"""客服处理工单。POST /gongdan/admin/handle/"""
permission_classes = [IsAuthenticated]
def post(self, request):
kefu, perms, err = _require_kefu_perm(request, PERM_HANDLE)
if err:
return err
d = request.data
handler_uid = getattr(request.user, 'UserUID', '') or getattr(request.user, 'Phone', '')
handler_name = getattr(request.user, 'NickName', None) or getattr(request.user, 'Phone', '') or handler_uid
try:
gd = handle_gongdan(
gongdan_id=(d.get('gongdan_id') or '').strip(),
handler_uid=handler_uid,
handler_name=handler_name,
action=d.get('action') or 'done',
chuli_jieguo=d.get('chuli_jieguo') or d.get('jieguo') or '',
)
return _ok(serialize_gongdan(gd, include_images=True, include_shengji=True), '处理成功')
except GongdanError as e:
return _err(str(e))
except Exception as e:
logger.error('处理工单失败: %s', e, exc_info=True)
return _err('处理失败', http=500)