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
1014 lines
42 KiB
Python
1014 lines
42 KiB
Python
"""shop.views.shop_product_views - 商品类型专区与商品数据管理视图."""
|
||
# ==================== 标准库 ====================
|
||
import io
|
||
import json
|
||
import time
|
||
import random
|
||
import string
|
||
import hmac
|
||
import hashlib
|
||
import requests
|
||
import logging
|
||
import traceback
|
||
import calendar
|
||
import threading
|
||
import xmltodict
|
||
from decimal import Decimal
|
||
from collections import defaultdict
|
||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||
|
||
from django.conf import settings
|
||
from django.db import models, transaction, IntegrityError
|
||
from django.db.models import F, Sum, Count, Q, Prefetch
|
||
from gvsdsdk.fluent import db, func, FQ
|
||
from django.core.cache import cache
|
||
from django.core.paginator import Paginator, EmptyPage
|
||
from django.core.exceptions import ObjectDoesNotExist
|
||
from django.utils import timezone
|
||
from django.http import HttpResponse
|
||
|
||
from rest_framework.views import APIView
|
||
from rest_framework.response import Response
|
||
from rest_framework import status
|
||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||
from rest_framework_simplejwt.tokens import RefreshToken
|
||
|
||
from utils.oss_utils import upload_to_oss, delete_from_oss, get_oss_client
|
||
from utils.money import yuan_to_fen
|
||
|
||
from ..utils import verify_shop_permission
|
||
from shop.utils import verify_shop_permission, update_dianpu_daily_stat
|
||
from products.utils import update_shangpin_daily_stat
|
||
from orders.utils import update_daily_payout
|
||
from backend.utils import update_dashou_daily_by_action
|
||
|
||
from ..models import YonghuPingzheng, Dianpu, DianpuShouzhiMeiriTongji
|
||
|
||
from users.models import UserDashou, UserShangjia, UserGuanshi, UserZuzhang
|
||
from users.business_models import User
|
||
|
||
from shop.models import YonghuDianpuBangding, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
|
||
|
||
from products.models import ShangpinLeixing, ShangpinZhuanqu, Shangpin, Huiyuan
|
||
|
||
from orders.models import (
|
||
Order, PlatformOrderExt, PlayerDeliveryImage, RefundRecord,
|
||
OrderPlayerHistory, Penalty
|
||
)
|
||
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
class ShopProductConfigView(APIView):
|
||
"""
|
||
获取商品发布所需配置:公共类型、店铺商品类型、专区及各自商品数量
|
||
URL: /shangdian/dphqspxx
|
||
权限: JWT认证 + 店铺归属验证
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
# 1. 身份验证,获取店铺对象
|
||
frontend_user_id = request.data.get('userId')
|
||
if not frontend_user_id:
|
||
return Response({'code': 400, 'msg': '缺少必要参数 userId'})
|
||
|
||
dianpu, error = verify_shop_permission(request, frontend_user_id)
|
||
if error:
|
||
return error
|
||
|
||
# ---------- 2. 公共商品类型(需审核正常) ----------
|
||
try:
|
||
public_types_qs = ShangpinLeixing.query.filter(shenhezhuangtai=1).values(
|
||
'id', 'jieshao', 'tupian_url'
|
||
)
|
||
public_types = [{
|
||
'id': pt['id'],
|
||
'jieshao': pt['jieshao'] or '',
|
||
'tupian_url': pt['tupian_url'] or '',
|
||
} for pt in public_types_qs]
|
||
except Exception as e:
|
||
logger.exception("查询公共商品类型失败")
|
||
return Response({'code': 500, 'msg': '服务器错误'})
|
||
|
||
# ---------- 3. 店铺商品类型 ----------
|
||
try:
|
||
shop_types = ShangpinLeixingDianpu.query.filter(
|
||
dianpu_id=dianpu.id
|
||
).select_related('gonggong_leixing').order_by('-paixu')
|
||
except Exception as e:
|
||
logger.exception("查询店铺商品类型失败")
|
||
return Response({'code': 500, 'msg': '服务器错误'})
|
||
|
||
type_ids = []
|
||
shop_product_types = []
|
||
for t in shop_types:
|
||
shop_product_types.append({
|
||
'id': t.id,
|
||
'jieshao': t.jieshao or '',
|
||
'tupian_url': t.tupian_url or '',
|
||
'paixu': t.paixu,
|
||
'shangjia_zhuangtai': t.shangjia_zhuangtai,
|
||
'fengjin_zhuangtai': t.fengjin_zhuangtai,
|
||
'shenhe_zhuangtai': t.shenhe_zhuangtai,
|
||
'gonggong_leixing_id': t.gonggong_leixing_id,
|
||
})
|
||
type_ids.append(t.id)
|
||
|
||
# ---------- 4. 各类型商品数量统计 ----------
|
||
type_product_counts = {}
|
||
if type_ids:
|
||
counts = Shangpin.query.filter(
|
||
dianpu_id=dianpu.id,
|
||
dianpu_leixing_id__in=type_ids
|
||
).values('dianpu_leixing_id').annotate(cnt=Count('id'))
|
||
type_product_counts = {item['dianpu_leixing_id']: item['cnt'] for item in counts}
|
||
|
||
# ---------- 5. 专区列表 ----------
|
||
try:
|
||
zhuanqu_qs = ShangpinZhuanqu.query.filter(
|
||
dianpu_id=dianpu.id
|
||
).order_by('-paixu').values(
|
||
'id', 'mingzi', 'paixu', 'shangjia_zhuangtai',
|
||
'fengjin_zhuangtai', 'dianpu_leixing_id'
|
||
)
|
||
zhuanqu_list = []
|
||
zhuanqu_ids = []
|
||
for zq in zhuanqu_qs:
|
||
zhuanqu_list.append({
|
||
'id': zq['id'],
|
||
'mingzi': zq['mingzi'] or '',
|
||
'paixu': zq['paixu'],
|
||
'shangjia_zhuangtai': zq['shangjia_zhuangtai'],
|
||
'fengjin_zhuangtai': zq['fengjin_zhuangtai'],
|
||
'dianpu_leixing_id': zq['dianpu_leixing_id'],
|
||
})
|
||
zhuanqu_ids.append(zq['id'])
|
||
except Exception as e:
|
||
logger.exception("查询专区失败")
|
||
return Response({'code': 500, 'msg': '服务器错误'})
|
||
|
||
# ---------- 6. 各专区商品数量统计 ----------
|
||
zhuanqu_product_counts = {}
|
||
if zhuanqu_ids:
|
||
counts = Shangpin.query.filter(
|
||
dianpu_id=dianpu.id,
|
||
zhuanqu_id__in=zhuanqu_ids
|
||
).values('zhuanqu_id').annotate(cnt=Count('id'))
|
||
zhuanqu_product_counts = {item['zhuanqu_id']: item['cnt'] for item in counts}
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': 'success',
|
||
'data': {
|
||
'public_types': public_types,
|
||
'shop_product_types': shop_product_types,
|
||
'zhuanqu_list': zhuanqu_list,
|
||
'type_product_counts': type_product_counts,
|
||
'zhuanqu_product_counts': zhuanqu_product_counts,
|
||
}
|
||
})
|
||
|
||
|
||
|
||
|
||
class ShopProductModifyView(APIView):
|
||
"""店铺商品类型与专区管理(增、改、删)"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
frontend_user_id = request.data.get('userId') or request.POST.get('userId')
|
||
if not frontend_user_id:
|
||
return Response({'code': 400, 'msg': '缺少必要参数 userId'})
|
||
|
||
dianpu, error = verify_shop_permission(request, frontend_user_id)
|
||
if error:
|
||
return error
|
||
|
||
action = request.data.get('action') or request.POST.get('action')
|
||
if not action:
|
||
return Response({'code': 400, 'msg': '缺少操作类型 action'})
|
||
|
||
handlers = {
|
||
'add_type': self._add_type,
|
||
'update_type': self._update_type,
|
||
'delete_type': self._delete_type,
|
||
'add_zhuanqu': self._add_zhuanqu,
|
||
'update_zhuanqu': self._update_zhuanqu,
|
||
'delete_zhuanqu': self._delete_zhuanqu,
|
||
}
|
||
handler = handlers.get(action)
|
||
if not handler:
|
||
return Response({'code': 400, 'msg': '不支持的操作类型'})
|
||
|
||
return handler(dianpu, request)
|
||
|
||
except Exception as e:
|
||
logger.exception("ShopProductModifyView 全局异常")
|
||
return Response({'code': 500, 'msg': '服务器内部错误'})
|
||
|
||
# ---------- 商品类型操作 ----------
|
||
@transaction.atomic
|
||
def _add_type(self, dianpu, request):
|
||
"""添加店铺商品类型"""
|
||
try:
|
||
jieshao = request.data.get('jieshao') or request.POST.get('jieshao')
|
||
gonggong_leixing_id = request.data.get('gonggong_leixing_id') or request.POST.get('gonggong_leixing_id')
|
||
paixu = request.data.get('paixu', 0) or request.POST.get('paixu', 0)
|
||
|
||
if not jieshao or not gonggong_leixing_id:
|
||
return Response({'code': 400, 'msg': '缺少介绍或公共类型ID'})
|
||
|
||
# 验证公共类型存在且正常
|
||
if not ShangpinLeixing.query.filter(id=gonggong_leixing_id, shenhezhuangtai=1).exists():
|
||
return Response({'code': 400, 'msg': '所选公共商品类型无效'})
|
||
|
||
image_file = request.FILES.get('image')
|
||
if not image_file:
|
||
return Response({'code': 400, 'msg': '必须上传类型图片'})
|
||
|
||
# 上传图片
|
||
file_path = f"dianpu/shangpin/shangpinleixing/{dianpu.id}_{int(time.time())}.png"
|
||
full_url = upload_to_oss(image_file, file_path)
|
||
if not full_url:
|
||
return Response({'code': 500, 'msg': '图片上传失败'})
|
||
|
||
# 提取相对路径
|
||
tupian_url = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1)
|
||
|
||
# 审核状态
|
||
audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first()
|
||
shenhe_status = not (audit_config and audit_config.kaiqi_shenhe)
|
||
|
||
ShangpinLeixingDianpu.query.create(
|
||
dianpu_id=dianpu.id,
|
||
gonggong_leixing_id=gonggong_leixing_id,
|
||
jieshao=jieshao,
|
||
tupian_url=tupian_url,
|
||
paixu=paixu,
|
||
shangjia_zhuangtai=True,
|
||
fengjin_zhuangtai=False,
|
||
shenhe_zhuangtai=shenhe_status,
|
||
)
|
||
return Response({'code': 0, 'msg': '商品类型添加成功'})
|
||
except Exception as e:
|
||
logger.exception("添加商品类型失败")
|
||
return Response({'code': 500, 'msg': '添加失败,请稍后重试'})
|
||
|
||
@transaction.atomic
|
||
def _update_type(self, dianpu, request):
|
||
"""修改店铺商品类型,支持更换图片(先删旧图)"""
|
||
type_id = request.data.get('type_id') or request.POST.get('type_id')
|
||
if not type_id:
|
||
return Response({'code': 400, 'msg': '缺少 type_id'})
|
||
|
||
try:
|
||
obj = ShangpinLeixingDianpu.objects.select_for_update().get(
|
||
id=type_id, dianpu_id=dianpu.id
|
||
)
|
||
except ShangpinLeixingDianpu.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '商品类型不存在'})
|
||
except Exception as e:
|
||
logger.exception("查询商品类型异常")
|
||
return Response({'code': 500, 'msg': '服务器错误'})
|
||
|
||
try:
|
||
update_fields = []
|
||
content_changed = False
|
||
|
||
# 简介
|
||
if 'jieshao' in request.data or 'jieshao' in request.POST:
|
||
obj.jieshao = request.data.get('jieshao') or request.POST.get('jieshao')
|
||
update_fields.append('jieshao')
|
||
content_changed = True
|
||
|
||
# 映射公共类型
|
||
if 'gonggong_leixing_id' in request.data or 'gonggong_leixing_id' in request.POST:
|
||
new_id = request.data.get('gonggong_leixing_id') or request.POST.get('gonggong_leixing_id')
|
||
if not ShangpinLeixing.query.filter(id=new_id, shenhezhuangtai=1).exists():
|
||
return Response({'code': 400, 'msg': '公共商品类型无效'})
|
||
obj.gonggong_leixing_id = new_id
|
||
update_fields.append('gonggong_leixing_id')
|
||
content_changed = True
|
||
|
||
# 排序
|
||
if 'paixu' in request.data or 'paixu' in request.POST:
|
||
obj.paixu = request.data.get('paixu') or request.POST.get('paixu')
|
||
update_fields.append('paixu')
|
||
|
||
# 上架状态
|
||
if 'shangjia_zhuangtai' in request.data or 'shangjia_zhuangtai' in request.POST:
|
||
val = request.data.get('shangjia_zhuangtai') or request.POST.get('shangjia_zhuangtai')
|
||
if isinstance(val, str):
|
||
val = val.lower() in ('true', '1', 'yes')
|
||
obj.shangjia_zhuangtai = bool(val)
|
||
update_fields.append('shangjia_zhuangtai')
|
||
|
||
# 图片更换:先删除旧图,再上传新图
|
||
image_file = request.FILES.get('image')
|
||
if image_file:
|
||
if obj.tupian_url:
|
||
delete_from_oss(obj.tupian_url) # 删除旧图片
|
||
file_path = f"dianpu/shangpin/shangpinleixing/{dianpu.id}_{int(time.time())}.png"
|
||
full_url = upload_to_oss(image_file, file_path)
|
||
if not full_url:
|
||
return Response({'code': 500, 'msg': '图片上传失败'})
|
||
obj.tupian_url = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1)
|
||
update_fields.append('tupian_url')
|
||
content_changed = True
|
||
|
||
# 审核重置
|
||
if content_changed:
|
||
audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first()
|
||
if audit_config and audit_config.kaiqi_shenhe:
|
||
obj.shenhe_zhuangtai = False
|
||
update_fields.append('shenhe_zhuangtai')
|
||
|
||
if update_fields:
|
||
obj.save(update_fields=update_fields)
|
||
return Response({'code': 0, 'msg': '商品类型更新成功'})
|
||
except Exception as e:
|
||
logger.exception("修改商品类型失败")
|
||
return Response({'code': 500, 'msg': '修改失败,请稍后重试'})
|
||
|
||
@transaction.atomic
|
||
def _delete_type(self, dianpu, request):
|
||
"""删除店铺商品类型,可选是否同时删除关联商品"""
|
||
type_id = request.data.get('id') or request.POST.get('id')
|
||
if not type_id:
|
||
return Response({'code': 400, 'msg': '缺少类型ID'})
|
||
|
||
try:
|
||
obj = ShangpinLeixingDianpu.objects.select_for_update().get(
|
||
id=type_id, dianpu_id=dianpu.id
|
||
)
|
||
except ShangpinLeixingDianpu.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '商品类型不存在'})
|
||
except Exception as e:
|
||
logger.exception("查询商品类型异常")
|
||
return Response({'code': 500, 'msg': '服务器错误'})
|
||
|
||
try:
|
||
delete_products = request.data.get('delete_products') or request.POST.get('delete_products')
|
||
delete_products = str(delete_products).lower() in ('true', '1', 'yes')
|
||
|
||
if delete_products:
|
||
# 删除该类型下所有关联商品(同时清理图片)
|
||
products = Shangpin.query.filter(
|
||
dianpu_leixing_id=obj.id, dianpu_id=dianpu.id
|
||
)
|
||
for p in products:
|
||
if p.tupian_url:
|
||
delete_from_oss(p.tupian_url)
|
||
if p.guize_tupian:
|
||
delete_from_oss(p.guize_tupian)
|
||
p.delete()
|
||
else:
|
||
# 仅解除关联,不删商品
|
||
Shangpin.query.filter(
|
||
dianpu_leixing_id=obj.id, dianpu_id=dianpu.id
|
||
).update(dianpu_leixing=None)
|
||
|
||
# 删除类型自身图片
|
||
if obj.tupian_url:
|
||
delete_from_oss(obj.tupian_url)
|
||
obj.delete()
|
||
return Response({'code': 0, 'msg': '类型删除成功'})
|
||
except Exception as e:
|
||
logger.exception("删除商品类型失败")
|
||
return Response({'code': 500, 'msg': '删除失败,请稍后重试'})
|
||
|
||
# ---------- 专区操作 ----------
|
||
@transaction.atomic
|
||
def _add_zhuanqu(self, dianpu, request):
|
||
"""添加专区,自动从店铺商品类型获取 leixing_id(公共商品类型ID)"""
|
||
try:
|
||
mingzi = request.data.get('mingzi')
|
||
dianpu_leixing_id = request.data.get('dianpu_leixing_id')
|
||
paixu = request.data.get('paixu', 0)
|
||
|
||
if not mingzi or not dianpu_leixing_id:
|
||
return Response({'code': 400, 'msg': '缺少名称或所属商品类型'})
|
||
|
||
# 获取店铺商品类型,并拿到映射的公共类型ID
|
||
try:
|
||
shop_type = ShangpinLeixingDianpu.query.get(
|
||
id=dianpu_leixing_id, dianpu_id=dianpu.id
|
||
)
|
||
leixing_id = shop_type.gonggong_leixing_id # 这就是专区需要的 leixing_id
|
||
except ShangpinLeixingDianpu.DoesNotExist:
|
||
return Response({'code': 400, 'msg': '所选商品类型无效'})
|
||
|
||
# 验证公共类型有效
|
||
if not ShangpinLeixing.query.filter(id=leixing_id, shenhezhuangtai=1).exists():
|
||
return Response({'code': 400, 'msg': '关联的公共商品类型无效'})
|
||
|
||
ShangpinZhuanqu.query.create(
|
||
dianpu_id=dianpu.id,
|
||
mingzi=mingzi,
|
||
dianpu_leixing_id=dianpu_leixing_id,
|
||
leixing_id=leixing_id, # 自动填充
|
||
paixu=paixu,
|
||
shangjia_zhuangtai=True,
|
||
fengjin_zhuangtai=False,
|
||
)
|
||
return Response({'code': 0, 'msg': '专区添加成功'})
|
||
except Exception as e:
|
||
logger.exception("添加专区失败")
|
||
return Response({'code': 500, 'msg': '添加失败,请稍后重试'})
|
||
|
||
@transaction.atomic
|
||
def _update_zhuanqu(self, dianpu, request):
|
||
"""修改专区,若更换所属商品类型则同步更新 leixing_id"""
|
||
zhuanqu_id = request.data.get('zhuanqu_id')
|
||
if not zhuanqu_id:
|
||
return Response({'code': 400, 'msg': '缺少 zhuanqu_id'})
|
||
|
||
try:
|
||
obj = ShangpinZhuanqu.objects.select_for_update().get(
|
||
id=zhuanqu_id, dianpu_id=dianpu.id
|
||
)
|
||
except ShangpinZhuanqu.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '专区不存在'})
|
||
except Exception as e:
|
||
logger.exception("查询专区异常")
|
||
return Response({'code': 500, 'msg': '服务器错误'})
|
||
|
||
try:
|
||
update_fields = []
|
||
|
||
if 'mingzi' in request.data:
|
||
obj.mingzi = request.data.get('mingzi')
|
||
update_fields.append('mingzi')
|
||
|
||
if 'dianpu_leixing_id' in request.data:
|
||
new_id = request.data.get('dianpu_leixing_id')
|
||
try:
|
||
shop_type = ShangpinLeixingDianpu.query.get(
|
||
id=new_id, dianpu_id=dianpu.id
|
||
)
|
||
obj.dianpu_leixing_id = new_id
|
||
obj.leixing_id = shop_type.gonggong_leixing_id # 同步更新公共类型ID
|
||
update_fields.extend(['dianpu_leixing_id', 'leixing_id'])
|
||
except ShangpinLeixingDianpu.DoesNotExist:
|
||
return Response({'code': 400, 'msg': '所选商品类型无效'})
|
||
|
||
if 'paixu' in request.data:
|
||
obj.paixu = request.data.get('paixu')
|
||
update_fields.append('paixu')
|
||
|
||
if 'shangjia_zhuangtai' in request.data:
|
||
val = request.data.get('shangjia_zhuangtai')
|
||
if isinstance(val, str):
|
||
val = val.lower() in ('true', '1', 'yes')
|
||
obj.shangjia_zhuangtai = bool(val)
|
||
update_fields.append('shangjia_zhuangtai')
|
||
|
||
if update_fields:
|
||
obj.save(update_fields=update_fields)
|
||
return Response({'code': 0, 'msg': '专区更新成功'})
|
||
except Exception as e:
|
||
logger.exception("修改专区失败")
|
||
return Response({'code': 500, 'msg': '修改失败,请稍后重试'})
|
||
|
||
@transaction.atomic
|
||
def _delete_zhuanqu(self, dianpu, request):
|
||
"""删除专区,可选是否同时删除关联商品"""
|
||
zhuanqu_id = request.data.get('id')
|
||
if not zhuanqu_id:
|
||
return Response({'code': 400, 'msg': '缺少专区ID'})
|
||
|
||
try:
|
||
obj = ShangpinZhuanqu.objects.select_for_update().get(
|
||
id=zhuanqu_id, dianpu_id=dianpu.id
|
||
)
|
||
except ShangpinZhuanqu.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '专区不存在'})
|
||
except Exception as e:
|
||
logger.exception("查询专区异常")
|
||
return Response({'code': 500, 'msg': '服务器错误'})
|
||
|
||
try:
|
||
delete_products = request.data.get('delete_products')
|
||
delete_products = str(delete_products).lower() in ('true', '1', 'yes')
|
||
|
||
if delete_products:
|
||
products = Shangpin.query.filter(
|
||
zhuanqu_id=obj.id, dianpu_id=dianpu.id
|
||
)
|
||
for p in products:
|
||
if p.tupian_url:
|
||
delete_from_oss(p.tupian_url)
|
||
if p.guize_tupian:
|
||
delete_from_oss(p.guize_tupian)
|
||
p.delete()
|
||
else:
|
||
Shangpin.query.filter(
|
||
zhuanqu_id=obj.id, dianpu_id=dianpu.id
|
||
).update(zhuanqu_id=None)
|
||
|
||
obj.delete()
|
||
return Response({'code': 0, 'msg': '专区删除成功'})
|
||
except Exception as e:
|
||
logger.exception("删除专区失败")
|
||
return Response({'code': 500, 'msg': '删除失败,请稍后重试'})
|
||
|
||
|
||
|
||
class ShopProductListView(APIView):
|
||
"""
|
||
商品列表接口,支持筛选、分页。
|
||
URL: /shangdian/sdhqspsj
|
||
权限: JWT认证 + 店铺验证
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
# 1. 身份验证,获取店铺对象
|
||
frontend_user_id = request.data.get('userId')
|
||
if not frontend_user_id:
|
||
return Response({'code': 400, 'msg': '缺少必要参数 userId'})
|
||
|
||
dianpu, error = verify_shop_permission(request, frontend_user_id)
|
||
if error:
|
||
return error
|
||
|
||
# 2. 安全解析分页与筛选参数
|
||
try:
|
||
page = int(request.data.get('page', 1))
|
||
page_size = int(request.data.get('page_size', 20))
|
||
except (TypeError, ValueError):
|
||
return Response({'code': 400, 'msg': '分页参数格式错误'})
|
||
|
||
keyword = str(request.data.get('keyword', '')).strip()
|
||
shop_product_type_id = request.data.get('shop_product_type_id') # 店铺商品类型ID
|
||
zhuanqu_id = request.data.get('zhuanqu_id') # 专区ID
|
||
|
||
# 布尔型筛选字段
|
||
def parse_bool(val):
|
||
if val is None or val == '':
|
||
return None
|
||
if isinstance(val, bool):
|
||
return val
|
||
if isinstance(val, str):
|
||
return val.lower() in ('true', '1', 'yes')
|
||
return bool(val)
|
||
|
||
shenhe = parse_bool(request.data.get('shenhe_zhuangtai'))
|
||
shangjia = parse_bool(request.data.get('shangjia_zhuangtai'))
|
||
fengjin = parse_bool(request.data.get('fengjin_zhuangtai'))
|
||
|
||
# 3. 构建查询(只查当前店铺的商品)
|
||
products = Shangpin.query.filter(dianpu_id=dianpu.id).select_related(
|
||
'dianpu_leixing'
|
||
).only(
|
||
'id', 'biaoqian', 'jiage', 'kucun', 'leixing_id', 'zhuanqu_id',
|
||
'zhenshi_xiaoliang', 'duiwai_xiaoliang', 'jieshao', 'xiadan_xuzhi',
|
||
'guize_tupian', 'yaoqiuleixing', 'huiyuan_id', 'yongjin',
|
||
'tupian_url', 'ewai_dashou_fencheng', 'paixu',
|
||
'dianpu_leixing_id', 'shangjia_zhuangtai', 'fengjin_zhuangtai',
|
||
'shenhe_zhuangtai', 'CreateTime', 'UpdateTime'
|
||
)
|
||
|
||
# 筛选
|
||
if shop_product_type_id is not None:
|
||
try:
|
||
products = products.filter(dianpu_leixing_id=int(shop_product_type_id))
|
||
except (ValueError, TypeError):
|
||
return Response({'code': 400, 'msg': 'shop_product_type_id 格式错误'})
|
||
|
||
if zhuanqu_id is not None:
|
||
try:
|
||
products = products.filter(zhuanqu_id=int(zhuanqu_id))
|
||
except (ValueError, TypeError):
|
||
return Response({'code': 400, 'msg': 'zhuanqu_id 格式错误'})
|
||
|
||
if shenhe is not None:
|
||
products = products.filter(shenhe_zhuangtai=shenhe)
|
||
if shangjia is not None:
|
||
products = products.filter(shangjia_zhuangtai=shangjia)
|
||
if fengjin is not None:
|
||
products = products.filter(fengjin_zhuangtai=fengjin)
|
||
|
||
# 关键词搜索(商品ID、标题、介绍)
|
||
if keyword:
|
||
products = products.filter(
|
||
models.Q(id__icontains=keyword) |
|
||
models.Q(biaoqian__icontains=keyword) |
|
||
models.Q(jieshao__icontains=keyword)
|
||
)
|
||
|
||
# 4. 分页
|
||
total_count = products.count()
|
||
paginator = Paginator(products, page_size)
|
||
page_obj = paginator.get_page(page)
|
||
|
||
# 5. 构造返回数据
|
||
product_list = []
|
||
for p in page_obj:
|
||
product_list.append({
|
||
'id': p.id,
|
||
'biaoqian': p.biaoqian or '',
|
||
'jiage': p.jiage,
|
||
'kucun': p.kucun,
|
||
'leixing_id': p.leixing_id,
|
||
'zhuanqu_id': p.zhuanqu_id,
|
||
'zhenshi_xiaoliang': p.zhenshi_xiaoliang,
|
||
'duiwai_xiaoliang': p.duiwai_xiaoliang,
|
||
'jieshao': p.jieshao or '',
|
||
'xiadan_xuzhi': p.xiadan_xuzhi or '',
|
||
'guize_tupian': p.guize_tupian or '',
|
||
'yaoqiuleixing': p.yaoqiuleixing,
|
||
'huiyuan_id': p.huiyuan_id or '',
|
||
'yongjin': p.yongjin,
|
||
'tupian_url': p.tupian_url or '',
|
||
'ewai_dashou_fencheng': p.ewai_dashou_fencheng,
|
||
'paixu': p.paixu,
|
||
'dianpu_leixing_id': p.dianpu_leixing_id,
|
||
'shangjia_zhuangtai': p.shangjia_zhuangtai,
|
||
'fengjin_zhuangtai': p.fengjin_zhuangtai,
|
||
'shenhe_zhuangtai': p.shenhe_zhuangtai,
|
||
'CreateTime': p.CreateTime.isoformat() if p.CreateTime else '',
|
||
'UpdateTime': p.UpdateTime.isoformat() if p.UpdateTime else '',
|
||
})
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'list': product_list,
|
||
'total': total_count,
|
||
}
|
||
})
|
||
|
||
|
||
|
||
class ShopProductModifyView1(APIView):
|
||
"""
|
||
商品统一操作:修改、添加、删除
|
||
URL: /shangdian/xgdpsplx1
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
frontend_user_id = request.data.get('userId')
|
||
if not frontend_user_id:
|
||
return Response({'code': 400, 'msg': '缺少必要参数 userId'})
|
||
|
||
dianpu, error = verify_shop_permission(request, frontend_user_id)
|
||
if error:
|
||
return error
|
||
|
||
action = request.data.get('action')
|
||
if not action:
|
||
return Response({'code': 400, 'msg': '缺少操作类型 action'})
|
||
|
||
if action == 'update_product':
|
||
return self._update_product(dianpu, request.data, request.FILES)
|
||
elif action == 'add_product':
|
||
return self._add_product(dianpu, request.data, request.FILES)
|
||
elif action == 'delete_product':
|
||
return self._delete_product(dianpu, request.data)
|
||
else:
|
||
return Response({'code': 400, 'msg': '不支持的操作类型'})
|
||
|
||
# ---------- 修改商品 ----------
|
||
@transaction.atomic
|
||
def _update_product(self, dianpu, data, files):
|
||
product_id = data.get('product_id')
|
||
if not product_id:
|
||
return Response({'code': 400, 'msg': '缺少 product_id'})
|
||
|
||
try:
|
||
product = Shangpin.objects.select_for_update().get(id=product_id, dianpu_id=dianpu.id)
|
||
except Shangpin.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '商品不存在'})
|
||
|
||
update_fields = []
|
||
content_changed = False
|
||
|
||
# 辅助函数:安全转换为整数,失败则返回原值(让其报错)
|
||
def to_int(val):
|
||
try:
|
||
return int(val)
|
||
except (TypeError, ValueError):
|
||
return val
|
||
|
||
def to_float(val):
|
||
try:
|
||
return float(val)
|
||
except (TypeError, ValueError):
|
||
return val
|
||
|
||
# 标题
|
||
if 'biaoqian' in data:
|
||
product.biaoqian = data['biaoqian']
|
||
update_fields.append('biaoqian')
|
||
content_changed = True
|
||
|
||
# 价格
|
||
if 'jiage' in data:
|
||
product.jiage = to_float(data['jiage'])
|
||
update_fields.append('jiage')
|
||
content_changed = True
|
||
|
||
# 库存
|
||
if 'kucun' in data:
|
||
product.kucun = to_int(data['kucun'])
|
||
update_fields.append('kucun')
|
||
content_changed = True
|
||
|
||
# 店铺商品类型
|
||
if 'dianpu_leixing_id' in data:
|
||
new_type_id = to_int(data['dianpu_leixing_id'])
|
||
if not ShangpinLeixingDianpu.query.filter(id=new_type_id, dianpu_id=dianpu.id).exists():
|
||
return Response({'code': 400, 'msg': '无效的店铺商品类型'})
|
||
product.dianpu_leixing_id = new_type_id
|
||
update_fields.append('dianpu_leixing_id')
|
||
content_changed = True
|
||
|
||
# 专区
|
||
if 'zhuanqu_id' in data:
|
||
new_zq_id = data['zhuanqu_id']
|
||
if new_zq_id is not None and new_zq_id != '':
|
||
new_zq_id = to_int(new_zq_id)
|
||
if not ShangpinZhuanqu.query.filter(id=new_zq_id, dianpu_id=dianpu.id).exists():
|
||
return Response({'code': 400, 'msg': '无效的专区'})
|
||
product.zhuanqu_id = new_zq_id
|
||
else:
|
||
product.zhuanqu_id = None
|
||
update_fields.append('zhuanqu_id')
|
||
content_changed = True
|
||
|
||
# 介绍
|
||
if 'jieshao' in data:
|
||
product.jieshao = data['jieshao']
|
||
update_fields.append('jieshao')
|
||
content_changed = True
|
||
|
||
# 下单须知
|
||
if 'xiadan_xuzhi' in data:
|
||
product.xiadan_xuzhi = data['xiadan_xuzhi']
|
||
update_fields.append('xiadan_xuzhi')
|
||
|
||
# ★ 抢单要求类型及会员/佣金自动处理
|
||
if 'yaoqiuleixing' in data:
|
||
yqlx = to_int(data['yaoqiuleixing'])
|
||
if yqlx not in [1, 2]:
|
||
return Response({'code': 400, 'msg': '抢单要求类型无效'})
|
||
product.yaoqiuleixing = yqlx
|
||
update_fields.append('yaoqiuleixing')
|
||
content_changed = True
|
||
|
||
if yqlx == 1:
|
||
# 会员抢单:优先使用前端显式传递的非空会员ID
|
||
if 'huiyuan_id' in data and data.get('huiyuan_id'):
|
||
product.huiyuan_id = data['huiyuan_id']
|
||
update_fields.append('huiyuan_id')
|
||
product.yongjin = None
|
||
if 'yongjin' not in update_fields:
|
||
update_fields.append('yongjin')
|
||
else:
|
||
# 自动从公共类型获取会员ID,或转为佣金
|
||
try:
|
||
shop_type = ShangpinLeixingDianpu.query.select_related('gonggong_leixing').get(
|
||
id=product.dianpu_leixing_id, dianpu_id=dianpu.id
|
||
)
|
||
public_type = shop_type.gonggong_leixing
|
||
if not public_type:
|
||
raise ShangpinLeixing.DoesNotExist
|
||
auto_huiyuan_id = public_type.huiyuan_id
|
||
if auto_huiyuan_id:
|
||
product.huiyuan_id = auto_huiyuan_id
|
||
update_fields.append('huiyuan_id')
|
||
product.yongjin = None
|
||
if 'yongjin' not in update_fields:
|
||
update_fields.append('yongjin')
|
||
elif public_type.yaoqiuleixing == 2 and public_type.yongjin:
|
||
# 无会员,强制转为佣金
|
||
product.yaoqiuleixing = 2
|
||
product.yongjin = public_type.yongjin
|
||
product.huiyuan_id = None
|
||
# yaoqiuleixing 已在 update_fields 中,需要添加 huiyuan_id 和 yongjin
|
||
update_fields.append('huiyuan_id')
|
||
update_fields.append('yongjin')
|
||
else:
|
||
return Response({'code': 400, 'msg': '会员抢单缺少会员ID,公共类型也无会员ID或佣金'})
|
||
except (ShangpinLeixingDianpu.DoesNotExist, ShangpinLeixing.DoesNotExist):
|
||
return Response({'code': 400, 'msg': '店铺商品类型无效'})
|
||
elif yqlx == 2:
|
||
# 佣金抢单,清除会员ID,设置佣金
|
||
product.huiyuan_id = None
|
||
update_fields.append('huiyuan_id')
|
||
if 'yongjin' in data:
|
||
product.yongjin = to_float(data['yongjin']) or 0
|
||
else:
|
||
product.yongjin = 0
|
||
update_fields.append('yongjin')
|
||
else:
|
||
# 未修改抢单类型,保留原有独立修改逻辑
|
||
if 'huiyuan_id' in data:
|
||
product.huiyuan_id = data['huiyuan_id'] or None
|
||
update_fields.append('huiyuan_id')
|
||
content_changed = True
|
||
if 'yongjin' in data:
|
||
product.yongjin = to_float(data['yongjin']) or 0
|
||
update_fields.append('yongjin')
|
||
content_changed = True
|
||
|
||
# 打手额外分成金额
|
||
if 'ewai_dashou_fencheng' in data:
|
||
fencheng = to_float(data['ewai_dashou_fencheng']) or 0
|
||
|
||
|
||
if fencheng > (product.jiage or 0):
|
||
return Response({'code': 400, 'msg': '分成金额不能超过商品价格'})
|
||
product.ewai_dashou_fencheng = fencheng
|
||
update_fields.append('ewai_dashou_fencheng')
|
||
content_changed = True
|
||
|
||
# 排序
|
||
if 'paixu' in data:
|
||
product.paixu = to_int(data['paixu'])
|
||
update_fields.append('paixu')
|
||
|
||
# 上架状态(保持原逻辑,兼容字符串)
|
||
if 'shangjia_zhuangtai' in data:
|
||
val = data['shangjia_zhuangtai']
|
||
if isinstance(val, str):
|
||
val = val.lower() in ('true', '1', 'yes')
|
||
product.shangjia_zhuangtai = bool(val)
|
||
update_fields.append('shangjia_zhuangtai')
|
||
|
||
# ★ 商品图片真上传
|
||
image_file = files.get('image')
|
||
if image_file:
|
||
if product.tupian_url:
|
||
delete_from_oss(product.tupian_url)
|
||
file_path = f"dianpu/shangpin/shangpin/{dianpu.id}_{int(time.time())}.png"
|
||
full_url = upload_to_oss(image_file, file_path)
|
||
if not full_url:
|
||
return Response({'code': 500, 'msg': '商品图片上传失败'})
|
||
product.tupian_url = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1)
|
||
update_fields.append('tupian_url')
|
||
content_changed = True
|
||
|
||
# ★ 规则图片真上传
|
||
rule_file = files.get('rule_image')
|
||
remove_rule = data.get('remove_rule_image', '0')
|
||
if rule_file:
|
||
if product.guize_tupian:
|
||
delete_from_oss(product.guize_tupian)
|
||
file_path = f"dianpu/shangpin/shangpinguize/{dianpu.id}_{int(time.time())}.png"
|
||
full_url = upload_to_oss(rule_file, file_path)
|
||
if full_url:
|
||
product.guize_tupian = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1)
|
||
update_fields.append('guize_tupian')
|
||
content_changed = True
|
||
elif remove_rule == '1' and product.guize_tupian:
|
||
delete_from_oss(product.guize_tupian)
|
||
product.guize_tupian = None
|
||
update_fields.append('guize_tupian')
|
||
content_changed = True
|
||
|
||
if content_changed:
|
||
audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first()
|
||
if audit_config and audit_config.kaiqi_shenhe:
|
||
product.shenhe_zhuangtai = False
|
||
update_fields.append('shenhe_zhuangtai')
|
||
|
||
if update_fields:
|
||
product.save(update_fields=update_fields + ['UpdateTime'])
|
||
return Response({'code': 0, 'msg': '商品修改成功'})
|
||
|
||
# ---------- 添加商品(增强会员/佣金默认逻辑) ----------
|
||
@transaction.atomic
|
||
def _add_product(self, dianpu, data, files):
|
||
biaoqian = data.get('biaoqian')
|
||
jiage = data.get('jiage')
|
||
kucun = data.get('kucun')
|
||
dianpu_leixing_id = data.get('dianpu_leixing_id')
|
||
zhuanqu_id = data.get('zhuanqu_id')
|
||
jieshao = data.get('jieshao')
|
||
|
||
if not all([biaoqian, jiage is not None, kucun is not None, dianpu_leixing_id, zhuanqu_id, jieshao]):
|
||
return Response({'code': 400, 'msg': '必填项缺失'})
|
||
|
||
try:
|
||
shop_type = ShangpinLeixingDianpu.query.select_related('gonggong_leixing').get(id=dianpu_leixing_id, dianpu_id=dianpu.id)
|
||
except ShangpinLeixingDianpu.DoesNotExist:
|
||
return Response({'code': 400, 'msg': '无效的店铺商品类型'})
|
||
|
||
public_type = shop_type.gonggong_leixing
|
||
if not public_type or public_type.shenhezhuangtai != 1:
|
||
return Response({'code': 400, 'msg': '关联的公共商品类型无效'})
|
||
|
||
# 商品图片上传
|
||
image_file = files.get('image')
|
||
if not image_file:
|
||
return Response({'code': 400, 'msg': '必须上传商品图片'})
|
||
file_path = f"dianpu/shangpin/shangpin/{dianpu.id}_{int(time.time())}.png"
|
||
full_url = upload_to_oss(image_file, file_path)
|
||
if not full_url:
|
||
return Response({'code': 500, 'msg': '商品图片上传失败'})
|
||
tupian_url = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1)
|
||
|
||
# 规则图片
|
||
guize_tupian = None
|
||
rule_file = files.get('rule_image')
|
||
if rule_file:
|
||
file_path = f"dianpu/shangpin/shangpinguize/{dianpu.id}_{int(time.time())}.png"
|
||
full_url = upload_to_oss(rule_file, file_path)
|
||
if full_url:
|
||
guize_tupian = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1)
|
||
|
||
yaoqiuleixing = int(data.get('yaoqiuleixing', 1))
|
||
if yaoqiuleixing == 1:
|
||
huiyuan_id = data.get('huiyuan_id')
|
||
# 如果前端没传有效的会员ID,尝试从公共类型获取
|
||
if not huiyuan_id:
|
||
huiyuan_id = public_type.huiyuan_id
|
||
# 仍然没有会员ID,检查公共类型是否可转为佣金
|
||
if not huiyuan_id:
|
||
if public_type.yaoqiuleixing == 2 and public_type.yongjin:
|
||
# 自动转为佣金抢单
|
||
yaoqiuleixing = 2
|
||
yongjin = public_type.yongjin
|
||
huiyuan_id = None
|
||
else:
|
||
return Response({'code': 400, 'msg': '会员抢单缺少会员ID,且无法自动获取'})
|
||
else:
|
||
yongjin = None
|
||
else:
|
||
huiyuan_id = None
|
||
yongjin = data.get('yongjin') or public_type.yongjin or 0
|
||
|
||
ewai_fencheng = data.get('ewai_dashou_fencheng') or 0
|
||
|
||
|
||
try:
|
||
ewai_val = float(ewai_fencheng)
|
||
jiage_val = float(jiage)
|
||
if ewai_val > jiage_val:
|
||
return Response({'code': 400, 'msg': '分成金额不能超过商品价格'})
|
||
except ValueError:
|
||
# 处理转换失败的情况,比如记录日志或设为默认值
|
||
print("无法转换为数字")
|
||
|
||
|
||
|
||
|
||
'''if ewai_fencheng > jiage:
|
||
return Response({'code': 400, 'msg': '分成金额不能超过商品价格'})'''
|
||
|
||
shenhe_status = True
|
||
audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first()
|
||
if audit_config and audit_config.kaiqi_shenhe:
|
||
shenhe_status = False
|
||
|
||
product = Shangpin.query.create(
|
||
biaoqian=biaoqian,
|
||
jiage=jiage,
|
||
kucun=kucun,
|
||
dianpu_id=dianpu.id,
|
||
dianpu_leixing_id=dianpu_leixing_id,
|
||
zhuanqu_id=zhuanqu_id,
|
||
leixing_id=public_type.id,
|
||
jieshao=jieshao,
|
||
xiadan_xuzhi=data.get('xiadan_xuzhi', ''),
|
||
guize_tupian=guize_tupian,
|
||
tupian_url=tupian_url,
|
||
yaoqiuleixing=yaoqiuleixing,
|
||
huiyuan_id=huiyuan_id,
|
||
yongjin=yongjin,
|
||
ewai_dashou_fencheng=ewai_fencheng,
|
||
kaioi_ewai_dashou_fencheng=True,
|
||
paixu=data.get('paixu', 0),
|
||
shangjia_zhuangtai=data.get('shangjia_zhuangtai', True),
|
||
fengjin_zhuangtai=False,
|
||
shenhe_zhuangtai=shenhe_status,
|
||
shenhezhuangtai=1,
|
||
)
|
||
return Response({'code': 0, 'msg': '商品添加成功', 'product_id': product.id})
|
||
|
||
# ---------- 删除商品(完全不变) ----------
|
||
@transaction.atomic
|
||
def _delete_product(self, dianpu, data):
|
||
product_id = data.get('product_id')
|
||
if not product_id:
|
||
return Response({'code': 400, 'msg': '缺少 product_id'})
|
||
|
||
try:
|
||
product = Shangpin.objects.select_for_update().get(id=product_id, dianpu_id=dianpu.id)
|
||
except Shangpin.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '商品不存在'})
|
||
|
||
if product.tupian_url:
|
||
delete_from_oss(product.tupian_url)
|
||
if product.guize_tupian:
|
||
delete_from_oss(product.guize_tupian)
|
||
|
||
product.delete()
|
||
return Response({'code': 0, 'msg': '商品已删除'})
|
||
|