Files
Django/backend/views/titles.py
XingQue 60fdf8c427 fix: 加固称号增删改接口参数校验
兼容空 leixing/中文类型名,规范化板块与称号 ID,避免误报无效类型;修正 update_feiyong 复用逻辑。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-16 15:53:31 +08:00

410 lines
17 KiB
Python
Raw Permalink 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.
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 ====================
# 请确保已有以下导入,如果没有请在文件头部添加