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, Prefetch 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 KhpzhqView(APIView): """ 获取所有考核配置数据(称号列表、板块、费用) POST /houtai/khpzhq """ permission_classes = [IsAuthenticated] def post(self, request): try: username_from_frontend = request.data.get('phone', None) kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) if kefu_obj is None: return permissions if 'kaohepeizhi' not in permissions: return Response({'code': 403, 'msg': '无考核配置权限'}, status=403) bankuai_list = list(Bankuai.query.values('bankuai_id', 'mingcheng')) chenghao_qs = Chenghao.query.prefetch_related( Prefetch('kaohecishufeiyong_set', queryset=KaoheCishuFeiyong.objects.order_by('cishu')) ).all() chenghao_list = [] for ch in chenghao_qs: # 获取费用:按次数排序,转为 {cishu: feiyong} 字典 # prefetch_related 已用 Prefetch 排序,直接遍历不会绕过缓存 feiyong_dict = { item.cishu: float(item.feiyong) for item in ch.kaohecishufeiyong_set.all() } max_cishu = max(feiyong_dict.keys()) if feiyong_dict else 0 chenghao_list.append({ 'id': ch.id, 'mingcheng': ch.mingcheng, 'leixing': ch.leixing, 'leixing_name': ch.get_leixing_display(), 'bankuai_id': ch.bankuai_id, 'tubiao_url': ch.tubiao_url or '', 'texiao_miaoshu': ch.texiao_miaoshu or '', 'CreateTime': ch.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if ch.CreateTime else '', 'kaohe_guize': ch.kaohe_guize or '', 'kaioi_jinpai': ch.kaioi_jinpai, 'feiyong': feiyong_dict, # {1:100.0, 2:200.0} 'max_cishu': max_cishu }) return Response({ 'code': 0, 'msg': 'ok', 'data': { 'bankuai': bankuai_list, 'chenghao': chenghao_list } }) except Exception as e: logger.exception("KhpzhqView 接口错误") return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) _CHENGHAO_LEIXING_OK = frozenset({'dashou', 'boss', 'shangjia', 'guanshi', 'zuzhang'}) _CHENGHAO_LEIXING_ALIAS = { '打手': 'dashou', '老板': 'boss', '商家': 'shangjia', '管事': 'guanshi', '组长': 'zuzhang', } def _normalize_chenghao_leixing(raw, default='dashou'): """兼容 null / 中文显示名 / 首尾空格,统一为合法 leixing 代码。""" if raw is None or raw == '': return default s = str(raw).strip() if not s: return default if s in _CHENGHAO_LEIXING_OK: return s mapped = _CHENGHAO_LEIXING_ALIAS.get(s) or _CHENGHAO_LEIXING_ALIAS.get(s.lower()) if mapped: return mapped lower = s.lower() if lower in _CHENGHAO_LEIXING_OK: return lower return None def _parse_optional_bankuai_id(raw): """空值视为未绑定板块;非法/不存在返回 False。""" if raw is None or raw == '': return None try: bid = int(raw) except (TypeError, ValueError): return False if bid <= 0: return None if not Bankuai.query.filter(bankuai_id=bid).exists(): return False return bid def _parse_chenghao_id(raw): if raw is None or str(raw).strip() == '': return None try: return int(raw) except (TypeError, ValueError): return None class ChzsgcView(APIView): """ 称号(标签)增删改查统一接口 POST /houtai/chzsgc """ permission_classes = [IsAuthenticated] def post(self, request): try: username_from_frontend = request.data.get('phone', None) kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) if kefu_obj is None: return permissions if 'kaohepeizhi' not in permissions: return Response({'code': 403, 'msg': '无考核配置权限'}, status=403) action = request.data.get('action') if not action: return Response({'code': 1, 'msg': '缺少 action 参数'}, status=400) # ========== 创建称号 ========== if action == 'create': mingcheng = (request.data.get('mingcheng') or '').strip() leixing = _normalize_chenghao_leixing(request.data.get('leixing'), default='dashou') bankuai_id = _parse_optional_bankuai_id(request.data.get('bankuai_id')) texiao_miaoshu = request.data.get('texiao_miaoshu', '') or '' kaohe_guize = request.data.get('kaohe_guize', '') or '' kaioi_jinpai = bool(request.data.get('kaioi_jinpai', False)) feiyong_list = request.data.get('feiyong_list', []) if not mingcheng: return Response({'code': 1, 'msg': '称号名称不能为空'}, status=400) if leixing is None: logger.warning('ChzsgcView create 无效 leixing=%r', request.data.get('leixing')) return Response({'code': 1, 'msg': '无效的称号类型'}, status=400) if bankuai_id is False: return Response({'code': 1, 'msg': '板块不存在'}, status=400) if Chenghao.query.filter(mingcheng=mingcheng).exists(): return Response({'code': 1, 'msg': '称号名称已存在'}, status=400) with transaction.atomic(): ch = Chenghao.query.create( mingcheng=mingcheng, leixing=leixing, bankuai_id=bankuai_id, texiao_miaoshu=texiao_miaoshu, kaohe_guize=kaohe_guize, kaioi_jinpai=kaioi_jinpai ) # 处理费用:前端传来 [{cishu:1, feiyong:100}, ...] if feiyong_list: for item in feiyong_list: cishu = int(item.get('cishu', 1)) feiyong = float(item.get('feiyong', 0)) KaoheCishuFeiyong.query.update_or_create( chenghao=ch, cishu=cishu, defaults={'feiyong': feiyong} ) else: # 默认添加第一次免费 KaoheCishuFeiyong.query.create(chenghao=ch, cishu=1, feiyong=0) return Response({'code': 0, 'msg': '创建成功', 'data': {'chenghao_id': ch.id}}) # ========== 修改称号 ========== elif action == 'update': ch_id = _parse_chenghao_id(request.data.get('chenghao_id')) if not ch_id: return Response({'code': 1, 'msg': '缺少称号ID'}, status=400) try: ch = Chenghao.query.get(id=ch_id) except Chenghao.DoesNotExist: return Response({'code': 1, 'msg': '称号不存在'}, status=404) mingcheng = (request.data.get('mingcheng') if request.data.get('mingcheng') is not None else ch.mingcheng) or '' mingcheng = str(mingcheng).strip() raw_leixing = request.data.get('leixing', ch.leixing) leixing = _normalize_chenghao_leixing(raw_leixing, default=ch.leixing or 'dashou') if 'bankuai_id' in request.data: bankuai_id = _parse_optional_bankuai_id(request.data.get('bankuai_id')) else: bankuai_id = ch.bankuai_id texiao_miaoshu = request.data.get('texiao_miaoshu', ch.texiao_miaoshu) kaohe_guize = request.data.get('kaohe_guize', ch.kaohe_guize) kaioi_jinpai = request.data.get('kaioi_jinpai', ch.kaioi_jinpai) if not mingcheng: return Response({'code': 1, 'msg': '称号名称不能为空'}, status=400) if leixing is None: logger.warning('ChzsgcView update 无效 leixing=%r ch_id=%s', raw_leixing, ch_id) return Response({'code': 1, 'msg': '无效的称号类型'}, status=400) if bankuai_id is False: return Response({'code': 1, 'msg': '板块不存在'}, status=400) if Chenghao.query.filter(mingcheng=mingcheng).exclude(id=ch_id).exists(): return Response({'code': 1, 'msg': '称号名称已存在'}, status=400) ch.mingcheng = mingcheng ch.leixing = leixing ch.bankuai_id = bankuai_id ch.texiao_miaoshu = texiao_miaoshu ch.kaohe_guize = kaohe_guize ch.kaioi_jinpai = bool(kaioi_jinpai) ch.save() # 如果前端传递了 feiyong_list,则全量替换费用 feiyong_list = request.data.get('feiyong_list') if feiyong_list is not None: with transaction.atomic(): ch.kaohecishufeiyong_set.all().delete() for item in feiyong_list: KaoheCishuFeiyong.query.create( chenghao=ch, cishu=int(item.get('cishu', 1)), feiyong=float(item.get('feiyong', 0)) ) return Response({'code': 0, 'msg': '修改成功'}) # ========== 删除称号 ========== elif action == 'delete': ch_id = _parse_chenghao_id(request.data.get('chenghao_id')) if not ch_id: return Response({'code': 1, 'msg': '缺少称号ID'}, status=400) try: ch = Chenghao.query.get(id=ch_id) except Chenghao.DoesNotExist: return Response({'code': 1, 'msg': '称号不存在'}, status=404) ch.delete() return Response({'code': 0, 'msg': '删除成功'}) # ========== 添加/修改某次费用 ========== elif action in ('add_feiyong', 'update_feiyong'): ch_id = _parse_chenghao_id(request.data.get('chenghao_id')) cishu = request.data.get('cishu') feiyong = request.data.get('feiyong', 0) if not ch_id or cishu is None or cishu == '': return Response({'code': 1, 'msg': '缺少称号ID或次数'}, status=400) try: ch = Chenghao.query.get(id=ch_id) except Chenghao.DoesNotExist: return Response({'code': 1, 'msg': '称号不存在'}, status=404) if int(cishu) < 1: return Response({'code': 1, 'msg': '次数必须>=1'}, status=400) KaoheCishuFeiyong.query.update_or_create( chenghao=ch, cishu=int(cishu), defaults={'feiyong': feiyong} ) return Response({'code': 0, 'msg': '添加/修改费用成功'}) # ========== 删除某次费用 ========== elif action == 'remove_feiyong': ch_id = _parse_chenghao_id(request.data.get('chenghao_id')) cishu = request.data.get('cishu') if not ch_id or cishu is None or cishu == '': return Response({'code': 1, 'msg': '缺少称号ID或次数'}, status=400) if int(cishu) == 1: return Response({'code': 1, 'msg': '不能删除第1次考核费用'}, status=400) try: ch = Chenghao.query.get(id=ch_id) except Chenghao.DoesNotExist: return Response({'code': 1, 'msg': '称号不存在'}, status=404) KaoheCishuFeiyong.query.filter(chenghao=ch, cishu=int(cishu)).delete() return Response({'code': 0, 'msg': '删除成功'}) else: return Response({'code': 1, 'msg': '未知操作'}, status=400) except Exception as e: logger.exception("ChzsgcView 接口错误") return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500) # ==================== 后端 views.py ==================== # 请确保已有以下导入,如果没有请在文件头部添加