6403 lines
252 KiB
Python
6403 lines
252 KiB
Python
# ==================== 标准库 ====================
|
||
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 (
|
||
UserMain, AdminProfile, UserDashou, UserGuanshi, UserZuzhang, UserShangjia
|
||
)
|
||
|
||
from orders.models import Lilubiao
|
||
from config.models import Szjilu, Gonggao, Lunbo
|
||
|
||
from orders.models import Fadan # 用于罚款类型
|
||
|
||
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.dashou_profile
|
||
except UserDashou.DoesNotExist:
|
||
logger.warning(f"用户{request.user.yonghuid}不是打手,尝试访问打手接口")
|
||
return Response({
|
||
'code': 400,
|
||
'message': '用户不是打手,无法访问此接口'
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 2. 检查打手账号状态
|
||
if dashou.zhanghaozhuangtai != 1:
|
||
logger.warning(f"打手{request.user.yonghuid}账号状态异常: {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.dashou_profile
|
||
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.yonghuid,
|
||
jine=jine,
|
||
leixing=2, # 充值类型:2=押金
|
||
shuoming=settings.YAJIN_PAY_DESCRIPTION
|
||
)
|
||
|
||
logger.info(f"创建押金充值订单: 用户{request.user.yonghuid}, 订单{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__yonghuid=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. 押金分红(仅微信确认到账后;费率 Lilubiao fadanpingtai=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 = Lilubiao.query.filter(fadanpingtai='12').first()
|
||
rate = lilu_obj.lilu if lilu_obj and lilu_obj.lilu 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__yonghuid=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={
|
||
'zongsy': Decimal('0.00'),
|
||
'zongls': Decimal('0.00'),
|
||
'zongzc': Decimal('0.00'),
|
||
'jrzc': Decimal('0.00'),
|
||
'jrls': Decimal('0.00')
|
||
}
|
||
)
|
||
|
||
# 更新三个字段
|
||
szjilu.zongsy += jine_decimal # 总收益
|
||
szjilu.zongls += jine_decimal # 总流水
|
||
szjilu.jrls += 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,
|
||
yonghuid=request.user.yonghuid
|
||
)
|
||
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.yonghuid}删除未支付订单: {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,
|
||
yonghuid=request.user.yonghuid
|
||
)
|
||
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.dashou_profile
|
||
yajin = float(dashou.yajin) # 转为浮点数方便前端处理
|
||
|
||
logger.info(f"轮询成功: 订单{dingdanid}已支付,用户{request.user.yonghuid}押金为{yajin}元")
|
||
|
||
return Response({
|
||
'code': 200,
|
||
'message': '支付流程全部完成',
|
||
'yajin': yajin # 返回当前押金(前端需要更新全局变量)
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
except UserDashou.DoesNotExist:
|
||
logger.error(f"打手信息不存在: 用户{request.user.yonghuid}")
|
||
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.dashou_profile
|
||
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.yonghuid,
|
||
jine=jine,
|
||
leixing=3, # 充值类型:3=积分
|
||
shuoming=settings.JIFEN_PAY_DESCRIPTION
|
||
)
|
||
|
||
logger.info(f"创建积分充值订单: 用户{request.user.yonghuid}, 订单{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={
|
||
'zongsy': Decimal('0.00'),
|
||
'zongls': Decimal('0.00'),
|
||
'zongzc': Decimal('0.00'),
|
||
'jrzc': Decimal('0.00'),
|
||
'jrls': Decimal('0.00')
|
||
}
|
||
)
|
||
|
||
# 更新三个字段
|
||
szjilu.zongsy += jine_decimal # 总收益
|
||
szjilu.zongls += jine_decimal # 总流水
|
||
szjilu.jrls += 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__yonghuid=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,
|
||
yonghuid=request.user.yonghuid
|
||
)
|
||
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.yonghuid}删除未支付积分订单: {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,
|
||
yonghuid=request.user.yonghuid
|
||
)
|
||
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.dashou_profile
|
||
jifen = dashou.jifen # 直接使用整数
|
||
|
||
logger.info(f"积分轮询成功: 订单{dingdanid}已支付,用户{request.user.yonghuid}积分为{jifen}")
|
||
|
||
return Response({
|
||
'code': 200,
|
||
'message': '支付流程全部完成',
|
||
'jifen': jifen # 返回当前积分(前端需要更新全局变量)
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
except UserDashou.DoesNotExist:
|
||
logger.error(f"打手信息不存在: 用户{request.user.yonghuid}")
|
||
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.dashou_profile
|
||
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.yonghuid,
|
||
jine=jine,
|
||
leixing=1, # 充值类型:1=会员
|
||
shuoming=shuoming,
|
||
huiyuan_id=huiyuanid # 存储会员ID
|
||
)
|
||
|
||
logger.info(
|
||
f"创建会员充值订单: 用户{request.user.yonghuid}, 订单{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={'zongsy': Decimal('0.00'), 'zongls': Decimal('0.00'), 'zongzc': Decimal('0.00'),
|
||
'jrzc': Decimal('0.00'), 'jrls': Decimal('0.00')}
|
||
)
|
||
szjilu.zongsy += jine_decimal
|
||
szjilu.zongls += jine_decimal
|
||
szjilu.jrls += 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__yonghuid=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__yonghuid=order.yonghuid)
|
||
guanshi_id = dashou.yaoqingren
|
||
if not guanshi_id:
|
||
logger.info(f"打手{order.yonghuid}没有邀请人,无分红")
|
||
return
|
||
|
||
# 获取管事信息
|
||
try:
|
||
guanshi = UserGuanshi.query.get(user__yonghuid=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__yonghuid=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,
|
||
yonghuid=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,
|
||
yonghuid=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,
|
||
yonghuid=request.user.yonghuid
|
||
)
|
||
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.yonghuid}删除未支付会员订单: {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,
|
||
yonghuid=request.user.yonghuid
|
||
)
|
||
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.yonghuid,
|
||
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.dashou_profile
|
||
|
||
logger.info(f"会员轮询成功: 订单{dingdanid}已支付,用户{request.user.yonghuid}, 会员{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.shop_profile
|
||
except UserShangjia.DoesNotExist:
|
||
logger.warning(f"用户{request.user.yonghuid}尝试充值但不是商家")
|
||
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.yonghuid,
|
||
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={
|
||
'zongsy': Decimal('0.00'),
|
||
'zongls': Decimal('0.00'),
|
||
'zongzc': Decimal('0.00'),
|
||
'jrzc': Decimal('0.00'),
|
||
'jrls': Decimal('0.00')
|
||
}
|
||
)
|
||
|
||
# 更新三个字段
|
||
szjilu.zongsy += jine_decimal # 总收益
|
||
szjilu.zongls += jine_decimal # 总流水
|
||
szjilu.jrls += 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__yonghuid=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,
|
||
yonghuid=request.user.yonghuid,
|
||
leixing=4 # 确保是商家充值订单
|
||
)
|
||
except Czjilu.DoesNotExist:
|
||
logger.warning(f"商家订单不存在或不属于当前用户: {dingdanid}, 用户{request.user.yonghuid}")
|
||
return Response({
|
||
'code': 400,
|
||
'message': '订单不存在或不属于当前用户'
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 3. 检查订单状态是否为未支付(9)
|
||
if order.zhuangtai == 9:
|
||
# 删除订单
|
||
order.delete()
|
||
logger.info(f"商家{request.user.shop_profile.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,
|
||
yonghuid=request.user.yonghuid,
|
||
leixing=4 # 商家充值类型
|
||
)
|
||
except Czjilu.DoesNotExist:
|
||
logger.warning(f"商家订单不存在: {dingdanid}, 用户{request.user.yonghuid}")
|
||
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.shop_profile
|
||
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.yonghuid}")
|
||
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.yonghuid
|
||
|
||
# 获取分页参数
|
||
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.yonghuid}")
|
||
return Response({
|
||
'code': 400,
|
||
'message': '缺少账号参数',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 2. 获取当前登录用户
|
||
current_user = request.user
|
||
|
||
# 3. 验证用户类型是否为管理员
|
||
if current_user.user_type != 'admin':
|
||
logger.warning(
|
||
f"权限验证失败:用户 {current_user.yonghuid} 的用户类型为 {current_user.user_type},非管理员")
|
||
return Response({
|
||
'code': 401,
|
||
'message': '无管理员权限',
|
||
'data': None
|
||
}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 4. 验证手机号与传递的账号是否一致
|
||
if current_user.phone != zhanghao:
|
||
logger.warning(
|
||
f"账号验证失败:用户 {current_user.yonghuid} 的phone={current_user.phone},传递的zhanghao={zhanghao}")
|
||
return Response({
|
||
'code': 401,
|
||
'message': '账号验证失败',
|
||
'data': None
|
||
}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 5. 验证管理员扩展表是否存在
|
||
try:
|
||
admin_profile = current_user.admin_profile
|
||
logger.info(f"管理员验证通过:用户 {current_user.yonghuid},手机号 {current_user.phone}")
|
||
except AdminProfile.DoesNotExist:
|
||
logger.error(
|
||
f"管理员扩展表不存在:用户 {current_user.yonghuid} 虽然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.user_type != '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.user_type != '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.user_type != '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.user_type != '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.user_type != '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.user_type != '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.user_type != '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.user_type != 'admin':
|
||
return Response(
|
||
{'code': 401, 'message': '身份验证失败'},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
# 5. 验证管理员扩展表数据
|
||
try:
|
||
guanliyuan = dangqianyonghu.admin_profile
|
||
except AdminProfile.DoesNotExist:
|
||
return Response(
|
||
{'code': 401, 'message': '身份验证失败'},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
# 6. 查询公告数据(类型=1的公告)
|
||
gonggao_obj = Gonggao.query.filter(leixing=1).first()
|
||
shangpingonggao = gonggao_obj.jieshao if gonggao_obj else ''
|
||
|
||
# 7. 查询轮播图数据(类型=1的轮播图)
|
||
lunbo_objs = Lunbo.query.filter(leixing=1).values('tupian_url')
|
||
shangpinlunbo = [item['tupian_url'] 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.user_type != 'admin':
|
||
return Response(
|
||
{'code': 401, 'message': '身份验证失败'},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
# 5. 验证管理员扩展表数据
|
||
try:
|
||
guanliyuan = dangqianyonghu.admin_profile
|
||
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(leixing=1).first()
|
||
|
||
# 如果不存在,则创建
|
||
if not gonggao_obj:
|
||
gonggao_obj = Gonggao(leixing=1)
|
||
|
||
# 9. 更新公告内容
|
||
gonggao_obj.jieshao = 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.user_type != 'admin':
|
||
return Response(
|
||
{'code': 401, 'message': '身份验证失败'},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
try:
|
||
guanliyuan = dangqianyonghu.admin_profile
|
||
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.user_type != 'admin':
|
||
return Response(
|
||
{'code': 401, 'message': '身份验证失败'},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
try:
|
||
guanliyuan = dangqianyonghu.admin_profile
|
||
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(tupian_url=tupian_url, leixing=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(
|
||
tupian_url=oss_file_path, # 存相对路径
|
||
leixing=1
|
||
)
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'message': '添加成功',
|
||
'data': {
|
||
'tupian_url': lunbo_obj.tupian_url
|
||
}
|
||
})
|
||
|
||
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.user_type != 'admin':
|
||
return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
try:
|
||
guanliyuan = dangqianyonghu.admin_profile
|
||
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.user_type != 'admin':
|
||
logger.warning(f"非管理员访问: user_type={dangqianyonghu.user_type}")
|
||
return Response(
|
||
{'code': 401, 'message': '身份验证失败', 'data': None},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
# 3. 验证管理员扩展表
|
||
try:
|
||
guanliyuan = dangqianyonghu.admin_profile
|
||
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.user_type != 'admin':
|
||
return Response(
|
||
{'code': 401, 'message': '身份验证失败', 'data': None},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
try:
|
||
guanliyuan = dangqianyonghu.admin_profile
|
||
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.user_type != 'admin':
|
||
return Response(
|
||
{'code': 401, 'message': '身份验证失败', 'data': None},
|
||
status=status.HTTP_401_UNAUTHORIZED
|
||
)
|
||
|
||
try:
|
||
guanliyuan = dangqianyonghu.admin_profile
|
||
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, 'admin_profile'):
|
||
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, 'admin_profile'):
|
||
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.yonghuid).count()
|
||
|
||
# 3.3 分红次数(总记录数)
|
||
base_queryset = Gsfenhong.query.filter(zuzhang_id=current_user.yonghuid).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 = UserMain.query.filter(yonghuid__in=guanshi_ids).select_related(
|
||
'boss_profile'
|
||
).only(
|
||
'yonghuid', 'avatar',
|
||
'boss_profile__nickname'
|
||
)
|
||
|
||
info = {}
|
||
for user in users:
|
||
avatar = user.avatar or ''
|
||
# 从 boss_profile 获取昵称
|
||
if hasattr(user, 'boss_profile') and user.boss_profile:
|
||
nicheng = user.boss_profile.nickname or ''
|
||
else:
|
||
# 如果没有 boss_profile,使用默认昵称
|
||
nicheng = f"用户{user.yonghuid[-4:]}"
|
||
info[user.yonghuid] = {'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, # 身份ID:1打手佣金 2管事分红 3组长分红 4打手押金
|
||
"jieshao": "打手佣金抵扣",
|
||
"xuyao": "22.22" # 所需抵扣金额(保留两位小数)
|
||
},
|
||
...
|
||
]
|
||
}
|
||
"""
|
||
permission_classes = [permissions.IsAuthenticated]
|
||
|
||
# 身份ID与对应扩展表模型、余额字段、费率查询条件的映射
|
||
IDENTITY_CONFIG = [
|
||
{
|
||
'id': 1,
|
||
'name': '打手佣金抵扣',
|
||
'profile_attr': 'dashou_profile', # UserMain 的反向 related_name
|
||
'balance_field': 'yue',
|
||
'rate_key': '5',
|
||
'model': UserDashou,
|
||
},
|
||
{
|
||
'id': 2,
|
||
'name': '管事分红抵扣',
|
||
'profile_attr': 'guanshi_profile',
|
||
'balance_field': 'yue',
|
||
'rate_key': '6',
|
||
'model': UserGuanshi,
|
||
},
|
||
{
|
||
'id': 3,
|
||
'name': '组长分红抵扣',
|
||
'profile_attr': 'zuzhang_profile',
|
||
'balance_field': 'ketixian_jine',
|
||
'rate_key': '8',
|
||
'model': UserZuzhang,
|
||
},
|
||
# 新增打手押金抵扣
|
||
{
|
||
'id': 4,
|
||
'name': '打手押金抵扣',
|
||
'profile_attr': 'dashou_profile', # 与打手佣金共用扩展表
|
||
'balance_field': 'yajin', # 押金字段
|
||
'rate_key': '11', # 押金提现费率对应的 fadanpingtai
|
||
'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 = Fadan.query.get(id=fadan_id, beichufa_id=user.yonghuid)
|
||
target_price = fadan.fakuanjine
|
||
except Fadan.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '罚单不存在或不属于当前用户'}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
# 获取所有费率(打手佣金、管事分红、组长分红、打手押金)
|
||
rates = {}
|
||
for key in ['5', '6', '8', '11']:
|
||
try:
|
||
rate_obj = Lilubiao.query.get(fadanpingtai=key)
|
||
rates[key] = rate_obj.lilu
|
||
except Lilubiao.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 = Fadan.query.get(id=fadan_id, beichufa_id=request.user.yonghuid)
|
||
target_price = fadan_obj.fakuanjine
|
||
except Fadan.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '罚单不存在或不属于当前用户'}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
identity = IDENTITY_CONFIG.get(shenfen_id)
|
||
try:
|
||
rate_obj = Lilubiao.query.get(fadanpingtai=identity['rate_key'])
|
||
rate = rate_obj.lilu
|
||
if rate >= 1:
|
||
return Response({'code': 400, 'msg': f"{identity['name']}费率异常"}, status=status.HTTP_400_BAD_REQUEST)
|
||
except Lilubiao.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.yonghuid
|
||
|
||
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 |