兼容空 leixing/中文类型名,规范化板块与称号 ID,避免误报无效类型;修正 update_feiyong 复用逻辑。 Co-authored-by: Cursor <cursoragent@cursor.com>
410 lines
17 KiB
Python
410 lines
17 KiB
Python
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 ====================
|
||
# 请确保已有以下导入,如果没有请在文件头部添加
|
||
|
||
|
||
|
||
|