P1 性能优化(避免循环内 N+1 / N 次 DB 查询): - shop_order_views._batch_images: 移除 ThreadPoolExecutor 掩盖的 N 次查询,改单次批量 + Python 侧分组 - product_query.DashouHuiyuanList: 5N 查询 -> 3 次批量预取 + 本地闭包判断 - roles.GetRolePermissionView / 用户列表: 循环内 RolePermission/Permission/UserRole/Role -> 批量 __in 预取 - guanli / zuzhang 杜次分红 4.2/4.3: 循环内 update_or_create + 单条 delete -> bulk_create + bulk_update + 批量 delete - admin_config QQ 群配置: 循环内 exists/first/save/create -> 批量预取 + bulk_create + bulk_update - jituan 服务层多处合并统计查询、批量预取 map - rank 多处循环 N+1 改批量预取 - backend 多处循环内 count/create 改批量 - config/orders/merchant_ops 多处循环 N+1 改批量预取 其他改动: - users/views/kefu.py 拆分为 kefu_base/kefu_dashou/kefu_orders/kefu_punishment/kefu_withdraw 5 个文件 - 删除遗留脚本 check_prod_uid.py / create_rbac_tables.sql
363 lines
14 KiB
Python
363 lines
14 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
|
||
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 HqbkxxView(APIView):
|
||
"""
|
||
获取板块配置所需全部数据
|
||
POST /houtai/hqbkxx
|
||
Body: { "phone": "管理员手机号" }
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
# 1. 从前端获取传递的账号,用于防越权校验
|
||
username_from_frontend = request.data.get('phone', None)
|
||
|
||
# 2. 调用公共验证方法
|
||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||
|
||
# 3. 如果第一个返回值为 None,说明验证失败,直接返回第二个值
|
||
if kefu_obj is None:
|
||
return permissions
|
||
|
||
# ---------- 验证通过 ----------
|
||
# 4. 检查是否有板块管理权限
|
||
if 'bankuai' not in permissions:
|
||
return Response({'code': 403, 'msg': '无板块管理权限'}, status=403)
|
||
scope_err = block_non_group_catalog_scope(request)
|
||
if scope_err:
|
||
return Response({'code': 400, 'msg': scope_err})
|
||
|
||
# 5. 查询所有板块
|
||
bankuai_list = []
|
||
for bk in Bankuai.query.all():
|
||
bankuai_list.append({
|
||
'bankuai_id': bk.bankuai_id,
|
||
'mingcheng': bk.mingcheng,
|
||
'CreateTime': bk.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if bk.CreateTime else ''
|
||
})
|
||
|
||
# 6. 查询所有商品类型
|
||
shangpin_leixing_list = []
|
||
for sp in ShangpinLeixing.query.all():
|
||
shangpin_leixing_list.append({
|
||
'id': sp.id,
|
||
'jieshao': sp.jieshao,
|
||
'tupian_url': sp.tupian_url or '',
|
||
'bankuai_id': sp.bankuai_id
|
||
})
|
||
|
||
# 7. 查询会员(附带当前俱乐部售价)
|
||
from jituan.constants import CLUB_ID_DEFAULT, DATA_SCOPE_ALL
|
||
from jituan.models import ClubHuiyuanPrice
|
||
from jituan.services.club_context import resolve_club_id_from_request, resolve_club_scope
|
||
|
||
scope = resolve_club_scope(request)
|
||
club_id = resolve_club_id_from_request(request)
|
||
price_map = {}
|
||
if scope != DATA_SCOPE_ALL:
|
||
for row in ClubHuiyuanPrice.query.filter(club_id=club_id):
|
||
price_map[row.huiyuan_id] = row
|
||
|
||
huiyuan_list = []
|
||
# 在受限 scope 下预过滤,避免 Huiyuan 全表遍历
|
||
# 注:当 scope != DATA_SCOPE_ALL 且 club_id != CLUB_ID_DEFAULT 时,
|
||
# 只有 price_map 中存在记录的会员才可能被保留(无记录的会被跳过)
|
||
huiyuan_qs = Huiyuan.query.all()
|
||
if scope != DATA_SCOPE_ALL and club_id != CLUB_ID_DEFAULT:
|
||
huiyuan_qs = huiyuan_qs.filter(huiyuan_id__in=list(price_map.keys()))
|
||
# 仅查询所需字段,减少内存占用(dict 比 model 实例更轻)
|
||
huiyuan_qs = huiyuan_qs.values('huiyuan_id', 'jieshao', 'bankuai_id', 'jiage', 'guanshifc', 'zuzhangfc')
|
||
for hy in huiyuan_qs:
|
||
row = price_map.get(hy['huiyuan_id'])
|
||
if scope != DATA_SCOPE_ALL and club_id != CLUB_ID_DEFAULT and row is None:
|
||
continue
|
||
if row is not None and not row.is_enabled:
|
||
continue
|
||
item = {
|
||
'huiyuan_id': hy['huiyuan_id'],
|
||
'jieshao': hy['jieshao'],
|
||
'bankuai_id': hy['bankuai_id'],
|
||
'jiage': str(row.jiage) if row else str(hy['jiage']),
|
||
'guanshifc': str(row.guanshifc) if row else str(hy['guanshifc']),
|
||
'zuzhangfc': str(row.zuzhangfc) if row else str(hy['zuzhangfc']),
|
||
}
|
||
huiyuan_list.append(item)
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': 'ok',
|
||
'data': {
|
||
'bankuai': bankuai_list,
|
||
'shangpin_leixing': shangpin_leixing_list,
|
||
'huiyuan': huiyuan_list,
|
||
'club_id': club_id,
|
||
'scope': scope,
|
||
}
|
||
})
|
||
|
||
|
||
class BkxgView(APIView):
|
||
"""
|
||
板块增删改统一接口
|
||
POST /houtai/bkxg
|
||
Body: {
|
||
"phone": "管理员手机号",
|
||
"action": "create | update | delete | add_items | remove_items",
|
||
"bankuai_id": int,
|
||
"bankuai_name": str,
|
||
"item_type": "shangpin_leixing | huiyuan",
|
||
"item_ids": [1,2,3]
|
||
}
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
# 1. 从前端获取传递的账号
|
||
username_from_frontend = request.data.get('phone', None)
|
||
|
||
# 2. 调用公共验证方法
|
||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||
|
||
# 3. 验证失败直接返回
|
||
if kefu_obj is None:
|
||
return permissions
|
||
|
||
# 4. 检查板块管理权限
|
||
if 'bankuai' not in permissions:
|
||
return Response({'code': 403, 'msg': '无板块管理权限'}, status=403)
|
||
scope_err = block_non_group_catalog_scope(request)
|
||
if scope_err:
|
||
return Response({'code': 400, 'msg': scope_err})
|
||
|
||
# 5. 获取操作类型
|
||
action = request.data.get('action')
|
||
if not action:
|
||
return Response({'code': 1, 'msg': '缺少 action 参数'}, status=400)
|
||
|
||
# ==================== 创建板块 ====================
|
||
if action == 'create':
|
||
bankuai_name = request.data.get('bankuai_name', '').strip()
|
||
if not bankuai_name:
|
||
return Response({'code': 1, 'msg': '板块名称不能为空'}, status=400)
|
||
if Bankuai.query.filter(mingcheng=bankuai_name).exists():
|
||
return Response({'code': 1, 'msg': '板块名称已存在'}, status=400)
|
||
|
||
shangpin_ids = request.data.get('shangpin_ids', [])
|
||
huiyuan_ids = request.data.get('huiyuan_ids', [])
|
||
|
||
with transaction.atomic():
|
||
new_bk = Bankuai.query.create(mingcheng=bankuai_name)
|
||
if shangpin_ids:
|
||
ShangpinLeixing.query.filter(
|
||
id__in=shangpin_ids,
|
||
bankuai__isnull=True
|
||
).update(bankuai=new_bk)
|
||
if huiyuan_ids:
|
||
Huiyuan.query.filter(
|
||
huiyuan_id__in=huiyuan_ids,
|
||
bankuai__isnull=True
|
||
).update(bankuai=new_bk)
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '创建成功',
|
||
'data': {'bankuai_id': new_bk.bankuai_id}
|
||
})
|
||
|
||
# ==================== 以下操作需要 bankuai_id ====================
|
||
bankuai_id = request.data.get('bankuai_id')
|
||
if not bankuai_id:
|
||
return Response({'code': 1, 'msg': '缺少 bankuai_id'}, status=400)
|
||
|
||
try:
|
||
bk = Bankuai.query.get(bankuai_id=bankuai_id)
|
||
except Bankuai.DoesNotExist:
|
||
return Response({'code': 1, 'msg': '板块不存在'}, status=404)
|
||
|
||
# ==================== 修改板块名称 ====================
|
||
if action == 'update':
|
||
new_name = request.data.get('bankuai_name', '').strip()
|
||
if not new_name:
|
||
return Response({'code': 1, 'msg': '板块名称不能为空'}, status=400)
|
||
if Bankuai.query.filter(mingcheng=new_name).exclude(bankuai_id=bankuai_id).exists():
|
||
return Response({'code': 1, 'msg': '板块名称已存在'}, status=400)
|
||
bk.mingcheng = new_name
|
||
bk.save()
|
||
return Response({'code': 0, 'msg': '修改成功'})
|
||
|
||
# ==================== 删除板块 ====================
|
||
elif action == 'delete':
|
||
with transaction.atomic():
|
||
ShangpinLeixing.query.filter(bankuai=bk).update(bankuai=None)
|
||
Huiyuan.query.filter(bankuai=bk).update(bankuai=None)
|
||
bk.delete()
|
||
return Response({'code': 0, 'msg': '删除成功'})
|
||
|
||
# ==================== 添加绑定 ====================
|
||
elif action == 'add_items':
|
||
item_type = request.data.get('item_type')
|
||
item_ids = request.data.get('item_ids', [])
|
||
if not item_type or not item_ids:
|
||
return Response({'code': 1, 'msg': '缺少 item_type 或 item_ids'}, status=400)
|
||
|
||
if item_type == 'shangpin_leixing':
|
||
ShangpinLeixing.query.filter(
|
||
id__in=item_ids,
|
||
bankuai__isnull=True
|
||
).update(bankuai=bk)
|
||
elif item_type == 'huiyuan':
|
||
Huiyuan.query.filter(
|
||
huiyuan_id__in=item_ids,
|
||
bankuai__isnull=True
|
||
).update(bankuai=bk)
|
||
else:
|
||
return Response({'code': 1, 'msg': '无效的 item_type'}, status=400)
|
||
return Response({'code': 0, 'msg': '添加成功'})
|
||
|
||
# ==================== 移除绑定 ====================
|
||
elif action == 'remove_items':
|
||
item_type = request.data.get('item_type')
|
||
item_ids = request.data.get('item_ids', [])
|
||
if not item_type or not item_ids:
|
||
return Response({'code': 1, 'msg': '缺少 item_type 或 item_ids'}, status=400)
|
||
|
||
if item_type == 'shangpin_leixing':
|
||
ShangpinLeixing.query.filter(
|
||
id__in=item_ids,
|
||
bankuai=bk
|
||
).update(bankuai=None)
|
||
elif item_type == 'huiyuan':
|
||
Huiyuan.query.filter(
|
||
huiyuan_id__in=item_ids,
|
||
bankuai=bk
|
||
).update(bankuai=None)
|
||
else:
|
||
return Response({'code': 1, 'msg': '无效的 item_type'}, status=400)
|
||
return Response({'code': 0, 'msg': '移除成功'})
|
||
|
||
else:
|
||
return Response({'code': 1, 'msg': '未知操作'}, status=400)
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|