"""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': '商品已删除'})