"""users.views.kefu_punishment - auto-generated by split script.""" """users.views.kefu - auto-generated by split script.""" import os import sys import json import re import time import uuid import random import string import requests import hashlib import traceback import decimal import jwt import ipaddress import xmltodict from datetime import datetime, timedelta from decimal import Decimal import defusedxml.ElementTree as ET from django.conf import settings from django.db import models, transaction, IntegrityError, DatabaseError, connection from django.db.models import Q, F, Sum, Count, Case, When, IntegerField, Prefetch from django.core.cache import cache from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.core.exceptions import ObjectDoesNotExist from django.shortcuts import get_object_or_404 from django.utils import timezone from django.http import HttpResponse from django.views import View from django.views.decorators.csrf import csrf_exempt from django.contrib.auth.hashers import make_password from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import permissions, status from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.parsers import MultiPartParser, FormParser, JSONParser from rest_framework.throttling import AnonRateThrottle from rest_framework_simplejwt.tokens import RefreshToken from rest_framework_simplejwt.authentication import JWTAuthentication from gvsdsdk.fluent import db, func, FQ from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss from utils.money import yuan_to_fen from utils.fadan_utils import check_fadan_qiangdan_eligible from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode from users.fadan_fenhong_utils import process_fadan_fenhong from orders.utils import ( update_daily_payout, settle_shangjia_order_guanshi_fenhong ) from backend.utils import ( update_dashou_daily_by_action, update_guanshi_daily_by_action, update_shangjia_daily, update_zuzhang_daily_by_action, verify_kefu_permission ) from rank.utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan from ..models import ( UserBoss, UserDashou, UserShangjia, UserGuanshi, UserZuzhang, UserKefu, AdminProfile, OfficialAccountUser, TixianAutoRecord, TixianShenheJilu, Tixianjilu, Xiugaijilu, RankingRecord ) from users.business_models import User from ..tixian_shenhe_services import ( load_audit_meta_map, refund_balance, sync_audit_and_jilu_status, mark_transfer_success, release_collect_quota_for_record, close_audit_wechat_failed, query_wechat_transfer_bill, check_shijidaozhang_within_limit, check_dashou_trial_blocks_commission_withdraw, WX_STATE_FAIL, WX_STATE_SUCCESS, WX_STATE_WAIT, WX_STATE_CANCELING, ) from ..tixian_shenhe_views import process_audit_collect from orders.models import ( Order, PlatformOrderExt, MerchantOrderExt, CommissionRate, PenaltyRecord, PenaltyEvidenceImage, RefundRecord, PlayerDeliveryImage, Penalty, PenaltyAppealImage ) from products.models import ( ShangpinLeixing, Huiyuan, Huiyuangoumai, Gsfenhong, Czjilu ) from config.models import Qunpeizhi from backend.models import MerchantDailyStats from rank.models import ( KaohePayTemp, Chenghao, ShenheJilu, KaoheCishuFeiyong, YonghuChenghao ) from users.models import UserShenheguan from products.utils import update_shangpin_daily_stat from shop.utils import update_dianpu_daily_stat from rank.utils import create_shenhe_jilu_from_temp import logging logger = logging.getLogger(__name__) class KefuPunishmentListView(APIView): """ 客服获取处罚记录列表接口 请求:POST /yonghu/kefu_cfgl 参数:{ "phone": "客服手机号", "status": [0,3] 或 [1,2], # 状态数组 "dingdan_id": "订单ID(可选)", "dashouid": "打手ID(可选)", "page": 1, "page_size": 20 } 认证:JWT 返回:{ "code": 0, "data": { "list": [处罚记录], "total": 总数, "stats": { "zongshu": 总记录数, "daichuli": 待处理数, "yichuli": 已处理数 } } } """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] @staticmethod def _parse_status_list(raw): """兼容 status=[0,3]、\"0,3\"、单数字;空/all 表示不过滤。""" if raw is None: return None if isinstance(raw, str): s = raw.strip().lower() if not s or s in ('all', 'null', 'none'): return None if s.startswith('['): import json try: raw = json.loads(s) except (TypeError, ValueError): return None else: raw = [x.strip() for x in s.split(',') if x.strip()] if isinstance(raw, (int, float)): return [int(raw)] if isinstance(raw, list): out = [] for x in raw: try: out.append(int(x)) except (TypeError, ValueError): continue return out or None return None def post(self, request): # 1. 获取参数 phone = request.data.get('phone', '').strip() status_list = self._parse_status_list(request.data.get('status')) dingdan_id = request.data.get('dingdan_id', '').strip() dashouid = request.data.get('dashouid', '').strip() try: page = int(request.data.get('page', 1)) page_size = int(request.data.get('page_size', 20)) except ValueError: return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST) if not phone: return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) # 2. 客服身份验证 current_user = request.user # 1. 获取前端参数 username = request.data.get('phone', '').strip() if not username: return Response({'code': 400, 'msg': '缺少username'}) # 2. 公共权限校验 kefu, permissions = verify_kefu_permission(request, username) if kefu is None: return permissions # 3. 检查平台订单管理权限(002ab) if '005aa' not in permissions: return Response({'code': 403, 'msg': '您无权查看平台订单列表'}) # 3. 获取统计信息(不分页) from jituan.services.club_penalty import filter_penalty_record_qs from jituan.services.club_user_access import list_response_meta base_qs = filter_penalty_record_qs(PenaltyRecord.query.all(), request) agg = base_qs.aggregate( zongshu=Count('id'), daichuli=Count('id', filter=Q(ApplyStatus__in=[0, 3])), yichuli=Count('id', filter=Q(ApplyStatus__in=[1, 2])), ) stats = { 'zongshu': agg['zongshu'], 'daichuli': agg['daichuli'], 'yichuli': agg['yichuli'], } queryset = base_qs if status_list and isinstance(status_list, list): queryset = queryset.filter(ApplyStatus__in=status_list) if dingdan_id: queryset = queryset.filter(OrderID=dingdan_id) if dashouid: queryset = queryset.filter(PlayerID=dashouid) # 5. 分页 page_obj = queryset.order_by('-CreateTime').paginate(page=page, per_page=page_size) total = page_obj.total records = page_obj.items # 6. 构建返回列表 list_data = [] for item in records: ct = item.CreateTime ut = item.UpdateTime list_data.append({ 'dingdan_id': item.OrderID, 'dashouid': item.PlayerID, 'qingqiuid': item.ApplicantID, 'cfliyou': item.PenaltyReason, 'sqzhuangtai': item.ApplyStatus, 'jifen': item.DeductedPoints, 'CreateTime': ct, 'UpdateTime': ut, 'create_time': ct.isoformat() if ct else '', 'update_time': ut.isoformat() if ut else '', }) return Response({ 'code': 0, 'data': { 'list': list_data, 'total': total, 'stats': stats, **list_response_meta(request), } }) class KefuPunishmentDetailView(APIView): """ 客服获取处罚详情接口 请求:POST /yonghu/kefu_cf_detail 参数:{ "phone": "客服手机号", "dingdan_id": "订单ID" } 认证:JWT 返回:code=0 + 处罚记录详情(含图片) """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): phone = request.data.get('phone', '').strip() dingdan_id = request.data.get('dingdan_id', '').strip() if not phone or not dingdan_id: return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) # 客服身份验证 current_user = request.user if getattr(current_user, 'Phone', '') != phone: logger.warning(f"手机号不匹配: {phone}") return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) if current_user.UserType not in ('kefu', 'admin'): logger.warning(f"用户类型非客服: {current_user.UserUID}") return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser kefu = None if not is_admin: try: kefu = current_user.KefuProfile except ObjectDoesNotExist: logger.warning(f"客服扩展表不存在: {current_user.UserUID}") return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) if kefu.zhuangtai != 1: logger.warning(f"客服账号已禁用: {current_user.UserUID}") return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) # 查询处罚记录(按创建时间倒序取最新一条) try: record = PenaltyRecord.query.filter(OrderID=dingdan_id).order_by('-CreateTime').first() if not record: return Response({'code': 404, 'msg': '处罚记录不存在'}, status=status.HTTP_404_NOT_FOUND) except Exception as e: logger.error(f"查询处罚记录失败: {e}") return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 查询商家证据图片 zhengju_images = [] shensu_images = [] try: # 商家证据图片:上传者为请求人 (ApplicantID) zhengju_qs = PenaltyEvidenceImage.query.filter(OrderID=dingdan_id, UserID=record.ApplicantID) zhengju_images = [item.ImageURL for item in zhengju_qs if item.ImageURL] # 打手申诉图片:上传者为打手 (PlayerID) shensu_qs = PenaltyEvidenceImage.query.filter(OrderID=dingdan_id, UserID=record.PlayerID) shensu_images = [item.ImageURL for item in shensu_qs if item.ImageURL] except Exception as e: logger.error(f"查询图片失败: {e}") # 构建返回数据 data = { 'dingdan_id': record.OrderID, 'dashouid': record.PlayerID, 'qingqiuid': record.ApplicantID, 'chuliid': record.ProcessorID, 'cfliyou': record.PenaltyReason, 'sqzhuangtai': record.ApplyStatus, 'bhliyou': record.RejectReason, 'jifen': record.DeductedPoints, 'ssliyou': record.AppealReason or '', 'zhengju_tupian': zhengju_images, 'shensu_tupian': shensu_images, 'CreateTime': record.CreateTime, 'UpdateTime': record.UpdateTime, } return Response({'code': 0, 'data': data}) class KefuPunishmentActionView(APIView): """ 客服同意/驳回处罚接口 请求:POST /yonghu/kefu_cf_action 参数:{ "phone": "客服手机号", "dingdan_id": "订单ID", "action": 1(同意) / 2(驳回), "reject_reason": "驳回理由(驳回时必填)" } 认证:JWT 返回:code=0 成功 """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): phone = request.data.get('phone', '').strip() dingdan_id = request.data.get('dingdan_id', '').strip() action = request.data.get('action') reject_reason = request.data.get('reject_reason', '').strip() if not phone or not dingdan_id or action not in [1, 2]: return Response({'code': 401, 'msg': '参数错误'}, status=status.HTTP_400_BAD_REQUEST) if action == 2 and not reject_reason: return Response({'code': 400, 'msg': '驳回理由不能为空'}, status=status.HTTP_400_BAD_REQUEST) # 客服身份验证(完整验证,此处略) current_user = request.user if getattr(current_user, 'Phone', '') != phone: return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) if current_user.UserType not in ('kefu', 'admin'): return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser kefu = None if not is_admin: try: kefu = current_user.KefuProfile if kefu.zhuangtai != 1: return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) except ObjectDoesNotExist: return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) # 查询待处理的处罚记录(状态0或3) try: record = PenaltyRecord.query.filter( OrderID=dingdan_id, ApplyStatus__in=[0, 3] ).order_by('-CreateTime').first() if not record: exists = PenaltyRecord.query.filter(OrderID=dingdan_id).exists() if exists: return Response({'code': 400, 'msg': '该处罚记录已处理'}, status=status.HTTP_400_BAD_REQUEST) else: return Response({'code': 404, 'msg': '处罚记录不存在'}, status=status.HTTP_404_NOT_FOUND) except Exception as e: logger.error(f"查询处罚记录失败: {e}") return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) with transaction.atomic(): # 处理重复记录:同一订单的其他待处理/申诉中记录标记为驳回 duplicate_records = PenaltyRecord.query.filter( OrderID=dingdan_id, ApplyStatus__in=[0, 3] ).exclude(id=record.id) for dup in duplicate_records: dup.ApplyStatus = 2 dup.ProcessorID = phone dup.RejectReason = "系统自动驳回:存在重复处罚记录" dup.save() logger.info(f"自动驳回重复处罚记录 ID={dup.id}") if action == 1: # 同意 # 可选验证打手存在 if record.PlayerID: try: dashou = UserDashou.query.get(user__UserUID=record.PlayerID) except UserDashou.DoesNotExist: return Response({'code': 400, 'msg': '被处罚打手不存在'}, status=status.HTTP_400_BAD_REQUEST) record.ApplyStatus = 1 record.ProcessorID = phone record.save() # 更新商家订单扩展表 try: dingdan = Order.query.select_related('shangjia_kuozhan').get(OrderID=dingdan_id) if hasattr(dingdan, 'shangjia_kuozhan'): dingdan.shangjia_kuozhan.PenaltyApplyStatus = 1 dingdan.shangjia_kuozhan.save() except Exception as e: logger.warning(f"更新商家扩展表失败: {e}") elif action == 2: # 驳回 # 返还积分 if record.DeductedPoints > 0 and record.PlayerID: try: dashou = UserDashou.query.get(user__UserUID=record.PlayerID) if dashou.jifen <= 1000: # 安全限制 dashou.jifen += record.DeductedPoints dashou.save() logger.info(f"驳回处罚,打手{record.PlayerID}返还积分{record.DeductedPoints}") else: logger.warning(f"打手积分异常({dashou.jifen}),不再返还") except UserDashou.DoesNotExist: logger.warning(f"打手{record.PlayerID}不存在,无法返还积分") record.ApplyStatus = 2 record.ProcessorID = phone record.RejectReason = reject_reason record.save() try: dingdan = Order.query.select_related('shangjia_kuozhan').get(OrderID=dingdan_id) if hasattr(dingdan, 'shangjia_kuozhan'): dingdan.shangjia_kuozhan.PenaltyApplyStatus = 2 dingdan.shangjia_kuozhan.RejectReason = reject_reason dingdan.shangjia_kuozhan.save() except Exception as e: logger.warning(f"更新商家扩展表失败: {e}") return Response({'code': 0, 'msg': '处理成功'}) class KefuPunishView(APIView): """ 客服处罚打手接口 请求:POST /yonghu/kefuchufa 参数:{ "phone": "客服手机号", "dingdan_id": "订单ID", "reason": "处罚原因(可选)" } 认证:JWT 返回:code=0 成功 """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): # 1. 获取参数 phone = request.data.get('phone', '').strip() dingdan_id = request.data.get('dingdan_id', '').strip() reason = request.data.get('reason', '').strip() if not phone or not dingdan_id: return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST) # 2. 客服身份验证 current_user = request.user if getattr(current_user, 'Phone', '') != phone: logger.warning(f"手机号不匹配: {phone}") return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) if current_user.UserType not in ('kefu', 'admin'): logger.warning(f"用户类型非客服: {current_user.UserUID}") return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser kefu = None if not is_admin: try: kefu = current_user.KefuProfile except ObjectDoesNotExist: logger.warning(f"客服扩展表不存在: {current_user.UserUID}") return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) if kefu.zhuangtai != 1: logger.warning(f"客服账号已禁用: {current_user.UserUID}") return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED) # 3. 查询订单 try: order = Order.query.get(OrderID=dingdan_id) except Order.DoesNotExist: return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) # 4. 获取打手ID dashou_id = order.PlayerID if not dashou_id: return Response({'code': 400, 'msg': '订单未接单,无法处罚打手'}, status=status.HTTP_400_BAD_REQUEST) # 5. 查询打手 try: dashou_user = User.query.select_related('DashouProfile').get(UserUID=dashou_id) dashou = dashou_user.DashouProfile except User.DoesNotExist: return Response({'code': 404, 'msg': '打手用户不存在'}, status=status.HTTP_404_NOT_FOUND) except ObjectDoesNotExist: return Response({'code': 400, 'msg': '该用户不是打手'}, status=status.HTTP_400_BAD_REQUEST) # 6. 使用事务 from jituan.services.club_penalty import resolve_penalty_record_club_id with transaction.atomic(): # 创建处罚记录(积分扣除5分) jifen = 5 chufa = PenaltyRecord.query.create( PlayerID=dashou_id, ApplicantID=phone, # 客服手机号作为请求人 PenaltyReason=reason or '', ApplyStatus=0, # 待处理 DeductedPoints=jifen, OrderID=dingdan_id, ClubID=resolve_penalty_record_club_id( order_id=dingdan_id, player_id=dashou_id, request=request, user=request.user, ), ) # 扣除打手积分 if dashou.jifen >= jifen: dashou.jifen -= jifen else: dashou.jifen = 0 dashou.save() # 可选:更新订单的 clkf 字段记录处理客服 order.AssignedCS = phone order.save() return Response({'code': 0, 'msg': '处罚成功'})