"""商品发布与管理后台视图。 包含商品类型/专区/商品发布、规则图片上传、会员信息查询/修改/发布、 管理员商品/公告/专区/轮播/类型修改、商品内容获取与修改、规则修改。 """ # ==================== 标准库 ==================== import os import json import time import random import string import uuid import hashlib import requests import logging import decimal from datetime import timedelta from decimal import Decimal, InvalidOperation, ROUND_CEILING import defusedxml.ElementTree as ET # ==================== Django 内置 ==================== from django.conf import settings from django.db import transaction, connection from django.db.models import Q from gvsdsdk.fluent import db, func, FQ from django.shortcuts import render, get_object_or_404 from django.http import HttpResponse from django.utils import timezone # ==================== DRF 框架 ==================== from rest_framework.views import APIView, View from rest_framework.response import Response from rest_framework import permissions, status from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.parsers import MultiPartParser, FormParser, JSONParser from rest_framework.throttling import AnonRateThrottle # ==================== 项目工具函数 ==================== from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss from utils.money import format_yuan, quantize_yuan, yuan_to_fen from backend.utils import ( update_guanshi_daily_by_action, update_guanshi_xufei_daily, update_zuzhang_daily_by_action ) from users.fadan_fenhong_utils import process_fadan_fenhong # ==================== 各App模型 ==================== from ..models import ( ShangpinLeixing, ShangpinZhuanqu, Shangpin, Huiyuan, Czjilu, Gsfenhong, Huiyuangoumai, DuociFenhong ) from users.models import ( AdminProfile, UserDashou, UserGuanshi, UserZuzhang, UserShangjia ) from users.business_models import User from orders.models import CommissionRate from config.models import Gonggao, Lunbo from orders.models import Penalty # 用于罚款类型 from jituan.services.club_config import ( get_commission_rate, get_commission_rate_object, get_huiyuan_price, get_huiyuan_fenchong, ) from jituan.services.club_context import resolve_effective_club_id from jituan.services.club_write import resolve_club_id_for_write from jituan.services.club_user import get_payment_openid from jituan.services.club_penalty import resolve_gsfenhong_club_id import traceback # ==================== 日志实例 ==================== logger = logging.getLogger(__name__) class ShangpinLeixingFabuView(APIView): """ 商品类型发布接口 接收图片、类型介绍、抢单要求类型等字段 需要管理员权限 """ permission_classes = [permissions.IsAuthenticated] parser_classes = (MultiPartParser, FormParser) # 需要处理文件上传 def post(self, request): try: # === 1. 权限验证 === # 获取当前用户 user_main = request.user # 1.1 验证前端传递的账号是否匹配(使用phone字段) zhanghao = request.data.get('zhanghao', '').strip() if not zhanghao: return Response( {'code': 401, 'msg': '未提供账号信息', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 验证账号是否匹配(使用phone字段比较) if user_main.Phone != zhanghao: return Response( {'code': 401, 'msg': '账号验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 1.2 验证是否为管理员(检查user_type字段) if user_main.UserType != 'admin': return Response( {'code': 401, 'msg': '权限不足', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 1.3 验证管理员扩展表(使用AdminProfile模型) try: admin_profile = AdminProfile.query.get(user=user_main) except AdminProfile.DoesNotExist: return Response( {'code': 401, 'msg': '管理员信息不存在', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # === 2. 接收并验证字段 === # 2.1 必填字段验证 jieshao = request.data.get('jieshao', '').strip() yaoqiuleixing = request.data.get('yaoqiuleixing') if not jieshao: return Response( {'code': 400, 'msg': '类型介绍不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) if yaoqiuleixing not in ['1', '2']: return Response( {'code': 400, 'msg': '抢单要求类型无效', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 2.2 验证图片 tupian_file = request.FILES.get('file') if not tupian_file: return Response( {'code': 400, 'msg': '请上传类型图片', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 验证图片格式和大小 is_valid, error_msg = validate_image(tupian_file) if not is_valid: return Response( {'code': 400, 'msg': f'图片验证失败: {error_msg}', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 2.3 根据抢单要求类型验证对应字段 huiyuan_id = request.data.get('huiyuan_id', '').strip() yongjin = request.data.get('yongjin', '').strip() # 转换yaoqiuleixing为整数便于处理 yaoqiuleixing_int = int(yaoqiuleixing) if yaoqiuleixing_int == 1: # 会员抢单 if not huiyuan_id: return Response( {'code': 400, 'msg': '会员抢单必须选择会员', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 验证会员ID是否存在 try: huiyuan_obj = Huiyuan.query.get(huiyuan_id=huiyuan_id) except Huiyuan.DoesNotExist: return Response( {'code': 400, 'msg': '会员ID不存在', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 会员ID为字符串,佣金为空 yongjin_value = None huiyuan_id_value = huiyuan_id else: # yaoqiuleixing_int == 2 佣金抢单 if not yongjin: return Response( {'code': 400, 'msg': '佣金抢单必须填写佣金金额', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) try: # 验证佣金为有效数字 yongjin_value = float(yongjin) if yongjin_value <= 0: return Response( {'code': 400, 'msg': '佣金金额必须大于0', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) except ValueError: return Response( {'code': 400, 'msg': '佣金金额格式错误', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) huiyuan_id_value = None # === 3. 创建商品类型记录 === with transaction.atomic(): # 3.1 创建商品类型对象 leixing_obj = ShangpinLeixing( jieshao=jieshao, yaoqiuleixing=yaoqiuleixing_int, huiyuan_id=huiyuan_id_value, yongjin=yongjin_value, # tupian_url 先留空,上传成功后再更新 ) # 保存到数据库获取ID leixing_obj.save() leixing_id = leixing_obj.id # === 4. 处理图片上传 === try: # 4.1 生成文件名和路径 file_extension = os.path.splitext(tupian_file.name)[1].lower() if not file_extension: file_extension = '.jpg' # 生成唯一文件名:商品类型ID + 随机UUID + 后缀 unique_filename = f"{leixing_id}_{uuid.uuid4().hex}{file_extension}" # 构建OSS文件路径:a_long/leixing/{leixing_id}/{filename} oss_folder = 'a_long' # 根文件夹 leixing_folder = 'leixing' # 类型文件夹 # 构建完整路径 oss_file_path = f"{oss_folder}/{leixing_folder}/{leixing_id}/{unique_filename}" # 4.2 上传到OSS tupian_url = upload_to_oss(tupian_file, oss_file_path) if not tupian_url: # 上传失败,抛出异常触发回滚 raise Exception("图片上传到OSS失败") # 4.3 更新商品类型的图片URL(存储相对路径) leixing_obj.tupian_url = oss_file_path # 存相对路径,方便拼接 leixing_obj.save() except Exception as oss_error: # 图片上传失败,事务会回滚,商品类型记录也会被删除 print(f"OSS上传失败: {oss_error}") # 手动抛出异常触发事务回滚 raise Exception(f"图片上传失败: {str(oss_error)}") # === 5. 构建返回数据 === response_data = { 'id': leixing_id, 'jieshao': jieshao, 'yaoqiuleixing': yaoqiuleixing_int, 'tupian_url': leixing_obj.tupian_url, # 相对路径 'huiyuan_id': huiyuan_id_value, 'yongjin': str(yongjin_value) if yongjin_value else None } # 返回成功响应 return Response({ 'code': 0, 'msg': '商品类型添加成功', 'data': response_data }) except Exception as e: # 捕获所有异常,返回500错误 traceback.print_exc() return Response({ 'code': 500, 'msg': f'系统错误: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class ShangpinZhuanquFabuView(APIView): """ 商品专区发布接口 接收专区名称、类型ID 需要管理员权限 """ permission_classes = [permissions.IsAuthenticated] def post(self, request): try: # === 1. 权限验证 === user_main = request.user # 1.1 验证账号(使用phone字段) zhanghao = request.data.get('zhanghao', '').strip() if not zhanghao: return Response( {'code': 401, 'msg': '未提供账号信息', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) if user_main.Phone != zhanghao: return Response( {'code': 401, 'msg': '账号验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 1.2 验证是否为管理员(检查user_type字段) if user_main.UserType != 'admin': return Response( {'code': 401, 'msg': '权限不足', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 1.3 验证管理员扩展表(使用AdminProfile模型) try: admin_profile = AdminProfile.query.get(user=user_main) except AdminProfile.DoesNotExist: return Response( {'code': 401, 'msg': '管理员信息不存在', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # === 2. 接收并验证字段 === mingzi = request.data.get('mingzi', '').strip() leixing_id = request.data.get('leixing_id') tupian_url = (request.data.get('tupian_url') or '').strip() # 2.1 验证必填字段 if not mingzi: return Response( {'code': 400, 'msg': '专区名称不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) if not leixing_id: return Response( {'code': 400, 'msg': '类型ID不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 2.2 验证类型ID是否存在 try: # 验证商品类型是否存在 leixing_obj = ShangpinLeixing.query.get(id=leixing_id) except ShangpinLeixing.DoesNotExist: return Response( {'code': 400, 'msg': '商品类型不存在', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 2.3 验证名称长度(15汉字以内) if len(mingzi) > 45: # 数据库字段最大长度 return Response( {'code': 400, 'msg': '专区名称过长', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # === 3. 创建商品专区记录 === club_id = resolve_club_id_for_write(request) zhuanqu_obj = ShangpinZhuanqu( mingzi=mingzi, leixing_id=leixing_id, club_id=club_id, tupian_url=tupian_url, ) zhuanqu_obj.save() # === 4. 构建返回数据 === response_data = { 'id': zhuanqu_obj.id, 'mingzi': mingzi, 'leixing_id': leixing_id, 'tupian_url': tupian_url, } # 返回成功响应 return Response({ 'code': 0, 'msg': '商品专区添加成功', 'data': response_data }) except Exception as e: traceback.print_exc() return Response({ 'code': 500, 'msg': f'系统错误: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class ShangpinFabuView(APIView): """ 商品发布接口 接收商品信息、图片等字段 需要管理员权限 """ permission_classes = [permissions.IsAuthenticated] parser_classes = (MultiPartParser, FormParser) # 需要处理文件上传 def post(self, request): try: # === 1. 权限验证 === user_main = request.user # 1.1 验证账号(使用phone字段) zhanghao = request.data.get('zhanghao', '').strip() if not zhanghao: return Response( {'code': 401, 'msg': '未提供账号信息', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) if user_main.Phone != zhanghao: return Response( {'code': 401, 'msg': '账号验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 1.2 验证是否为管理员(检查user_type字段) if user_main.UserType != 'admin': return Response( {'code': 401, 'msg': '权限不足', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 1.3 验证管理员扩展表(使用AdminProfile模型) try: admin_profile = AdminProfile.query.get(user=user_main) except AdminProfile.DoesNotExist: return Response( {'code': 401, 'msg': '管理员信息不存在', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # === 2. 接收并验证字段 === # 2.1 验证必填字段 biaoti = request.data.get('biaoti', '').strip() jiage = request.data.get('jiage', '').strip() kucun = request.data.get('kucun', '').strip() leixing_id = request.data.get('leixing_id') zhuanqu_id = request.data.get('zhuanqu_id') yaoqiuleixing = request.data.get('yaoqiuleixing') jieshao = request.data.get('jieshao', '').strip() # 验证图片 shangpin_tupian = request.FILES.get('shangpin_tupian') if not shangpin_tupian: return Response( {'code': 400, 'msg': '请上传商品图片', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 验证图片格式和大小 is_valid, error_msg = validate_image(shangpin_tupian) if not is_valid: return Response( {'code': 400, 'msg': f'商品图片验证失败: {error_msg}', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 2.2 验证必填字段是否为空 required_fields = { 'biaoti': biaoti, 'jiage': jiage, 'kucun': kucun, 'leixing_id': leixing_id, 'zhuanqu_id': zhuanqu_id, 'yaoqiuleixing': yaoqiuleixing, 'jieshao': jieshao } for field_name, field_value in required_fields.items(): if not field_value: return Response( {'code': 400, 'msg': f'{field_name}不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 2.3 验证数值字段 try: jiage_value = float(jiage) if jiage_value <= 0: return Response( {'code': 400, 'msg': '商品价格必须大于0', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) except ValueError: return Response( {'code': 400, 'msg': '商品价格格式错误', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) try: kucun_value = int(kucun) if kucun_value < 0: return Response( {'code': 400, 'msg': '库存不能为负数', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) except ValueError: return Response( {'code': 400, 'msg': '库存必须为整数', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 验证抢单要求类型 if yaoqiuleixing not in ['1', '2']: return Response( {'code': 400, 'msg': '抢单要求类型无效', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # === 3. 验证关联数据 === # 3.1 验证商品类型 try: leixing_obj = ShangpinLeixing.query.get(id=leixing_id) except ShangpinLeixing.DoesNotExist: return Response( {'code': 400, 'msg': '商品类型不存在', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 3.2 根据抢单要求类型验证商品类型 yaoqiuleixing_int = int(yaoqiuleixing) if yaoqiuleixing_int == 1: # 会员抢单 # 需要验证商品类型的抢单要求类型是否为1 if leixing_obj.yaoqiuleixing != 1: return Response( {'code': 400, 'msg': '商品类型与抢单要求类型不匹配', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 获取会员ID(从前端或商品类型) huiyuan_id = request.data.get('huiyuan_id', leixing_obj.huiyuan_id) if not huiyuan_id: return Response( {'code': 400, 'msg': '会员ID不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 验证会员是否存在 try: huiyuan_obj = Huiyuan.query.get(huiyuan_id=huiyuan_id) except Huiyuan.DoesNotExist: return Response( {'code': 400, 'msg': '会员不存在', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 设置会员相关字段 huiyuan_id_value = huiyuan_id yongjin_value = None else: # 佣金抢单 # 需要验证商品类型的抢单要求类型是否为2 if leixing_obj.yaoqiuleixing != 2: return Response( {'code': 400, 'msg': '商品类型与抢单要求类型不匹配', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 获取佣金(从前端或商品类型) yongjin = request.data.get('yongjin', leixing_obj.yongjin) if not yongjin: return Response( {'code': 400, 'msg': '佣金金额不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) try: yongjin_value = float(yongjin) if yongjin_value <= 0: return Response( {'code': 400, 'msg': '佣金金额必须大于0', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) except ValueError: return Response( {'code': 400, 'msg': '佣金金额格式错误', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) huiyuan_id_value = None # 3.3 验证商品专区 try: zhuanqu_obj = ShangpinZhuanqu.query.get(id=zhuanqu_id, leixing_id=leixing_id) except ShangpinZhuanqu.DoesNotExist: return Response( {'code': 400, 'msg': '商品专区不存在或与商品类型不匹配', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # === 4. 创建商品记录 === with transaction.atomic(): # 4.1 获取其他字段 xuzhi = request.data.get('xuzhi', '').strip() xiaoliang = request.data.get('xiaoliang', '0').strip() # 转换销量为整数 try: duiwai_xiaoliang_value = int(xiaoliang) if xiaoliang else 0 except ValueError: duiwai_xiaoliang_value = 0 # 4.2 创建商品对象 club_id = resolve_club_id_for_write(request) shangpin_obj = Shangpin( biaoqian=biaoti, jiage=jiage_value, kucun=kucun_value, leixing_id=leixing_id, zhuanqu_id=zhuanqu_id, duiwai_xiaoliang=duiwai_xiaoliang_value, jieshao=jieshao, xiadan_xuzhi=xuzhi, yaoqiuleixing=yaoqiuleixing_int, huiyuan_id=huiyuan_id_value, yongjin=yongjin_value, club_id=club_id, # tupian_url 先留空,上传成功后再更新 ) # 保存到数据库获取ID shangpin_obj.save() shangpin_id = shangpin_obj.id # === 5. 处理商品图片上传 === try: # 5.1 生成文件名和路径 file_extension = os.path.splitext(shangpin_tupian.name)[1].lower() if not file_extension: file_extension = '.jpg' # 构建OSS文件路径:a_long/Spcc{shangpin_id}/{filename} oss_folder = 'a_long' # 根文件夹 spcc_folder = f"Spcc{shangpin_id}" # 商品图片文件夹 # 生成文件名:Spcc{shangpin_id}{file_extension} unique_filename = f"Spcc{shangpin_id}{file_extension}" # 构建完整路径 oss_file_path = f"{oss_folder}/{spcc_folder}/{unique_filename}" # 5.2 上传到OSS tupian_url = upload_to_oss(shangpin_tupian, oss_file_path) if not tupian_url: # 上传失败,抛出异常触发回滚 raise Exception("商品图片上传到OSS失败") # 5.3 更新商品的图片URL(存储相对路径) shangpin_obj.tupian_url = oss_file_path # 存相对路径 shangpin_obj.save() except Exception as oss_error: # 图片上传失败,事务会回滚,商品记录也会被删除 #print(f"商品图片OSS上传失败: {oss_error}") # 手动抛出异常触发事务回滚 raise Exception(f"商品图片上传失败: {str(oss_error)}") # === 6. 构建返回数据 === response_data = { 'shangpin_id': shangpin_id, 'biaoti': biaoti, 'jiage': str(jiage_value), 'kucun': kucun_value, 'leixing_id': leixing_id, 'zhuanqu_id': zhuanqu_id, 'xiaoliang': duiwai_xiaoliang_value, 'jieshao': jieshao, 'xuzhi': xuzhi, 'yaoqiuleixing': yaoqiuleixing_int, 'huiyuan_id': huiyuan_id_value, 'yongjin': str(yongjin_value) if yongjin_value else None, 'tupian_url': shangpin_obj.tupian_url # 相对路径 } # 返回成功响应 return Response({ 'code': 0, 'msg': '商品发布成功', 'data': response_data }) except Exception as e: # 捕获所有异常,返回500错误 traceback.print_exc() return Response({ 'code': 500, 'msg': f'系统错误: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class GuizeTupianShangchuanView(APIView): """ 规则图片上传接口 接收商品ID和规则图片 需要管理员权限 """ permission_classes = [permissions.IsAuthenticated] parser_classes = (MultiPartParser, FormParser) # 需要处理文件上传 def post(self, request): try: # === 1. 权限验证 === user_main = request.user # 1.1 验证账号(使用phone字段) zhanghao = request.data.get('zhanghao', '').strip() if not zhanghao: return Response( {'code': 401, 'msg': '未提供账号信息', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) if user_main.Phone != zhanghao: return Response( {'code': 401, 'msg': '账号验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 1.2 验证是否为管理员(检查user_type字段) if user_main.UserType != 'admin': return Response( {'code': 401, 'msg': '权限不足', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 1.3 验证管理员扩展表(使用AdminProfile模型) try: admin_profile = AdminProfile.query.get(user=user_main) except AdminProfile.DoesNotExist: return Response( {'code': 401, 'msg': '管理员信息不存在', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # === 2. 接收并验证字段 === # 2.1 验证商品ID shangpin_id = request.data.get('shangpin_id') if not shangpin_id: return Response( {'code': 400, 'msg': '商品ID不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 2.2 验证规则图片 guize_tupian = request.FILES.get('guize_tupian') if not guize_tupian: return Response( {'code': 400, 'msg': '请上传规则图片', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 验证图片格式和大小 is_valid, error_msg = validate_image(guize_tupian) if not is_valid: return Response( {'code': 400, 'msg': f'规则图片验证失败: {error_msg}', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # === 3. 验证商品是否存在 === try: shangpin_obj = Shangpin.query.get(id=shangpin_id) except Shangpin.DoesNotExist: return Response( {'code': 400, 'msg': '商品不存在', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # === 4. 处理规则图片上传 === try: # 4.1 生成文件名和路径 file_extension = os.path.splitext(guize_tupian.name)[1].lower() if not file_extension: file_extension = '.jpg' # 构建OSS文件路径:a_long/Spgz{shangpin_id}/{filename} oss_folder = 'a_long' # 根文件夹 spgz_folder = f"Spgz{shangpin_id}" # 规则图片文件夹 # 生成文件名:Spgz{shangpin_id}{file_extension} unique_filename = f"Spgz{shangpin_id}{file_extension}" # 构建完整路径 oss_file_path = f"{oss_folder}/{spgz_folder}/{unique_filename}" # 4.2 上传到OSS tupian_url = upload_to_oss(guize_tupian, oss_file_path) if not tupian_url: # 上传失败 return Response({ 'code': 500, 'msg': '规则图片上传失败', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 4.3 更新商品的规则图片URL(存储相对路径) shangpin_obj.guize_tupian = oss_file_path # 存相对路径 shangpin_obj.save() except Exception as oss_error: # OSS上传失败 print(f"规则图片OSS上传失败: {oss_error}") return Response({ 'code': 500, 'msg': f'规则图片上传失败: {str(oss_error)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # === 5. 构建返回数据 === response_data = { 'shangpin_id': shangpin_id, 'guize_tupian_url': oss_file_path # 相对路径 } # 返回成功响应 return Response({ 'code': 0, 'msg': '规则图片上传成功', 'data': response_data }) except Exception as e: # 捕获所有异常,返回500错误 traceback.print_exc() return Response({ 'code': 500, 'msg': f'系统错误: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class AdHuiyuanHqView(APIView): """ 管理员获取会员信息和商品类型接口 需要管理员权限 """ permission_classes = [permissions.IsAuthenticated] def post(self, request): try: # === 1. 权限验证 === # 获取当前用户 user_main = request.user # 1.1 验证前端传递的账号是否匹配(使用phone字段) zhanghao = request.data.get('zhanghao', '').strip() if not zhanghao: return Response( {'code': 401, 'msg': '权限验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 验证账号是否匹配(使用phone字段比较) if user_main.Phone != zhanghao: return Response( {'code': 401, 'msg': '权限验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 1.2 验证是否为管理员(检查user_type字段) if user_main.UserType != 'admin': return Response( {'code': 401, 'msg': '权限验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 1.3 验证管理员扩展表(使用AdminProfile模型) try: admin_profile = AdminProfile.query.get(user=user_main) except AdminProfile.DoesNotExist: return Response( {'code': 401, 'msg': '权限验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # === 2. 查询会员表数据 === # 查询所有会员数据,按创建时间倒序排列 huiyuan_queryset = Huiyuan.query.all().order_by('-CreateTime') # 构建会员数据列表 huiyuan_list = [] for huiyuan in huiyuan_queryset: huiyuan_data = { 'huiyuan_id': huiyuan.huiyuan_id, # 会员ID 'jieshao': huiyuan.jieshao, # 会员介绍(会员标题) 'jtjieshao': huiyuan.jtjieshao if huiyuan.jtjieshao else '', # 会员规则介绍(具体介绍) 'jiage': str(huiyuan.jiage), # 会员价格(转换为字符串,避免Decimal序列化问题) 'guanshifc': str(huiyuan.guanshifc), # 管事分成(转换为字符串) 'goumai_cishu': huiyuan.goumai_cishu, # 购买次数(销量) 'CreateTime': huiyuan.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if huiyuan.CreateTime else None, # 创建时间 'UpdateTime': huiyuan.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if huiyuan.UpdateTime else None # 更新时间 } huiyuan_list.append(huiyuan_data) # === 3. 查询商品类型表数据 === # 筛选抢单要求类型等于数字1的所有数据(yaoqiuleixing=1) shangpinleixing_queryset = ShangpinLeixing.query.filter( yaoqiuleixing=1 # 抢单要求类型为1(会员抢单) ).order_by('id') # 按ID排序 # 构建商品类型数据列表 shangpinleixing_list = [] for leixing in shangpinleixing_queryset: # 注意:这里只需要特定的字段 leixing_data = { 'id': leixing.id, # 商品类型ID 'huiyuan_id': leixing.huiyuan_id, # 会员ID(关联字段) 'jieshao': leixing.jieshao, # 类型介绍(商品类型名称) 'tupian_url': leixing.tupian_url if leixing.tupian_url else '' # 商品类型图片相对URL } shangpinleixing_list.append(leixing_data) # === 4. 构建返回数据 === response_data = { 'huiyuanList': huiyuan_list, 'shangpinleixingList': shangpinleixing_list } # 返回成功响应 return Response({ 'code': 0, 'msg': '获取数据成功', 'data': response_data }) except Exception as e: # 捕获所有异常,返回500错误 traceback.print_exc() return Response({ 'code': 500, 'msg': f'系统错误: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class HuiyuanXiugaiView(APIView): """ 会员修改接口 需要管理员权限 """ permission_classes = [permissions.IsAuthenticated] def post(self, request): try: # === 1. 权限验证 === user_main = request.user # 1.1 验证前端传递的账号是否匹配(使用phone字段) zhanghao = request.data.get('zhanghao', '').strip() if not zhanghao: return Response( {'code': 401, 'msg': '权限验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) if user_main.Phone != zhanghao: return Response( {'code': 401, 'msg': '权限验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 1.2 验证是否为管理员(检查user_type字段) if user_main.UserType != 'admin': return Response( {'code': 401, 'msg': '权限验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 1.3 验证管理员扩展表 try: admin_profile = AdminProfile.query.get(user=user_main) except AdminProfile.DoesNotExist: return Response( {'code': 401, 'msg': '权限验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # === 2. 接收并验证必填字段 === huiyuan_id = request.data.get('huiyuan_id', '').strip() if not huiyuan_id: return Response( {'code': 400, 'msg': '会员ID不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # === 3. 查询会员数据 === try: huiyuan_obj = Huiyuan.query.get(huiyuan_id=huiyuan_id) except Huiyuan.DoesNotExist: return Response( {'code': 400, 'msg': '会员不存在', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # === 4. 更新会员信息 === # 4.1 获取前端传递的字段(只更新有值的字段) jieshao = request.data.get('jieshao', '').strip() jtjieshao = request.data.get('jtjieshao', '').strip() jiage = request.data.get('jiage', '').strip() guanshifc = request.data.get('guanshifc', '').strip() # 4.2 更新字段 if jieshao: # 验证名称长度(30个字符以内) if len(jieshao) > 30: return Response( {'code': 400, 'msg': '会员名称不能超过30个字符', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) huiyuan_obj.jieshao = jieshao if jtjieshao: # 验证介绍长度(没有长度限制,但做一下基本检查) if len(jtjieshao) > 2000: return Response( {'code': 400, 'msg': '会员介绍过长', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) huiyuan_obj.jtjieshao = jtjieshao if jiage: try: jiage_value = float(jiage) if jiage_value < 0: return Response( {'code': 400, 'msg': '会员价格不能为负数', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) if jiage_value > 100000: return Response( {'code': 400, 'msg': '会员价格不能超过10万元', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) huiyuan_obj.jiage = jiage_value except ValueError: return Response( {'code': 400, 'msg': '会员价格格式错误', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) if guanshifc: try: guanshifc_value = float(guanshifc) if guanshifc_value < 0: return Response( {'code': 400, 'msg': '管事分成不能为负数', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 检查管事分成是否超过会员价格 if hasattr(huiyuan_obj, 'jiage') and huiyuan_obj.jiage: if guanshifc_value > float(huiyuan_obj.jiage): return Response( {'code': 400, 'msg': '管事分成不能超过会员价格', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) huiyuan_obj.guanshifc = guanshifc_value except ValueError: return Response( {'code': 400, 'msg': '管事分成格式错误', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 4.3 检查是否有任何字段需要更新 if not (jieshao or jtjieshao or jiage or guanshifc): return Response( {'code': 400, 'msg': '没有需要更新的字段', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 4.4 保存更新 huiyuan_obj.save() # === 5. 构建返回数据 === response_data = { 'huiyuan_id': huiyuan_obj.huiyuan_id, 'jieshao': huiyuan_obj.jieshao, 'jtjieshao': huiyuan_obj.jtjieshao if huiyuan_obj.jtjieshao else '', 'jiage': str(huiyuan_obj.jiage), 'guanshifc': str(huiyuan_obj.guanshifc), 'goumai_cishu': huiyuan_obj.goumai_cishu, 'CreateTime': huiyuan_obj.CreateTime.strftime( '%Y-%m-%d %H:%M:%S') if huiyuan_obj.CreateTime else None, 'UpdateTime': huiyuan_obj.UpdateTime.strftime( '%Y-%m-%d %H:%M:%S') if huiyuan_obj.UpdateTime else None } # 返回成功响应 return Response({ 'code': 0, 'msg': '会员信息修改成功', 'data': response_data }) except Exception as e: # 捕获所有异常,返回500错误 traceback.print_exc() return Response({ 'code': 500, 'msg': f'系统错误: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class AdHuiyuanFbView(APIView): """ 会员发布接口 需要管理员权限 """ permission_classes = [permissions.IsAuthenticated] def post(self, request): try: # === 1. 权限验证 === user_main = request.user # 1.1 验证前端传递的账号是否匹配 zhanghao = request.data.get('zhanghao', '').strip() if not zhanghao: return Response( {'code': 401, 'msg': '权限验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) if user_main.Phone != zhanghao: return Response( {'code': 401, 'msg': '权限验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 1.2 验证是否为管理员 if user_main.UserType != 'admin': return Response( {'code': 401, 'msg': '权限验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 1.3 验证管理员扩展表 try: admin_profile = AdminProfile.query.get(user=user_main) except AdminProfile.DoesNotExist: return Response( {'code': 401, 'msg': '权限验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # === 2. 接收并验证字段 === jieshao = request.data.get('jieshao', '').strip() jtjieshao = request.data.get('jtjieshao', '').strip() jiage = request.data.get('jiage', '').strip() guanshifc = request.data.get('guanshifc', '').strip() # 2.1 验证必填字段 if not jieshao: return Response( {'code': 400, 'msg': '会员名称不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) if not jiage: return Response( {'code': 400, 'msg': '会员价格不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) if not guanshifc: return Response( {'code': 400, 'msg': '管事分成不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 2.2 验证字段长度和格式 if len(jieshao) > 30: return Response( {'code': 400, 'msg': '会员名称不能超过30个字符', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) if jtjieshao and len(jtjieshao) > 2000: return Response( {'code': 400, 'msg': '会员介绍过长', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 2.3 验证价格和分成格式 try: jiage_value = float(jiage) if jiage_value <= 0: return Response( {'code': 400, 'msg': '会员价格必须大于0', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) if jiage_value > 100000: return Response( {'code': 400, 'msg': '会员价格不能超过10万元', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) except ValueError: return Response( {'code': 400, 'msg': '会员价格格式错误', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) try: guanshifc_value = float(guanshifc) if guanshifc_value < 0: return Response( {'code': 400, 'msg': '管事分成不能为负数', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) if guanshifc_value > jiage_value: return Response( {'code': 400, 'msg': '管事分成不能超过会员价格', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) except ValueError: return Response( {'code': 400, 'msg': '管事分成格式错误', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # === 3. 生成会员ID === # 生成6位唯一会员ID,格式:V + 5位随机数字字母 def generate_huiyuan_id(): # 使用V开头,后跟5位随机字符(数字和大写字母) chars = string.digits + string.ascii_uppercase while True: random_part = ''.join(random.choices(chars, k=5)) huiyuan_id = f"V{random_part}" # 检查是否已存在 if not Huiyuan.query.filter(huiyuan_id=huiyuan_id).exists(): return huiyuan_id huiyuan_id = generate_huiyuan_id() # === 4. 创建会员记录 === with transaction.atomic(): huiyuan_obj = Huiyuan( huiyuan_id=huiyuan_id, jieshao=jieshao, jtjieshao=jtjieshao if jtjieshao else '', jiage=jiage_value, guanshifc=guanshifc_value, goumai_cishu=0, # 初始购买次数为0 ) huiyuan_obj.save() # === 5. 构建返回数据 === response_data = { 'huiyuan_id': huiyuan_obj.huiyuan_id, 'jieshao': huiyuan_obj.jieshao, 'jtjieshao': huiyuan_obj.jtjieshao, 'jiage': str(huiyuan_obj.jiage), 'guanshifc': str(huiyuan_obj.guanshifc), 'goumai_cishu': huiyuan_obj.goumai_cishu, 'CreateTime': huiyuan_obj.CreateTime.strftime( '%Y-%m-%d %H:%M:%S') if huiyuan_obj.CreateTime else None, 'UpdateTime': huiyuan_obj.UpdateTime.strftime( '%Y-%m-%d %H:%M:%S') if huiyuan_obj.UpdateTime else None } # 返回成功响应 return Response({ 'code': 0, 'msg': '会员发布成功', 'data': response_data }) except Exception as e: # 捕获所有异常,返回500错误 traceback.print_exc() return Response({ 'code': 500, 'msg': f'系统错误: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class AdShangpinHuQu(APIView): """ 后台商品管理页面数据获取接口 路径: /shangpin/adsphq/ 方法: POST 权限: JWT认证 + 管理员权限 功能: 获取商品管理页面需要的所有数据 """ permission_classes = [IsAuthenticated] def post(self, request): try: # 1. 获取前端传递的账号 qianzhanghao = request.data.get('zhanghao', '').strip() if not qianzhanghao: return Response( {'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED ) # 2. 从JWT Token获取当前用户 dangqianyonghu = request.user # 3. 验证账号匹配 if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao: return Response( {'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED ) # 4. 验证用户类型为管理员 if dangqianyonghu.UserType != 'admin': return Response( {'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED ) # 5. 验证管理员扩展表数据 try: guanliyuan = dangqianyonghu.AdminProfile except AdminProfile.DoesNotExist: return Response( {'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED ) # 6. 查询公告与轮播(按俱乐部,默认抢单池) from jituan.services.display_config import get_gonggao_content, get_lunbo_urls shangpingonggao = get_gonggao_content(request, notice_type=1) shangpinlunbo = get_lunbo_urls(request, page_key='order_pool', image_type=1) # 8. 查询商品类型数据 shangpinleixing_data = ShangpinLeixing.query.all().values( 'id', # 类型ID 'jieshao', # 类型介绍(类型名称) 'tupian_url', # 类型图片URL 'yaoqiuleixing', # 抢单要求类型(1=会员抢单,2=佣金抢单) 'huiyuan_id', # 会员ID(如果yaoqiuleixing=1) 'yongjin' # 佣金要求(如果yaoqiuleixing=2) ) # 9. 查询商品专区数据(按当前俱乐部) admin_club_id = resolve_effective_club_id(request, getattr(request, 'user', None)) or 'xq' shangpinzhuanqu_data = ShangpinZhuanqu.query.filter(club_id=admin_club_id).values( 'id', # 专区ID 'mingzi', # 专区名称 'leixing_id', # 所属类型ID 'club_id', ) # 10. 查询会员数据 huiyuan_data = Huiyuan.query.all().values( 'huiyuan_id', # 会员ID 'jieshao', # 会员介绍(会员名称) 'jiage' # 会员价格 ) # 11. 查询商品数据(按当前俱乐部) shangpin_data = Shangpin.query.filter(club_id=admin_club_id).values( 'id', # 商品ID 'biaoqian', # 商品标题/标签 'jiage', # 商品价格 'leixing_id', # 商品类型ID 'zhuanqu_id', # 商品专区ID 'tupian_url', # 商品图片URL 'duiwai_xiaoliang', # 商品对外销量 'club_id', ) # 12. 构造返回数据 response_data = { 'code': 0, 'message': '获取成功', 'data': { 'shangpingonggao': shangpingonggao, # 商品公告 'shangpinlunbo': shangpinlunbo, # 轮播图相对URL数组 'shangpinleixing': list(shangpinleixing_data), # 商品类型列表 'shangpinzhuanqu': list(shangpinzhuanqu_data), # 商品专区列表 'huiyuanliebiao': list(huiyuan_data), # 会员列表 'shangpinliebiao': list(shangpin_data), # 商品列表 } } return Response(response_data, status=status.HTTP_200_OK) except Exception as e: print(f"获取商品管理数据异常: {e}") return Response( {'code': 500, 'message': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) # shangpin/views.py class AdGonggaoXiuGai(APIView): """ 后台编辑公告接口 路径: /shangpin/adggxg/ 方法: POST 权限: JWT认证 + 管理员权限 功能: 修改公告内容 """ permission_classes = [IsAuthenticated] def post(self, request): try: # 1. 获取前端传递的账号 qianzhanghao = request.data.get('zhanghao', '').strip() if not qianzhanghao: return Response( {'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED ) # 2. 从JWT Token获取当前用户 dangqianyonghu = request.user # 3. 验证账号匹配 if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao: return Response( {'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED ) # 4. 验证用户类型为管理员 if dangqianyonghu.UserType != 'admin': return Response( {'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED ) # 5. 验证管理员扩展表数据 try: guanliyuan = dangqianyonghu.AdminProfile except AdminProfile.DoesNotExist: return Response( {'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED ) # 6. 获取公告内容 gonggao_neirong = request.data.get('gonggao', '').strip() # 7. 验证公告内容 if not gonggao_neirong: return Response( {'code': 400, 'message': '公告内容不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) if len(gonggao_neirong) > 300: return Response( {'code': 400, 'message': '公告内容不能超过300字', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 8. 查询公告表,类型为1的公告 gonggao_obj = Gonggao.query.filter(NoticeType=1).first() # 如果不存在,则创建 if not gonggao_obj: gonggao_obj = Gonggao(NoticeType=1) # 9. 更新公告内容 gonggao_obj.Content = gonggao_neirong gonggao_obj.save() # 10. 返回成功响应 return Response({ 'code': 0, 'message': '公告更新成功', 'data': { 'gonggao': gonggao_neirong } }) except Exception as e: print(f"编辑公告异常: {e}") return Response( {'code': 500, 'message': '服务器内部错误', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) class AdZhuanquXiuGai(APIView): """ 后台修改专区名称接口 路径: /shangpin/adzqxg/ 方法: POST 权限: JWT认证 + 管理员权限 功能: 修改专区名称 """ permission_classes = [IsAuthenticated] def post(self, request): try: # 1. 权限验证 qianzhanghao = request.data.get('zhanghao', '').strip() if not qianzhanghao: return Response( {'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED ) dangqianyonghu = request.user if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao: return Response( {'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED ) if dangqianyonghu.UserType != 'admin': return Response( {'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED ) try: guanliyuan = dangqianyonghu.AdminProfile except AdminProfile.DoesNotExist: return Response( {'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED ) # 2. 获取专区ID和新的专区名称(可选图标) zhuanqu_id = request.data.get('zhuanqu_id') mingzi = request.data.get('mingzi', '').strip() tupian_url = request.data.get('tupian_url', None) if not zhuanqu_id: return Response( {'code': 400, 'message': '专区ID不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) if not mingzi: return Response( {'code': 400, 'message': '专区名称不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) if len(mingzi) > 10: return Response( {'code': 400, 'message': '专区名称不能大于10个字', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 3. 查询专区 try: zhuanqu_obj = ShangpinZhuanqu.query.get(id=zhuanqu_id) except ShangpinZhuanqu.DoesNotExist: return Response( {'code': 400, 'message': '专区不存在', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 4. 更新专区名称(及可选图标;未传 tupian_url 则不改图) zhuanqu_obj.mingzi = mingzi if tupian_url is not None: zhuanqu_obj.tupian_url = (tupian_url or '').strip() zhuanqu_obj.save() # 5. 返回成功响应 return Response({ 'code': 0, 'message': '专区名称修改成功', 'data': { 'id': zhuanqu_obj.id, 'mingzi': zhuanqu_obj.mingzi, 'leixing_id': zhuanqu_obj.leixing_id, 'tupian_url': getattr(zhuanqu_obj, 'tupian_url', '') or '', } }) except Exception as e: print(f"修改专区名称异常: {e}") return Response( {'code': 500, 'message': '服务器内部错误', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) class AdLunboXiuGai(APIView): """ 后台编辑轮播图接口 路径: /shangpin/adlunboxg/ 方法: POST 权限: JWT认证 + 管理员权限 功能: 添加或删除轮播图 """ permission_classes = [IsAuthenticated] parser_classes = (MultiPartParser, FormParser,JSONParser) def post(self, request): try: # 1. 权限验证 qianzhanghao = request.data.get('zhanghao', '').strip() if not qianzhanghao: return Response( {'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED ) dangqianyonghu = request.user if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao: return Response( {'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED ) if dangqianyonghu.UserType != 'admin': return Response( {'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED ) try: guanliyuan = dangqianyonghu.AdminProfile except AdminProfile.DoesNotExist: return Response( {'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED ) # 2. 获取操作类型 action = request.data.get('action', '').strip() # 'add' 或 'delete' if action not in ['add', 'delete']: return Response( {'code': 400, 'message': '操作类型无效', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 3. 处理删除操作 if action == 'delete': tupian_url = request.data.get('tupian_url', '').strip() if not tupian_url: return Response( {'code': 400, 'message': '图片URL不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 查询轮播图记录 lunbo_obj = Lunbo.query.filter(ImageURL=tupian_url, ImageType=1).first() if not lunbo_obj: return Response( {'code': 400, 'message': '轮播图不存在', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 从OSS删除图片 delete_success = delete_from_oss(tupian_url) if not delete_success: return Response( {'code': 500, 'message': 'OSS删除失败', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) # 从数据库删除记录 lunbo_obj.delete() return Response({ 'code': 0, 'message': '删除成功', 'data': None }) # 4. 处理添加操作 else: # action == 'add' tupian_file = request.FILES.get('file') if not tupian_file: return Response( {'code': 400, 'message': '请上传图片', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 验证图片 is_valid, error_msg = validate_image(tupian_file) if not is_valid: #print(f"[轮播图上传] 图片验证失败详情: {error_msg}") # 还可以记录文件大小、名称等信息 #print(f"[轮播图上传] 问题文件: {tupian_file.name}, Size: {tupian_file.size}") #print(is_valid) return Response( {'code': 400, 'message': f'图片验证失败: {error_msg}', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 生成文件名和路径 file_extension = os.path.splitext(tupian_file.name)[1].lower() if not file_extension: file_extension = '.jpg' # 生成6位随机字符串作为文件名 random_str = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6)) unique_filename = f"{random_str}{file_extension}" # 构建OSS文件路径:a_long/lunbo/{filename} oss_folder = 'a_long' lunbo_folder = 'lunbo' oss_file_path = f"{oss_folder}/{lunbo_folder}/{unique_filename}" # 上传到OSS tupian_url = upload_to_oss(tupian_file, oss_file_path) if not tupian_url: return Response( {'code': 500, 'message': '图片上传失败', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) # 创建轮播图记录 lunbo_obj = Lunbo.query.create( ImageURL=oss_file_path, # 存相对路径 ImageType=1 ) return Response({ 'code': 0, 'message': '添加成功', 'data': { 'tupian_url': lunbo_obj.ImageURL } }) except Exception as e: #print(f"编辑轮播图异常: {e}") return Response( {'code': 500, 'message': '服务器内部错误', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) class AdLeixingXiuGai(APIView): """ 后台编辑商品类型接口 - 优化版 支持:只改图片、只改字段、两者都改 拒绝:两者都不改 """ permission_classes = [IsAuthenticated] parser_classes = (MultiPartParser, FormParser, JSONParser) # 添加JSONParser支持 def post(self, request): try: # 1. 权限验证(保持不变) qianzhanghao = request.data.get('zhanghao', '').strip() if not qianzhanghao: return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) dangqianyonghu = request.user if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao: return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) if dangqianyonghu.UserType != 'admin': return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) try: guanliyuan = dangqianyonghu.AdminProfile except AdminProfile.DoesNotExist: return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) # 2. 获取商品类型ID leixing_id = request.data.get('leixing_id') if not leixing_id: return Response({'code': 400, 'message': '商品类型ID不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST) # 3. 查询商品类型 try: leixing_obj = ShangpinLeixing.query.get(id=leixing_id) except ShangpinLeixing.DoesNotExist: return Response({'code': 400, 'message': '商品类型不存在', 'data': None}, status=status.HTTP_400_BAD_REQUEST) # 4. 获取字段并初始化变更标记 has_changed = False jieshao = request.data.get('jieshao', '').strip() yaoqiuleixing = request.data.get('yaoqiuleixing') huiyuan_id = request.data.get('huiyuan_id', '').strip() yongjin = request.data.get('yongjin', '').strip() tupian_url = request.data.get('tupian_url', '').strip() # 原有图片URL # 5. 检查非图片字段是否有变更 if jieshao and jieshao != leixing_obj.jieshao: leixing_obj.jieshao = jieshao has_changed = True if yaoqiuleixing and int(yaoqiuleixing) != leixing_obj.yaoqiuleixing: leixing_obj.yaoqiuleixing = int(yaoqiuleixing) has_changed = True # 根据类型清空另一个字段 if leixing_obj.yaoqiuleixing == 1: leixing_obj.yongjin = None if huiyuan_id: leixing_obj.huiyuan_id = huiyuan_id # 验证会员存在 try: Huiyuan.query.get(huiyuan_id=huiyuan_id) except Huiyuan.DoesNotExist: return Response({'code': 400, 'msg': '会员ID不存在', 'data': None}, status=status.HTTP_400_BAD_REQUEST) else: # 类型2 leixing_obj.huiyuan_id = None if yongjin: try: leixing_obj.yongjin = float(yongjin) if leixing_obj.yongjin <= 0: return Response({'code': 400, 'message': '佣金金额必须大于0', 'data': None}, status=status.HTTP_400_BAD_REQUEST) except ValueError: return Response({'code': 400, 'message': '佣金金额格式错误', 'data': None}, status=status.HTTP_400_BAD_REQUEST) # ===== 在现有代码的第5步后面添加 ===== # 5.5 检查抢单要求类型不变时,会员ID或佣金是否有变更 if yaoqiuleixing and int(yaoqiuleixing) == leixing_obj.yaoqiuleixing: # 类型没有变化,但需要检查对应字段的变化 # 情况1:会员抢单 (yaoqiuleixing == 1) if leixing_obj.yaoqiuleixing == 1: # 检查会员ID是否变更 if huiyuan_id and huiyuan_id != leixing_obj.huiyuan_id: # 验证会员存在 try: Huiyuan.query.get(huiyuan_id=huiyuan_id) except Huiyuan.DoesNotExist: return Response({'code': 400, 'msg': '会员ID不存在', 'data': None}, status=status.HTTP_400_BAD_REQUEST) leixing_obj.huiyuan_id = huiyuan_id has_changed = True # print(f"会员ID变更: {leixing_obj.huiyuan_id} -> {huiyuan_id}") # 情况2:佣金抢单 (yaoqiuleixing == 2) elif leixing_obj.yaoqiuleixing == 2: # 检查佣金是否变更 if yongjin: try: new_yongjin = float(yongjin) if new_yongjin <= 0: return Response({'code': 400, 'message': '佣金金额必须大于0', 'data': None}, status=status.HTTP_400_BAD_REQUEST) # 判断是否有变化(考虑None的情况) current_yongjin = leixing_obj.yongjin if (current_yongjin is None and new_yongjin != 0) or \ (current_yongjin is not None and abs( float(current_yongjin) - new_yongjin) > 0.001): leixing_obj.yongjin = new_yongjin has_changed = True # print(f"佣金变更: {current_yongjin} -> {new_yongjin}") except ValueError: return Response({'code': 400, 'message': '佣金金额格式错误', 'data': None}, status=status.HTTP_400_BAD_REQUEST) # ===== 添加结束 ===== # 6. 处理图片上传(如果有新图片) tupian_file = request.FILES.get('file') if tupian_file: # 验证图片 is_valid, error_msg = validate_image(tupian_file) if not is_valid: return Response({'code': 400, 'message': f'图片验证失败: {error_msg}', 'data': None}, status=status.HTTP_400_BAD_REQUEST) # 删除旧图片(如果存在) if leixing_obj.tupian_url: delete_from_oss(leixing_obj.tupian_url) # 生成文件名和路径 file_extension = os.path.splitext(tupian_file.name)[1].lower() or '.jpg' oss_folder = 'a_long' leixing_folder = 'leixing' # 使用原文件名或生成新文件名 if leixing_obj.tupian_url: original_filename = leixing_obj.tupian_url.split('/')[-1] else: original_filename = f"{leixing_id}_{uuid.uuid4().hex}{file_extension}" oss_file_path = f"{oss_folder}/{leixing_folder}/{leixing_id}/{original_filename}" # 上传到OSS new_tupian_url = upload_to_oss(tupian_file, oss_file_path) if not new_tupian_url: return Response({'code': 500, 'message': '图片上传失败', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) leixing_obj.tupian_url = oss_file_path # 存相对路径 has_changed = True elif tupian_url and tupian_url != leixing_obj.tupian_url: # 前端传了新URL(可能是从其他地方选择的) leixing_obj.tupian_url = tupian_url has_changed = True # 7. 验证是否至少有变更 if not has_changed: return Response({ 'code': 400, 'message': '没有检测到任何更改', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 8. 保存并返回 leixing_obj.save() # 8. 保存商品类型 leixing_obj.save() # 🔴 ===========【新增:同步更新关联商品】=========== try: # 1. 准备商品更新字典 shangpin_update_fields = {} # 始终同步抢单要求类型 shangpin_update_fields['yaoqiuleixing'] = leixing_obj.yaoqiuleixing # 根据抢单类型,同步对应字段并清空另一个 if leixing_obj.yaoqiuleixing == 1: # 会员抢单 if leixing_obj.huiyuan_id: # 仅当会员ID非空时更新 shangpin_update_fields['huiyuan_id'] = leixing_obj.huiyuan_id # 清空佣金字段 shangpin_update_fields['yongjin'] = None elif leixing_obj.yaoqiuleixing == 2: # 佣金抢单 if leixing_obj.yongjin: # 仅当佣金非空时更新 shangpin_update_fields['yongjin'] = leixing_obj.yongjin # 清空会员字段 shangpin_update_fields['huiyuan_id'] = None # 2. 执行批量更新(确保只有实际有变化的字段才会执行) if shangpin_update_fields: # 查询所有属于该类型的商品(使用filter而非all以提高效率) affected_count = Shangpin.query.filter( leixing_id=leixing_obj.id ).update(**shangpin_update_fields) # 可选:记录日志以便追踪 # logger.info(f"商品类型{leixing_obj.id}修改成功,已同步更新{affected_count}个关联商品") except Exception as sync_error: # 商品同步失败不影响商品类型本身的修改,但记录错误 # 可根据需求决定是否抛出异常回滚整个事务 # 当前设计:记录错误但允许商品类型修改成功 logger.error(f"修改十五") # 可在此添加更详细的错误日志记录 # 🔴 ===========【新增结束】=========== response_data = { 'id': leixing_obj.id, 'jieshao': leixing_obj.jieshao, 'tupian_url': leixing_obj.tupian_url, 'yaoqiuleixing': leixing_obj.yaoqiuleixing, 'huiyuan_id': leixing_obj.huiyuan_id, 'yongjin': str(leixing_obj.yongjin) if leixing_obj.yongjin else None } return Response({ 'code': 0, 'message': '商品类型修改成功', 'data': response_data }) except Exception as e: print(f"编辑商品类型异常: {e}") return Response({'code': 500, 'message': '服务器内部错误', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class AdShangpinNeiRongHuoQu(APIView): """ 后台获取商品详情接口 路径: /shangpin/adspnrhq/ 方法: POST 权限: JWT认证 + 管理员权限 功能: 获取指定商品的详细信息 """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): try: # 1. 获取前端数据 qianzhanghao = request.data.get('zhanghao', '').strip() shangpin_id = request.data.get('shangpin_id') if not qianzhanghao or not shangpin_id: return Response( {'code': 400, 'message': '参数不完整', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 2. 权限验证 dangqianyonghu = request.user # 验证phone字段匹配 if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao: logger.warning( f"账号不匹配: 当前用户phone={getattr(dangqianyonghu, 'Phone', '无')}, 前端账号={qianzhanghao}") return Response( {'code': 401, 'message': '身份验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 验证用户类型 if dangqianyonghu.UserType != 'admin': logger.warning(f"非管理员访问: user_type={dangqianyonghu.UserType}") return Response( {'code': 401, 'message': '身份验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 3. 验证管理员扩展表 try: guanliyuan = dangqianyonghu.AdminProfile except AdminProfile.DoesNotExist: logger.warning(f"管理员扩展表不存在: user_id={dangqianyonghu.id}") return Response( {'code': 401, 'message': '身份验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 4. 查询商品数据 try: shangpin_obj = Shangpin.query.get(id=shangpin_id) except Shangpin.DoesNotExist: return Response( {'code': 404, 'message': '商品不存在', 'data': None}, status=status.HTTP_404_NOT_FOUND ) # 5. 构建返回数据(与前端字段对应) response_data = { 'tupian_url': shangpin_obj.tupian_url or '', # 商品图片相对URL 'biaoti': shangpin_obj.biaoqian or '', # 商品标题(前端用biaoti,对应biaoqian) 'jieshao': shangpin_obj.jieshao or '', # 商品介绍 'jiage': str(shangpin_obj.jiage) if shangpin_obj.jiage else '0.00', # 商品价格 'kucun': shangpin_obj.kucun, # 商品库存 'zhenshixiaoliang': shangpin_obj.zhenshi_xiaoliang, # 商品真实销量 'duiwaixiaoliang': shangpin_obj.duiwai_xiaoliang, # 商品对外销量 'xiadanxuzhi': shangpin_obj.xiadan_xuzhi or '', # 下单须知 'guizetupian_url': shangpin_obj.guize_tupian or '', # 规则图片相对URL 'yajin': str(shangpin_obj.yajin) if hasattr(shangpin_obj, 'yajin') and shangpin_obj.yajin else None, # 押金(可能不存在) 'chuangjianshijian': shangpin_obj.CreateTime.strftime( '%Y-%m-%d %H:%M:%S') if shangpin_obj.CreateTime else '', 'genggaishijian': shangpin_obj.UpdateTime.strftime( '%Y-%m-%d %H:%M:%S') if shangpin_obj.UpdateTime else '', # 额外字段,前端可能不需要但可以保留 'leixing_id': shangpin_obj.leixing_id, 'zhuanqu_id': shangpin_obj.zhuanqu_id, 'yaoqiuleixing': shangpin_obj.yaoqiuleixing, 'huiyuan_id': shangpin_obj.huiyuan_id, 'yongjin': str(shangpin_obj.yongjin) if shangpin_obj.yongjin else None, } return Response({ 'code': 0, 'message': '获取成功', 'data': response_data }) except Exception as e: logger.error(f"获取商品详情异常: {str(e)}", exc_info=True) return Response( {'code': 500, 'message': '服务器内部错误', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) class AdShangpinXiuGai(APIView): """ 后台更改商品数据接口 - 修复版 修复:1. 每次上传生成新文件名 2. 解决浏览器缓存问题 """ permission_classes = [IsAuthenticated] parser_classes = (MultiPartParser, FormParser, JSONParser) def post(self, request): try: # 1. 权限验证 qianzhanghao = request.data.get('zhanghao', '').strip() shangpin_id = request.data.get('shangpin_id') if not qianzhanghao or not shangpin_id: return Response( {'code': 400, 'message': '参数不完整', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 验证身份 dangqianyonghu = request.user if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao: return Response( {'code': 401, 'message': '身份验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) if dangqianyonghu.UserType != 'admin': return Response( {'code': 401, 'message': '身份验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) try: guanliyuan = dangqianyonghu.AdminProfile except AdminProfile.DoesNotExist: return Response( {'code': 401, 'message': '身份验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 2. 判断操作类型:删除还是修改 action = request.data.get('action', 'update').strip().lower() if action == 'delete': return self._delete_shangpin(shangpin_id) else: return self._update_shangpin(request, shangpin_id) except Exception as e: logger.error(f"更改商品数据异常: {str(e)}", exc_info=True) return Response( {'code': 500, 'message': '服务器内部错误', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) def _delete_shangpin(self, shangpin_id): """删除商品逻辑""" try: # 查询商品 try: shangpin_obj = Shangpin.query.get(id=shangpin_id) except Shangpin.DoesNotExist: return Response( {'code': 404, 'message': '商品不存在', 'data': None}, status=status.HTTP_404_NOT_FOUND ) # 获取图片URL(如果有) tupian_url = shangpin_obj.tupian_url guizetupian_url = shangpin_obj.guize_tupian # 异步或同步删除OSS图片 if tupian_url: try: delete_from_oss(tupian_url) except Exception as e: logger.warning(f"删除商品图片失败: {tupian_url}, 错误: {e}") if guizetupian_url: try: delete_from_oss(guizetupian_url) except Exception as e: logger.warning(f"删除规则图片失败: {guizetupian_url}, 错误: {e}") # 删除数据库记录 shangpin_obj.delete() return Response({ 'code': 0, 'message': '删除成功', 'data': { 'shangpin_id': shangpin_id, 'deleted': True } }) except Exception as e: logger.error(f"删除商品异常: {str(e)}", exc_info=True) return Response( {'code': 500, 'message': '删除失败', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) def _update_shangpin(self, request, shangpin_id): """修改商品逻辑 - 修复:每次上传生成新文件名""" try: # 查询商品 try: shangpin_obj = Shangpin.query.get(id=shangpin_id) except Shangpin.DoesNotExist: return Response( {'code': 404, 'message': '商品不存在', 'data': None}, status=status.HTTP_404_NOT_FOUND ) has_changes = False # 获取原图片URL(如果有) old_tupian_url = shangpin_obj.tupian_url # 3. 处理商品图片上传(如果有新商品图片) tupian_file = request.FILES.get('tupian_file') new_tupian_url = None if tupian_file: # 验证图片 is_valid, error_msg = validate_image(tupian_file) if not is_valid: return Response( {'code': 400, 'message': f'图片验证失败: {error_msg}', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 生成新文件名:时间戳 + 随机数 + 原扩展名 timestamp = int(time.time() * 1000) % 100000000 # 8位时间戳 random_num = random.randint(1000, 9999) file_extension = os.path.splitext(tupian_file.name)[1].lower() or '.jpg' new_filename = f"sp_{timestamp}_{random_num}{file_extension}" # 确定OSS路径 oss_folder = 'a_long' shangpin_folder = 'shangpin' oss_file_path = f"{oss_folder}/{shangpin_folder}/{new_filename}" # 删除旧图片(如果存在) if old_tupian_url: try: delete_from_oss(old_tupian_url) logger.info(f"已删除旧商品图片: {old_tupian_url}") except Exception as e: logger.warning(f"删除旧商品图片失败: {old_tupian_url}, 错误: {e}") # 继续上传新图片,不中断流程 # 上传到OSS new_tupian_url = upload_to_oss(tupian_file, oss_file_path) if not new_tupian_url: return Response( {'code': 500, 'message': '商品图片上传失败', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) # 更新数据库中的图片URL shangpin_obj.tupian_url = oss_file_path # 存相对路径 has_changes = True logger.info(f"商品图片更新成功: {oss_file_path}") # 4. 处理其他字段(只更新有变化的字段) # 注意:这里同时处理 request.data(文本字段)和可能有文件的情况 field_mapping = { 'biaoti': 'biaoqian', 'jieshao': 'jieshao', 'jiage': 'jiage', 'kucun': 'kucun', 'duiwaixiaoliang': 'duiwai_xiaoliang', 'xiadanxuzhi': 'xiadan_xuzhi', 'yajin': 'yajin', } for front_field, db_field in field_mapping.items(): # 注意:request.data 可能包含文件和其他字段,但DRF会处理好 if front_field in request.data: new_value = request.data.get(front_field) # 空字符串处理 if new_value == '': new_value = None if new_value is not None: # 获取当前值 current_value = getattr(shangpin_obj, db_field) # 类型转换 try: if db_field in ['jiage', 'yajin']: # 价格和押金 if new_value: new_value = Decimal(str(new_value)) else: new_value = None elif db_field in ['kucun', 'duiwai_xiaoliang']: # 库存和对外销量 if new_value: new_value = int(float(new_value)) else: new_value = 0 except (ValueError, TypeError, InvalidOperation) as e: logger.warning(f"字段{front_field}转换失败: {new_value}, 错误: {e}") continue # 检查是否有变化(处理None值) if current_value != new_value: setattr(shangpin_obj, db_field, new_value) has_changes = True # 5. 如果没有检测到任何更改 if not has_changes: return Response({ 'code': 400, 'message': '没有检测到任何更改', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 6. 保存更改 shangpin_obj.save() # 7. 返回更新后的数据 response_data = { 'tupian_url': shangpin_obj.tupian_url or '', 'biaoti': shangpin_obj.biaoqian or '', 'jieshao': shangpin_obj.jieshao or '', 'jiage': str(shangpin_obj.jiage) if shangpin_obj.jiage else '0.00', 'kucun': shangpin_obj.kucun, 'duiwaixiaoliang': shangpin_obj.duiwai_xiaoliang, 'xiadanxuzhi': shangpin_obj.xiadan_xuzhi or '', 'yajin': str(shangpin_obj.yongjin) if shangpin_obj.yongjin else None, 'genggaishijian': shangpin_obj.UpdateTime.strftime( '%Y-%m-%d %H:%M:%S') if shangpin_obj.UpdateTime else '', } return Response({ 'code': 0, 'message': '更新成功', 'data': response_data }) except Exception as e: logger.error(f"更新商品异常: {str(e)}", exc_info=True) return Response( {'code': 500, 'message': '更新失败', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) class AdShangpinGuizeXiuGai(APIView): """ 后台规则图片更换接口 - 修复版 修复:每次上传生成新文件名,避免浏览器缓存 """ permission_classes = [IsAuthenticated] parser_classes = (MultiPartParser, FormParser, JSONParser) def post(self, request): try: # 1. 权限验证 qianzhanghao = request.data.get('zhanghao', '').strip() shangpin_id = request.data.get('shangpin_id') action = request.data.get('action', 'update').strip().lower() if not qianzhanghao or not shangpin_id: return Response( {'code': 400, 'message': '参数不完整', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 验证身份 dangqianyonghu = request.user if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao: return Response( {'code': 401, 'message': '身份验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) if dangqianyonghu.UserType != 'admin': return Response( {'code': 401, 'message': '身份验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) try: guanliyuan = dangqianyonghu.AdminProfile except AdminProfile.DoesNotExist: return Response( {'code': 401, 'message': '身份验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 2. 查询商品 try: shangpin_obj = Shangpin.query.get(id=shangpin_id) except Shangpin.DoesNotExist: return Response( {'code': 404, 'message': '商品不存在', 'data': None}, status=status.HTTP_404_NOT_FOUND ) # 3. 处理不同操作 if action == 'delete': return self._delete_guize_image(shangpin_obj) elif action == 'update': return self._update_guize_image(request, shangpin_obj) else: return Response( {'code': 400, 'message': '操作类型无效', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) except Exception as e: logger.error(f"规则图片操作异常: {str(e)}", exc_info=True) return Response( {'code': 500, 'message': '服务器内部错误', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) def _delete_guize_image(self, shangpin_obj): """删除规则图片""" guizetupian_url = shangpin_obj.guize_tupian if guizetupian_url: # 从OSS删除 try: delete_success = delete_from_oss(guizetupian_url) '''if delete_success: logger.info(f"已删除规则图片: {guizetupian_url}") else: logger.warning(f"OSS删除失败,但继续删除数据库记录: {guizetupian_url}")''' except Exception as e: logger.warning(f"删除规则图片异常: {e}") # 清除数据库记录 shangpin_obj.guize_tupian = '' shangpin_obj.save() return Response({ 'code': 0, 'message': '删除成功', 'data': { 'guizetupian_url': '' } }) def _update_guize_image(self, request, shangpin_obj): """更新规则图片 - 修复:每次上传生成新文件名""" guize_file = request.FILES.get('guize_file') if not guize_file: return Response( {'code': 400, 'message': '请上传规则图片', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 验证图片 is_valid, error_msg = validate_image(guize_file) if not is_valid: return Response( {'code': 400, 'message': f'图片验证失败: {error_msg}', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 获取原规则图片URL old_guizetupian_url = shangpin_obj.guize_tupian # 生成新文件名:时间戳 + 随机数 + 原扩展名 timestamp = int(time.time() * 1000) % 100000000 # 8位时间戳 random_num = random.randint(1000, 9999) file_extension = os.path.splitext(guize_file.name)[1].lower() or '.jpg' new_filename = f"gz_{timestamp}_{random_num}{file_extension}" # 确定OSS路径 oss_folder = 'a_long' guize_folder = 'guize' # 修改为你的要求:a_long/guize/ oss_file_path = f"{oss_folder}/{guize_folder}/{new_filename}" # 删除旧图片(如果存在) if old_guizetupian_url: try: delete_from_oss(old_guizetupian_url) #logger.info(f"已删除旧规则图片: {old_guizetupian_url}") except Exception as e: logger.warning(f"删除旧规则图片失败: {old_guizetupian_url}, 错误: {e}") # 继续上传新图片,不中断流程 # 上传到OSS new_guizetupian_url = upload_to_oss(guize_file, oss_file_path) if not new_guizetupian_url: return Response( {'code': 500, 'message': '规则图片上传失败', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) # 更新数据库(存相对路径) shangpin_obj.guize_tupian = oss_file_path shangpin_obj.save() #logger.info(f"规则图片更新成功: {oss_file_path}") return Response({ 'code': 0, 'message': '规则图片更新成功', 'data': { 'guizetupian_url': oss_file_path # 返回相对路径 } }) # views.py