Files
Django/products/views.py

6404 lines
258 KiB
Python
Raw 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 orders.utils import update_daily_income
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 gvsdsdk.models import User
from orders.models import CommissionRate
from config.models import Szjilu, Gonggao, Lunbo
from orders.models import Penalty # 用于罚款类型
import traceback
# ==================== 日志实例 ====================
logger = logging.getLogger(__name__)
class ShangpinHuoquView(APIView):
"""
商品数据获取接口
一次性返回商品类型、商品专区、商品列表
"""
# 应用节流(限制频率)
throttle_classes = [AnonRateThrottle]
permission_classes = [AllowAny]
def post(self, request):
"""
处理POST请求返回所有商品相关数据
"""
try:
logger.info("收到商品数据获取请求")
# 1. 查询商品类型(只查询正常状态的类型,并排序)
# 正常运营:只显示 shenhezhuangtai=2 的商品类型
leixing_queryset = ShangpinLeixing.query.filter(
shenhezhuangtai=1 # 只取正常状态
).order_by('-paixu', 'id') # 先按paixu降序再按id升序
# 注意:审核期间使用下面这行代码(先注释掉)
'''leixing_queryset = ShangpinLeixing.query.filter(
shenhezhuangtai=2 # 审核期间:只取审核专用状态
).order_by('-paixu', 'id')'''
shangpinleixing_data = []
for leixing in leixing_queryset:
shangpinleixing_data.append({
"id": leixing.id,
"jieshao": leixing.jieshao,
"tupian_url": leixing.tupian_url,
"paixu": leixing.paixu
})
# 2. 查询商品专区(只查询可见类型下的专区,并排序)
# 先获取可见的类型ID列表
visible_leixing_ids = [leixing['id'] for leixing in shangpinleixing_data]
if visible_leixing_ids:
zhuanqu_queryset = ShangpinZhuanqu.query.filter(
leixing_id__in=visible_leixing_ids,
shenhezhuangtai=1 # 🔴【添加】只筛选审核状态为2的专区
).order_by('-paixu', 'id') # 先按paixu降序再按id升序
else:
zhuanqu_queryset = ShangpinZhuanqu.query.none()
shangpinzhuanqu_data = []
for zhuanqu in zhuanqu_queryset:
shangpinzhuanqu_data.append({
"id": zhuanqu.id,
"mingzi": zhuanqu.mingzi,
"leixing_id": zhuanqu.leixing_id,
"paixu": zhuanqu.paixu
})
# 3. 查询商品列表(只查询可见专区下的商品,并排序)
# 先获取可见的专区ID列表
visible_zhuanqu_ids = [zhuanqu['id'] for zhuanqu in shangpinzhuanqu_data]
if visible_zhuanqu_ids:
# 注意这里使用values()只获取指定字段,提高查询效率
shangpin_queryset = Shangpin.query.filter(
zhuanqu_id__in=visible_zhuanqu_ids,
shenhezhuangtai=1 # 🔴【添加】只筛选审核状态为2的商品
).values(
'id', 'biaoqian', 'jiage', 'leixing_id',
'zhuanqu_id', 'tupian_url', 'duiwai_xiaoliang', 'paixu'
).order_by('-paixu', 'id') # 先按paixu降序再按id升序
# 将QuerySet转换为列表
shangpinliebiao_data = list(shangpin_queryset)
else:
shangpinliebiao_data = []
# 4. 构建返回数据
response_data = {
"shangpinleixing": shangpinleixing_data,
"shangpinzhuanqu": shangpinzhuanqu_data,
"shangpinliebiao": shangpinliebiao_data
}
logger.info(f"返回数据:类型{len(shangpinleixing_data)}个,"
f"专区{len(shangpinzhuanqu_data)}个,"
f"商品{len(shangpinliebiao_data)}")
# 5. 返回响应
return Response(response_data, status=status.HTTP_200_OK)
except Exception as e:
# 记录异常
logger.error(f"获取商品数据时发生错误:{str(e)}", exc_info=True)
# 返回错误响应
error_data = {
"error": "服务器内部错误",
"detail": "获取商品数据失败,请稍后重试"
}
return Response(error_data, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class ShangpinXiangqingHuoquView(APIView):
"""
商品详情接口 - 极简版
假设所有字段都存在,直接返回
"""
throttle_classes = [AnonRateThrottle]
permission_classes = [AllowAny]
def post(self, request):
try:
shangpin_id = request.data.get('shangpin_id')
if not shangpin_id:
return Response({
"code": 1,
"msg": "商品ID不能为空",
"data": None
}, status=400)
# 直接查询,数据量小,性能没问题
shangpin = Shangpin.query.filter(id=shangpin_id).first()
if not shangpin:
return Response({
"code": 2,
"msg": "商品不存在",
"data": None
}, status=404)
# 直接构建数据,不处理空值
data = {
"tupian": shangpin.tupian_url,
"biaoti": shangpin.biaoqian,
"jiage": shangpin.jiage,
"xiangqing": shangpin.jieshao,
"xiadanxuzhi": shangpin.xiadan_xuzhi,
"guize": shangpin.guize_tupian,
"xiaoliang": shangpin.duiwai_xiaoliang,
"kucun": shangpin.kucun,
}
return Response({
"code": 0,
"msg": "success",
"data": data
})
except Exception as e:
logger.error(f"接口异常: {e}")
return Response({
"code": 500,
"msg": "服务器错误",
"data": None
}, status=500)
class DashouHuiyuanList(APIView):
"""
获取打手会员商品列表
路径:/shangpin/dshyhq
请求方式POST
权限JWT认证
返回:会员商品列表,字段对应前端需求
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 检查用户是否是打手(通过反向查询)
try:
dashou = request.user.DashouProfile
except UserDashou.DoesNotExist:
logger.warning(f"用户{request.user.UserUID}不是打手,尝试访问打手接口")
return Response({
'code': 400,
'message': '用户不是打手,无法访问此接口'
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 检查打手账号状态
if dashou.zhanghaozhuangtai != 1:
logger.warning(f"打手{request.user.UserUID}账号状态异常: {dashou.zhanghaozhuangtai}")
return Response({
'code': 400,
'message': '账号状态异常,无法购买会员'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 查询所有会员数据ORM查询确保性能
huiyuan_queryset = Huiyuan.query.all().only(
'huiyuan_id', # 会员ID
'jieshao', # 会员名字注意jieshao字段实际存储的是会员名字
'jtjieshao', # 会员具体介绍
'jiage' # 会员价格
).order_by('jiage') # 按价格排序
# 4. 构建返回数据列表
huiyuan_list = []
for huiyuan in huiyuan_queryset:
huiyuan_list.append({
'id': huiyuan.huiyuan_id, # 会员ID对应前端id
'mingzi': huiyuan.jieshao, # 会员名字对应前端mingzi
'jiage': float(huiyuan.jiage), # 会员价格(转为浮点数)
'jieshao': huiyuan.jtjieshao # 会员介绍对应前端jieshao
})
# 5. 返回数据
return Response({
'code': 200,
'message': '获取会员列表成功',
'data': huiyuan_list # 对应前端接收的列表
}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"获取会员列表接口异常: {str(e)}", exc_info=True)
return Response({
'code': 500,
'message': '服务器内部错误,请稍后重试'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class YajinGoumai(APIView):
"""
押金充值支付参数接口
路径:/shangpin/yajingoumai
请求方式POST
权限JWT认证
参数jine金额1-10000元
返回:支付参数(payParams)和订单ID(dingdanid)
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 验证用户身份和状态
try:
dashou = request.user.DashouProfile
except UserDashou.DoesNotExist:
return Response({
'code': 400,
'message': '用户不是打手,无法充值押金'
}, status=status.HTTP_400_BAD_REQUEST)
if dashou.zhanghaozhuangtai != 1:
return Response({
'code': 400,
'message': '账号状态异常,无法进行充值'
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 获取并验证金额参数
jine = request.data.get('jine')
if not jine:
return Response({
'code': 400,
'message': '请填写充值金额'
}, status=status.HTTP_400_BAD_REQUEST)
try:
jine = int(jine)
except (ValueError, TypeError):
return Response({
'code': 400,
'message': '金额格式错误请填写1-10000的整数'
}, status=status.HTTP_400_BAD_REQUEST)
# 验证金额范围
if jine < 1 or jine > 10000:
return Response({
'code': 400,
'message': '充值金额必须在1-10000元之间'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 生成订单ID使用你提供的算法前缀改为CzYJ
timestamp_ms = int(time.time() * 1000)
timestamp_ns = int(time.time_ns() // 1000) % 1000000
random_num = random.randint(0, 99)
dingdanid = f"CzYJ{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}"
# 4. 创建充值记录订单
chongzhi_jilu = Czjilu.query.create(
dingdan_id=dingdanid,
zhuangtai=9, # 未支付状态
yonghuid=request.user.UserUID,
jine=jine,
leixing=2, # 充值类型2=押金
shuoming=settings.YAJIN_PAY_DESCRIPTION
)
logger.info(f"创建押金充值订单: 用户{request.user.UserUID}, 订单{dingdanid}, 金额{jine}")
# 5. 生成微信支付参数
try:
pay_params = self.generate_wechat_pay_params(
dingdanid=dingdanid,
jine=jine,
openid=request.user.OpenID, # 直接从request.user获取openid
pay_type='yajin'
)
except Exception as e:
# 支付参数生成失败,删除订单记录
chongzhi_jilu.delete()
logger.error(f"生成微信支付参数失败: {str(e)}")
return Response({
'code': 500,
'message': f'支付参数生成失败: {str(e)}'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# 6. 返回支付参数和订单ID
return Response({
'code': 200,
'message': '支付参数生成成功',
'payParams': pay_params, # 支付参数前端接收为payParams
'dingdanid': dingdanid # 订单ID前端接收为dingdanid
}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"押金充值接口异常: {str(e)}", exc_info=True)
return Response({
'code': 500,
'message': '服务器内部错误,请稍后重试'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type):
"""生成微信小程序支付参数"""
# 获取配置
APPID = settings.WEIXIN_APPID
MCHID = settings.WEIXIN_MCHID
KEY = settings.WEIXIN_SHANGHUMIYAO
# 根据支付类型获取对应的配置
if pay_type == 'yajin':
PAY_DESCRIPTION = settings.YAJIN_PAY_DESCRIPTION
NOTIFY_URL = settings.YAJIN_PAY_NOTIFY_URL
elif pay_type == 'jifen':
PAY_DESCRIPTION = settings.JIFEN_PAY_DESCRIPTION
NOTIFY_URL = settings.JIFEN_PAY_NOTIFY_URL
elif pay_type == 'huiyuan':
PAY_DESCRIPTION = settings.HUIYUAN_PAY_DESCRIPTION
NOTIFY_URL = settings.HUIYUAN_PAY_NOTIFY_URL
else:
raise Exception('未知的支付类型')
# 生成随机字符串
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
# 金额转换(元转分)
total_fee = int(jine * 100)
# 交易类型小程序支付使用JSAPI
trade_type = 'JSAPI'
# 获取客户端IP
user_ip = self.get_client_ip()
# 1. 第一次签名(统一下单接口)
sign_data = {
'appid': APPID,
'mch_id': MCHID,
'nonce_str': nonce_str,
'body': PAY_DESCRIPTION,
'out_trade_no': dingdanid,
'total_fee': total_fee,
'spbill_create_ip': user_ip,
'notify_url': NOTIFY_URL,
'trade_type': trade_type,
'openid': openid # 小程序支付必须传openid
}
# 按照参数名ASCII码从小到大排序
sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0])
# 拼接字符串
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value])
sign_string += f'&key={KEY}'
# MD5签名
sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
# 构造XML数据
xml_data = f"""<xml>
<appid>{APPID}</appid>
<body>{PAY_DESCRIPTION}</body>
<mch_id>{MCHID}</mch_id>
<nonce_str>{nonce_str}</nonce_str>
<notify_url>{NOTIFY_URL}</notify_url>
<openid>{openid}</openid>
<out_trade_no>{dingdanid}</out_trade_no>
<spbill_create_ip>{user_ip}</spbill_create_ip>
<total_fee>{total_fee}</total_fee>
<trade_type>{trade_type}</trade_type>
<sign>{sign}</sign>
</xml>"""
# 调用微信统一下单接口(官方地址)
response = requests.post(
'https://api.mch.weixin.qq.com/pay/unifiedorder',
data=xml_data.encode('utf-8'),
headers={'Content-Type': 'application/xml'},
timeout=10 # 设置超时
)
# 解析返回的XML
root = ET.fromstring(response.content)
return_code = root.find('return_code').text
result_code = root.find('result_code').text
if return_code == 'SUCCESS' and result_code == 'SUCCESS':
# 获取prepay_id
prepay_id = root.find('prepay_id').text
# 2. 第二次签名(小程序支付参数)
timestamp = str(int(time.time()))
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
package = f'prepay_id={prepay_id}'
sign_type = 'MD5'
# 生成支付签名注意这里key的大小写和字段名与第一次不同
pay_sign_data = {
'appId': APPID, # 注意这里是appId不是appid
'timeStamp': timestamp, # 注意这里是timeStamp不是timestamp
'nonceStr': nonce_str,
'package': package,
'signType': sign_type
}
sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0])
pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data])
pay_sign_string += f'&key={KEY}'
pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper()
# 返回前端需要的支付参数
return {
'appId': APPID, # 对应前端appId
'timeStamp': timestamp, # 对应前端timeStamp
'nonceStr': nonce_str, # 对应前端nonceStr
'package': package, # 对应前端package
'signType': sign_type, # 对应前端signType
'paySign': pay_sign # 对应前端paySign
}
else:
# 获取错误信息
err_code = root.find('err_code')
err_code_des = root.find('err_code_des')
return_msg = root.find('return_msg')
error_details = f"return_code: {return_code}"
if err_code is not None:
error_details += f", err_code: {err_code.text}"
if err_code_des is not None:
error_details += f", err_code_des: {err_code_des.text}"
if return_msg is not None:
error_details += f", return_msg: {return_msg.text}"
raise Exception(f"微信支付下单失败: {error_details}")
def get_client_ip(self):
"""获取客户端IP从DRF的request对象中获取"""
request = self.request
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip if ip else '127.0.0.1'
class YajinHuitiao(View):
"""
押金支付回调接口
路径:/shangpin/yajinhuitiao
注意这个接口不需要JWT认证由微信服务器调用
"""
def post(self, request):
try:
# 1. 解析微信回调XML数据
request_body = request.body
logger.info(f"收到微信支付回调,原始数据: {request_body.decode('utf-8')}")
root = ET.fromstring(request_body)
# 获取关键字段
return_code = root.find('return_code').text
result_code = root.find('result_code').text
out_trade_no = root.find('out_trade_no').text # 订单ID
transaction_id = root.find('transaction_id').text # 微信交易号
total_fee = int(root.find('total_fee').text) # 支付金额(分)
logger.info(f"回调数据解析: 订单{out_trade_no}, 状态{return_code}/{result_code}, 金额{total_fee}")
# 2. 验证基本返回状态
if return_code != 'SUCCESS' or result_code != 'SUCCESS':
logger.warning(f"微信支付回调状态异常: {return_code}/{result_code}")
return self.wechat_response('FAIL', '支付失败')
# 3. 验证签名(确保是微信发来的)
if not self.verify_signature(request_body):
logger.error(f"签名验证失败: {out_trade_no}")
return self.wechat_response('FAIL', '签名验证失败')
# 4. 查询订单
try:
order = Czjilu.query.get(dingdan_id=out_trade_no)
logger.info(f"找到订单: {order.dingdan_id}, 状态: {order.zhuangtai}, 金额: {order.jine}")
except Czjilu.DoesNotExist:
logger.error(f"订单不存在: {out_trade_no}")
return self.wechat_response('FAIL', '订单不存在')
# 5. 检查订单状态是否为未支付9
if order.zhuangtai != 9:
logger.warning(f"订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}")
# 已处理过的订单直接返回成功,避免微信重复回调
return self.wechat_response('SUCCESS', 'OK')
# 6. 验证金额(微信返回的是分,需要转换)
wechat_amount_yuan = total_fee / 100 # 转换为元
order_amount_yuan = float(order.jine) # 订单金额(元)
# 允许1分钱的误差浮点数比较
if abs(wechat_amount_yuan - order_amount_yuan) > 0.01:
logger.error(f"金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}")
return self.wechat_response('FAIL', '金额不一致')
# 7. 更新订单状态为已支付3
order.zhuangtai = 3
order.save(update_fields=['zhuangtai'])
logger.info(f"订单状态更新成功: {order.dingdan_id} -> 3")
# 8. 更新打手押金
try:
# 通过用户ID查找打手高效查询
dashou = UserDashou.query.get(user__UserUID=order.yonghuid)
# 增加押金使用Decimal确保精度
dashou.yajin += Decimal(str(order_amount_yuan))
dashou.save(update_fields=['yajin'])
logger.info(f"打手押金更新成功: 用户{order.yonghuid}, 增加{order_amount_yuan}元, 新押金{dashou.yajin}")
# 8b. 押金分红(仅微信确认到账后;费率 CommissionRate Platform=12
try:
guanshi_id = dashou.yaoqingren
if not guanshi_id:
logger.info(f"押金分红跳过: 打手{order.yonghuid}无邀请管事")
elif Gsfenhong.query.filter(dingdan_id=order.dingdan_id).exists():
logger.info(f"押金分红已处理: 订单{order.dingdan_id}")
else:
lilu_obj = CommissionRate.query.filter(Platform='12').first()
rate = lilu_obj.Rate if lilu_obj and lilu_obj.Rate is not None else Decimal('0')
if rate <= 0:
logger.info(f"押金分红跳过: 费率未配置或为0")
else:
recharge_amount = Decimal(str(order_amount_yuan))
fenhong_jine = (recharge_amount * rate).quantize(Decimal('0.01'))
if fenhong_jine > 0:
with transaction.atomic():
guanshi = UserGuanshi.objects.select_for_update().get(
user__UserUID=guanshi_id
)
guanshi.yue += fenhong_jine
guanshi.chongzhifenrun += fenhong_jine
guanshi.save(update_fields=['yue', 'chongzhifenrun'])
user_main = dashou.user
Gsfenhong.query.create(
dingdan_id=order.dingdan_id,
guanshi=guanshi_id,
dashouid=order.yonghuid,
shuoming='押金分红',
fenhong=fenhong_jine,
avatar=user_main.Avatar,
nicheng=dashou.nicheng or '未知打手',
fenhong_leixing=2,
)
logger.info(
f"押金分红成功: 订单{order.dingdan_id}, 管事{guanshi_id}, "
f"打手{order.yonghuid}, 金额{fenhong_jine}"
)
try:
update_guanshi_daily_by_action(
yonghuid=guanshi_id,
action=4,
amount=fenhong_jine,
)
except Exception as e:
logger.error(f"押金分红管事日统计更新失败: {e}")
except UserGuanshi.DoesNotExist:
logger.warning(f"押金分红跳过: 管事{guanshi_id}不存在")
except Exception as e:
logger.error(f"押金分红处理失败: {str(e)}", exc_info=True)
except UserDashou.DoesNotExist:
# 如果找不到打手,记录错误,但依然返回成功(避免微信重复回调)
logger.error(f"打手信息不存在: 用户{order.yonghuid}")
return self.wechat_response('SUCCESS', 'OK')
except Exception as e:
logger.error(f"更新打手押金失败: {str(e)}", exc_info=True)
return self.wechat_response('FAIL', '更新押金失败')
# ==== 在返回成功响应之前添加这段代码 ====
# 更新收支记录表
try:
# 获取订单金额确保是Decimal类型
jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine
# 查询或创建ID=1的收支记录
update_daily_income(jine_decimal)
szjilu, created = Szjilu.query.get_or_create(
id=1,
defaults={
'TotalIncome': Decimal('0.00'),
'TotalFlow': Decimal('0.00'),
'TotalExpense': Decimal('0.00'),
'DailyExpense': Decimal('0.00'),
'DailyFlow': Decimal('0.00')
}
)
# 更新三个字段
szjilu.TotalIncome += jine_decimal # 总收益
szjilu.TotalFlow += jine_decimal # 总流水
szjilu.DailyFlow += jine_decimal # 今日流水
szjilu.save()
logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}")
except Exception as e:
# 收支记录更新失败不影响主流程,只记录日志
logger.error(f"更新收支记录失败: {str(e)}", exc_info=True)
# ==== 添加结束 ====
# 9. 返回成功响应给微信
logger.info(f"押金支付回调处理完成: {out_trade_no}")
return self.wechat_response('SUCCESS', 'OK')
except ET.ParseError as e:
logger.error(f"XML解析失败: {str(e)}")
return self.wechat_response('FAIL', '数据格式错误')
except Exception as e:
logger.error(f"押金支付回调处理异常: {str(e)}", exc_info=True)
# 返回FAIL让微信重试
return self.wechat_response('FAIL', '处理异常')
def verify_signature(self, xml_data):
"""验证微信支付签名"""
try:
root = ET.fromstring(xml_data)
sign = root.find('sign').text
if not sign:
logger.error("签名字段不存在")
return False
# 获取所有参数并排除sign
params = {}
for child in root:
if child.tag != 'sign' and child.text is not None:
params[child.tag] = child.text
# 按照参数名ASCII码从小到大排序
sorted_params = sorted(params.items(), key=lambda x: x[0])
# 拼接字符串
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value])
sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}'
# 计算签名
calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
is_valid = sign == calculated_sign
if not is_valid:
logger.error(f"签名验证失败: 计算签名{calculated_sign}, 收到签名{sign}")
return is_valid
except Exception as e:
logger.error(f"验证签名异常: {str(e)}")
return False
def wechat_response(self, return_code, return_msg):
"""返回微信支付回调响应"""
xml_response = f"""<xml>
<return_code><![CDATA[{return_code}]]></return_code>
<return_msg><![CDATA[{return_msg}]]></return_msg>
</xml>"""
logger.info(f"返回微信响应: {return_code} - {return_msg}")
return HttpResponse(xml_response, content_type='application/xml')
# views.py
class YajinShibai(APIView):
"""
押金支付失败处理接口
路径:/shangpin/yajinshibai
请求方式POST
权限JWT认证
参数dingdanid订单ID
返回:删除结果
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 获取订单ID
dingdanid = request.data.get('dingdanid')
if not dingdanid:
return Response({
'code': 400,
'message': '订单ID不能为空'
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 查询订单(确保是当前用户的订单)
try:
order = Czjilu.query.get(
dingdan_id=dingdanid,
UserUID=request.user.UserUID
)
except Czjilu.DoesNotExist:
return Response({
'code': 400,
'message': '订单不存在或不属于当前用户'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 检查订单状态是否为未支付9
if order.zhuangtai == 9:
# 删除订单
order.delete()
logger.info(f"用户{request.user.UserUID}删除未支付订单: {dingdanid}")
return Response({
'code': 200,
'message': '订单已删除'
}, status=status.HTTP_200_OK)
else:
return Response({
'code': 400,
'message': '订单状态不是未支付,无法删除'
}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
logger.error(f"押金支付失败处理异常: {str(e)}", exc_info=True)
return Response({
'code': 500,
'message': '服务器内部错误'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# views.py
class YajinLunxun(APIView):
"""
押金支付成功轮询接口
路径:/shangpin/yajinlunxun
请求方式POST
权限JWT认证
参数dingdanid订单ID
返回:订单状态和当前押金
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 获取订单ID
dingdanid = request.data.get('dingdanid')
if not dingdanid:
return Response({
'code': 400,
'message': '订单ID不能为空'
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 查询订单(确保是当前用户的订单)
try:
order = Czjilu.query.get(
dingdan_id=dingdanid,
UserUID=request.user.UserUID
)
except Czjilu.DoesNotExist:
return Response({
'code': 400,
'message': '订单不存在或不属于当前用户'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 检查订单状态是否为已支付3
if order.zhuangtai != 3:
logger.info(f"订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}")
return Response({
'code': 400,
'message': '订单尚未支付完成'
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 获取打手押金(通过反向查询,最快捷的方式)
try:
dashou = request.user.DashouProfile
yajin = float(dashou.yajin) # 转为浮点数方便前端处理
logger.info(f"轮询成功: 订单{dingdanid}已支付,用户{request.user.UserUID}押金为{yajin}")
return Response({
'code': 200,
'message': '支付流程全部完成',
'yajin': yajin # 返回当前押金(前端需要更新全局变量)
}, status=status.HTTP_200_OK)
except UserDashou.DoesNotExist:
logger.error(f"打手信息不存在: 用户{request.user.UserUID}")
return Response({
'code': 400,
'message': '打手信息不存在'
}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
logger.error(f"押金轮询异常: {str(e)}", exc_info=True)
return Response({
'code': 500,
'message': '服务器内部错误'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# views.py
class JifenBuchong(APIView):
"""
积分补充支付参数接口
路径:/shangpin/jifenbc
请求方式POST
权限JWT认证
参数固定5元
返回:支付参数(payParams)和订单ID(dingdanid)
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 验证用户身份和状态
try:
dashou = request.user.DashouProfile
except UserDashou.DoesNotExist:
return Response({
'code': 400,
'message': '用户不是打手,无法补充积分'
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 检查打手账号状态
if dashou.zhanghaozhuangtai != 1:
return Response({
'code': 400,
'message': '账号状态异常,无法进行操作'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 检查积分是否已满满分10分
if dashou.jifen >= 10:
return Response({
'code': 400,
'message': '积分已满,无需补充'
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 生成订单ID前缀CzJF表示积分充值
timestamp_ms = int(time.time() * 1000)
timestamp_ns = int(time.time_ns() // 1000) % 1000000
random_num = random.randint(0, 99)
dingdanid = f"CzJF{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}"
# 5. 创建积分充值记录订单固定5元
jine = 5.00 # 固定5元
chongzhi_jilu = Czjilu.query.create(
dingdan_id=dingdanid,
zhuangtai=9, # 未支付状态
yonghuid=request.user.UserUID,
jine=jine,
leixing=3, # 充值类型3=积分
shuoming=settings.JIFEN_PAY_DESCRIPTION
)
logger.info(f"创建积分充值订单: 用户{request.user.UserUID}, 订单{dingdanid}, 金额{jine}")
# 6. 生成微信支付参数(完整版)
try:
pay_params = self.generate_wechat_pay_params(
dingdanid=dingdanid,
jine=jine,
openid=request.user.OpenID,
pay_type='jifen'
)
except Exception as e:
# 支付参数生成失败,删除订单记录
chongzhi_jilu.delete()
logger.error(f"生成积分支付参数失败: {str(e)}")
return Response({
'code': 500,
'message': f'支付参数生成失败: {str(e)}'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# 7. 返回支付参数和订单ID
return Response({
'code': 200,
'message': '支付参数生成成功',
'payParams': pay_params, # 对应前端payParams
'dingdanid': dingdanid # 对应前端dingdanid
}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"积分充值接口异常: {str(e)}", exc_info=True)
return Response({
'code': 500,
'message': '服务器内部错误,请稍后重试'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type):
"""生成微信支付参数(完整版,无省略)"""
# 微信支付配置 - 从settings中获取
APPID = settings.WEIXIN_APPID
MCHID = settings.WEIXIN_MCHID
KEY = settings.WEIXIN_SHANGHUMIYAO
# 根据支付类型获取对应的配置
if pay_type == 'yajin':
PAY_DESCRIPTION = settings.YAJIN_PAY_DESCRIPTION
NOTIFY_URL = settings.YAJIN_PAY_NOTIFY_URL
elif pay_type == 'jifen':
PAY_DESCRIPTION = settings.JIFEN_PAY_DESCRIPTION
NOTIFY_URL = settings.JIFEN_PAY_NOTIFY_URL
elif pay_type == 'huiyuan':
PAY_DESCRIPTION = settings.HUIYUAN_PAY_DESCRIPTION
NOTIFY_URL = settings.HUIYUAN_PAY_NOTIFY_URL
else:
raise Exception('未知的支付类型')
# 生成随机字符串
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
# 金额转换(元转分)
total_fee = int(jine * 100)
# 交易类型小程序支付使用JSAPI
trade_type = 'JSAPI'
# 获取客户端IP
user_ip = self.get_client_ip()
# 1. 第一次签名(统一下单接口)
sign_data = {
'appid': APPID,
'mch_id': MCHID,
'nonce_str': nonce_str,
'body': PAY_DESCRIPTION,
'out_trade_no': dingdanid,
'total_fee': total_fee,
'spbill_create_ip': user_ip,
'notify_url': NOTIFY_URL,
'trade_type': trade_type,
'openid': openid # 小程序支付必须传openid
}
# 按照参数名ASCII码从小到大排序
sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0])
# 拼接字符串
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value])
sign_string += f'&key={KEY}'
# MD5签名
sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
# 构造XML数据
xml_data = f"""<xml>
<appid>{APPID}</appid>
<body>{PAY_DESCRIPTION}</body>
<mch_id>{MCHID}</mch_id>
<nonce_str>{nonce_str}</nonce_str>
<notify_url>{NOTIFY_URL}</notify_url>
<openid>{openid}</openid>
<out_trade_no>{dingdanid}</out_trade_no>
<spbill_create_ip>{user_ip}</spbill_create_ip>
<total_fee>{total_fee}</total_fee>
<trade_type>{trade_type}</trade_type>
<sign>{sign}</sign>
</xml>"""
# 调用微信统一下单接口(官方地址)
response = requests.post(
'https://api.mch.weixin.qq.com/pay/unifiedorder',
data=xml_data.encode('utf-8'),
headers={'Content-Type': 'application/xml'},
timeout=10 # 设置超时
)
# 解析返回的XML
root = ET.fromstring(response.content)
return_code = root.find('return_code').text
result_code = root.find('result_code').text
if return_code == 'SUCCESS' and result_code == 'SUCCESS':
# 获取prepay_id
prepay_id = root.find('prepay_id').text
# 2. 第二次签名(小程序支付参数)
timestamp = str(int(time.time()))
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
package = f'prepay_id={prepay_id}'
sign_type = 'MD5'
# 生成支付签名注意这里key的大小写和字段名与第一次不同
pay_sign_data = {
'appId': APPID, # 注意这里是appId不是appid
'timeStamp': timestamp, # 注意这里是timeStamp不是timestamp
'nonceStr': nonce_str,
'package': package,
'signType': sign_type
}
sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0])
pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data])
pay_sign_string += f'&key={KEY}'
pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper()
# 返回前端需要的支付参数
return {
'appId': APPID, # 对应前端appId
'timeStamp': timestamp, # 对应前端timeStamp
'nonceStr': nonce_str, # 对应前端nonceStr
'package': package, # 对应前端package
'signType': sign_type, # 对应前端signType
'paySign': pay_sign # 对应前端paySign
}
else:
# 获取错误信息
err_code = root.find('err_code')
err_code_des = root.find('err_code_des')
return_msg = root.find('return_msg')
error_details = f"return_code: {return_code}"
if err_code is not None:
error_details += f", err_code: {err_code.text}"
if err_code_des is not None:
error_details += f", err_code_des: {err_code_des.text}"
if return_msg is not None:
error_details += f", return_msg: {return_msg.text}"
raise Exception(f"微信支付下单失败: {error_details}")
def get_client_ip(self):
"""获取客户端IP完整版"""
request = self.request
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip if ip else '127.0.0.1'
# views.py
class JifenHuitiao(View):
"""
积分支付回调接口
路径:/shangpin/jifenhuitiao
注意这个接口不需要JWT认证由微信服务器调用
"""
def post(self, request):
try:
# 1. 解析微信回调XML数据
request_body = request.body
logger.info(f"收到积分支付回调,原始数据: {request_body.decode('utf-8')}")
root = ET.fromstring(request_body)
# 获取关键字段
return_code = root.find('return_code').text
result_code = root.find('result_code').text
out_trade_no = root.find('out_trade_no').text # 订单ID
transaction_id = root.find('transaction_id').text # 微信交易号
total_fee = int(root.find('total_fee').text) # 支付金额(分)
logger.info(f"积分回调数据: 订单{out_trade_no}, 状态{return_code}/{result_code}, 金额{total_fee}")
# 2. 验证基本返回状态
if return_code != 'SUCCESS' or result_code != 'SUCCESS':
logger.warning(f"积分支付回调状态异常: {return_code}/{result_code}")
return self.wechat_response('FAIL', '支付失败')
# 3. 验证签名
if not self.verify_signature(request_body):
logger.error(f"积分签名验证失败: {out_trade_no}")
return self.wechat_response('FAIL', '签名验证失败')
# 4. 查询订单
try:
order = Czjilu.query.get(dingdan_id=out_trade_no)
logger.info(f"找到积分订单: {order.dingdan_id}, 状态: {order.zhuangtai}, 金额: {order.jine}")
except Czjilu.DoesNotExist:
logger.error(f"积分订单不存在: {out_trade_no}")
return self.wechat_response('FAIL', '订单不存在')
# 5. 检查订单状态是否为未支付9
if order.zhuangtai != 9:
logger.warning(f"积分订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}")
return self.wechat_response('SUCCESS', 'OK')
# 6. 验证金额固定5元
wechat_amount_yuan = total_fee / 100 # 转换为元
order_amount_yuan = float(order.jine) # 订单金额(元)
# 验证是否是5元允许1分钱误差
if abs(wechat_amount_yuan - 5.00) > 0.01:
logger.error(f"积分金额错误: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}")
return self.wechat_response('FAIL', '金额错误')
# ==== 在返回成功响应之前添加这段代码 ====
# 更新收支记录表
try:
# 获取订单金额确保是Decimal类型
jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine
update_daily_income(jine_decimal)
# 查询或创建ID=1的收支记录
szjilu, created = Szjilu.query.get_or_create(
id=1,
defaults={
'TotalIncome': Decimal('0.00'),
'TotalFlow': Decimal('0.00'),
'TotalExpense': Decimal('0.00'),
'DailyExpense': Decimal('0.00'),
'DailyFlow': Decimal('0.00')
}
)
# 更新三个字段
szjilu.TotalIncome += jine_decimal # 总收益
szjilu.TotalFlow += jine_decimal # 总流水
szjilu.DailyFlow += jine_decimal # 今日流水
szjilu.save()
logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}")
except Exception as e:
# 收支记录更新失败不影响主流程,只记录日志
logger.error(f"更新收支记录失败: {str(e)}", exc_info=True)
# ==== 添加结束 ====
# 7. 更新订单状态为已支付3
order.zhuangtai = 3
order.save(update_fields=['zhuangtai'])
logger.info(f"积分订单状态更新成功: {order.dingdan_id} -> 3")
# 8. 更新打手积分
try:
# 通过用户ID查找打手
dashou = UserDashou.query.get(user__UserUID=order.yonghuid)
# 检查积分是否小于10
if dashou.jifen < 10:
# 增加5积分但不超过10
new_jifen = dashou.jifen + 5
dashou.jifen = min(new_jifen, 10) # 确保不超过10
dashou.save(update_fields=['jifen'])
logger.info(f"打手积分更新成功: 用户{order.yonghuid}, 增加5积分, 新积分{dashou.jifen}")
else:
logger.warning(f"打手积分已满: 用户{order.yonghuid}, 当前积分{dashou.jifen}")
except UserDashou.DoesNotExist:
logger.error(f"打手信息不存在: 用户{order.yonghuid}")
return self.wechat_response('SUCCESS', 'OK')
except Exception as e:
logger.error(f"更新打手积分失败: {str(e)}", exc_info=True)
return self.wechat_response('FAIL', '更新积分失败')
# 9. 返回成功响应给微信
logger.info(f"积分支付回调处理完成: {out_trade_no}")
return self.wechat_response('SUCCESS', 'OK')
except ET.ParseError as e:
logger.error(f"积分XML解析失败: {str(e)}")
return self.wechat_response('FAIL', '数据格式错误')
except Exception as e:
logger.error(f"积分支付回调处理异常: {str(e)}", exc_info=True)
return self.wechat_response('FAIL', '处理异常')
def verify_signature(self, xml_data):
"""验证微信支付签名"""
try:
root = ET.fromstring(xml_data)
sign = root.find('sign').text
if not sign:
logger.error("积分数签名字段不存在")
return False
# 获取所有参数并排除sign
params = {}
for child in root:
if child.tag != 'sign' and child.text is not None:
params[child.tag] = child.text
# 按照参数名ASCII码从小到大排序
sorted_params = sorted(params.items(), key=lambda x: x[0])
# 拼接字符串
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value])
sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}'
# 计算签名
calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
is_valid = sign == calculated_sign
if not is_valid:
logger.error(f"积分签名验证失败: 计算签名{calculated_sign}, 收到签名{sign}")
return is_valid
except Exception as e:
logger.error(f"积分验证签名异常: {str(e)}")
return False
def wechat_response(self, return_code, return_msg):
"""返回微信支付回调响应"""
xml_response = f"""<xml>
<return_code><![CDATA[{return_code}]]></return_code>
<return_msg><![CDATA[{return_msg}]]></return_msg>
</xml>"""
logger.info(f"积分返回微信响应: {return_code} - {return_msg}")
return HttpResponse(xml_response, content_type='application/xml')
# views.py
class JifenShibai(APIView):
"""
积分支付失败处理接口
路径:/shangpin/jifenshibai
请求方式POST
权限JWT认证
参数dingdanid订单ID
返回:删除结果
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 获取订单ID
dingdanid = request.data.get('dingdanid')
if not dingdanid:
return Response({
'code': 400,
'message': '订单ID不能为空'
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 查询订单(确保是当前用户的订单)
try:
order = Czjilu.query.get(
dingdan_id=dingdanid,
UserUID=request.user.UserUID
)
except Czjilu.DoesNotExist:
return Response({
'code': 400,
'message': '订单不存在或不属于当前用户'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 检查订单类型是否为积分3
if order.leixing != 3:
return Response({
'code': 400,
'message': '订单类型错误,不是积分订单'
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 检查订单状态是否为未支付9
if order.zhuangtai == 9:
# 删除订单
order.delete()
logger.info(f"用户{request.user.UserUID}删除未支付积分订单: {dingdanid}")
return Response({
'code': 200,
'message': '积分订单已删除'
}, status=status.HTTP_200_OK)
else:
return Response({
'code': 400,
'message': '积分订单状态不是未支付,无法删除'
}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
logger.error(f"积分支付失败处理异常: {str(e)}", exc_info=True)
return Response({
'code': 500,
'message': '服务器内部错误'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# views.py
class JifenLunxun(APIView):
"""
积分支付成功轮询接口
路径:/shangpin/jifenlunxun
请求方式POST
权限JWT认证
参数dingdanid订单ID
返回:订单状态和当前积分
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 获取订单ID
dingdanid = request.data.get('dingdanid')
if not dingdanid:
return Response({
'code': 400,
'message': '订单ID不能为空'
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 查询订单(确保是当前用户的订单)
try:
order = Czjilu.query.get(
dingdan_id=dingdanid,
UserUID=request.user.UserUID
)
except Czjilu.DoesNotExist:
return Response({
'code': 400,
'message': '订单不存在或不属于当前用户'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 检查订单类型是否为积分3
if order.leixing != 3:
return Response({
'code': 400,
'message': '订单类型错误,不是积分订单'
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 检查订单状态是否为已支付3
if order.zhuangtai != 3:
logger.info(f"积分订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}")
return Response({
'code': 400,
'message': '积分订单尚未支付完成'
}, status=status.HTTP_400_BAD_REQUEST)
# 5. 获取打手积分(通过反向查询)
try:
dashou = request.user.DashouProfile
jifen = dashou.jifen # 直接使用整数
logger.info(f"积分轮询成功: 订单{dingdanid}已支付,用户{request.user.UserUID}积分为{jifen}")
return Response({
'code': 200,
'message': '支付流程全部完成',
'jifen': jifen # 返回当前积分(前端需要更新全局变量)
}, status=status.HTTP_200_OK)
except UserDashou.DoesNotExist:
logger.error(f"打手信息不存在: 用户{request.user.UserUID}")
return Response({
'code': 400,
'message': '打手信息不存在'
}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
logger.error(f"积分轮询异常: {str(e)}", exc_info=True)
return Response({
'code': 500,
'message': '服务器内部错误'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class HuiyuanGoumai(APIView):
"""
会员充值支付参数接口
路径:/shangpin/huiyuangm
请求方式POST
权限JWT认证
参数huiyuanid会员ID
返回:支付参数(payParams)和订单ID(dingdanid)
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 获取会员ID参数
huiyuanid = request.data.get('huiyuanid')
if not huiyuanid:
return Response({
'code': 400,
'message': '会员ID不能为空'
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 验证用户身份和状态
try:
dashou = request.user.DashouProfile
except UserDashou.DoesNotExist:
return Response({
'code': 400,
'message': '用户不是打手,无法购买会员'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 检查打手账号状态
if dashou.zhanghaozhuangtai != 1:
return Response({
'code': 400,
'message': '账号状态异常,无法进行购买'
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 查询会员信息
try:
huiyuan = Huiyuan.query.get(huiyuan_id=huiyuanid)
except Huiyuan.DoesNotExist:
return Response({
'code': 400,
'message': '会员信息不存在'
}, status=status.HTTP_400_BAD_REQUEST)
# 5. 生成订单ID前缀CzHY表示会员充值
timestamp_ms = int(time.time() * 1000)
timestamp_ns = int(time.time_ns() // 1000) % 1000000
random_num = random.randint(0, 99)
dingdanid = f"CzHY{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}"
# 6. 创建充值记录订单
jine = float(huiyuan.jiage) # 会员价格
# 组合订单说明:会员介绍 + 配置描述
shuoming = f"{huiyuan.jieshao} - {settings.HUIYUAN_PAY_DESCRIPTION}"
chongzhi_jilu = Czjilu.query.create(
dingdan_id=dingdanid,
zhuangtai=9, # 未支付状态
yonghuid=request.user.UserUID,
jine=jine,
leixing=1, # 充值类型1=会员
shuoming=shuoming,
huiyuan_id=huiyuanid # 存储会员ID
)
logger.info(
f"创建会员充值订单: 用户{request.user.UserUID}, 订单{dingdanid}, 会员{huiyuanid}, 金额{jine}")
# 7. 生成微信支付参数
try:
pay_params = self.generate_wechat_pay_params(
dingdanid=dingdanid,
jine=jine,
openid=request.user.OpenID,
pay_type='huiyuan'
)
except Exception as e:
# 支付参数生成失败,删除订单记录
chongzhi_jilu.delete()
logger.error(f"生成会员支付参数失败: {str(e)}")
return Response({
'code': 500,
'message': f'支付参数生成失败: {str(e)}'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# 8. 返回支付参数和订单ID
return Response({
'code': 200,
'message': '支付参数生成成功',
'payParams': pay_params, # 对应前端payParams
'dingdanid': dingdanid # 对应前端dingdanid
}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"会员充值接口异常: {str(e)}", exc_info=True)
return Response({
'code': 500,
'message': '服务器内部错误,请稍后重试'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type):
"""生成微信支付参数(完整版,无省略)"""
# 微信支付配置 - 从settings中获取
APPID = settings.WEIXIN_APPID
MCHID = settings.WEIXIN_MCHID
KEY = settings.WEIXIN_SHANGHUMIYAO
# 根据支付类型获取对应的配置
if pay_type == 'yajin':
PAY_DESCRIPTION = settings.YAJIN_PAY_DESCRIPTION
NOTIFY_URL = settings.YAJIN_PAY_NOTIFY_URL
elif pay_type == 'jifen':
PAY_DESCRIPTION = settings.JIFEN_PAY_DESCRIPTION
NOTIFY_URL = settings.JIFEN_PAY_NOTIFY_URL
elif pay_type == 'huiyuan':
PAY_DESCRIPTION = settings.HUIYUAN_PAY_DESCRIPTION
NOTIFY_URL = settings.HUIYUAN_PAY_NOTIFY_URL
else:
raise Exception('未知的支付类型')
# 生成随机字符串
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
# 金额转换(元转分)
total_fee = int(jine * 100)
# 交易类型小程序支付使用JSAPI
trade_type = 'JSAPI'
# 获取客户端IP
user_ip = self.get_client_ip()
# 1. 第一次签名(统一下单接口)
sign_data = {
'appid': APPID,
'mch_id': MCHID,
'nonce_str': nonce_str,
'body': PAY_DESCRIPTION,
'out_trade_no': dingdanid,
'total_fee': total_fee,
'spbill_create_ip': user_ip,
'notify_url': NOTIFY_URL,
'trade_type': trade_type,
'openid': openid # 小程序支付必须传openid
}
# 按照参数名ASCII码从小到大排序
sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0])
# 拼接字符串
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value])
sign_string += f'&key={KEY}'
# MD5签名
sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
# 构造XML数据
xml_data = f"""<xml>
<appid>{APPID}</appid>
<body>{PAY_DESCRIPTION}</body>
<mch_id>{MCHID}</mch_id>
<nonce_str>{nonce_str}</nonce_str>
<notify_url>{NOTIFY_URL}</notify_url>
<openid>{openid}</openid>
<out_trade_no>{dingdanid}</out_trade_no>
<spbill_create_ip>{user_ip}</spbill_create_ip>
<total_fee>{total_fee}</total_fee>
<trade_type>{trade_type}</trade_type>
<sign>{sign}</sign>
</xml>"""
# 调用微信统一下单接口(官方地址)
response = requests.post(
'https://api.mch.weixin.qq.com/pay/unifiedorder',
data=xml_data.encode('utf-8'),
headers={'Content-Type': 'application/xml'},
timeout=10 # 设置超时
)
# 解析返回的XML
root = ET.fromstring(response.content)
return_code = root.find('return_code').text
result_code = root.find('result_code').text
if return_code == 'SUCCESS' and result_code == 'SUCCESS':
# 获取prepay_id
prepay_id = root.find('prepay_id').text
# 2. 第二次签名(小程序支付参数)
timestamp = str(int(time.time()))
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
package = f'prepay_id={prepay_id}'
sign_type = 'MD5'
# 生成支付签名注意这里key的大小写和字段名与第一次不同
pay_sign_data = {
'appId': APPID, # 注意这里是appId不是appid
'timeStamp': timestamp, # 注意这里是timeStamp不是timestamp
'nonceStr': nonce_str,
'package': package,
'signType': sign_type
}
sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0])
pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data])
pay_sign_string += f'&key={KEY}'
pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper()
# 返回前端需要的支付参数
return {
'appId': APPID, # 对应前端appId
'timeStamp': timestamp, # 对应前端timeStamp
'nonceStr': nonce_str, # 对应前端nonceStr
'package': package, # 对应前端package
'signType': sign_type, # 对应前端signType
'paySign': pay_sign # 对应前端paySign
}
else:
# 获取错误信息
err_code = root.find('err_code')
err_code_des = root.find('err_code_des')
return_msg = root.find('return_msg')
error_details = f"return_code: {return_code}"
if err_code is not None:
error_details += f", err_code: {err_code.text}"
if err_code_des is not None:
error_details += f", err_code_des: {err_code_des.text}"
if return_msg is not None:
error_details += f", return_msg: {return_msg.text}"
raise Exception(f"微信支付下单失败: {error_details}")
def get_client_ip(self):
"""获取客户端IP完整版"""
request = self.request
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip if ip else '127.0.0.1'
class HuiyuanHuitiao(View):
"""
会员支付回调接口
路径:/shangpin/huiyuanhuitiao
"""
def post(self, request):
try:
# 1. 解析微信回调XML数据
request_body = request.body
logger.info(f"收到会员支付回调,原始数据: {request_body.decode('utf-8')}")
root = ET.fromstring(request_body)
return_code = root.find('return_code').text
result_code = root.find('result_code').text
out_trade_no = root.find('out_trade_no').text
transaction_id = root.find('transaction_id').text
total_fee = int(root.find('total_fee').text)
logger.info(f"会员回调数据: 订单{out_trade_no}, 状态{return_code}/{result_code}, 金额{total_fee}")
if return_code != 'SUCCESS' or result_code != 'SUCCESS':
logger.warning(f"会员支付回调状态异常: {return_code}/{result_code}")
return self.wechat_response('FAIL', '支付失败')
if not self.verify_signature(request_body):
logger.error(f"会员签名验证失败: {out_trade_no}")
return self.wechat_response('FAIL', '签名验证失败')
# 查询订单
try:
order = Czjilu.query.get(dingdan_id=out_trade_no)
except Czjilu.DoesNotExist:
logger.error(f"会员订单不存在: {out_trade_no}")
return self.wechat_response('FAIL', '订单不存在')
if order.leixing != 1:
logger.error(f"订单类型错误: {order.leixing}, 期望1(会员)")
return self.wechat_response('FAIL', '订单类型错误')
if order.zhuangtai != 9:
logger.warning(f"会员订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}")
return self.wechat_response('SUCCESS', 'OK')
# 验证金额
wechat_amount_yuan = total_fee / 100
order_amount_yuan = float(order.jine)
if abs(wechat_amount_yuan - order_amount_yuan) > 0.01:
logger.error(f"会员金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}")
return self.wechat_response('FAIL', '金额不一致')
# 查询会员信息
try:
huiyuan = Huiyuan.query.get(huiyuan_id=order.huiyuan_id)
except Huiyuan.DoesNotExist:
logger.error(f"会员信息不存在: {order.huiyuan_id}")
return self.wechat_response('FAIL', '会员信息不存在')
# ===== 更新收支记录 =====
try:
jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine
update_daily_income(jine_decimal)
szjilu, _ = Szjilu.query.get_or_create(
id=1,
defaults={'TotalIncome': Decimal('0.00'), 'TotalFlow': Decimal('0.00'), 'TotalExpense': Decimal('0.00'),
'DailyExpense': Decimal('0.00'), 'DailyFlow': Decimal('0.00')}
)
szjilu.TotalIncome += jine_decimal
szjilu.TotalFlow += jine_decimal
szjilu.DailyFlow += jine_decimal
szjilu.save()
logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}")
except Exception as e:
logger.error(f"更新收支记录失败: {str(e)}", exc_info=True)
# 更新订单状态
order.zhuangtai = 3
order.save(update_fields=['zhuangtai'])
logger.info(f"会员订单状态更新成功: {order.dingdan_id} -> 3")
# 更新会员购买次数
huiyuan.goumai_cishu += 1
huiyuan.save(update_fields=['goumai_cishu'])
# 处理会员购买记录
goumai_record, is_new, is_first_buy = self.process_huiyuan_goumai(
order.yonghuid, order.huiyuan_id, huiyuan
)
if is_first_buy:
self.add_first_buy_jifen(order.yonghuid)
# 🆕 新增:处理分红(包含管事和组长)
with transaction.atomic():
self.process_fenhong(order, huiyuan)
logger.info(f"会员支付回调处理完成: {out_trade_no}")
return self.wechat_response('SUCCESS', 'OK')
except ET.ParseError as e:
logger.error(f"会员XML解析失败: {str(e)}")
return self.wechat_response('FAIL', '数据格式错误')
except Exception as e:
logger.error(f"会员支付回调处理异常: {str(e)}", exc_info=True)
return self.wechat_response('FAIL', '处理异常')
# ---------- 以下为原方法,基本不变,但需要导入新增模型 ----------
def process_huiyuan_goumai(self, yonghuid, huiyuan_id, huiyuan):
"""处理会员购买记录(与原代码一致)"""
try:
existing_record = Huiyuangoumai.query.filter(
yonghu_id=yonghuid,
huiyuan_id=huiyuan_id
).first()
is_first_buy_any = False
if existing_record:
existing_record.daoqi_time += timedelta(days=30)
existing_record.huiyuan_zhuangtai = 1
existing_record.save()
logger.info(f"会员续费: 用户{yonghuid}, 会员{huiyuan_id}")
return existing_record, False, False
else:
other_huiyuan_count = Huiyuangoumai.query.filter(
yonghu_id=yonghuid
).exclude(huiyuan_id=huiyuan_id).count()
if other_huiyuan_count == 0:
is_first_buy_any = True
logger.info(f"用户{yonghuid}首次购买任何会员将获得10积分")
goumai_record = Huiyuangoumai.query.create(
yonghu_id=yonghuid,
huiyuan_id=huiyuan_id,
jieshao=huiyuan.jieshao,
huiyuan_zhuangtai=1,
daoqi_time=timezone.now() + timedelta(days=30)
)
logger.info(f"新购会员: 用户{yonghuid}, 会员{huiyuan_id}")
return goumai_record, True, is_first_buy_any
except Exception as e:
logger.error(f"处理会员购买记录失败: {str(e)}", exc_info=True)
if "unique" in str(e).lower() or "duplicate" in str(e).lower():
logger.warning(f"检测到重复创建尝试,已由数据库拦截: 用户{yonghuid}, 会员{huiyuan_id}")
return self.process_huiyuan_goumai(yonghuid, huiyuan_id, huiyuan)
raise
def add_first_buy_jifen(self, yonghuid):
"""首次购买积分奖励(与原代码一致)"""
try:
dashou = UserDashou.query.get(user__UserUID=yonghuid)
dashou.jifen += 10
dashou.save(update_fields=['jifen'])
logger.info(f"用户{yonghuid}首次购买会员,积分+10当前积分: {dashou.jifen}")
except UserDashou.DoesNotExist:
logger.error(f"找不到打手记录,无法增加积分: {yonghuid}")
except Exception as e:
logger.error(f"增加积分失败: {str(e)}")
# 🆕 新增:统一分红处理函数
def process_fenhong(self, order, huiyuan):
"""
处理管事和组长分红(支持多次分红配置)
"""
try:
# 获取打手信息
dashou = UserDashou.query.get(user__UserUID=order.yonghuid)
guanshi_id = dashou.yaoqingren
if not guanshi_id:
logger.info(f"打手{order.yonghuid}没有邀请人,无分红")
return
# 获取管事信息
try:
guanshi = UserGuanshi.query.get(user__UserUID=guanshi_id)
except UserGuanshi.DoesNotExist:
logger.warning(f"管事信息不存在: {guanshi_id}")
return
# 获取组长ID从管事扩展表的邀请人
zuzhang_id = guanshi.yaoqingren
# 获取打手头像和昵称
user_main = dashou.user
avatar = user_main.Avatar
nicheng = dashou.nicheng or "未知打手"
# 查询该打手针对该管事的已分红次数
existing_count = Gsfenhong.query.filter(
dashouid=order.yonghuid,
guanshi=guanshi_id
).count()
cishu = existing_count + 1 # 本次是第几次
# 获取订单金额(用于限制分红总额)
order_amount = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine
# 计算管事分红金额
guanshi_fenhong = self._calc_guanshi_fenhong(guanshi, huiyuan, guanshi_id, cishu)
if guanshi_fenhong is None:
logger.info(f"管事{guanshi_id}本次无分红")
guanshi_fenhong = Decimal('0.00')
# 计算组长分红金额(如果存在组长)
zuzhang_fenhong = Decimal('0.00')
if zuzhang_id:
# 获取组长信息
try:
zuzhang = UserZuzhang.query.get(user__UserUID=zuzhang_id)
except UserZuzhang.DoesNotExist:
logger.warning(f"组长信息不存在: {zuzhang_id}")
zuzhang = None
zuzhang_fenhong = self._calc_zuzhang_fenhong(zuzhang, huiyuan, zuzhang_id, cishu)
if zuzhang_fenhong is None:
logger.info(f"组长{zuzhang_id}本次无分红")
zuzhang_fenhong = Decimal('0.00')
else:
logger.info(f"管事{guanshi_id}没有组长,无组长分红")
# 检查分红总额是否超过订单金额(优先保证管事分红)
total_fenhong = guanshi_fenhong + zuzhang_fenhong
if total_fenhong > order_amount:
logger.warning(f"分红总额{total_fenhong}超过订单金额{order_amount}组长分红设为0")
zuzhang_fenhong = Decimal('0.00')
total_fenhong = guanshi_fenhong
if total_fenhong > order_amount:
logger.warning(f"管事分红{guanshi_fenhong}也超过订单金额强制设为0")
guanshi_fenhong = Decimal('0.00')
total_fenhong = Decimal('0.00')
# 创建分红记录先创建主记录即使金额为0也记录但为0无意义所以只有大于0才创建
if guanshi_fenhong > 0 or zuzhang_fenhong > 0:
# 更新管事扩展表
if guanshi_fenhong > 0:
guanshi.chongzhifenrun += guanshi_fenhong
guanshi.yue += guanshi_fenhong
guanshi.jinrichongzhi += 1
guanshi.jinyuechongzhi += 1
guanshi.save(update_fields=['chongzhifenrun', 'yue', 'jinrichongzhi', 'jinyuechongzhi'])
logger.info(f"管事分红成功: 管事{guanshi_id}, 金额{guanshi_fenhong}")
# 更新组长扩展表
if zuzhang_fenhong > 0 and zuzhang:
zuzhang.fenyong_zonge += zuzhang_fenhong
zuzhang.ketixian_jine += zuzhang_fenhong
zuzhang.jinri_fenyong += zuzhang_fenhong
zuzhang.jinyue_fenyong += zuzhang_fenhong
zuzhang.save(update_fields=['fenyong_zonge', 'ketixian_jine', 'jinri_fenyong', 'jinyue_fenyong'])
logger.info(f"组长分红成功: 组长{zuzhang_id}, 金额{zuzhang_fenhong}")
# 创建分红记录
Gsfenhong.query.create(
dingdan_id=order.dingdan_id,
guanshi=guanshi_id,
dashouid=order.yonghuid,
shuoming=order.shuoming,
fenhong=guanshi_fenhong,
avatar=avatar,
nicheng=nicheng,
# 新增字段
zuzhang_id=zuzhang_id if zuzhang_fenhong > 0 else None,
zuzhang_fenhong=zuzhang_fenhong if zuzhang_fenhong > 0 else None,
huiyuan_id=huiyuan.huiyuan_id,
fenhong_leixing=1,
)
# ========== 管事每日统计(新购) ==========
if guanshi_fenhong > 0 and cishu == 1:
try:
update_guanshi_daily_by_action(
yonghuid=guanshi_id,
action=2,
amount=guanshi_fenhong
)
logger.info(f"管事每日统计更新成功(新购):管事{guanshi_id},分红{guanshi_fenhong}")
except Exception as e:
logger.error(f"管事每日统计更新失败(新购):{str(e)}")
# ========== 管事每日统计(续费) ==========
if guanshi_fenhong > 0 and cishu > 1:
try:
update_guanshi_xufei_daily(
yonghuid=guanshi_id,
xufei_jine=guanshi_fenhong
)
logger.info(f"管事续费统计更新成功:管事{guanshi_id},续费分红{guanshi_fenhong}")
except Exception as e:
logger.error(f"管事续费统计更新失败:{str(e)}")
# ========== 组长每日统计(分佣收入) ==========
if zuzhang_fenhong > 0 and zuzhang_id:
try:
update_zuzhang_daily_by_action(
yonghuid=zuzhang_id,
action=2,
amount=zuzhang_fenhong
)
logger.info(f"组长每日统计更新成功:组长{zuzhang_id},分佣{zuzhang_fenhong}")
except Exception as e:
logger.error(f"组长每日统计更新失败:{str(e)}")
logger.info(f"分红记录创建成功: 订单{order.dingdan_id}")
else:
logger.info(f"分红金额均为0不创建记录")
except Exception as e:
logger.error(f"处理分红失败: {str(e)}", exc_info=True)
# 不抛出异常,避免影响主流程
def _calc_guanshi_fenhong(self, guanshi, huiyuan, guanshi_id, cishu):
"""计算管事分红金额"""
# 先查多次分红配置表
try:
config = DuociFenhong.query.get(
huiyuan=huiyuan,
UserUID=guanshi_id,
cishu=cishu
)
return Decimal(str(config.guanshi_fenhong))
except DuociFenhong.DoesNotExist:
pass
if cishu == 1:
# 第一次分红,取会员表默认金额
return Decimal(str(huiyuan.guanshifc))
else:
# 第二次及以上,检查是否开启二次分红
if guanshi.fenghong_erci_enabled:
return Decimal(str(guanshi.fenghong_erci))
else:
return None # 无分红
def _calc_zuzhang_fenhong(self, zuzhang, huiyuan, zuzhang_id, cishu):
"""计算组长分红金额"""
if zuzhang is None:
return None
# 查多次分红配置表
try:
config = DuociFenhong.query.get(
huiyuan=huiyuan,
UserUID=zuzhang_id,
cishu=cishu
)
return Decimal(str(config.zuzhang_fenhong))
except DuociFenhong.DoesNotExist:
pass
if cishu == 1:
# 第一次分红,取会员表默认组长分成
return Decimal(str(huiyuan.zuzhangfc))
else:
# 第二次及以上,检查组长是否开启额外分红
if zuzhang.kaioi_ewai_fenhong:
return Decimal(str(zuzhang.ewai_fenhong_jine))
else:
return None
# 以下为原辅助方法,保持原样
def verify_signature(self, xml_data):
"""验证微信支付签名(与原代码一致)"""
try:
root = ET.fromstring(xml_data)
sign = root.find('sign').text
if not sign:
logger.error("会员签名字段不存在")
return False
params = {}
for child in root:
if child.tag != 'sign' and child.text is not None:
params[child.tag] = child.text
sorted_params = sorted(params.items(), key=lambda x: x[0])
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value])
sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}'
calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
return sign == calculated_sign
except Exception as e:
logger.error(f"会员验证签名异常: {str(e)}")
return False
def wechat_response(self, return_code, return_msg):
"""返回微信回调响应"""
xml_response = f"""<xml>
<return_code><![CDATA[{return_code}]]></return_code>
<return_msg><![CDATA[{return_msg}]]></return_msg>
</xml>"""
logger.info(f"会员返回微信响应: {return_code} - {return_msg}")
return HttpResponse(xml_response, content_type='application/xml')
# views.py
class HuiyuanShibai(APIView):
"""
会员支付失败处理接口
路径:/shangpin/huiyuanshibai
请求方式POST
权限JWT认证
参数dingdanid订单ID
返回:删除结果
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 获取订单ID
dingdanid = request.data.get('dingdanid')
if not dingdanid:
return Response({
'code': 400,
'message': '订单ID不能为空'
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 查询订单(确保是当前用户的订单)
try:
order = Czjilu.query.get(
dingdan_id=dingdanid,
UserUID=request.user.UserUID
)
except Czjilu.DoesNotExist:
return Response({
'code': 400,
'message': '订单不存在或不属于当前用户'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 检查订单类型是否为会员1
if order.leixing != 1:
return Response({
'code': 400,
'message': '订单类型错误,不是会员订单'
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 检查订单状态是否为未支付9
if order.zhuangtai == 9:
# 删除订单
order.delete()
logger.info(f"用户{request.user.UserUID}删除未支付会员订单: {dingdanid}")
return Response({
'code': 200,
'message': '会员订单已删除'
}, status=status.HTTP_200_OK)
else:
return Response({
'code': 400,
'message': '会员订单状态不是未支付,无法删除'
}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
logger.error(f"会员支付失败处理异常: {str(e)}", exc_info=True)
return Response({
'code': 500,
'message': '服务器内部错误'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# views.py
class HuiyuanLunxun(APIView):
"""
会员支付成功轮询接口
路径:/shangpin/huiyuanlx
请求方式POST
权限JWT认证
参数dingdanid订单ID
返回:订单状态和会员信息
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 获取订单ID
dingdanid = request.data.get('dingdanid')
if not dingdanid:
return Response({
'code': 400,
'message': '订单ID不能为空'
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 查询订单(确保是当前用户的订单)
try:
order = Czjilu.query.get(
dingdan_id=dingdanid,
UserUID=request.user.UserUID
)
except Czjilu.DoesNotExist:
return Response({
'code': 400,
'message': '订单不存在或不属于当前用户'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 检查订单类型是否为会员1
if order.leixing != 1:
return Response({
'code': 400,
'message': '订单类型错误,不是会员订单'
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 检查订单状态是否为已支付3
if order.zhuangtai != 3:
logger.info(f"会员订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}")
return Response({
'code': 400,
'message': '会员订单尚未支付完成'
}, status=status.HTTP_400_BAD_REQUEST)
# 5. 查询会员信息
try:
huiyuan = Huiyuan.query.get(huiyuan_id=order.huiyuan_id)
# 6. 查询会员购买记录
goumai_record = Huiyuangoumai.query.filter(
yonghu_id=request.user.UserUID,
huiyuan_id=order.huiyuan_id
).first()
if not goumai_record:
return Response({
'code': 400,
'message': '会员购买记录不存在'
}, status=status.HTTP_400_BAD_REQUEST)
# 7. 格式化到期时间
#from django.utils.timezone import localtime
#daoqi_time = localtime(goumai_record.daoqi_time).strftime('%Y-%m-%d %H:%M:%S')
# 直接格式化,不使用时区转换
daoqi_time = goumai_record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S')
current_user = request.user
dashou = current_user.DashouProfile
logger.info(f"会员轮询成功: 订单{dingdanid}已支付,用户{request.user.UserUID}, 会员{order.huiyuan_id}")
# 8. 返回会员信息(用于更新前端全局变量)
return Response({
'code': 200,
'message': '支付流程全部完成',
'jifen': dashou.jifen,
'huiyuan': {
'huiyuanid': order.huiyuan_id,
'huiyuanming': huiyuan.jieshao,
'daoqi': daoqi_time
}
}, status=status.HTTP_200_OK)
except Huiyuan.DoesNotExist:
logger.error(f"会员信息不存在: {order.huiyuan_id}")
return Response({
'code': 400,
'message': '会员信息不存在'
}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
logger.error(f"会员轮询异常: {str(e)}", exc_info=True)
return Response({
'code': 500,
'message': '服务器内部错误'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# views.py in shangpin app
class ShangjiaChongzhi(APIView):
"""
商家余额充值支付参数接口
路径:/shangpin/sjcz
请求方式POST
权限JWT认证
参数jine金额1-10000元
返回:支付参数(payParams)和订单ID(dingdanid)
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 验证用户身份和商家状态
try:
shangjia = request.user.ShopProfile
except UserShangjia.DoesNotExist:
logger.warning(f"用户{request.user.UserUID}尝试充值但不是商家")
return Response({
'code': 400,
'message': '用户不是商家,无法充值'
}, status=status.HTTP_400_BAD_REQUEST)
# 检查商家状态
if shangjia.zhuangtai != 1:
logger.warning(f"商家{shangjia.nicheng}状态异常: {shangjia.zhuangtai}")
return Response({
'code': 400,
'message': '商家状态异常,无法进行充值'
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 获取并验证金额参数
jine = request.data.get('jine')
if not jine:
return Response({
'code': 400,
'message': '请填写充值金额'
}, status=status.HTTP_400_BAD_REQUEST)
try:
jine = float(jine)
except (ValueError, TypeError):
return Response({
'code': 400,
'message': '金额格式错误,请填写有效的数字'
}, status=status.HTTP_400_BAD_REQUEST)
# 验证金额范围
if jine < settings.SHANGJIA_CZ_MIN_AMOUNT or jine > settings.SHANGJIA_CZ_MAX_AMOUNT:
return Response({
'code': 400,
'message': f'充值金额必须在{settings.SHANGJIA_CZ_MIN_AMOUNT}-{settings.SHANGJIA_CZ_MAX_AMOUNT}元之间'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 生成订单ID使用配置的前缀
timestamp_ms = int(time.time() * 1000)
timestamp_ns = int(time.time_ns() // 1000) % 1000000
random_num = random.randint(0, 99)
dingdanid = f"{settings.SHANGJIA_CZ_PREFIX}{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}"
# 4. 创建充值记录订单
try:
chongzhi_jilu = Czjilu.query.create(
dingdan_id=dingdanid,
zhuangtai=9, # 未支付状态
yonghuid=request.user.UserUID,
jine=jine,
leixing=4, # 充值类型4=商家充值
shuoming=settings.SHANGJIA_CZ_PAY_DESCRIPTION
)
logger.info(f"创建商家充值订单: 商家{shangjia.nicheng}, 订单{dingdanid}, 金额{jine}")
except Exception as e:
logger.error(f"创建订单失败: {str(e)}")
return Response({
'code': 500,
'message': '订单创建失败,请重试'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# 5. 生成微信支付参数
try:
pay_params = self.generate_wechat_pay_params(
dingdanid=dingdanid,
jine=jine,
openid=request.user.OpenID,
pay_type='shangjia'
)
except Exception as e:
# 支付参数生成失败,删除订单记录
chongzhi_jilu.delete()
logger.error(f"生成微信支付参数失败: {str(e)}")
return Response({
'code': 500,
'message': f'支付参数生成失败: {str(e)}'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# 6. 返回支付参数和订单ID
logger.info(f"商家{shangjia.nicheng}支付参数生成成功: {dingdanid}")
return Response({
'code': 200,
'message': '支付参数生成成功',
'payParams': pay_params,
'dingdanid': dingdanid
}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"商家充值接口异常: {str(e)}", exc_info=True)
return Response({
'code': 500,
'message': '服务器内部错误,请稍后重试'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type):
"""生成微信小程序支付参数"""
# 获取配置
APPID = settings.WEIXIN_APPID
MCHID = settings.WEIXIN_MCHID
KEY = settings.WEIXIN_SHANGHUMIYAO
# 根据支付类型获取对应的配置
if pay_type == 'shangjia':
PAY_DESCRIPTION = settings.SHANGJIA_CZ_PAY_DESCRIPTION
NOTIFY_URL = settings.SHANGJIA_CZ_PAY_NOTIFY_URL
else:
raise Exception('未知的支付类型')
# 生成随机字符串
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
# 金额转换(元转分)
total_fee = int(Decimal(str(jine)) * 100)
# 交易类型小程序支付使用JSAPI
trade_type = 'JSAPI'
# 获取客户端IP
user_ip = self.get_client_ip()
# 1. 第一次签名(统一下单接口)
sign_data = {
'appid': APPID,
'mch_id': MCHID,
'nonce_str': nonce_str,
'body': PAY_DESCRIPTION,
'out_trade_no': dingdanid,
'total_fee': total_fee,
'spbill_create_ip': user_ip,
'notify_url': NOTIFY_URL,
'trade_type': trade_type,
'openid': openid # 小程序支付必须传openid
}
# 按照参数名ASCII码从小到大排序
sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0])
# 拼接字符串
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value])
sign_string += f'&key={KEY}'
# MD5签名
sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
# 构造XML数据
xml_data = f"""<xml>
<appid>{APPID}</appid>
<body>{PAY_DESCRIPTION}</body>
<mch_id>{MCHID}</mch_id>
<nonce_str>{nonce_str}</nonce_str>
<notify_url>{NOTIFY_URL}</notify_url>
<openid>{openid}</openid>
<out_trade_no>{dingdanid}</out_trade_no>
<spbill_create_ip>{user_ip}</spbill_create_ip>
<total_fee>{total_fee}</total_fee>
<trade_type>{trade_type}</trade_type>
<sign>{sign}</sign>
</xml>"""
# 调用微信统一下单接口
response = requests.post(
'https://api.mch.weixin.qq.com/pay/unifiedorder',
data=xml_data.encode('utf-8'),
headers={'Content-Type': 'application/xml'},
timeout=10
)
# 解析返回的XML
root = ET.fromstring(response.content)
return_code = root.find('return_code').text
result_code = root.find('result_code').text
if return_code == 'SUCCESS' and result_code == 'SUCCESS':
# 获取prepay_id
prepay_id = root.find('prepay_id').text
# 2. 第二次签名(小程序支付参数)
timestamp = str(int(time.time()))
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
package = f'prepay_id={prepay_id}'
sign_type = 'MD5'
# 生成支付签名
pay_sign_data = {
'appId': APPID,
'timeStamp': timestamp,
'nonceStr': nonce_str,
'package': package,
'signType': sign_type
}
sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0])
pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data])
pay_sign_string += f'&key={KEY}'
pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper()
# 返回前端需要的支付参数
return {
'appId': APPID,
'timeStamp': timestamp,
'nonceStr': nonce_str,
'package': package,
'signType': sign_type,
'paySign': pay_sign
}
else:
# 获取错误信息
err_code = root.find('err_code')
err_code_des = root.find('err_code_des')
return_msg = root.find('return_msg')
error_details = f"return_code: {return_code}"
if err_code is not None:
error_details += f", err_code: {err_code.text}"
if err_code_des is not None:
error_details += f", err_code_des: {err_code_des.text}"
if return_msg is not None:
error_details += f", return_msg: {return_msg.text}"
raise Exception(f"微信支付下单失败: {error_details}")
def get_client_ip(self):
"""获取客户端IP"""
request = self.request
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip if ip else '127.0.0.1'
class ShangjiaHuitiao(View):
"""
商家支付回调接口
路径:/shangpin/sjhuitiao
注意这个接口不需要JWT认证由微信服务器调用
"""
def post(self, request):
try:
# 1. 解析微信回调XML数据
request_body = request.body
logger.info(f"收到商家支付回调,原始数据: {request_body.decode('utf-8')}")
root = ET.fromstring(request_body)
# 获取关键字段
return_code = root.find('return_code').text
result_code = root.find('result_code').text
out_trade_no = root.find('out_trade_no').text # 订单ID
transaction_id = root.find('transaction_id').text # 微信交易号
total_fee = int(root.find('total_fee').text) # 支付金额(分)
logger.info(f"商家回调数据解析: 订单{out_trade_no}, 状态{return_code}/{result_code}, 金额{total_fee}")
# 2. 验证基本返回状态
if return_code != 'SUCCESS' or result_code != 'SUCCESS':
logger.warning(f"商家支付回调状态异常: {return_code}/{result_code}")
return self.wechat_response('FAIL', '支付失败')
# 3. 验证签名(确保是微信发来的)
if not self.verify_signature(request_body):
logger.error(f"商家支付签名验证失败: {out_trade_no}")
return self.wechat_response('FAIL', '签名验证失败')
# 4. 查询订单
try:
order = Czjilu.query.get(dingdan_id=out_trade_no)
logger.info(f"找到商家订单: {order.dingdan_id}, 状态: {order.zhuangtai}, 金额: {order.jine}")
except Czjilu.DoesNotExist:
logger.error(f"商家订单不存在: {out_trade_no}")
return self.wechat_response('FAIL', '订单不存在')
# 5. 检查订单状态是否为未支付9
if order.zhuangtai != 9:
logger.warning(f"商家订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}")
# 已处理过的订单直接返回成功,避免微信重复回调
return self.wechat_response('SUCCESS', 'OK')
# 6. 验证金额(微信返回的是分,需要转换)
wechat_amount_yuan = Decimal(str(total_fee)) / Decimal('100') # 转换为元
order_amount_yuan = Decimal(str(order.jine)) # 订单金额(元)
# 允许1分钱的误差
if abs(wechat_amount_yuan - order_amount_yuan) > Decimal('0.01'):
logger.error(f"商家订单金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}")
return self.wechat_response('FAIL', '金额不一致')
# ===== 在这里添加收支记录更新代码 =====
# 更新收支记录表
try:
# 获取订单金额确保是Decimal类型
jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine
update_daily_income(jine_decimal)
# 查询或创建ID=1的收支记录
szjilu, created = Szjilu.query.get_or_create(
id=1,
defaults={
'TotalIncome': Decimal('0.00'),
'TotalFlow': Decimal('0.00'),
'TotalExpense': Decimal('0.00'),
'DailyExpense': Decimal('0.00'),
'DailyFlow': Decimal('0.00')
}
)
# 更新三个字段
szjilu.TotalIncome += jine_decimal # 总收益
szjilu.TotalFlow += jine_decimal # 总流水
szjilu.DailyFlow += jine_decimal # 今日流水
szjilu.save()
logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}")
except Exception as e:
# 收支记录更新失败不影响主流程,只记录日志
logger.error(f"更新收支记录失败: {str(e)}", exc_info=True)
# ===== 添加结束 =====
# 7. 使用事务更新订单状态和商家余额
with transaction.atomic():
# 更新订单状态为已支付3
order.zhuangtai = 3
order.save(update_fields=['zhuangtai'])
logger.info(f"商家订单状态更新成功: {order.dingdan_id} -> 3")
# 更新商家余额
try:
# 通过用户ID查找商家高效查询
shangjia = UserShangjia.query.get(user__UserUID=order.yonghuid)
# 增加余额
shangjia.yue += order_amount_yuan
shangjia.save(update_fields=['yue'])
logger.info(f"商家余额更新成功: 商家{shangjia.nicheng}, 增加{order_amount_yuan}元, 新余额{shangjia.yue}")
except UserShangjia.DoesNotExist:
# 如果找不到商家,记录错误,但依然返回成功(避免微信重复回调)
logger.error(f"商家信息不存在: 用户{order.yonghuid}")
return self.wechat_response('SUCCESS', 'OK')
except Exception as e:
logger.error(f"更新商家余额失败: {str(e)}", exc_info=True)
# 回滚事务
raise
# 8. 返回成功响应给微信
logger.info(f"商家支付回调处理完成: {out_trade_no}")
return self.wechat_response('SUCCESS', 'OK')
except ET.ParseError as e:
logger.error(f"XML解析失败: {str(e)}")
return self.wechat_response('FAIL', '数据格式错误')
except Exception as e:
logger.error(f"商家支付回调处理异常: {str(e)}", exc_info=True)
# 返回FAIL让微信重试
return self.wechat_response('FAIL', '处理异常')
def verify_signature(self, xml_data):
"""验证微信支付签名"""
try:
root = ET.fromstring(xml_data)
sign = root.find('sign').text
if not sign:
logger.error("签名字段不存在")
return False
# 获取所有参数并排除sign
params = {}
for child in root:
if child.tag != 'sign' and child.text is not None:
params[child.tag] = child.text
# 按照参数名ASCII码从小到大排序
sorted_params = sorted(params.items(), key=lambda x: x[0])
# 拼接字符串
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value])
sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}'
# 计算签名
calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
is_valid = sign == calculated_sign
if not is_valid:
logger.error(f"商家支付签名验证失败: 计算签名{calculated_sign}, 收到签名{sign}")
return is_valid
except Exception as e:
logger.error(f"验证签名异常: {str(e)}")
return False
def wechat_response(self, return_code, return_msg):
"""返回微信支付回调响应"""
xml_response = f"""<xml>
<return_code><![CDATA[{return_code}]]></return_code>
<return_msg><![CDATA[{return_msg}]]></return_msg>
</xml>"""
logger.info(f"返回微信响应: {return_code} - {return_msg}")
return HttpResponse(xml_response, content_type='application/xml')
# views.py
class ShangjiaShibai(APIView):
"""
商家支付失败处理接口
路径:/shangpin/sjshibai
请求方式POST
权限JWT认证
参数dingdanid订单ID
返回:删除结果
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 获取订单ID
dingdanid = request.data.get('dingdanid')
if not dingdanid:
return Response({
'code': 400,
'message': '订单ID不能为空'
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 查询订单(确保是当前用户的订单)
try:
order = Czjilu.query.get(
dingdan_id=dingdanid,
UserUID=request.user.UserUID,
leixing=4 # 确保是商家充值订单
)
except Czjilu.DoesNotExist:
logger.warning(f"商家订单不存在或不属于当前用户: {dingdanid}, 用户{request.user.UserUID}")
return Response({
'code': 400,
'message': '订单不存在或不属于当前用户'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 检查订单状态是否为未支付9
if order.zhuangtai == 9:
# 删除订单
order.delete()
logger.info(f"商家{request.user.ShopProfile.nicheng}删除未支付订单: {dingdanid}")
return Response({
'code': 200,
'message': '订单已删除'
}, status=status.HTTP_200_OK)
else:
return Response({
'code': 400,
'message': '订单状态不是未支付,无法删除'
}, status=status.HTTP_400_BAD_REQUEST)
except UserShangjia.DoesNotExist:
return Response({
'code': 400,
'message': '用户不是商家'
}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
logger.error(f"商家支付失败处理异常: {str(e)}", exc_info=True)
return Response({
'code': 500,
'message': '服务器内部错误'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# views.py
class ShangjiaChenggong(APIView):
"""
商家支付成功轮询接口
路径:/shangpin/sjcg
请求方式POST
权限JWT认证
参数dingdanid订单ID
返回:订单状态和当前商家余额
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 获取订单ID
dingdanid = request.data.get('dingdanid')
if not dingdanid:
return Response({
'code': 400,
'message': '订单ID不能为空'
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 查询订单(确保是当前用户的商家订单)
try:
order = Czjilu.query.get(
dingdan_id=dingdanid,
UserUID=request.user.UserUID,
leixing=4 # 商家充值类型
)
except Czjilu.DoesNotExist:
logger.warning(f"商家订单不存在: {dingdanid}, 用户{request.user.UserUID}")
return Response({
'code': 400,
'message': '订单不存在或不属于当前用户'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 检查订单状态是否为已支付3
if order.zhuangtai != 3:
logger.info(f"商家订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}")
return Response({
'code': 400,
'message': '订单尚未支付完成'
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 获取商家余额(通过反向查询)
try:
shangjia = request.user.ShopProfile
yue = float(shangjia.yue) # 转为浮点数方便前端处理
logger.info(f"商家轮询成功: 订单{dingdanid}已支付,商家{shangjia.nicheng}余额为{yue}")
return Response({
'code': 200,
'message': '支付流程全部完成',
'yue': yue # 返回当前商家余额(前端需要更新全局变量)
}, status=status.HTTP_200_OK)
except UserShangjia.DoesNotExist:
logger.error(f"商家信息不存在: 用户{request.user.UserUID}")
return Response({
'code': 400,
'message': '商家信息不存在'
}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
logger.error(f"商家轮询异常: {str(e)}", exc_info=True)
return Response({
'code': 500,
'message': '服务器内部错误'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# 管事分红记录查询API
class GuanshiFenhongListAPI(APIView):
"""
管事分红记录查询接口
权限需要JWT认证
请求方式POST
分页参数page, page_size
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 获取当前管事用户ID
yonghuid = request.user.UserUID
# 获取分页参数
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 50))
if page < 1:
page = 1
if page_size < 1 or page_size > 100: # 限制最大100条
page_size = 50
# 计算偏移量
offset = (page - 1) * page_size
# 查询条件管事ID匹配
query = Q(guanshi=yonghuid)
# 先查询总数(使用分页查询优化)
# 查询总记录数
zong_tiaoshu = Gsfenhong.query.filter(query).count()
# 查询去重后的打手数量
chongzhi_dashou_shuliang = Gsfenhong.query.filter(query).values('dashouid').distinct().count()
# 查询当前页数据,按创建时间倒序排列
fenhong_list = Gsfenhong.query.filter(query) \
.order_by('-create_time') \
.values('dashouid', 'avatar', 'nicheng', 'fenhong', 'create_time')[offset:offset + page_size]
# 格式化数据
formatted_list = []
for item in fenhong_list:
formatted_list.append({
'yonghuid': item['dashouid'], # 打手ID
'avatar': item['avatar'] or '', # 打手头像
'nicheng': item['nicheng'] or '未知用户', # 打手昵称
'fenhong': str(item['fenhong']), # 分红金额
'create_time': item['create_time'].strftime('%Y-%m-%d %H:%M:%S') if item['create_time'] else ''
})
# 返回数据
return Response({
'code': 200,
'msg': '成功',
'data': {
'list': formatted_list,
'total': zong_tiaoshu, # 总记录数
'chongzhi_dashou_shuliang': chongzhi_dashou_shuliang, # 充值打手数量(去重)
'page': page,
'page_size': page_size,
'has_more': len(formatted_list) == page_size # 是否还有更多数据
}
})
except Exception as e:
logger.error(f"查询管事分红记录失败: {str(e)}", exc_info=True)
return Response({
'code': 500,
'msg': f'服务器错误: {str(e)}',
'data': {}
})
# views.py - 商品模块视图
class AdGetAllDataView(APIView):
"""
获取商品类型、商品专区、会员数据的接口
接口地址:/shangpin/adhqlx
请求方法POST
权限要求JWT Token认证 + 管理员权限
"""
# 需要JWT Token认证
permission_classes = [IsAuthenticated]
def post(self, request):
"""
处理POST请求返回商品相关数据
"""
try:
# 1. 获取请求数据
zhanghao = request.data.get('zhanghao')
if not zhanghao:
logger.warning(f"管理员接口:未接收到账号参数,请求用户:{request.user.UserUID}")
return Response({
'code': 400,
'message': '缺少账号参数',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 获取当前登录用户
current_user = request.user
# 3. 验证用户类型是否为管理员
if current_user.UserType != 'admin':
logger.warning(
f"权限验证失败:用户 {current_user.UserUID} 的用户类型为 {current_user.UserType},非管理员")
return Response({
'code': 401,
'message': '无管理员权限',
'data': None
}, status=status.HTTP_401_UNAUTHORIZED)
# 4. 验证手机号与传递的账号是否一致
if current_user.Phone != zhanghao:
logger.warning(
f"账号验证失败:用户 {current_user.UserUID} 的phone={current_user.Phone}传递的zhanghao={zhanghao}")
return Response({
'code': 401,
'message': '账号验证失败',
'data': None
}, status=status.HTTP_401_UNAUTHORIZED)
# 5. 验证管理员扩展表是否存在
try:
admin_profile = current_user.AdminProfile
logger.info(f"管理员验证通过:用户 {current_user.UserUID},手机号 {current_user.Phone}")
except AdminProfile.DoesNotExist:
logger.error(
f"管理员扩展表不存在:用户 {current_user.UserUID} 虽然user_type=admin但没有AdminProfile记录")
return Response({
'code': 401,
'message': '管理员信息不完整',
'data': None
}, status=status.HTTP_401_UNAUTHORIZED)
# 6. 查询商品类型数据
try:
# 使用select_related或prefetch_related优化查询如果需要关联查询
# 这里直接查询所有字段,然后提取需要的字段
shangpinleixing_queryset = ShangpinLeixing.query.all()
# 转换为前端需要的格式
shangpinleixing_list = []
for leixing in shangpinleixing_queryset:
leixing_data = {
'id': leixing.id,
'yaoqiuleixing': leixing.yaoqiuleixing if leixing.yaoqiuleixing is not None else None,
'huiyuan_id': leixing.huiyuan_id if leixing.huiyuan_id else None,
'yongjin': str(leixing.yongjin) if leixing.yongjin is not None else None,
'jieshao': leixing.jieshao if leixing.jieshao else '',
'tupian_url': leixing.tupian_url if leixing.tupian_url else ''
}
shangpinleixing_list.append(leixing_data)
#logger.info(f"成功查询到 {len(shangpinleixing_list)} 条商品类型数据")
except Exception as e:
logger.error(f"查询商品类型数据时出错:{str(e)}")
shangpinleixing_list = []
# 7. 查询商品专区数据
try:
# 查询所有商品专区
zhuanqu_queryset = ShangpinZhuanqu.query.all()
# 转换为前端需要的格式
zhuanqu_list = []
for zhuanqu in zhuanqu_queryset:
zhuanqu_data = {
'id': zhuanqu.id,
'mingzi': zhuanqu.mingzi if zhuanqu.mingzi else '',
'leixing_id': zhuanqu.leixing_id
}
zhuanqu_list.append(zhuanqu_data)
# logger.info(f"成功查询到 {len(zhuanqu_list)} 条商品专区数据")
except Exception as e:
logger.error(f"查询商品专区数据时出错:{str(e)}")
zhuanqu_list = []
# 8. 查询会员数据
try:
# 查询所有会员
huiyuan_queryset = Huiyuan.query.all()
# 转换为前端需要的格式
huiyuan_list = []
for huiyuan in huiyuan_queryset:
huiyuan_data = {
'huiyuan_id': huiyuan.huiyuan_id,
'jieshao': huiyuan.jieshao,
'jiage': str(huiyuan.jiage) # Decimal转为字符串
}
huiyuan_list.append(huiyuan_data)
# logger.info(f"成功查询到 {len(huiyuan_list)} 条会员数据")
except Exception as e:
logger.error(f"查询会员数据时出错:{str(e)}")
huiyuan_list = []
# 9. 检查会员列表是否为空
if not huiyuan_list:
logger.warning("会员列表为空,可能会影响商品发布功能")
# 10. 组织返回数据
response_data = {
'code': 0,
'message': '获取数据成功',
'data': {
'shangpinleixingList': shangpinleixing_list,
'zhuanquList': zhuanqu_list,
'huiyuanList': huiyuan_list
}
}
return Response(response_data, status=status.HTTP_200_OK)
except Exception as e:
# 捕获未预期的异常
logger.error(f"接口处理异常:{str(e)}", exc_info=True)
return Response({
'code': 500,
'message': f'服务器内部错误:{str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# views.py - shangpin模块
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')
# 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. 创建商品专区记录 ===
zhuanqu_obj = ShangpinZhuanqu(
mingzi=mingzi,
leixing_id=leixing_id
)
zhuanqu_obj.save()
# === 4. 构建返回数据 ===
response_data = {
'id': zhuanqu_obj.id,
'mingzi': mingzi,
'leixing_id': leixing_id
}
# 返回成功响应
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 创建商品对象
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,
# 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('-create_time')
# 构建会员数据列表
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, # 购买次数(销量)
'create_time': huiyuan.create_time.strftime('%Y-%m-%d %H:%M:%S') if huiyuan.create_time else None,
# 创建时间
'update_time': huiyuan.update_time.strftime('%Y-%m-%d %H:%M:%S') if huiyuan.update_time 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,
'create_time': huiyuan_obj.create_time.strftime(
'%Y-%m-%d %H:%M:%S') if huiyuan_obj.create_time else None,
'update_time': huiyuan_obj.update_time.strftime(
'%Y-%m-%d %H:%M:%S') if huiyuan_obj.update_time 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,
'create_time': huiyuan_obj.create_time.strftime(
'%Y-%m-%d %H:%M:%S') if huiyuan_obj.create_time else None,
'update_time': huiyuan_obj.update_time.strftime(
'%Y-%m-%d %H:%M:%S') if huiyuan_obj.update_time 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. 查询公告数据(类型=1的公告
gonggao_obj = Gonggao.query.filter(NoticeType=1).first()
shangpingonggao = gonggao_obj.Content if gonggao_obj else ''
# 7. 查询轮播图数据(类型=1的轮播图
lunbo_objs = Lunbo.query.filter(ImageType=1).values('ImageURL')
shangpinlunbo = [item['ImageURL'] for item in lunbo_objs]
# 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. 查询商品专区数据
shangpinzhuanqu_data = ShangpinZhuanqu.query.all().values(
'id', # 专区ID
'mingzi', # 专区名称
'leixing_id' # 所属类型ID
)
# 10. 查询会员数据
huiyuan_data = Huiyuan.query.all().values(
'huiyuan_id', # 会员ID
'jieshao', # 会员介绍(会员名称)
'jiage' # 会员价格
)
# 11. 查询商品数据
shangpin_data = Shangpin.query.all().values(
'id', # 商品ID
'biaoqian', # 商品标题/标签
'jiage', # 商品价格
'leixing_id', # 商品类型ID
'zhuanqu_id', # 商品专区ID
'tupian_url', # 商品图片URL
'duiwai_xiaoliang' # 商品对外销量
)
# 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()
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. 更新专区名称
zhuanqu_obj.mingzi = mingzi
zhuanqu_obj.save()
# 5. 返回成功响应
return Response({
'code': 0,
'message': '专区名称修改成功',
'data': {
'id': zhuanqu_obj.id,
'mingzi': zhuanqu_obj.mingzi,
'leixing_id': zhuanqu_obj.leixing_id
}
})
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)
logger = logging.getLogger(__name__)
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.create_time.strftime(
'%Y-%m-%d %H:%M:%S') if shangpin_obj.create_time else '',
'genggaishijian': shangpin_obj.update_time.strftime(
'%Y-%m-%d %H:%M:%S') if shangpin_obj.update_time 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.update_time.strftime(
'%Y-%m-%d %H:%M:%S') if shangpin_obj.update_time 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
class HuiyuanTypeListView(APIView):
"""
获取会员类型列表接口
路径:/shangpin/adhyjllxcx
方法POST
权限JWT Token + 管理员验证
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 🔥 1. 身份验证
user = request.user
# 检查是否是管理员
if not hasattr(user, 'AdminProfile'):
return Response({
'code': 401,
'message': '无权限访问',
'data': None
}, status=status.HTTP_401_UNAUTHORIZED)
# 🔥 2. 获取所有会员类型
huiyuan_list = Huiyuan.query.all().order_by('create_time')
# 🔥 3. 构建返回数据
huiyuan_data = []
for huiyuan in huiyuan_list:
huiyuan_data.append({
'id': huiyuan.huiyuan_id, # 会员ID
'jieshao': huiyuan.jieshao, # 会员介绍
'jiage': str(huiyuan.jiage), # 价格
'goumai_cishu': huiyuan.goumai_cishu, # 购买次数
'create_time': huiyuan.create_time.strftime('%Y-%m-%d %H:%M:%S') if huiyuan.create_time else None
})
return Response({
'code': 0,
'message': '获取成功',
'data': huiyuan_data
}, status=status.HTTP_200_OK)
except Exception as e:
print(f"获取会员类型列表失败: {str(e)}")
return Response({
'code': 500,
'message': '系统错误',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class HuiyuanRecordListView(APIView):
"""
获取会员记录列表接口
路径:/shangpin/adhyjlcxhq
方法POST
权限JWT Token + 管理员验证
参数:
- zhanghao: 管理员账号
- huiyuan_type: 会员类型ID
- jiluzhuangtai: 记录状态1=未过期, 2=已过期)
- page: 页码
- pagesize: 每页条数
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 🔥 1. 身份验证
user = request.user
# 检查是否是管理员
if not hasattr(user, 'AdminProfile'):
return Response({
'code': 401,
'message': '无权限访问',
'data': None
}, status=status.HTTP_401_UNAUTHORIZED)
# 🔥 2. 获取参数
zhanghao = request.data.get('zhanghao', '').strip()
huiyuan_type = request.data.get('huiyuan_type', '').strip()
jiluzhuangtai = request.data.get('jiluzhuangtai', 1) # 默认未过期
page = int(request.data.get('page', 1))
pagesize = int(request.data.get('pagesize', 5))
# 🔥 3. 参数验证
if not zhanghao:
return Response({
'code': 400,
'message': '管理员账号不能为空',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
if not huiyuan_type:
return Response({
'code': 400,
'message': '会员类型不能为空',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 🔥 4. 检查会员类型是否存在
huiyuan_obj = Huiyuan.query.filter(huiyuan_id=huiyuan_type).first()
if not huiyuan_obj:
return Response({
'code': 404,
'message': '会员类型不存在',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
# 🔥 5. 构建基础查询一次性获取所有符合条件的记录ID
now = timezone.now()
# 🔥 使用原生SQL优化查询避免N+1问题
with connection.cursor() as cursor:
# 先查询符合条件的总记录数
count_sql = """
SELECT COUNT(*)
FROM huiyuangoumai hg
WHERE hg.huiyuan_id = %s
"""
params = [huiyuan_type]
if jiluzhuangtai == 1: # 未过期
count_sql += " AND hg.daoqi_time > %s"
params.append(now)
else: # 已过期
count_sql += " AND hg.daoqi_time <= %s"
params.append(now)
cursor.execute(count_sql, params)
total_count = cursor.fetchone()[0]
# 🔥 6. 分页查询(使用窗口函数提高性能)
offset = (page - 1) * pagesize
record_sql = """
SELECT
hg.id,
hg.yonghu_id,
hg.huiyuan_id,
hg.jieshao,
hg.daoqi_time,
hg.create_time,
um.avatar,
ud.nicheng
FROM huiyuangoumai hg
LEFT JOIN user_main um ON hg.yonghu_id = um.yonghuid
LEFT JOIN user_dashou ud ON um.id = ud.user_id
WHERE hg.huiyuan_id = %s
"""
record_params = [huiyuan_type]
if jiluzhuangtai == 1: # 未过期
record_sql += " AND hg.daoqi_time > %s"
record_params.append(now)
else: # 已过期
record_sql += " AND hg.daoqi_time <= %s"
record_params.append(now)
record_sql += " ORDER BY hg.create_time DESC LIMIT %s OFFSET %s"
record_params.extend([pagesize, offset])
cursor.execute(record_sql, record_params)
records = cursor.fetchall()
# 🔥 7. 处理查询结果
record_list = []
for record in records:
record_list.append({
'id': record[0],
'yonghuid': record[1], # 用户ID
'huiyuan_id': record[2],
'jieshao': record[3],
'daoqi_time': record[4].strftime('%Y-%m-%d %H:%M:%S') if record[4] else None,
'create_time': record[5].strftime('%Y-%m-%d %H:%M:%S') if record[5] else None,
'touxiang': record[6], # 头像相对URL
'nicheng': record[7] or record[1], # 昵称或用户ID
'yonghu_type': 2 # 🔥 默认打手类型实际应该根据user_type判断
})
# 🔥 8. 判断是否还有更多数据
has_more = (page * pagesize) < total_count
# 🔥 9. 获取会员类型统计信息(所有会员类型的当前状态总数)
huiyuan_types = Huiyuan.query.all()
huiyuan_stats = []
for huiyuan in huiyuan_types:
# 使用COUNT查询统计
stats_sql = """
SELECT COUNT(*)
FROM huiyuangoumai
WHERE huiyuan_id = %s
"""
stats_params = [huiyuan.huiyuan_id]
if jiluzhuangtai == 1: # 未过期
stats_sql += " AND daoqi_time > %s"
stats_params.append(now)
else: # 已过期
stats_sql += " AND daoqi_time <= %s"
stats_params.append(now)
with connection.cursor() as cursor:
cursor.execute(stats_sql, stats_params)
count = cursor.fetchone()[0]
huiyuan_stats.append({
'id': huiyuan.huiyuan_id,
'jieshao': huiyuan.jieshao,
'count': count
})
return Response({
'code': 0,
'message': '获取成功',
'data': {
'list': record_list,
'total': total_count,
'current_page': page,
'page_size': pagesize,
'has_more': has_more,
'huiyuan_stats': huiyuan_stats, # 🔥 所有会员类型的统计
'current_huiyuan': {
'id': huiyuan_obj.huiyuan_id,
'jieshao': huiyuan_obj.jieshao,
'count': total_count
}
}
}, status=status.HTTP_200_OK)
except ValueError as e:
print(f"参数错误: {str(e)}")
return Response({
'code': 400,
'message': '参数错误',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
print(f"获取会员记录失败: {str(e)}")
traceback.print_exc()
return Response({
'code': 500,
'message': '系统错误',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class ZuzhangFenhongView(APIView):
"""
组长分红记录接口
路径:/shangpin/zuzhangfenhong
方法POST
权限JWT认证仅限组长身份
请求参数:
{
"page": 1, # 页码默认1
"page_size": 5 # 每页条数默认5
}
返回:
{
"code": 200,
"msg": "success",
"data": {
"fenhong_zonge": "1234.56", # 分红总额
"yaoqing_guanshi": 10, # 邀请管事数
"fenhong_cishu": 50, # 分红次数
"total": 50, # 总记录数
"has_more": false, # 是否有下一页
"list": [...] # 记录列表
}
}
"""
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
current_user = request.user
# 1. 验证组长身份
try:
zuzhang = UserZuzhang.query.get(user=current_user)
except UserZuzhang.DoesNotExist:
return Response({
'code': 403,
'msg': '当前用户不是组长身份',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 2. 请求参数
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 5))
page = max(page, 1)
page_size = max(page_size, 1)
# 3. 统计数据
# 3.1 分红总额(从组长扩展表)
fenhong_zonge = zuzhang.fenyong_zonge
# 3.2 邀请管事数统计邀请人等于当前用户ID的管事记录
yaoqing_guanshi = UserGuanshi.query.filter(yaoqingren=current_user.UserUID).count()
# 3.3 分红次数(总记录数)
base_queryset = Gsfenhong.query.filter(zuzhang_id=current_user.UserUID).order_by('-create_time')
fenhong_cishu = base_queryset.count()
# 4. 分页查询
start = (page - 1) * page_size
end = start + page_size
records = base_queryset[start:end]
# 5. 批量获取管事用户信息(只查 boss_profile 获取昵称)
guanshi_ids = list(set(rec.guanshi for rec in records if rec.guanshi))
guanshi_info_map = self._get_guanshi_info(guanshi_ids)
# 6. 组装列表数据
list_data = []
for rec in records:
# 打手信息(直接从记录中取快照)
dashou_avatar = rec.avatar or ''
dashou_nicheng = rec.nicheng or ''
dashou_yonghuid = rec.dashouid or ''
# 管事信息
guanshi_id = rec.guanshi
guanshi_info = guanshi_info_map.get(guanshi_id, {'avatar': '', 'nicheng': ''})
guanshi_avatar = guanshi_info['avatar']
guanshi_nicheng = guanshi_info['nicheng']
# 组长分红金额
fenhong = rec.zuzhang_fenhong
if fenhong is None:
fenhong = decimal.Decimal('0.00')
fenhong_str = str(fenhong.quantize(decimal.Decimal('0.00')))
# 时间格式化
create_time = rec.create_time.strftime('%Y-%m-%d %H:%M:%S') if rec.create_time else ''
list_data.append({
'dashou_avatar': dashou_avatar,
'dashou_nicheng': dashou_nicheng,
'dashou_yonghuid': dashou_yonghuid,
'guanshi_avatar': guanshi_avatar,
'guanshi_nicheng': guanshi_nicheng,
'guanshi_yonghuid': guanshi_id,
'fenhong': fenhong_str,
'create_time': create_time,
})
# 7. 是否有更多数据
has_more = (end < fenhong_cishu)
return Response({
'code': 200,
'msg': 'success',
'data': {
'fenhong_zonge': str(fenhong_zonge.quantize(decimal.Decimal('0.00'))),
'yaoqing_guanshi': yaoqing_guanshi,
'fenhong_cishu': fenhong_cishu,
'total': fenhong_cishu,
'has_more': has_more,
'list': list_data,
}
})
def _get_guanshi_info(self, guanshi_ids):
"""
批量获取管事用户的头像和昵称
昵称从老板扩展表boss_profile获取头像从主表获取
返回字典:{用户ID: {'avatar': 相对路径, 'nicheng': 昵称}}
"""
if not guanshi_ids:
return {}
# 查询用户主表,并预加载 boss_profile
users = User.query.filter(UserUID__in=guanshi_ids).select_related(
'BossProfile'
).only(
'UserUID', 'Avatar',
'BossProfile__nickname'
)
info = {}
for user in users:
avatar = user.Avatar or ''
# 从 boss_profile 获取昵称
if hasattr(user, 'BossProfile') and user.BossProfile:
nicheng = user.BossProfile.nickname or ''
else:
# 如果没有 boss_profile使用默认昵称
nicheng = f"用户{user.UserUID[-4:]}"
info[user.UserUID] = {'avatar': avatar, 'nicheng': nicheng}
# 对于未查询到的用户,提供默认值
for gid in guanshi_ids:
if gid not in info:
info[gid] = {'avatar': '', 'nicheng': f"用户{gid[-4:]}"}
return info
# 常量定义(可根据需要调整)
INTEGRAL_PRICE = Decimal('5.00') # 积分购买固定价格(元)
class CzhqdyView(APIView):
"""
获取可用抵扣身份接口
路径:/shangpin/czhqdy
方法POST
权限JWT认证
请求参数:
leixing: int # 1=会员2=押金3=积分4=罚款
huiyuan_id: str # 当 leixing=1 时必传会员ID
yajin_jine: decimal # 当 leixing=2 时必传,押金金额(元)
fadan_id: int # 当 leixing=4 时必传罚单ID
返回:
{
"code": 200,
"msg": "success",
"data": [
{
"id": 1, # 身份ID1打手佣金 2管事分红 3组长分红 4打手押金
"jieshao": "打手佣金抵扣",
"xuyao": "22.22" # 所需抵扣金额(保留两位小数)
},
...
]
}
"""
permission_classes = [permissions.IsAuthenticated]
# 身份ID与对应扩展表模型、余额字段、费率查询条件的映射
IDENTITY_CONFIG = [
{
'id': 1,
'name': '打手佣金抵扣',
'profile_attr': 'DashouProfile', # User 的反向 related_name
'balance_field': 'yue',
'rate_key': '5',
'model': UserDashou,
},
{
'id': 2,
'name': '管事分红抵扣',
'profile_attr': 'GuanshiProfile',
'balance_field': 'yue',
'rate_key': '6',
'model': UserGuanshi,
},
{
'id': 3,
'name': '组长分红抵扣',
'profile_attr': 'ZuzhangProfile',
'balance_field': 'ketixian_jine',
'rate_key': '8',
'model': UserZuzhang,
},
# 新增打手押金抵扣
{
'id': 4,
'name': '打手押金抵扣',
'profile_attr': 'DashouProfile', # 与打手佣金共用扩展表
'balance_field': 'yajin', # 押金字段
'rate_key': '11', # 押金提现费率对应的 Platform
'model': UserDashou,
},
]
def post(self, request):
# 获取参数
leixing = request.data.get('leixing')
huiyuan_id = request.data.get('huiyuan_id')
yajin_jine = request.data.get('yajin_jine')
fadan_id = request.data.get('fadan_id')
# 参数校验
if leixing not in [1, 2, 3, 4]:
return Response({'code': 400, 'msg': 'leixing 必须为 1,2,3,4'}, status=status.HTTP_400_BAD_REQUEST)
if leixing == 1 and not huiyuan_id:
return Response({'code': 400, 'msg': '购买会员时必须提供 huiyuan_id'}, status=status.HTTP_400_BAD_REQUEST)
if leixing == 2:
if not yajin_jine:
return Response({'code': 400, 'msg': '购买押金时必须提供 yajin_jine'}, status=status.HTTP_400_BAD_REQUEST)
try:
yajin_jine = Decimal(str(yajin_jine))
if yajin_jine <= 0:
raise ValueError
except:
return Response({'code': 400, 'msg': 'yajin_jine 必须为正数'}, status=status.HTTP_400_BAD_REQUEST)
if leixing == 4 and not fadan_id:
return Response({'code': 400, 'msg': '缴纳罚款时必须提供 fadan_id'}, status=status.HTTP_400_BAD_REQUEST)
# 获取当前用户
user = request.user
# 获取目标价格(需支付的原价)
target_price = None
if leixing == 1:
# 会员
try:
huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id)
target_price = huiyuan.jiage
except Huiyuan.DoesNotExist:
return Response({'code': 404, 'msg': '会员不存在'}, status=status.HTTP_404_NOT_FOUND)
elif leixing == 2:
# 押金
target_price = yajin_jine
elif leixing == 3:
# 积分(固定价格)
target_price = INTEGRAL_PRICE
elif leixing == 4:
# 罚款缴纳
try:
fadan = Penalty.query.get(id=fadan_id, PenalizedUserID=user.UserUID)
target_price = fadan.FineAmount
except Penalty.DoesNotExist:
return Response({'code': 404, 'msg': '罚单不存在或不属于当前用户'}, status=status.HTTP_404_NOT_FOUND)
# 获取所有费率(打手佣金、管事分红、组长分红、打手押金)
rates = {}
for key in ['5', '6', '8', '11']:
try:
rate_obj = CommissionRate.query.get(Platform=key)
rates[key] = rate_obj.Rate
except CommissionRate.DoesNotExist:
rates[key] = None # 未配置则跳过
# 构建可用身份列表
available_list = []
for ident in self.IDENTITY_CONFIG:
# 检查用户是否有该扩展表
profile = getattr(user, ident['profile_attr'], None)
if not profile:
continue
# 获取余额
balance = getattr(profile, ident['balance_field'], Decimal('0.00'))
# 获取费率
rate = rates.get(ident['rate_key'])
if rate is None:
continue # 费率未配置,跳过该身份
# 计算所需抵扣金额: target_price / (1 - rate) ,向上取整到分
try:
if rate >= 1:
continue # 防止除零及不合理费率
required = target_price / (Decimal('1.0') - rate)
# 向上取整到分(保留两位小数,最后一位进位)
required = required.quantize(Decimal('0.01'), rounding=ROUND_CEILING)
except Exception:
continue # 计算异常,跳过
# 余额是否足够
if balance >= required:
available_list.append({
'id': ident['id'],
'jieshao': ident['name'],
'xuyao': str(required),
})
return Response({
'code': 200,
'msg': 'success',
'data': available_list
})
INTEGRAL_INCREMENT = 5
MAX_INTEGRAL = 10
IDENTITY_CONFIG = {
1: {
'name': '打手佣金',
'model': UserDashou,
'balance_field': 'yue',
'status_field': 'zhanghaozhuangtai',
'rate_key': '5',
},
2: {
'name': '管事分红',
'model': UserGuanshi,
'balance_field': 'yue',
'status_field': 'zhuangtai',
'rate_key': '6',
},
3: {
'name': '组长分红',
'model': UserZuzhang,
'balance_field': 'ketixian_jine',
'status_field': 'zhuangtai',
'rate_key': '8',
},
4: {
'name': '打手押金',
'model': UserDashou,
'balance_field': 'yajin',
'status_field': 'zhanghaozhuangtai',
'rate_key': '11',
},
}
class DsqrgmdhView(APIView):
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
try:
leixing = request.data.get('leixing')
shenfen_id = request.data.get('shenfen_id')
huiyuan_id = request.data.get('huiyuan_id')
yajin_jine = request.data.get('yajin_jine')
fadan_id = request.data.get('fadan_id')
logger.info(f"抵扣支付请求: leixing={leixing}, shenfen_id={shenfen_id}, huiyuan_id={huiyuan_id}, yajin_jine={yajin_jine}, fadan_id={fadan_id}")
if leixing not in [1, 2, 3, 4]:
return Response({'code': 400, 'msg': 'leixing 必须为 1/2/3/4'}, status=status.HTTP_400_BAD_REQUEST)
if shenfen_id not in [1, 2, 3, 4]:
return Response({'code': 400, 'msg': 'shenfen_id 必须为 1/2/3/4'}, status=status.HTTP_400_BAD_REQUEST)
if leixing == 1 and not huiyuan_id:
return Response({'code': 400, 'msg': '购买会员时必须提供 huiyuan_id'}, status=status.HTTP_400_BAD_REQUEST)
if leixing == 2:
if not yajin_jine:
return Response({'code': 400, 'msg': '购买押金时必须提供 yajin_jine'}, status=status.HTTP_400_BAD_REQUEST)
try:
yajin_jine = Decimal(str(yajin_jine))
if yajin_jine <= 0:
raise ValueError
if yajin_jine > Decimal('10000'):
return Response({'code': 400, 'msg': '押金金额不能超过10000元'}, status=status.HTTP_400_BAD_REQUEST)
except:
return Response({'code': 400, 'msg': 'yajin_jine 必须为正数'}, status=status.HTTP_400_BAD_REQUEST)
target_price = None
if leixing == 1:
try:
huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id)
target_price = huiyuan.jiage
except Huiyuan.DoesNotExist:
return Response({'code': 404, 'msg': '会员不存在'}, status=status.HTTP_404_NOT_FOUND)
elif leixing == 2:
target_price = yajin_jine
elif leixing == 3:
target_price = INTEGRAL_PRICE
elif leixing == 4:
if not fadan_id:
return Response({'code': 400, 'msg': '缺少罚单ID'}, status=status.HTTP_400_BAD_REQUEST)
try:
fadan_obj = Penalty.query.get(id=fadan_id, PenalizedUserID=request.user.UserUID)
target_price = fadan_obj.FineAmount
except Penalty.DoesNotExist:
return Response({'code': 404, 'msg': '罚单不存在或不属于当前用户'}, status=status.HTTP_404_NOT_FOUND)
identity = IDENTITY_CONFIG.get(shenfen_id)
try:
rate_obj = CommissionRate.query.get(Platform=identity['rate_key'])
rate = rate_obj.Rate
if rate >= 1:
return Response({'code': 400, 'msg': f"{identity['name']}费率异常"}, status=status.HTTP_400_BAD_REQUEST)
except CommissionRate.DoesNotExist:
return Response({'code': 400, 'msg': f"{identity['name']}费率未配置"}, status=status.HTTP_400_BAD_REQUEST)
logger.info(f"目标价格={target_price}, 费率={rate}")
try:
required = target_price / (Decimal('1.0') - rate)
required = required.quantize(Decimal('0.01'), rounding=ROUND_CEILING)
logger.info(f"所需抵扣金额={required}")
except Exception as e:
logger.error(f"金额计算错误: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': f'金额计算错误: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
user = request.user
yonghuid = user.UserUID
try:
profile = identity['model'].query.get(user=user)
except identity['model'].DoesNotExist:
return Response({'code': 403, 'msg': f'您不是{identity["name"]}身份'})
status_value = getattr(profile, identity['status_field'])
if status_value != 1:
return Response({'code': 403, 'msg': f'{identity["name"]}账号状态异常,无法使用'})
balance = getattr(profile, identity['balance_field'])
if balance < required:
return Response({'code': 400, 'msg': f'{identity["name"]}余额不足,需要{required}元,当前{balance}'}, status=status.HTTP_400_BAD_REQUEST)
try:
with transaction.atomic():
locked_profile = identity['model'].objects.select_for_update().get(id=profile.id)
current_balance = getattr(locked_profile, identity['balance_field'])
if current_balance < required:
raise ValueError(f"{identity['name']}余额不足,需要{required}元,当前{current_balance}")
setattr(locked_profile, identity['balance_field'], current_balance - required)
locked_profile.save(update_fields=[identity['balance_field']])
logger.info(f"扣款成功: {identity['name']} 扣除 {required} 元,剩余 {current_balance - required}")
timestamp = str(int(time.time() * 1000))[-12:]
rand_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4))
dingdan_id = f"CZ{timestamp}{rand_str}"
if leixing == 1:
action_desc = f"购买会员 {huiyuan.jieshao}"
elif leixing == 2:
action_desc = f"充值押金 {yajin_jine}"
elif leixing == 3:
action_desc = "补充积分"
elif leixing == 4:
action_desc = f"缴纳罚款 {target_price}"
shuoming = f"{identity['name']}抵扣 {action_desc},抵扣金额{required}"
czjilu = Czjilu.query.create(
dingdan_id=dingdan_id,
huiyuan_id=huiyuan_id if leixing == 1 else None,
zhuangtai=1,
yonghuid=yonghuid,
jine=required,
leixing=leixing,
shuoming=shuoming,
)
response_data = {}
if leixing == 1:
huiyuan_record, is_first_buy = self._process_member_purchase(yonghuid, huiyuan_id, huiyuan)
response_data['huiyuan'] = {
'huiyuanid': huiyuan_id,
'huiyuanming': huiyuan.jieshao,
'daoqi': huiyuan_record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S') if hasattr(huiyuan_record, 'daoqi_time') else '',
}
if is_first_buy:
try:
dashou = UserDashou.query.get(user=user)
dashou.jifen += 10
dashou.save(update_fields=['jifen'])
response_data['jifen'] = dashou.jifen
except UserDashou.DoesNotExist:
pass
elif leixing == 2:
try:
dashou = UserDashou.objects.select_for_update().get(user=user)
dashou.yajin += yajin_jine
dashou.save(update_fields=['yajin'])
response_data['yajin'] = dashou.yajin
except UserDashou.DoesNotExist:
return Response({'code': 500, 'msg': '打手信息不存在,无法充值押金'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
elif leixing == 4:
if fadan_obj.zhuangtai not in [1, 3]:
return Response({'code': 400, 'msg': '罚单状态不可缴纳'}, status=status.HTTP_400_BAD_REQUEST)
fadan_obj.zhuangtai = 2
fadan_obj.save(update_fields=['zhuangtai'])
response_data['fadan_id'] = fadan_obj.id
response_data['fadan_status'] = fadan_obj.zhuangtai
# ========== 修改点2: 使用最新的分红处理方法 ==========
success, msg = process_fadan_fenhong(fadan_obj.id)
if not success:
logger.error(f"罚单 {fadan_obj.id} 分红处理失败: {msg}")
else:
logger.info(f"罚单 {fadan_obj.id} 分红处理成功: {msg}")
else: # leixing == 3
try:
dashou = UserDashou.objects.select_for_update().get(user=user)
new_jifen = dashou.jifen + INTEGRAL_INCREMENT
dashou.jifen = min(new_jifen, MAX_INTEGRAL)
dashou.save(update_fields=['jifen'])
response_data['jifen'] = dashou.jifen
except UserDashou.DoesNotExist:
return Response({'code': 500, 'msg': '打手信息不存在,无法补充积分'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
except Exception as e:
logger.error(f"事务处理异常: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': f'支付处理失败: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
final_profile = identity['model'].query.get(user=user)
response_data['balance'] = str(getattr(final_profile, identity['balance_field']))
return Response({
'code': 200,
'msg': 'success',
'data': response_data,
})
except Exception as e:
logger.error(f"接口顶层异常: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': f'系统错误: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def _process_member_purchase(self, yonghuid, huiyuan_id, huiyuan_obj):
is_first_buy_any = False
try:
record = Huiyuangoumai.query.filter(
yonghu_id=yonghuid,
huiyuan_id=huiyuan_id
).first()
if record:
record.daoqi_time += timedelta(days=30)
record.huiyuan_zhuangtai = 1
record.save(update_fields=['daoqi_time', 'huiyuan_zhuangtai'])
else:
other_count = Huiyuangoumai.query.filter(yonghu_id=yonghuid).exclude(huiyuan_id=huiyuan_id).count()
if other_count == 0:
is_first_buy_any = True
record = Huiyuangoumai.query.create(
yonghu_id=yonghuid,
huiyuan_id=huiyuan_id,
jieshao=huiyuan_obj.jieshao,
huiyuan_zhuangtai=1,
daoqi_time=timezone.now() + timedelta(days=30)
)
return record, is_first_buy_any
except Exception as e:
if 'unique' in str(e).lower() or 'duplicate' in str(e).lower():
record = Huiyuangoumai.query.get(yonghu_id=yonghuid, huiyuan_id=huiyuan_id)
return record, False
raise