Files
Django/products/views/shangpin_admin.py
XingQue 6a107e873d feat: UFO 转盘购票与专区图标(不影响现有俱乐部点单)
新增转盘活动/奖品/机会表与 Czjilu leixing=7 履约;专区 tupian_url;hqsp/详情下发 shi_zhuanpan。普通点单与抢单主链路保持不变。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-22 18:59:26 +08:00

2559 lines
103 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.
"""商品发布与管理后台视图。
包含商品类型/专区/商品发布、规则图片上传、会员信息查询/修改/发布、
管理员商品/公告/专区/轮播/类型修改、商品内容获取与修改、规则修改。
"""
# ==================== 标准库 ====================
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