import hmac import threading import traceback import uuid import hashlib import xmltodict import time import logging import requests import os import secrets import random import string from calendar import monthrange from decimal import Decimal from datetime import date, datetime, timedelta from django.utils import timezone from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from django.core.paginator import Paginator from django.db import models, transaction, IntegrityError from django.db.models import Q, Count, Sum, F, Exists, OuterRef from gvsdsdk.fluent import db, func, FQ from django.db.models.functions import TruncDate, TruncMonth, TruncYear from django.contrib.auth.hashers import make_password from django.views.decorators.csrf import csrf_exempt from django.utils.decorators import method_decorator from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.parsers import JSONParser, MultiPartParser, FormParser # 工具类导入 from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss from backend.utils import ( update_dashou_daily_by_action, update_shangjia_daily ) from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request from jituan.services.catalog_scope import block_non_group_catalog_scope from jituan.services.club_penalty import ( filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs, forbid_penalty_out_of_scope, resolve_penalty_record_club_id, ) from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta from shop.utils import update_dianpu_daily_stat from products.utils import update_shangpin_daily_stat from orders.notice_tasks import dingdan_guangbo from ..utils import ( verify_kefu_permission, PERMISSION_TO_SHENFEN, SHENFEN_PROFILE_MAP, has_fadan_view_permission, has_merchant_order_permission, check_fadan_permission, pick_order_id, pick_penalty_create_params, write_xiugai_log, XIUGAI_LEIXING_DASHOU, XIUGAI_LEIXING_GUANSHI, XIUGAI_LEIXING_SHANGJIA, XIUGAI_LEIXING_ZUZHANG, XIUGAI_LEIXING_LABEL, ) # models 集中导入 ## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构) from gvsdsdk.models import Role, Permission, RolePermission, UserRole ## backend from backend.models import WithdrawalDailyStats ## users from users.models import ( UserGuanshi, UserBoss, UserZuzhang, UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan ) from users.business_models import User ## orders from orders.models import ( CommissionRate, PlayerDeliveryImage, PenaltyRecord, OrderPlayerHistory, Order, RefundRecord, MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus ) ## shop from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi ## products from products.models import ( Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan, DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong ) ## config from config.models import ( WithdrawConfig, ShangjiaLianjie, AccountPermission, TixianQuotaDefault, DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage ) ## rank from rank.models import ( KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong, YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu ) # 序列化器 from ..serializers import PopupPageSerializer # 全局常量、日志对象 logger = logging.getLogger('houtai') SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd'] class FaKuanTongJiView(APIView): """ 罚款统计接口 POST /houtai/htglyhqcfsltj """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): username = request.data.get('username', '').strip() if not username: return Response({'code': 400, 'msg': '参数不完整'}) try: kefu, permissions = verify_kefu_permission(request, username) if kefu is None: return permissions if not has_fadan_view_permission(permissions, username): return Response({'code': 403, 'msg': '无权限查看罚款统计'}) from utils.penalty_status import penalty_platform_audit_pending_q pending_q = penalty_platform_audit_pending_q() stats = filter_penalty_qs(Penalty.query.all(), request).aggregate( total=Count('id'), daijiaona=Count('id', filter=Q(Status=1)), shensuzhong=Count('id', filter=Q(Status=3)), yijiaona=Count('id', filter=Q(Status=2)), yibohui=Count('id', filter=Q(Status=4)), pingtai_shenhe=Count('id', filter=pending_q), ) data = {k: v or 0 for k, v in stats.items()} from jituan.services.club_user_access import list_response_meta return Response({'code': 0, 'msg': '成功', 'data': {**data, **list_response_meta(request)}}) except Exception as e: logger.error(f"罚款统计异常: {traceback.format_exc()}") return Response({'code': 500, 'msg': '系统繁忙'}) class FaKuanLieBiaoView(APIView): """ 罚款列表接口 POST /houtai/hthqfklb """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): username = request.data.get('username', '').strip() if not username: return Response({'code': 400, 'msg': '参数不完整'}) try: kefu, permissions = verify_kefu_permission(request, username) if kefu is None: return permissions if not has_fadan_view_permission(permissions, username): return Response({'code': 403, 'msg': '无权限查看罚款列表'}) page = int(request.data.get('page', 1)) page_size = int(request.data.get('page_size', 20)) if page_size > 100: page_size = 100 qs = filter_penalty_qs(Penalty.query.all(), request) # 状态筛选(5=平台审核中,仅 zhuangtai=5) from utils.penalty_status import penalty_platform_audit_pending_q zhuangtai_raw = request.data.get('zhuangtai') if zhuangtai_raw is not None and str(zhuangtai_raw).strip() != '': try: zt = int(zhuangtai_raw) if zt == 5: qs = qs.filter(penalty_platform_audit_pending_q()) elif zt in (1, 2, 3, 4): qs = qs.filter(Status=zt) except (ValueError, TypeError): pass # 被处罚人ID PenalizedUserID = request.data.get('PenalizedUserID', '').strip() if PenalizedUserID: qs = qs.filter(PenalizedUserID=PenalizedUserID) # 被处罚人身份 beichufa_shenfen = request.data.get('beichufa_shenfen') if beichufa_shenfen is not None and int(beichufa_shenfen) in [1, 2, 3, 4]: qs = qs.filter(Identity=int(beichufa_shenfen)) # 模糊搜索(订单ID、被罚人ID、申请人ID) sousuo = request.data.get('sousuo', '').strip() if sousuo: qs = qs.filter( Q(RelatedOrderID__icontains=sousuo) | Q(PenalizedUserID__icontains=sousuo) | Q(ApplicantID__icontains=sousuo) ) qs = qs.order_by('-CreateTime') paginator = Paginator(qs, page_size) try: page_obj = paginator.page(page) except Exception: return Response({ 'code': 0, 'msg': '成功', 'data': {'list': [], 'total': 0, 'page': page, 'page_size': page_size} }) records = list(page_obj) penalty_ids = [r.id for r in records] # 批量获取图片并分组 zhengju_map = {} shensu_map = {} if penalty_ids: tupians = PenaltyAppealImage.query.filter( Penalty_id__in=penalty_ids ).values('Penalty_id', 'ImageURL', 'Purpose') for t in tupians: fid = t['Penalty_id'] url = t['ImageURL'] if t['Purpose'] == 1: zhengju_map.setdefault(fid, []).append(url) else: shensu_map.setdefault(fid, []).append(url) from utils.penalty_status import penalty_display_status, is_penalty_platform_audit_pending # 组装列表数据 results = [] for r in records: results.append({ 'id': r.id, 'beichufa_id': r.PenalizedUserID, 'shenfen': r.Identity, 'shenfen_text': dict(Penalty._meta.get_field('Identity').choices).get(r.Identity, ''), 'shenqing_chufa': r.ApplicantID or '', 'shenqingren_shenfen': r.ApplicantIdentity, 'chufaliyou': r.Reason or '', 'fakuanjine': str(r.FineAmount), 'guanliandingdan_id': r.RelatedOrderID or '', 'zhuangtai': r.Status, 'zhuangtai_text': penalty_display_status(r), 'platform_audit_pending': is_penalty_platform_audit_pending(r), 'yingxiang_qiangdan': r.AffectsGrabbing, 'shensuliyou': r.AppealReason or '', 'bohuiliyou': r.RejectReason or '', 'chulizhe': r.ProcessorID or '', 'chulizhe_shenfen': r.ProcessorIdentity, 'zhengju_tupian': zhengju_map.get(r.id, []), 'shensu_tupian': shensu_map.get(r.id, []), 'creat_time': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '', 'create_time': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '', 'UpdateTime': r.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if r.UpdateTime else '', }) from jituan.services.club_user_access import list_response_meta return Response({ 'code': 0, 'msg': '成功', 'data': { 'list': results, 'total': paginator.count, 'page': page, 'page_size': page_size, **list_response_meta(request), } }) except Exception as e: logger.error(f"罚款列表异常: {traceback.format_exc()}") return Response({'code': 500, 'msg': '系统繁忙'}) class FaKuanChuLiView(APIView): """ 处理罚款申诉(同意/拒绝) POST /houtai/glyclfk """ permission_classes = [IsAuthenticated] parser_classes = [MultiPartParser, FormParser, JSONParser] def post(self, request): try: username = request.data.get('username', '').strip() penalty_id = request.data.get('penalty_id') or request.data.get('fadan_id') action = request.data.get('action', '').strip() chuli_liyou = request.data.get('chuli_liyou', '').strip() if not all([username, penalty_id, action]): return Response({'code': 400, 'msg': '参数不完整'}) kefu, permissions = verify_kefu_permission(request, username) if kefu is None: return permissions with transaction.atomic(): try: penalty = Penalty.objects.select_for_update().get(id=penalty_id) except Penalty.DoesNotExist: return Response({'code': 404, 'msg': '罚单不存在'}) deny = forbid_penalty_out_of_scope(request, penalty) if deny: return deny if penalty.Status != 3: return Response({'code': 400, 'msg': '当前状态不可处理'}) if not check_fadan_permission(permissions, penalty.Identity): return Response({'code': 403, 'msg': '无权限处理该类罚单'}) # 处理上传的图片 uploaded_urls = [] files = request.FILES.getlist('tupian') or [] if not files: single = request.FILES.get('tupian') if single: files = [single] for file in files: valid, msg = validate_image(file) if not valid: return Response({'code': 400, 'msg': msg}) ext = file.name.split('.')[-1] if '.' in file.name else 'jpg' file_name = f"chufa_{penalty_id}_{int(time.time() * 1000)}.{ext}" file_path = f"chufatupian/kechufatupian/{file_name}" url = upload_to_oss(file, file_path) if not url: return Response({'code': 500, 'msg': '图片上传失败'}) uploaded_urls.append(file_path) # 更新状态 if action == 'agree': # 同意申诉 → 驳回处罚 penalty.Status = 4 penalty.RejectReason = chuli_liyou elif action == 'reject': # 拒绝申诉 → 回到待缴纳 penalty.Status = 1 penalty.RejectReason = chuli_liyou else: return Response({'code': 400, 'msg': '无效的操作类型'}) # 记录处理者(使用 request.user 的 phone) penalty.ProcessorID = request.user.Phone penalty.ProcessorIdentity = 2 # 这里假设处理者是售后身份,可根据实际业务获取 penalty.save() # 保存图片(证据图片,用途1) for relative_url in uploaded_urls: PenaltyAppealImage.query.create( Penalty_id=penalty_id, PenalizedUserID=penalty.PenalizedUserID, ImageURL=relative_url, Purpose=1 ) logger.info(f"罚款处理成功: {penalty_id}, 操作: {action}") return Response({'code': 0, 'msg': '处理成功'}) except Exception as e: logger.error(f"罚款处理异常: {traceback.format_exc()}") return Response({'code': 500, 'msg': '系统繁忙'}) class FaKuanPingTaiShenHeView(APIView): """ 平台审核商家提交的罚款(同意/驳回) POST /houtai/glyptshfk action: platform_approve | platform_reject """ permission_classes = [IsAuthenticated] parser_classes = [MultiPartParser, FormParser, JSONParser] def post(self, request): try: from utils.penalty_status import ( PENALTY_PLATFORM_AUDIT, PENALTY_PENDING_PAY, PENALTY_APPEALING, PENALTY_REJECTED, ) username = request.data.get('username', '').strip() penalty_id = request.data.get('penalty_id') or request.data.get('fadan_id') action = request.data.get('action', '').strip() chuli_liyou = request.data.get('chuli_liyou', '').strip() if not all([username, penalty_id, action]): return Response({'code': 400, 'msg': '参数不完整'}) kefu, permissions = verify_kefu_permission(request, username) if kefu is None: return permissions with transaction.atomic(): try: penalty = Penalty.objects.select_for_update().get(id=penalty_id) except Penalty.DoesNotExist: return Response({'code': 404, 'msg': '罚单不存在'}) deny = forbid_penalty_out_of_scope(request, penalty) if deny: return deny if penalty.Status != PENALTY_PLATFORM_AUDIT: return Response({'code': 400, 'msg': '当前状态不可平台审核'}) if not check_fadan_permission(permissions, penalty.Identity): return Response({'code': 403, 'msg': '无权限处理该类罚单'}) if action == 'platform_approve': if penalty.AppealReason: penalty.Status = PENALTY_APPEALING else: penalty.Status = PENALTY_PENDING_PAY penalty.ApproveReason = chuli_liyou or penalty.ApproveReason or '' elif action == 'platform_reject': penalty.Status = PENALTY_REJECTED penalty.RejectReason = chuli_liyou or '平台审核驳回' else: return Response({'code': 400, 'msg': '无效的操作类型'}) penalty.ProcessorID = request.user.Phone penalty.ProcessorIdentity = 3 penalty.save() logger.info(f"罚款平台审核成功: {penalty_id}, 操作: {action}") return Response({'code': 0, 'msg': '平台审核完成'}) except Exception as e: logger.error(f"罚款平台审核异常: {traceback.format_exc()}") return Response({'code': 500, 'msg': '系统繁忙'}) class FaKuanChuangJianView(APIView): """ 管理员创建罚款 POST /houtai/htfksc """ permission_classes = [IsAuthenticated] parser_classes = [MultiPartParser, FormParser, JSONParser] def post(self, request): try: username = request.data.get('username', '').strip() PenalizedUserID, Reason, FineAmount, AffectsGrabbing = pick_penalty_create_params(request.data) shenfen = request.data.get('shenfen') if not all([username, PenalizedUserID, shenfen, Reason, FineAmount is not None]): return Response({'code': 400, 'msg': '参数不完整'}) try: shenfen = int(shenfen) FineAmount = float(FineAmount) except (ValueError, TypeError): return Response({'code': 400, 'msg': '参数格式错误'}) if shenfen not in [1, 2, 3, 4]: return Response({'code': 400, 'msg': '无效的被罚人身份'}) kefu, permissions = verify_kefu_permission(request, username) if kefu is None: return permissions if not check_fadan_permission(permissions, shenfen): return Response({'code': 403, 'msg': '无权限创建该类罚单'}) # 验证用户是否存在对应的扩展表 try: user = User.query.get(UserUID=PenalizedUserID) profile_attr = SHENFEN_PROFILE_MAP.get(shenfen) if not profile_attr or not hasattr(user, profile_attr): return Response({'code': 400, 'msg': '该用户不是所选身份'}) except User.DoesNotExist: return Response({'code': 404, 'msg': '用户不存在'}) # 处理图片上传 uploaded_urls = [] files = request.FILES.getlist('tupian') or [] if not files: single = request.FILES.get('tupian') if single: files = [single] for file in files: valid, msg = validate_image(file) if not valid: return Response({'code': 400, 'msg': msg}) ext = file.name.split('.')[-1] if '.' in file.name else 'jpg' file_name = f"chufa_{int(time.time() * 1000)}.{ext}" file_path = f"chufatupian/kechufatupian/{file_name}" url = upload_to_oss(file, file_path) if not url: return Response({'code': 500, 'msg': '图片上传失败'}) uploaded_urls.append(file_path) with transaction.atomic(): penalty = Penalty.query.create( PenalizedUserID=PenalizedUserID, Identity=shenfen, ApplicantID=request.user.Phone, # 申请人(客服)手机号 ApplicantIdentity=2, # 售后身份 Reason=Reason, FineAmount=FineAmount, AffectsGrabbing=AffectsGrabbing, Status=1, # 待缴纳 ClubID=resolve_penalty_club_id( penalized_user_id=PenalizedUserID, request=request, user=request.user, ), ) for relative_url in uploaded_urls: PenaltyAppealImage.query.create( Penalty=penalty, PenalizedUserID=PenalizedUserID, ImageURL=relative_url, Purpose=1 ) from users.fadan_fenhong_utils import lock_penalty_bonus lock_penalty_bonus(penalty) logger.info(f"罚款创建成功: {penalty.id}") return Response({'code': 0, 'msg': '罚款已创建', 'data': {'id': penalty.id}}) except Exception as e: logger.error(f"创建罚款异常: {traceback.format_exc()}") return Response({'code': 500, 'msg': '系统繁忙'}) class FineApplyView(APIView): """ 客服罚款申请接口(基于订单对打手发起罚款) POST /houtai/kffkdssq 权限: JWT认证 + 客服权限验证 请求参数: username (必填) : 客服用户名(手机号) OrderID (必填) : 订单ID Reason (必填) : 罚款原因 FineAmount (必填) : 罚款金额(元) AffectsGrabbing (可选) : 是否影响抢单,1=是,0=否,默认1 """ permission_classes = [IsAuthenticated] parser_classes = [MultiPartParser, FormParser, JSONParser] def post(self, request): try: username = request.data.get('username', '').strip() OrderID = pick_order_id(request.data) _, Reason, FineAmount, AffectsGrabbing = pick_penalty_create_params(request.data) if not all([username, OrderID, Reason, FineAmount is not None]): return Response({'code': 400, 'msg': '参数不完整'}) try: FineAmount = float(FineAmount) except (ValueError, TypeError): return Response({'code': 400, 'msg': '罚款金额格式错误'}) kefu, permissions = verify_kefu_permission(request, username) if kefu is None: return permissions # 验证订单存在 try: order = Order.query.get(OrderID=OrderID) except Order.DoesNotExist: return Response({'code': 404, 'msg': '订单不存在'}) # 获取打手ID dashou_id = order.PlayerID if not dashou_id: return Response({'code': 400, 'msg': '该订单无接单打手,无法罚款'}) # 重复罚款检查 if Penalty.query.filter(PenalizedUserID=dashou_id, RelatedOrderID=OrderID).exists(): return Response({'code': 400, 'msg': '该打手已被罚款过'}) # 检查权限(打手身份 shenfen=1) if not check_fadan_permission(permissions, 1): return Response({'code': 403, 'msg': '无权限处罚打手'}) # 处理图片上传 uploaded_urls = [] files = request.FILES.getlist('tupian') or [] if not files: single = request.FILES.get('tupian') if single: files = [single] for file in files: valid, msg = validate_image(file) if not valid: return Response({'code': 400, 'msg': msg}) ext = file.name.split('.')[-1] if '.' in file.name else 'jpg' file_name = f"chufa_{int(time.time() * 1000)}.{ext}" file_path = f"chufatupian/kechufatupian/{file_name}" url = upload_to_oss(file, file_path) if not url: return Response({'code': 500, 'msg': '图片上传失败'}) uploaded_urls.append(file_path) # 创建罚单 with transaction.atomic(): penalty = Penalty.query.create( PenalizedUserID=dashou_id, ApplicantID=request.user.Phone, Identity=1, # 被处罚人身份:1 打手 Reason=Reason, FineAmount=FineAmount, RelatedOrderID=OrderID, Status=1, # 待缴纳 AffectsGrabbing=AffectsGrabbing, ApplicantIdentity=1, # 申请人身份:1 客服 ClubID=resolve_penalty_club_id( order=order, request=request, user=request.user, applicant_id=getattr(request.user, 'UserUID', None), ), ) for relative_url in uploaded_urls: PenaltyAppealImage.query.create( Penalty=penalty, PenalizedUserID=dashou_id, ImageURL=relative_url, Purpose=1 ) from users.fadan_fenhong_utils import lock_penalty_bonus lock_penalty_bonus(penalty) logger.info(f"客服罚款申请成功: 订单 {OrderID}, 打手 {dashou_id}, 金额 {FineAmount}") return Response({'code': 0, 'msg': '罚款已生成', 'data': {'id': penalty.id}}) except Exception as e: logger.error(f"客服罚款申请异常: {traceback.format_exc()}") return Response({'code': 500, 'msg': '系统繁忙'}) class PunishDashouView(APIView): """ 客服处罚打手接口(扣除积分) POST /houtai/kpcf 权限: JWT认证 + 客服权限验证 请求参数: username (必填) : 客服用户名(手机号) OrderID (必填) : 订单ID reason (可选) : 处罚原因 """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): try: username = request.data.get('username', '').strip() OrderID = request.data.get('OrderID', '').strip() reason = request.data.get('reason', '').strip() if not all([username, OrderID]): return Response({'code': 400, 'msg': '参数不完整'}) kefu, permissions = verify_kefu_permission(request, username) if kefu is None: return permissions # 检查权限(打手身份 shenfen=1) if not check_fadan_permission(permissions, 1): return Response({'code': 403, 'msg': '无权限处罚打手'}) # 查询订单 try: order = Order.query.get(OrderID=OrderID) except Order.DoesNotExist: return Response({'code': 404, 'msg': '订单不存在'}) # 获取打手ID dashou_id = order.PlayerID if not dashou_id: return Response({'code': 400, 'msg': '订单未接单,无法处罚打手'}) # 查询打手 try: dashou_user = User.query.get(UserUID=dashou_id) dashou = dashou_user.DashouProfile except User.DoesNotExist: return Response({'code': 404, 'msg': '打手用户不存在'}) except ObjectDoesNotExist: return Response({'code': 400, 'msg': '该用户不是打手'}) # 创建处罚记录(扣除积分5分) jifen = 5 with transaction.atomic(): penalty_record = PenaltyRecord.query.create( PlayerID=dashou_id, ApplicantID=username, PenaltyReason=reason or '', ApplyStatus=0, DeductedPoints=jifen, OrderID=OrderID, ClubID=resolve_penalty_record_club_id( order_id=OrderID, player_id=dashou_id, request=request, user=request.user, applicant_id=getattr(request.user, 'UserUID', None) or username, ), ) # 扣除打手积分 dashou.jifen = F('jifen') - jifen dashou.save(update_fields=['jifen']) logger.info(f"客服处罚打手成功: 打手 {dashou_id}, 订单 {OrderID}, 扣除积分 {jifen}") return Response({'code': 0, 'msg': '处罚成功', 'data': {'penalty_record_id': penalty_record.id}}) except Exception as e: logger.error(f"客服处罚打手异常: {traceback.format_exc()}") return Response({'code': 500, 'msg': '系统繁忙'})