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
491 lines
20 KiB
Python
491 lines
20 KiB
Python
import io
|
||
import os
|
||
import re
|
||
import time
|
||
import random
|
||
import secrets
|
||
import hashlib
|
||
import threading
|
||
import urllib.parse
|
||
from decimal import Decimal, InvalidOperation
|
||
|
||
from django.conf import settings
|
||
from django.db import transaction, connection
|
||
from django.db.models import Q, F, Max, Prefetch
|
||
from gvsdsdk.fluent import db, func, FQ
|
||
from django.core.cache import cache
|
||
from django.core.paginator import Paginator
|
||
from django.utils import timezone
|
||
|
||
from rest_framework.views import APIView
|
||
from rest_framework.response import Response
|
||
from rest_framework import status, permissions
|
||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||
from rest_framework.throttling import AnonRateThrottle, SimpleRateThrottle
|
||
|
||
from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image
|
||
from utils.weixin_broadcast import WeixinBroadcastSender
|
||
from utils.weixin_token import get_weixin_mini_access_token, is_weixin_token_invalid
|
||
# from utils.ip_security import *
|
||
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
|
||
|
||
from backend.utils import update_shangjia_daily
|
||
from utils.chat_utils import subscribe_merchant_link_chat
|
||
from utils.pdd_order_validator import validate_pdd_order_id, validate_cn_mobile
|
||
|
||
from ..models import (
|
||
Gonggao, Lunbo, Tupianpeizhi, Qunpeizhi,
|
||
ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig,
|
||
MiniappScriptScene, MiniappScriptAutoReply,
|
||
)
|
||
|
||
from users.models import (
|
||
AdminProfile, UserDashou, UserBoss,
|
||
UserShangjia, UserGuanshi, UserZuzhang
|
||
)
|
||
from users.business_models import User
|
||
|
||
from orders.models import CommissionRate, Order, MerchantOrderExt
|
||
from orders.notice_tasks import dingdan_guangbo
|
||
|
||
from rank.models import Chenghao, DingdanBiaoqian
|
||
|
||
from products.models import ShangpinLeixing, Huiyuangoumai
|
||
|
||
from ..serializers import PopupConfigSerializer
|
||
import traceback
|
||
import requests
|
||
|
||
import logging
|
||
logger = logging.getLogger(__name__)
|
||
|
||
class ShangjiaMobanListView(APIView):
|
||
"""
|
||
商家订单模板列表接口
|
||
POST /peizhi/sjddmblb
|
||
权限: JWT认证
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
user = request.user
|
||
yonghu_id = user.UserUID
|
||
|
||
leixing_id = request.data.get('shangpinTypeId')
|
||
if not leixing_id:
|
||
return Response({'code': 400, 'msg': '商品类型ID不能为空', 'data': {}}, status=400)
|
||
|
||
get_type = int(request.data.get('getType', 2))
|
||
page = int(request.data.get('page', 1))
|
||
page_size = int(request.data.get('pageSize', 50))
|
||
|
||
keyword = request.data.get('keyword', '')
|
||
min_price = request.data.get('minPrice', '')
|
||
label_id = request.data.get('labelId', None)
|
||
if label_id is not None:
|
||
try:
|
||
label_id = int(label_id)
|
||
except (ValueError, TypeError):
|
||
label_id = None
|
||
|
||
query = Q(UserID=yonghu_id, ProductTypeID=leixing_id)
|
||
|
||
if get_type == 1: # 搜索模式
|
||
if keyword:
|
||
keyword = str(keyword).strip()
|
||
try:
|
||
mid = int(keyword)
|
||
query &= Q(id=mid) | Q(TemplateDesc__icontains=keyword)
|
||
except ValueError:
|
||
query &= Q(TemplateDesc__icontains=keyword)
|
||
if min_price:
|
||
try:
|
||
query &= Q(Price__gte=Decimal(str(min_price)))
|
||
except:
|
||
pass
|
||
if label_id:
|
||
query &= Q(TitleID=label_id)
|
||
|
||
mobans = ShangjiaMoban.query.filter(query).order_by('-id')
|
||
formatted = []
|
||
for moban in mobans:
|
||
texiao_json = ''
|
||
label_name = ''
|
||
if moban.TitleID:
|
||
try:
|
||
ch = Chenghao.query.get(id=moban.TitleID)
|
||
label_name = ch.mingcheng
|
||
texiao_json = ch.texiao_miaoshu or ''
|
||
except Chenghao.DoesNotExist:
|
||
pass
|
||
formatted.append({
|
||
'mobanId': moban.id,
|
||
'shangpinTypeId': moban.ProductTypeID,
|
||
'jieshao': moban.TemplateDesc,
|
||
'jiage': str(moban.Price),
|
||
'fabushuliang': moban.PublishedCount,
|
||
'commissionEnabled': moban.CommissionGrabEnabled,
|
||
'commissionValue': str(moban.CommissionPrice) if moban.CommissionPrice is not None else None,
|
||
'labelId': moban.TitleID,
|
||
'labelName': label_name,
|
||
'texiaoJson': texiao_json,
|
||
'createTime': moban.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if moban.CreateTime else ''
|
||
})
|
||
return Response({'code': 200, 'msg': '搜索成功', 'data': {'list': formatted, 'total': len(formatted), 'hasMore': False}})
|
||
|
||
# 分页模式
|
||
queryset = ShangjiaMoban.query.filter(query).order_by('-id')
|
||
paginator = Paginator(queryset, page_size)
|
||
if page < 1 or page > paginator.num_pages:
|
||
return Response({'code': 400, 'msg': '页码超出范围', 'data': {}}, status=400)
|
||
|
||
current_page = paginator.page(page)
|
||
# 批量预取所有称号,避免循环内 N+1
|
||
_mobans_list = list(current_page.object_list)
|
||
_title_ids = [m.TitleID for m in _mobans_list if m.TitleID]
|
||
_ch_map = {
|
||
ch.id: ch
|
||
for ch in Chenghao.query.filter(id__in=_title_ids).only('id', 'mingcheng', 'texiao_miaoshu')
|
||
} if _title_ids else {}
|
||
formatted = []
|
||
for moban in _mobans_list:
|
||
texiao_json = ''
|
||
label_name = ''
|
||
ch = _ch_map.get(moban.TitleID)
|
||
if ch:
|
||
label_name = ch.mingcheng
|
||
texiao_json = ch.texiao_miaoshu or ''
|
||
formatted.append({
|
||
'mobanId': moban.id,
|
||
'shangpinTypeId': moban.ProductTypeID,
|
||
'jieshao': moban.TemplateDesc,
|
||
'jiage': str(moban.Price),
|
||
'fabushuliang': moban.PublishedCount,
|
||
'commissionEnabled': moban.CommissionGrabEnabled,
|
||
'commissionValue': str(moban.CommissionPrice) if moban.CommissionPrice is not None else None,
|
||
'labelId': moban.TitleID,
|
||
'labelName': label_name,
|
||
'texiaoJson': texiao_json,
|
||
'createTime': moban.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if moban.CreateTime else ''
|
||
})
|
||
|
||
has_more = current_page.has_next()
|
||
if len(formatted) < page_size:
|
||
has_more = False
|
||
|
||
return Response({
|
||
'code': 200,
|
||
'msg': '获取成功',
|
||
'data': {
|
||
'list': formatted,
|
||
'total': paginator.count,
|
||
'hasMore': has_more,
|
||
'currentPage': page,
|
||
'pageSize': page_size,
|
||
'totalPages': paginator.num_pages
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取模板列表失败: {str(e)}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500)
|
||
|
||
|
||
|
||
|
||
|
||
class ShangjiaTianjiaMobanView(APIView):
|
||
"""
|
||
商家添加订单模板接口
|
||
POST /peizhi/sjtjddmb
|
||
权限: JWT认证
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
# 1. 获取当前商家用户ID
|
||
user = request.user
|
||
yonghu_id = user.UserUID
|
||
|
||
# 2. 接收并清洗参数,避免类型错误
|
||
leixing_id = request.data.get('shangpinTypeId')
|
||
jieshao = request.data.get('jieshao', '').strip() if request.data.get('jieshao') else ''
|
||
jiage = request.data.get('jiage', '')
|
||
# 佣金相关
|
||
commission_enabled = request.data.get('commissionEnabled', False)
|
||
commission_value = request.data.get('commissionValue', None)
|
||
# 标签ID(称号ID)
|
||
label_id = request.data.get('labelId', None)
|
||
|
||
# 3. 必填验证
|
||
if not leixing_id or not jieshao or not jiage:
|
||
logger.warning(f"参数缺失 - leixing_id:{leixing_id}, jieshao:{jieshao}, jiage:{jiage}")
|
||
return Response({
|
||
'code': 400,
|
||
'msg': '缺少必填字段(订单类型、介绍、价格)',
|
||
'data': {}
|
||
}, status=400)
|
||
|
||
# 4. 价格格式验证
|
||
try:
|
||
jiage_decimal = Decimal(str(jiage))
|
||
if jiage_decimal <= 0:
|
||
return Response({'code': 400, 'msg': '价格必须大于0', 'data': {}}, status=400)
|
||
if jiage_decimal > 10000:
|
||
return Response({'code': 400, 'msg': '价格不能超过10000元', 'data': {}}, status=400)
|
||
except (InvalidOperation, ValueError, TypeError):
|
||
return Response({'code': 400, 'msg': '价格格式错误', 'data': {}}, status=400)
|
||
|
||
# 5. 介绍长度验证
|
||
if len(jieshao) > 100:
|
||
return Response({'code': 400, 'msg': '订单介绍不能超过100字', 'data': {}}, status=400)
|
||
|
||
# 6. 佣金验证(若启用)
|
||
CommissionGrabEnabled = False
|
||
CommissionPrice = None
|
||
if commission_enabled:
|
||
CommissionGrabEnabled = True
|
||
if not commission_value:
|
||
return Response({'code': 400, 'msg': '开启佣金时,佣金金额不能为空', 'data': {}}, status=400)
|
||
try:
|
||
comm_decimal = Decimal(str(commission_value))
|
||
if comm_decimal <= 0:
|
||
return Response({'code': 400, 'msg': '佣金必须大于0', 'data': {}}, status=400)
|
||
CommissionPrice = comm_decimal
|
||
except (InvalidOperation, ValueError, TypeError):
|
||
return Response({'code': 400, 'msg': '佣金格式错误', 'data': {}}, status=400)
|
||
|
||
# 7. 标签ID清洗(前端可能传数字或字符串)
|
||
cleaned_label_id = None
|
||
if label_id is not None and str(label_id).strip():
|
||
try:
|
||
cleaned_label_id = int(label_id)
|
||
except (ValueError, TypeError):
|
||
return Response({'code': 400, 'msg': '标签ID格式错误', 'data': {}}, status=400)
|
||
|
||
# 8. 验证商品类型是否存在且可用
|
||
try:
|
||
shangpin_obj = ShangpinLeixing.query.get(
|
||
id=leixing_id,
|
||
shenhezhuangtai=1 # 审核通过
|
||
)
|
||
except ShangpinLeixing.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '商品类型不存在或已禁用', 'data': {}}, status=404)
|
||
|
||
# 9. 检查同一用户、同一类型下是否已存在相同介绍的模板
|
||
if ShangjiaMoban.query.filter(
|
||
UserID=yonghu_id,
|
||
ProductTypeID=leixing_id,
|
||
TemplateDesc=jieshao
|
||
).exists():
|
||
return Response({'code': 400, 'msg': '已存在相同介绍的模板', 'data': {}}, status=400)
|
||
|
||
# 10. 执行数据库创建(事务)
|
||
with transaction.atomic():
|
||
moban = ShangjiaMoban.query.create(
|
||
UserID=yonghu_id,
|
||
ProductTypeID=leixing_id,
|
||
TemplateDesc=jieshao,
|
||
Price=jiage_decimal,
|
||
PublishedCount=0,
|
||
CommissionGrabEnabled=CommissionGrabEnabled,
|
||
CommissionPrice=CommissionPrice,
|
||
TitleID=cleaned_label_id
|
||
)
|
||
|
||
# 11. 返回成功数据(字段与前端对齐)
|
||
return Response({
|
||
'code': 200,
|
||
'msg': '模板创建成功',
|
||
'data': {
|
||
'mobanId': moban.id,
|
||
'shangpinTypeId': leixing_id,
|
||
'jieshao': moban.TemplateDesc,
|
||
'jiage': str(moban.Price),
|
||
'fabushuliang': moban.PublishedCount,
|
||
'createTime': moban.CreateTime.strftime('%Y-%m-%d %H:%M:%S'),
|
||
'commissionEnabled': moban.CommissionGrabEnabled,
|
||
'commissionValue': str(moban.CommissionPrice) if moban.CommissionPrice is not None else None,
|
||
'labelId': moban.TitleID
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
# 打印完整错误堆栈到日志
|
||
logger.error(f"添加模板失败: {str(e)}", exc_info=True)
|
||
return Response({
|
||
'code': 500,
|
||
'msg': '服务器内部错误,添加模板失败,请联系管理员',
|
||
'data': {}
|
||
}, status=500)
|
||
|
||
|
||
|
||
|
||
class ShangjiaShanchuMobanView(APIView):
|
||
"""
|
||
删除模板
|
||
POST /peizhi/sjddmbsc
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
user = request.user
|
||
yonghu_id = user.UserUID
|
||
|
||
moban_id = request.data.get('mobanId')
|
||
leixing_id = request.data.get('shangpinTypeId')
|
||
|
||
if not all([moban_id, leixing_id]):
|
||
return Response({'code': 400, 'msg': '参数缺失', 'data': {}}, status=400)
|
||
|
||
try:
|
||
moban = ShangjiaMoban.query.get(id=moban_id, UserID=yonghu_id, ProductTypeID=leixing_id)
|
||
except ShangjiaMoban.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '模板不存在', 'data': {}}, status=404)
|
||
|
||
# 强制硬删除
|
||
moban.delete()
|
||
|
||
return Response({'code': 200, 'msg': '删除成功', 'data': {'mobanId': moban_id, 'shangpinTypeId': leixing_id}})
|
||
except Exception as e:
|
||
logger.error(f"删除模板失败: {str(e)}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500)
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class ShangjiaGengxinMobanView(APIView):
|
||
"""
|
||
商家更新订单模板接口
|
||
POST /peizhi/sjddmbgx
|
||
权限: JWT认证
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
user = request.user
|
||
yonghu_id = user.UserUID
|
||
|
||
moban_id = request.data.get('mobanId')
|
||
leixing_id = request.data.get('shangpinTypeId')
|
||
new_jieshao = request.data.get('jieshao', '').strip()
|
||
new_jiage = request.data.get('jiage', '').strip()
|
||
# 新增字段
|
||
commission_enabled = request.data.get('commissionEnabled')
|
||
commission_value = request.data.get('commissionValue')
|
||
label_id = request.data.get('labelId')
|
||
|
||
if not all([moban_id, leixing_id]):
|
||
return Response({'code': 400, 'msg': '模板ID和商品类型ID不能为空', 'data': {}}, status=400)
|
||
|
||
# 查询模板
|
||
try:
|
||
moban = ShangjiaMoban.query.get(
|
||
id=moban_id,
|
||
UserID=yonghu_id,
|
||
ProductTypeID=leixing_id
|
||
)
|
||
except ShangjiaMoban.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '模板不存在或无权修改', 'data': {}}, status=404)
|
||
|
||
update_fields = []
|
||
|
||
# 修改介绍
|
||
if new_jieshao:
|
||
if len(new_jieshao) > 100:
|
||
return Response({'code': 400, 'msg': '订单介绍不能超过100字', 'data': {}}, status=400)
|
||
if new_jieshao != moban.TemplateDesc:
|
||
moban.TemplateDesc = new_jieshao
|
||
update_fields.append('TemplateDesc')
|
||
|
||
# 修改价格
|
||
if new_jiage:
|
||
try:
|
||
jiage_decimal = Decimal(new_jiage)
|
||
if jiage_decimal <= 0 or jiage_decimal > 10000:
|
||
return Response({'code': 400, 'msg': '价格必须在0.1-10000元之间', 'data': {}}, status=400)
|
||
if jiage_decimal != moban.Price:
|
||
moban.Price = jiage_decimal
|
||
update_fields.append('Price')
|
||
except (InvalidOperation, ValueError, TypeError):
|
||
return Response({'code': 400, 'msg': '价格格式错误', 'data': {}}, status=400)
|
||
|
||
# 修改标签
|
||
if label_id is not None: # 允许传空字符串或不传,传0表示清空
|
||
new_label = int(label_id) if label_id else None
|
||
if new_label != moban.TitleID:
|
||
moban.TitleID = new_label
|
||
update_fields.append('TitleID')
|
||
|
||
# 修改佣金设置
|
||
if commission_enabled is not None:
|
||
enabled = bool(commission_enabled)
|
||
if enabled:
|
||
if not commission_value:
|
||
return Response({'code': 400, 'msg': '佣金价格不能为空', 'data': {}}, status=400)
|
||
try:
|
||
comm_decimal = Decimal(commission_value)
|
||
if comm_decimal < 0.1 or comm_decimal > 50:
|
||
return Response({'code': 400, 'msg': '佣金需在0.1-50元之间', 'data': {}}, status=400)
|
||
if moban.CommissionGrabEnabled != True or moban.CommissionPrice != comm_decimal:
|
||
moban.CommissionGrabEnabled = True
|
||
moban.CommissionPrice = comm_decimal
|
||
update_fields.extend(['CommissionGrabEnabled', 'CommissionPrice'])
|
||
except (InvalidOperation, ValueError, TypeError):
|
||
return Response({'code': 400, 'msg': '佣金格式错误', 'data': {}}, status=400)
|
||
else:
|
||
if moban.CommissionGrabEnabled != False or moban.CommissionPrice is not None:
|
||
moban.CommissionGrabEnabled = False
|
||
moban.CommissionPrice = None
|
||
update_fields.extend(['CommissionGrabEnabled', 'CommissionPrice'])
|
||
|
||
if not update_fields:
|
||
return Response({'code': 400, 'msg': '没有修改内容', 'data': {}}, status=400)
|
||
|
||
# 检查介绍唯一性
|
||
if 'TemplateDesc' in update_fields:
|
||
if ShangjiaMoban.query.filter(
|
||
UserID=yonghu_id,
|
||
ProductTypeID=leixing_id,
|
||
TemplateDesc=moban.TemplateDesc
|
||
).exclude(id=moban_id).exists():
|
||
return Response({'code': 400, 'msg': '已存在相同介绍的模板', 'data': {}}, status=400)
|
||
|
||
with transaction.atomic():
|
||
moban.save(update_fields=update_fields)
|
||
|
||
# 返回最新数据
|
||
return Response({
|
||
'code': 200,
|
||
'msg': '模板修改成功',
|
||
'data': {
|
||
'mobanId': moban.id,
|
||
'shangpinTypeId': moban.ProductTypeID,
|
||
'jieshao': moban.TemplateDesc,
|
||
'jiage': str(moban.Price),
|
||
'fabushuliang': moban.PublishedCount,
|
||
'commissionEnabled': moban.CommissionGrabEnabled,
|
||
'commissionValue': str(moban.CommissionPrice) if moban.CommissionPrice else None,
|
||
'labelId': moban.TitleID,
|
||
'updateTime': moban.UpdateTime.strftime('%Y-%m-%d %H:%M:%S')
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
logger = logging.getLogger(__name__)
|
||
logger.error(f"修改商家模板失败: {str(e)}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500)
|
||
|
||
|
||
|
||
|
||
|