Files
Django/shop/views/shop_product_views.py
XingQue 6515568ec0 feat: 只看审核按俱乐部隔离,商品管理支持俱乐部视图
未绑店出货开关按 club_id 配置;后台商品列表可在具体小程序下管理并切换审核展示。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 03:20:03 +08:00

1018 lines
43 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.
"""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)
# 审核状态(按店铺所属俱乐部)
from shop.services.shenhe_config import is_kaiqi_shenhe_enabled
shop_club = getattr(dianpu, 'club_id', None) or 'xq'
shenhe_status = not is_kaiqi_shenhe_enabled(shop_club)
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:
from shop.services.shenhe_config import is_kaiqi_shenhe_enabled
shop_club = getattr(dianpu, 'club_id', None) or 'xq'
if is_kaiqi_shenhe_enabled(shop_club):
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:
from shop.services.shenhe_config import is_kaiqi_shenhe_enabled
shop_club = getattr(dianpu, 'club_id', None) or 'xq'
if is_kaiqi_shenhe_enabled(shop_club):
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
from shop.services.shenhe_config import is_kaiqi_shenhe_enabled
shop_club = getattr(dianpu, 'club_id', None) or 'xq'
if is_kaiqi_shenhe_enabled(shop_club):
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': '商品已删除'})