# ==================== 标准库 ==================== import os import json import time import random import string import uuid import hashlib import requests import logging import decimal from datetime import timedelta from decimal import Decimal, InvalidOperation, ROUND_CEILING import defusedxml.ElementTree as ET # ==================== Django 内置 ==================== from django.conf import settings from django.db import transaction, connection from django.db.models import Q from gvsdsdk.fluent import db, func, FQ from django.shortcuts import render, get_object_or_404 from django.http import HttpResponse from django.utils import timezone # ==================== DRF 框架 ==================== from rest_framework.views import APIView, View from rest_framework.response import Response from rest_framework import permissions, status from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.parsers import MultiPartParser, FormParser, JSONParser from rest_framework.throttling import AnonRateThrottle # ==================== 项目工具函数 ==================== from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss from orders.utils import update_daily_income from backend.utils import ( update_guanshi_daily_by_action, update_guanshi_xufei_daily, update_zuzhang_daily_by_action ) from users.fadan_fenhong_utils import process_fadan_fenhong # ==================== 各App模型 ==================== from .models import ( ShangpinLeixing, ShangpinZhuanqu, Shangpin, Huiyuan, Czjilu, Gsfenhong, Huiyuangoumai, DuociFenhong ) from users.models import ( AdminProfile, UserDashou, UserGuanshi, UserZuzhang, UserShangjia ) from users.business_models import User from orders.models import CommissionRate from config.models import Gonggao, Lunbo from orders.models import Penalty # 用于罚款类型 from jituan.services.club_config import ( get_commission_rate, get_commission_rate_object, get_huiyuan_price, get_huiyuan_fenchong, ) from jituan.services.club_context import resolve_club_id_from_request from jituan.services.club_write import resolve_club_id_for_write from jituan.services.club_penalty import resolve_gsfenhong_club_id import traceback # ==================== 日志实例 ==================== logger = logging.getLogger(__name__) class ShangpinHuoquView(APIView): """ 商品数据获取接口 一次性返回商品类型、商品专区、商品列表 """ # 应用节流(限制频率) throttle_classes = [AnonRateThrottle] permission_classes = [AllowAny] def post(self, request): """ 处理POST请求,返回所有商品相关数据 """ try: logger.info("收到商品数据获取请求") # 1. 查询商品类型(只查询正常状态的类型,并排序) # 正常运营:只显示 shenhezhuangtai=2 的商品类型 leixing_queryset = ShangpinLeixing.query.filter( shenhezhuangtai=1 # 只取正常状态 ).order_by('-paixu', 'id') # 先按paixu降序,再按id升序 # 注意:审核期间使用下面这行代码(先注释掉) '''leixing_queryset = ShangpinLeixing.query.filter( shenhezhuangtai=2 # 审核期间:只取审核专用状态 ).order_by('-paixu', 'id')''' shangpinleixing_data = [] for leixing in leixing_queryset: shangpinleixing_data.append({ "id": leixing.id, "jieshao": leixing.jieshao, "tupian_url": leixing.tupian_url, "paixu": leixing.paixu }) # 2. 查询商品专区(只查询可见类型下的专区,并排序) # 先获取可见的类型ID列表 visible_leixing_ids = [leixing['id'] for leixing in shangpinleixing_data] if visible_leixing_ids: zhuanqu_queryset = ShangpinZhuanqu.query.filter( leixing_id__in=visible_leixing_ids, shenhezhuangtai=1 # 🔴【添加】只筛选审核状态为2的专区 ).order_by('-paixu', 'id') # 先按paixu降序,再按id升序 else: zhuanqu_queryset = ShangpinZhuanqu.query.none() shangpinzhuanqu_data = [] for zhuanqu in zhuanqu_queryset: shangpinzhuanqu_data.append({ "id": zhuanqu.id, "mingzi": zhuanqu.mingzi, "leixing_id": zhuanqu.leixing_id, "paixu": zhuanqu.paixu }) # 3. 查询商品列表(只查询可见专区下的商品,并排序) # 先获取可见的专区ID列表 visible_zhuanqu_ids = [zhuanqu['id'] for zhuanqu in shangpinzhuanqu_data] if visible_zhuanqu_ids: # 注意:这里使用values()只获取指定字段,提高查询效率 shangpin_queryset = Shangpin.query.filter( zhuanqu_id__in=visible_zhuanqu_ids, shenhezhuangtai=1 # 🔴【添加】只筛选审核状态为2的商品 ).values( 'id', 'biaoqian', 'jiage', 'leixing_id', 'zhuanqu_id', 'tupian_url', 'duiwai_xiaoliang', 'paixu' ).order_by('-paixu', 'id') # 先按paixu降序,再按id升序 # 将QuerySet转换为列表 shangpinliebiao_data = list(shangpin_queryset) else: shangpinliebiao_data = [] # 4. 构建返回数据 response_data = { "shangpinleixing": shangpinleixing_data, "shangpinzhuanqu": shangpinzhuanqu_data, "shangpinliebiao": shangpinliebiao_data } logger.info(f"返回数据:类型{len(shangpinleixing_data)}个," f"专区{len(shangpinzhuanqu_data)}个," f"商品{len(shangpinliebiao_data)}个") # 5. 返回响应 return Response(response_data, status=status.HTTP_200_OK) except Exception as e: # 记录异常 logger.error(f"获取商品数据时发生错误:{str(e)}", exc_info=True) # 返回错误响应 error_data = { "error": "服务器内部错误", "detail": "获取商品数据失败,请稍后重试" } return Response(error_data, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class ShangpinXiangqingHuoquView(APIView): """ 商品详情接口 - 极简版 假设所有字段都存在,直接返回 """ throttle_classes = [AnonRateThrottle] permission_classes = [AllowAny] def post(self, request): try: shangpin_id = request.data.get('shangpin_id') if not shangpin_id: return Response({ "code": 1, "msg": "商品ID不能为空", "data": None }, status=400) # 直接查询,数据量小,性能没问题 shangpin = Shangpin.query.filter(id=shangpin_id).first() if not shangpin: return Response({ "code": 2, "msg": "商品不存在", "data": None }, status=404) # 直接构建数据,不处理空值 data = { "tupian": shangpin.tupian_url, "biaoti": shangpin.biaoqian, "jiage": shangpin.jiage, "xiangqing": shangpin.jieshao, "xiadanxuzhi": shangpin.xiadan_xuzhi, "guize": shangpin.guize_tupian, "xiaoliang": shangpin.duiwai_xiaoliang, "kucun": shangpin.kucun, } return Response({ "code": 0, "msg": "success", "data": data }) except Exception as e: logger.error(f"接口异常: {e}") return Response({ "code": 500, "msg": "服务器错误", "data": None }, status=500) class DashouHuiyuanList(APIView): """ 获取打手会员商品列表 路径:/shangpin/dshyhq 请求方式:POST 权限:JWT认证 返回:会员商品列表,字段对应前端需求 """ permission_classes = [IsAuthenticated] def post(self, request): try: # 1. 检查用户是否是打手(通过反向查询) try: dashou = request.user.DashouProfile except UserDashou.DoesNotExist: logger.warning(f"用户{request.user.UserUID}不是打手,尝试访问打手接口") return Response({ 'code': 400, 'message': '用户不是打手,无法访问此接口' }, status=status.HTTP_400_BAD_REQUEST) # 2. 检查打手账号状态 if dashou.zhanghaozhuangtai != 1: logger.warning(f"打手{request.user.UserUID}账号状态异常: {dashou.zhanghaozhuangtai}") return Response({ 'code': 400, 'message': '账号状态异常,无法购买会员' }, status=status.HTTP_400_BAD_REQUEST) # 3. 查询所有会员数据(ORM查询,确保性能) club_id = resolve_club_id_from_request(request) 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(get_huiyuan_price(club_id, huiyuan.huiyuan_id, huiyuan)), 'jieshao': huiyuan.jtjieshao # 会员介绍(对应前端jieshao) }) # 5. 返回数据 return Response({ 'code': 200, 'message': '获取会员列表成功', 'data': huiyuan_list # 对应前端接收的列表 }, status=status.HTTP_200_OK) except Exception as e: logger.error(f"获取会员列表接口异常: {str(e)}", exc_info=True) return Response({ 'code': 500, 'message': '服务器内部错误,请稍后重试' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class YajinGoumai(APIView): """ 押金充值支付参数接口 路径:/shangpin/yajingoumai 请求方式:POST 权限:JWT认证 参数:jine(金额,1-10000元) 返回:支付参数(payParams)和订单ID(dingdanid) """ permission_classes = [IsAuthenticated] def post(self, request): try: # 1. 验证用户身份和状态 try: dashou = request.user.DashouProfile except UserDashou.DoesNotExist: return Response({ 'code': 400, 'message': '用户不是打手,无法充值押金' }, status=status.HTTP_400_BAD_REQUEST) if dashou.zhanghaozhuangtai != 1: return Response({ 'code': 400, 'message': '账号状态异常,无法进行充值' }, status=status.HTTP_400_BAD_REQUEST) # 2. 获取并验证金额参数 jine = request.data.get('jine') if not jine: return Response({ 'code': 400, 'message': '请填写充值金额' }, status=status.HTTP_400_BAD_REQUEST) try: jine = int(jine) except (ValueError, TypeError): return Response({ 'code': 400, 'message': '金额格式错误,请填写1-10000的整数' }, status=status.HTTP_400_BAD_REQUEST) # 验证金额范围 if jine < 1 or jine > 10000: return Response({ 'code': 400, 'message': '充值金额必须在1-10000元之间' }, status=status.HTTP_400_BAD_REQUEST) # 3. 生成订单ID(使用你提供的算法,前缀改为CzYJ) timestamp_ms = int(time.time() * 1000) timestamp_ns = int(time.time_ns() // 1000) % 1000000 random_num = random.randint(0, 99) dingdanid = f"CzYJ{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}" # 4. 创建充值记录订单 chongzhi_jilu = Czjilu.query.create( dingdan_id=dingdanid, zhuangtai=9, # 未支付状态 yonghuid=request.user.UserUID, jine=jine, leixing=2, # 充值类型:2=押金 shuoming=settings.YAJIN_PAY_DESCRIPTION, club_id=resolve_club_id_for_write(request, request.user), ) logger.info(f"创建押金充值订单: 用户{request.user.UserUID}, 订单{dingdanid}, 金额{jine}元") # 5. 生成微信支付参数 try: pay_params = self.generate_wechat_pay_params( dingdanid=dingdanid, jine=jine, openid=request.user.OpenID, # 直接从request.user获取openid pay_type='yajin' ) except Exception as e: # 支付参数生成失败,删除订单记录 chongzhi_jilu.delete() logger.error(f"生成微信支付参数失败: {str(e)}") return Response({ 'code': 500, 'message': f'支付参数生成失败: {str(e)}' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 6. 返回支付参数和订单ID return Response({ 'code': 200, 'message': '支付参数生成成功', 'payParams': pay_params, # 支付参数(前端接收为payParams) 'dingdanid': dingdanid # 订单ID(前端接收为dingdanid) }, status=status.HTTP_200_OK) except Exception as e: logger.error(f"押金充值接口异常: {str(e)}", exc_info=True) return Response({ 'code': 500, 'message': '服务器内部错误,请稍后重试' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type): """生成微信小程序支付参数""" # 获取配置 from jituan.services.club_write import resolve_club_id_for_write from jituan.services.wechat_pay import get_wechat_v2_config pay_cfg = get_wechat_v2_config(resolve_club_id_for_write(self.request, self.request.user)) APPID = pay_cfg['appid'] MCHID = pay_cfg['mch_id'] KEY = pay_cfg['key'] # 根据支付类型获取对应的配置 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""" {APPID} {PAY_DESCRIPTION} {MCHID} {nonce_str} {NOTIFY_URL} {openid} {dingdanid} {user_ip} {total_fee} {trade_type} {sign} """ # 调用微信统一下单接口(官方地址) 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}分") from jituan.services.wechat_pay import club_id_from_czjilu, verify_wechat_v2_signature notify_club = club_id_from_czjilu(out_trade_no) # 2. 验证基本返回状态 if return_code != 'SUCCESS' or result_code != 'SUCCESS': logger.warning(f"微信支付回调状态异常: {return_code}/{result_code}") return self.wechat_response('FAIL', '支付失败') # 3. 验证签名(确保是微信发来的) if not verify_wechat_v2_signature(request_body, notify_club): logger.error(f"签名验证失败: {out_trade_no}") return self.wechat_response('FAIL', '签名验证失败') # 4. 查询订单 try: order = Czjilu.query.get(dingdan_id=out_trade_no) logger.info(f"找到订单: {order.dingdan_id}, 状态: {order.zhuangtai}, 金额: {order.jine}") except Czjilu.DoesNotExist: logger.error(f"订单不存在: {out_trade_no}") return self.wechat_response('FAIL', '订单不存在') # 5. 检查订单状态是否为未支付(9) if order.zhuangtai != 9: logger.warning(f"订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}") # 已处理过的订单直接返回成功,避免微信重复回调 return self.wechat_response('SUCCESS', 'OK') # 6. 验证金额(微信返回的是分,需要转换) wechat_amount_yuan = total_fee / 100 # 转换为元 order_amount_yuan = float(order.jine) # 订单金额(元) # 允许1分钱的误差(浮点数比较) if abs(wechat_amount_yuan - order_amount_yuan) > 0.01: logger.error(f"金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元") return self.wechat_response('FAIL', '金额不一致') # 7. 更新订单状态为已支付(3) order.zhuangtai = 3 order.save(update_fields=['zhuangtai']) logger.info(f"订单状态更新成功: {order.dingdan_id} -> 3") # 8. 更新打手押金 try: # 通过用户ID查找打手(高效查询) dashou = UserDashou.query.get(user__UserUID=order.yonghuid) # 增加押金(使用Decimal确保精度) dashou.yajin += Decimal(str(order_amount_yuan)) dashou.save(update_fields=['yajin']) logger.info(f"打手押金更新成功: 用户{order.yonghuid}, 增加{order_amount_yuan}元, 新押金{dashou.yajin}") # 8b. 押金分红(仅微信确认到账后;费率 CommissionRate Platform=12) try: guanshi_id = dashou.yaoqingren if not guanshi_id: logger.info(f"押金分红跳过: 打手{order.yonghuid}无邀请管事") elif Gsfenhong.query.filter(dingdan_id=order.dingdan_id).exists(): logger.info(f"押金分红已处理: 订单{order.dingdan_id}") else: lilu_obj = CommissionRate.query.filter(Platform='12').first() rate = lilu_obj.Rate if lilu_obj and lilu_obj.Rate is not None else Decimal('0') if rate <= 0: logger.info(f"押金分红跳过: 费率未配置或为0") else: recharge_amount = Decimal(str(order_amount_yuan)) fenhong_jine = (recharge_amount * rate).quantize(Decimal('0.01')) if fenhong_jine > 0: with transaction.atomic(): guanshi = UserGuanshi.objects.select_for_update().get( user__UserUID=guanshi_id ) guanshi.yue += fenhong_jine guanshi.chongzhifenrun += fenhong_jine guanshi.save(update_fields=['yue', 'chongzhifenrun']) user_main = dashou.user Gsfenhong.query.create( dingdan_id=order.dingdan_id, guanshi=guanshi_id, dashouid=order.yonghuid, shuoming='押金分红', fenhong=fenhong_jine, avatar=user_main.Avatar, nicheng=dashou.nicheng or '未知打手', fenhong_leixing=2, club_id=resolve_gsfenhong_club_id( dingdan_id=order.dingdan_id, dashouid=order.yonghuid, czjilu=order, ), ) 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, getattr(order, 'club_id', None)) from jituan.services.szjilu_accounting import apply_szjilu_income apply_szjilu_income(jine_decimal, getattr(order, 'club_id', None)) 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""" """ 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.UserUID ) except Czjilu.DoesNotExist: return Response({ 'code': 400, 'message': '订单不存在或不属于当前用户' }, status=status.HTTP_400_BAD_REQUEST) # 3. 检查订单状态是否为未支付(9) if order.zhuangtai == 9: # 删除订单 order.delete() logger.info(f"用户{request.user.UserUID}删除未支付订单: {dingdanid}") return Response({ 'code': 200, 'message': '订单已删除' }, status=status.HTTP_200_OK) else: return Response({ 'code': 400, 'message': '订单状态不是未支付,无法删除' }, status=status.HTTP_400_BAD_REQUEST) except Exception as e: logger.error(f"押金支付失败处理异常: {str(e)}", exc_info=True) return Response({ 'code': 500, 'message': '服务器内部错误' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # views.py class YajinLunxun(APIView): """ 押金支付成功轮询接口 路径:/shangpin/yajinlunxun 请求方式:POST 权限:JWT认证 参数:dingdanid(订单ID) 返回:订单状态和当前押金 """ permission_classes = [IsAuthenticated] def post(self, request): try: # 1. 获取订单ID dingdanid = request.data.get('dingdanid') if not dingdanid: return Response({ 'code': 400, 'message': '订单ID不能为空' }, status=status.HTTP_400_BAD_REQUEST) # 2. 查询订单(确保是当前用户的订单) try: order = Czjilu.query.get( dingdan_id=dingdanid, yonghuid=request.user.UserUID ) except Czjilu.DoesNotExist: return Response({ 'code': 400, 'message': '订单不存在或不属于当前用户' }, status=status.HTTP_400_BAD_REQUEST) # 3. 检查订单状态是否为已支付(3) if order.zhuangtai != 3: logger.info(f"订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}") return Response({ 'code': 400, 'message': '订单尚未支付完成' }, status=status.HTTP_400_BAD_REQUEST) # 4. 获取打手押金(通过反向查询,最快捷的方式) try: dashou = request.user.DashouProfile yajin = float(dashou.yajin) # 转为浮点数方便前端处理 logger.info(f"轮询成功: 订单{dingdanid}已支付,用户{request.user.UserUID}押金为{yajin}元") return Response({ 'code': 200, 'message': '支付流程全部完成', 'yajin': yajin # 返回当前押金(前端需要更新全局变量) }, status=status.HTTP_200_OK) except UserDashou.DoesNotExist: logger.error(f"打手信息不存在: 用户{request.user.UserUID}") return Response({ 'code': 400, 'message': '打手信息不存在' }, status=status.HTTP_400_BAD_REQUEST) except Exception as e: logger.error(f"押金轮询异常: {str(e)}", exc_info=True) return Response({ 'code': 500, 'message': '服务器内部错误' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # views.py class JifenBuchong(APIView): """ 积分补充支付参数接口 路径:/shangpin/jifenbc 请求方式:POST 权限:JWT认证 参数:无(固定5元) 返回:支付参数(payParams)和订单ID(dingdanid) """ permission_classes = [IsAuthenticated] def post(self, request): try: # 1. 验证用户身份和状态 try: dashou = request.user.DashouProfile except UserDashou.DoesNotExist: return Response({ 'code': 400, 'message': '用户不是打手,无法补充积分' }, status=status.HTTP_400_BAD_REQUEST) # 2. 检查打手账号状态 if dashou.zhanghaozhuangtai != 1: return Response({ 'code': 400, 'message': '账号状态异常,无法进行操作' }, status=status.HTTP_400_BAD_REQUEST) # 3. 检查积分是否已满(满分10分) if dashou.jifen >= 10: return Response({ 'code': 400, 'message': '积分已满,无需补充' }, status=status.HTTP_400_BAD_REQUEST) # 4. 生成订单ID(前缀CzJF表示积分充值) timestamp_ms = int(time.time() * 1000) timestamp_ns = int(time.time_ns() // 1000) % 1000000 random_num = random.randint(0, 99) dingdanid = f"CzJF{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}" # 5. 创建积分充值记录订单(固定5元) jine = 5.00 # 固定5元 chongzhi_jilu = Czjilu.query.create( dingdan_id=dingdanid, zhuangtai=9, # 未支付状态 yonghuid=request.user.UserUID, jine=jine, leixing=3, # 充值类型:3=积分 shuoming=settings.JIFEN_PAY_DESCRIPTION, club_id=resolve_club_id_for_write(request, request.user), ) logger.info(f"创建积分充值订单: 用户{request.user.UserUID}, 订单{dingdanid}, 金额{jine}元") # 6. 生成微信支付参数(完整版) try: pay_params = self.generate_wechat_pay_params( dingdanid=dingdanid, jine=jine, openid=request.user.OpenID, pay_type='jifen' ) except Exception as e: # 支付参数生成失败,删除订单记录 chongzhi_jilu.delete() logger.error(f"生成积分支付参数失败: {str(e)}") return Response({ 'code': 500, 'message': f'支付参数生成失败: {str(e)}' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 7. 返回支付参数和订单ID return Response({ 'code': 200, 'message': '支付参数生成成功', 'payParams': pay_params, # 对应前端payParams 'dingdanid': dingdanid # 对应前端dingdanid }, status=status.HTTP_200_OK) except Exception as e: logger.error(f"积分充值接口异常: {str(e)}", exc_info=True) return Response({ 'code': 500, 'message': '服务器内部错误,请稍后重试' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type): """生成微信支付参数(完整版,无省略)""" # 微信支付配置 - 从settings中获取 from jituan.services.club_write import resolve_club_id_for_write from jituan.services.wechat_pay import get_wechat_v2_config pay_cfg = get_wechat_v2_config(resolve_club_id_for_write(self.request, self.request.user)) APPID = pay_cfg['appid'] MCHID = pay_cfg['mch_id'] KEY = pay_cfg['key'] # 根据支付类型获取对应的配置 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""" {APPID} {PAY_DESCRIPTION} {MCHID} {nonce_str} {NOTIFY_URL} {openid} {dingdanid} {user_ip} {total_fee} {trade_type} {sign} """ # 调用微信统一下单接口(官方地址) 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. 验证签名 from jituan.services.wechat_pay import club_id_from_czjilu, verify_wechat_v2_signature notify_club = club_id_from_czjilu(out_trade_no) if not verify_wechat_v2_signature(request_body, notify_club): 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, getattr(order, 'club_id', None)) update_daily_income(jine_decimal, getattr(order, 'club_id', None)) from jituan.services.szjilu_accounting import apply_szjilu_income apply_szjilu_income(jine_decimal, getattr(order, 'club_id', None)) logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}元") except Exception as e: # 收支记录更新失败不影响主流程,只记录日志 logger.error(f"更新收支记录失败: {str(e)}", exc_info=True) # ==== 添加结束 ==== # 7. 更新订单状态为已支付(3) order.zhuangtai = 3 order.save(update_fields=['zhuangtai']) logger.info(f"积分订单状态更新成功: {order.dingdan_id} -> 3") # 8. 更新打手积分 try: # 通过用户ID查找打手 dashou = UserDashou.query.get(user__UserUID=order.yonghuid) # 检查积分是否小于10 if dashou.jifen < 10: # 增加5积分,但不超过10 new_jifen = dashou.jifen + 5 dashou.jifen = min(new_jifen, 10) # 确保不超过10 dashou.save(update_fields=['jifen']) logger.info(f"打手积分更新成功: 用户{order.yonghuid}, 增加5积分, 新积分{dashou.jifen}") else: logger.warning(f"打手积分已满: 用户{order.yonghuid}, 当前积分{dashou.jifen}") except UserDashou.DoesNotExist: logger.error(f"打手信息不存在: 用户{order.yonghuid}") return self.wechat_response('SUCCESS', 'OK') except Exception as e: logger.error(f"更新打手积分失败: {str(e)}", exc_info=True) return self.wechat_response('FAIL', '更新积分失败') # 9. 返回成功响应给微信 logger.info(f"积分支付回调处理完成: {out_trade_no}") return self.wechat_response('SUCCESS', 'OK') except ET.ParseError as e: logger.error(f"积分XML解析失败: {str(e)}") return self.wechat_response('FAIL', '数据格式错误') except Exception as e: logger.error(f"积分支付回调处理异常: {str(e)}", exc_info=True) return self.wechat_response('FAIL', '处理异常') def verify_signature(self, xml_data): """验证微信支付签名""" try: root = ET.fromstring(xml_data) sign = root.find('sign').text if not sign: logger.error("积分数签名字段不存在") return False # 获取所有参数并排除sign params = {} for child in root: if child.tag != 'sign' and child.text is not None: params[child.tag] = child.text # 按照参数名ASCII码从小到大排序 sorted_params = sorted(params.items(), key=lambda x: x[0]) # 拼接字符串 sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value]) sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}' # 计算签名 calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() is_valid = sign == calculated_sign if not is_valid: logger.error(f"积分签名验证失败: 计算签名{calculated_sign}, 收到签名{sign}") return is_valid except Exception as e: logger.error(f"积分验证签名异常: {str(e)}") return False def wechat_response(self, return_code, return_msg): """返回微信支付回调响应""" xml_response = f""" """ 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.UserUID ) except Czjilu.DoesNotExist: return Response({ 'code': 400, 'message': '订单不存在或不属于当前用户' }, status=status.HTTP_400_BAD_REQUEST) # 3. 检查订单类型是否为积分(3) if order.leixing != 3: return Response({ 'code': 400, 'message': '订单类型错误,不是积分订单' }, status=status.HTTP_400_BAD_REQUEST) # 4. 检查订单状态是否为未支付(9) if order.zhuangtai == 9: # 删除订单 order.delete() logger.info(f"用户{request.user.UserUID}删除未支付积分订单: {dingdanid}") return Response({ 'code': 200, 'message': '积分订单已删除' }, status=status.HTTP_200_OK) else: return Response({ 'code': 400, 'message': '积分订单状态不是未支付,无法删除' }, status=status.HTTP_400_BAD_REQUEST) except Exception as e: logger.error(f"积分支付失败处理异常: {str(e)}", exc_info=True) return Response({ 'code': 500, 'message': '服务器内部错误' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # views.py class JifenLunxun(APIView): """ 积分支付成功轮询接口 路径:/shangpin/jifenlunxun 请求方式:POST 权限:JWT认证 参数:dingdanid(订单ID) 返回:订单状态和当前积分 """ permission_classes = [IsAuthenticated] def post(self, request): try: # 1. 获取订单ID dingdanid = request.data.get('dingdanid') if not dingdanid: return Response({ 'code': 400, 'message': '订单ID不能为空' }, status=status.HTTP_400_BAD_REQUEST) # 2. 查询订单(确保是当前用户的订单) try: order = Czjilu.query.get( dingdan_id=dingdanid, yonghuid=request.user.UserUID ) except Czjilu.DoesNotExist: return Response({ 'code': 400, 'message': '订单不存在或不属于当前用户' }, status=status.HTTP_400_BAD_REQUEST) # 3. 检查订单类型是否为积分(3) if order.leixing != 3: return Response({ 'code': 400, 'message': '订单类型错误,不是积分订单' }, status=status.HTTP_400_BAD_REQUEST) # 4. 检查订单状态是否为已支付(3) if order.zhuangtai != 3: logger.info(f"积分订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}") return Response({ 'code': 400, 'message': '积分订单尚未支付完成' }, status=status.HTTP_400_BAD_REQUEST) # 5. 获取打手积分(通过反向查询) try: dashou = request.user.DashouProfile jifen = dashou.jifen # 直接使用整数 logger.info(f"积分轮询成功: 订单{dingdanid}已支付,用户{request.user.UserUID}积分为{jifen}") return Response({ 'code': 200, 'message': '支付流程全部完成', 'jifen': jifen # 返回当前积分(前端需要更新全局变量) }, status=status.HTTP_200_OK) except UserDashou.DoesNotExist: logger.error(f"打手信息不存在: 用户{request.user.UserUID}") return Response({ 'code': 400, 'message': '打手信息不存在' }, status=status.HTTP_400_BAD_REQUEST) except Exception as e: logger.error(f"积分轮询异常: {str(e)}", exc_info=True) return Response({ 'code': 500, 'message': '服务器内部错误' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class HuiyuanGoumai(APIView): """ 会员充值支付参数接口 路径:/shangpin/huiyuangm 请求方式:POST 权限:JWT认证 参数:huiyuanid(会员ID) 返回:支付参数(payParams)和订单ID(dingdanid) """ permission_classes = [IsAuthenticated] def post(self, request): try: # 1. 获取会员ID参数 huiyuanid = request.data.get('huiyuanid') if not huiyuanid: return Response({ 'code': 400, 'message': '会员ID不能为空' }, status=status.HTTP_400_BAD_REQUEST) # 2. 验证用户身份和状态 try: dashou = request.user.DashouProfile except UserDashou.DoesNotExist: return Response({ 'code': 400, 'message': '用户不是打手,无法购买会员' }, status=status.HTTP_400_BAD_REQUEST) # 3. 检查打手账号状态 if dashou.zhanghaozhuangtai != 1: return Response({ 'code': 400, 'message': '账号状态异常,无法进行购买' }, status=status.HTTP_400_BAD_REQUEST) # 4. 查询会员信息 try: huiyuan = Huiyuan.query.get(huiyuan_id=huiyuanid) except Huiyuan.DoesNotExist: return Response({ 'code': 400, 'message': '会员信息不存在' }, status=status.HTTP_400_BAD_REQUEST) # 5. 生成订单ID(前缀CzHY表示会员充值) timestamp_ms = int(time.time() * 1000) timestamp_ns = int(time.time_ns() // 1000) % 1000000 random_num = random.randint(0, 99) dingdanid = f"CzHY{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}" # 6. 创建充值记录订单 club_id = resolve_club_id_from_request(request) jine = float(get_huiyuan_price(club_id, huiyuanid, huiyuan)) # 组合订单说明:会员介绍 + 配置描述 shuoming = f"{huiyuan.jieshao} - {settings.HUIYUAN_PAY_DESCRIPTION}" chongzhi_jilu = Czjilu.query.create( dingdan_id=dingdanid, zhuangtai=9, # 未支付状态 yonghuid=request.user.UserUID, jine=jine, leixing=1, # 充值类型:1=会员 shuoming=shuoming, huiyuan_id=huiyuanid, # 存储会员ID club_id=club_id, ) logger.info( f"创建会员充值订单: 用户{request.user.UserUID}, 订单{dingdanid}, 会员{huiyuanid}, 金额{jine}元") # 7. 生成微信支付参数 try: pay_params = self.generate_wechat_pay_params( dingdanid=dingdanid, jine=jine, openid=request.user.OpenID, pay_type='huiyuan' ) except Exception as e: # 支付参数生成失败,删除订单记录 chongzhi_jilu.delete() logger.error(f"生成会员支付参数失败: {str(e)}") return Response({ 'code': 500, 'message': f'支付参数生成失败: {str(e)}' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 8. 返回支付参数和订单ID return Response({ 'code': 200, 'message': '支付参数生成成功', 'payParams': pay_params, # 对应前端payParams 'dingdanid': dingdanid # 对应前端dingdanid }, status=status.HTTP_200_OK) except Exception as e: logger.error(f"会员充值接口异常: {str(e)}", exc_info=True) return Response({ 'code': 500, 'message': '服务器内部错误,请稍后重试' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type): """生成微信支付参数(完整版,无省略)""" # 微信支付配置 - 从settings中获取 from jituan.services.club_write import resolve_club_id_for_write from jituan.services.wechat_pay import get_wechat_v2_config pay_cfg = get_wechat_v2_config(resolve_club_id_for_write(self.request, self.request.user)) APPID = pay_cfg['appid'] MCHID = pay_cfg['mch_id'] KEY = pay_cfg['key'] # 根据支付类型获取对应的配置 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""" {APPID} {PAY_DESCRIPTION} {MCHID} {nonce_str} {NOTIFY_URL} {openid} {dingdanid} {user_ip} {total_fee} {trade_type} {sign} """ # 调用微信统一下单接口(官方地址) 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', '支付失败') from jituan.services.wechat_pay import club_id_from_czjilu, verify_wechat_v2_signature notify_club = club_id_from_czjilu(out_trade_no) if not verify_wechat_v2_signature(request_body, notify_club): 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, getattr(order, 'club_id', None)) from jituan.services.szjilu_accounting import apply_szjilu_income apply_szjilu_income(jine_decimal, getattr(order, 'club_id', None)) 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), club_id=get_user_club_id(User.query.filter(UserUID=yonghuid).first()), ) logger.info(f"新购会员: 用户{yonghuid}, 会员{huiyuan_id}") return goumai_record, True, is_first_buy_any except Exception as e: logger.error(f"处理会员购买记录失败: {str(e)}", exc_info=True) if "unique" in str(e).lower() or "duplicate" in str(e).lower(): logger.warning(f"检测到重复创建尝试,已由数据库拦截: 用户{yonghuid}, 会员{huiyuan_id}") return self.process_huiyuan_goumai(yonghuid, huiyuan_id, huiyuan) raise def add_first_buy_jifen(self, yonghuid): """首次购买积分奖励(与原代码一致)""" try: dashou = UserDashou.query.get(user__UserUID=yonghuid) dashou.jifen += 10 dashou.save(update_fields=['jifen']) logger.info(f"用户{yonghuid}首次购买会员,积分+10,当前积分: {dashou.jifen}") except UserDashou.DoesNotExist: logger.error(f"找不到打手记录,无法增加积分: {yonghuid}") except Exception as e: logger.error(f"增加积分失败: {str(e)}") # 🆕 新增:统一分红处理函数 def process_fenhong(self, order, huiyuan): """ 处理管事和组长分红(支持多次分红配置) """ try: # 获取打手信息 dashou = UserDashou.query.get(user__UserUID=order.yonghuid) guanshi_id = dashou.yaoqingren if not guanshi_id: logger.info(f"打手{order.yonghuid}没有邀请人,无分红") return # 获取管事信息 try: guanshi = UserGuanshi.query.get(user__UserUID=guanshi_id) except UserGuanshi.DoesNotExist: logger.warning(f"管事信息不存在: {guanshi_id}") return # 获取组长ID(从管事扩展表的邀请人) zuzhang_id = guanshi.yaoqingren # 获取打手头像和昵称 user_main = dashou.user avatar = user_main.Avatar nicheng = dashou.nicheng or "未知打手" # 查询该打手针对该管事的已分红次数 existing_count = Gsfenhong.query.filter( dashouid=order.yonghuid, guanshi=guanshi_id ).count() cishu = existing_count + 1 # 本次是第几次 # 获取订单金额(用于限制分红总额) order_amount = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine club_id = getattr(order, 'club_id', None) or resolve_club_id_for_write(user=dashou.user) guanshi_fc, zuzhang_fc = get_huiyuan_fenchong(club_id, huiyuan.huiyuan_id, huiyuan) # 计算管事分红金额 guanshi_fenhong = self._calc_guanshi_fenhong( guanshi, huiyuan, guanshi_id, cishu, default_fc=guanshi_fc, ) if guanshi_fenhong is None: logger.info(f"管事{guanshi_id}本次无分红") guanshi_fenhong = Decimal('0.00') # 计算组长分红金额(如果存在组长) zuzhang_fenhong = Decimal('0.00') if zuzhang_id: # 获取组长信息 try: zuzhang = UserZuzhang.query.get(user__UserUID=zuzhang_id) except UserZuzhang.DoesNotExist: logger.warning(f"组长信息不存在: {zuzhang_id}") zuzhang = None zuzhang_fenhong = self._calc_zuzhang_fenhong( zuzhang, huiyuan, zuzhang_id, cishu, default_fc=zuzhang_fc, ) 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, club_id=resolve_gsfenhong_club_id( dingdan_id=order.dingdan_id, dashouid=order.yonghuid, czjilu=order, ), ) # ========== 管事每日统计(新购) ========== 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, default_fc=None): """计算管事分红金额""" # 先查多次分红配置表 try: cid = resolve_gsfenhong_club_id(dashouid=guanshi_id) config = DuociFenhong.query.get( club_id=cid, huiyuan=huiyuan, yonghuid=guanshi_id, cishu=cishu, ) return Decimal(str(config.guanshi_fenhong)) except DuociFenhong.DoesNotExist: pass if cishu == 1: base = default_fc if default_fc is not None else huiyuan.guanshifc return Decimal(str(base)) 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, default_fc=None): """计算组长分红金额""" if zuzhang is None: return None # 查多次分红配置表 try: cid = resolve_gsfenhong_club_id(dashouid=zuzhang_id) config = DuociFenhong.query.get( club_id=cid, huiyuan=huiyuan, yonghuid=zuzhang_id, cishu=cishu, ) return Decimal(str(config.zuzhang_fenhong)) except DuociFenhong.DoesNotExist: pass if cishu == 1: base = default_fc if default_fc is not None else huiyuan.zuzhangfc return Decimal(str(base)) 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""" """ 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.UserUID ) except Czjilu.DoesNotExist: return Response({ 'code': 400, 'message': '订单不存在或不属于当前用户' }, status=status.HTTP_400_BAD_REQUEST) # 3. 检查订单类型是否为会员(1) if order.leixing != 1: return Response({ 'code': 400, 'message': '订单类型错误,不是会员订单' }, status=status.HTTP_400_BAD_REQUEST) # 4. 检查订单状态是否为未支付(9) if order.zhuangtai == 9: # 删除订单 order.delete() logger.info(f"用户{request.user.UserUID}删除未支付会员订单: {dingdanid}") return Response({ 'code': 200, 'message': '会员订单已删除' }, status=status.HTTP_200_OK) else: return Response({ 'code': 400, 'message': '会员订单状态不是未支付,无法删除' }, status=status.HTTP_400_BAD_REQUEST) except Exception as e: logger.error(f"会员支付失败处理异常: {str(e)}", exc_info=True) return Response({ 'code': 500, 'message': '服务器内部错误' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # views.py class HuiyuanLunxun(APIView): """ 会员支付成功轮询接口 路径:/shangpin/huiyuanlx 请求方式:POST 权限:JWT认证 参数:dingdanid(订单ID) 返回:订单状态和会员信息 """ permission_classes = [IsAuthenticated] def post(self, request): try: # 1. 获取订单ID dingdanid = request.data.get('dingdanid') if not dingdanid: return Response({ 'code': 400, 'message': '订单ID不能为空' }, status=status.HTTP_400_BAD_REQUEST) # 2. 查询订单(确保是当前用户的订单) try: order = Czjilu.query.get( dingdan_id=dingdanid, yonghuid=request.user.UserUID ) except Czjilu.DoesNotExist: return Response({ 'code': 400, 'message': '订单不存在或不属于当前用户' }, status=status.HTTP_400_BAD_REQUEST) # 3. 检查订单类型是否为会员(1) if order.leixing != 1: return Response({ 'code': 400, 'message': '订单类型错误,不是会员订单' }, status=status.HTTP_400_BAD_REQUEST) # 4. 检查订单状态是否为已支付(3) if order.zhuangtai != 3: logger.info(f"会员订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}") return Response({ 'code': 400, 'message': '会员订单尚未支付完成' }, status=status.HTTP_400_BAD_REQUEST) # 5. 查询会员信息 try: huiyuan = Huiyuan.query.get(huiyuan_id=order.huiyuan_id) # 6. 查询会员购买记录 goumai_record = Huiyuangoumai.query.filter( yonghu_id=request.user.UserUID, huiyuan_id=order.huiyuan_id ).first() if not goumai_record: return Response({ 'code': 400, 'message': '会员购买记录不存在' }, status=status.HTTP_400_BAD_REQUEST) # 7. 格式化到期时间 #from django.utils.timezone import localtime #daoqi_time = localtime(goumai_record.daoqi_time).strftime('%Y-%m-%d %H:%M:%S') # 直接格式化,不使用时区转换 daoqi_time = goumai_record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S') current_user = request.user dashou = current_user.DashouProfile logger.info(f"会员轮询成功: 订单{dingdanid}已支付,用户{request.user.UserUID}, 会员{order.huiyuan_id}") # 8. 返回会员信息(用于更新前端全局变量) return Response({ 'code': 200, 'message': '支付流程全部完成', 'jifen': dashou.jifen, 'huiyuan': { 'huiyuanid': order.huiyuan_id, 'huiyuanming': huiyuan.jieshao, 'daoqi': daoqi_time } }, status=status.HTTP_200_OK) except Huiyuan.DoesNotExist: logger.error(f"会员信息不存在: {order.huiyuan_id}") return Response({ 'code': 400, 'message': '会员信息不存在' }, status=status.HTTP_400_BAD_REQUEST) except Exception as e: logger.error(f"会员轮询异常: {str(e)}", exc_info=True) return Response({ 'code': 500, 'message': '服务器内部错误' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # views.py in shangpin app class ShangjiaChongzhi(APIView): """ 商家余额充值支付参数接口 路径:/shangpin/sjcz 请求方式:POST 权限:JWT认证 参数:jine(金额,1-10000元) 返回:支付参数(payParams)和订单ID(dingdanid) """ permission_classes = [IsAuthenticated] def post(self, request): try: # 1. 验证用户身份和商家状态 try: shangjia = request.user.ShopProfile except UserShangjia.DoesNotExist: logger.warning(f"用户{request.user.UserUID}尝试充值但不是商家") return Response({ 'code': 400, 'message': '用户不是商家,无法充值' }, status=status.HTTP_400_BAD_REQUEST) # 检查商家状态 if shangjia.zhuangtai != 1: logger.warning(f"商家{shangjia.nicheng}状态异常: {shangjia.zhuangtai}") return Response({ 'code': 400, 'message': '商家状态异常,无法进行充值' }, status=status.HTTP_400_BAD_REQUEST) # 2. 获取并验证金额参数 jine = request.data.get('jine') if not jine: return Response({ 'code': 400, 'message': '请填写充值金额' }, status=status.HTTP_400_BAD_REQUEST) try: jine = float(jine) except (ValueError, TypeError): return Response({ 'code': 400, 'message': '金额格式错误,请填写有效的数字' }, status=status.HTTP_400_BAD_REQUEST) # 验证金额范围 if jine < settings.SHANGJIA_CZ_MIN_AMOUNT or jine > settings.SHANGJIA_CZ_MAX_AMOUNT: return Response({ 'code': 400, 'message': f'充值金额必须在{settings.SHANGJIA_CZ_MIN_AMOUNT}-{settings.SHANGJIA_CZ_MAX_AMOUNT}元之间' }, status=status.HTTP_400_BAD_REQUEST) # 3. 生成订单ID(使用配置的前缀) timestamp_ms = int(time.time() * 1000) timestamp_ns = int(time.time_ns() // 1000) % 1000000 random_num = random.randint(0, 99) dingdanid = f"{settings.SHANGJIA_CZ_PREFIX}{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}" # 4. 创建充值记录订单 try: chongzhi_jilu = Czjilu.query.create( dingdan_id=dingdanid, zhuangtai=9, # 未支付状态 yonghuid=request.user.UserUID, jine=jine, leixing=4, # 充值类型:4=商家充值 shuoming=settings.SHANGJIA_CZ_PAY_DESCRIPTION, club_id=resolve_club_id_for_write(request, request.user), ) 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): """生成微信小程序支付参数""" # 获取配置 from jituan.services.club_write import resolve_club_id_for_write from jituan.services.wechat_pay import get_wechat_v2_config pay_cfg = get_wechat_v2_config(resolve_club_id_for_write(self.request, self.request.user)) APPID = pay_cfg['appid'] MCHID = pay_cfg['mch_id'] KEY = pay_cfg['key'] # 根据支付类型获取对应的配置 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""" {APPID} {PAY_DESCRIPTION} {MCHID} {nonce_str} {NOTIFY_URL} {openid} {dingdanid} {user_ip} {total_fee} {trade_type} {sign} """ # 调用微信统一下单接口 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. 验证签名(确保是微信发来的) from jituan.services.wechat_pay import club_id_from_czjilu, verify_wechat_v2_signature notify_club = club_id_from_czjilu(out_trade_no) if not verify_wechat_v2_signature(request_body, notify_club): 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, getattr(order, 'club_id', None)) update_daily_income(jine_decimal, getattr(order, 'club_id', None)) from jituan.services.szjilu_accounting import apply_szjilu_income apply_szjilu_income(jine_decimal, getattr(order, 'club_id', None)) logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}元") except Exception as e: # 收支记录更新失败不影响主流程,只记录日志 logger.error(f"更新收支记录失败: {str(e)}", exc_info=True) # ===== 添加结束 ===== # 7. 使用事务更新订单状态和商家余额 with transaction.atomic(): # 更新订单状态为已支付(3) order.zhuangtai = 3 order.save(update_fields=['zhuangtai']) logger.info(f"商家订单状态更新成功: {order.dingdan_id} -> 3") # 更新商家余额 try: # 通过用户ID查找商家(高效查询) shangjia = UserShangjia.query.get(user__UserUID=order.yonghuid) # 增加余额 shangjia.yue += order_amount_yuan shangjia.save(update_fields=['yue']) logger.info(f"商家余额更新成功: 商家{shangjia.nicheng}, 增加{order_amount_yuan}元, 新余额{shangjia.yue}") except UserShangjia.DoesNotExist: # 如果找不到商家,记录错误,但依然返回成功(避免微信重复回调) logger.error(f"商家信息不存在: 用户{order.yonghuid}") return self.wechat_response('SUCCESS', 'OK') except Exception as e: logger.error(f"更新商家余额失败: {str(e)}", exc_info=True) # 回滚事务 raise # 8. 返回成功响应给微信 logger.info(f"商家支付回调处理完成: {out_trade_no}") return self.wechat_response('SUCCESS', 'OK') except ET.ParseError as e: logger.error(f"XML解析失败: {str(e)}") return self.wechat_response('FAIL', '数据格式错误') except Exception as e: logger.error(f"商家支付回调处理异常: {str(e)}", exc_info=True) # 返回FAIL让微信重试 return self.wechat_response('FAIL', '处理异常') def verify_signature(self, xml_data): """验证微信支付签名""" try: root = ET.fromstring(xml_data) sign = root.find('sign').text if not sign: logger.error("签名字段不存在") return False # 获取所有参数并排除sign params = {} for child in root: if child.tag != 'sign' and child.text is not None: params[child.tag] = child.text # 按照参数名ASCII码从小到大排序 sorted_params = sorted(params.items(), key=lambda x: x[0]) # 拼接字符串 sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value]) sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}' # 计算签名 calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() is_valid = sign == calculated_sign if not is_valid: logger.error(f"商家支付签名验证失败: 计算签名{calculated_sign}, 收到签名{sign}") return is_valid except Exception as e: logger.error(f"验证签名异常: {str(e)}") return False def wechat_response(self, return_code, return_msg): """返回微信支付回调响应""" xml_response = f""" """ logger.info(f"返回微信响应: {return_code} - {return_msg}") return HttpResponse(xml_response, content_type='application/xml') # views.py class ShangjiaShibai(APIView): """ 商家支付失败处理接口 路径:/shangpin/sjshibai 请求方式:POST 权限:JWT认证 参数:dingdanid(订单ID) 返回:删除结果 """ permission_classes = [IsAuthenticated] def post(self, request): try: # 1. 获取订单ID dingdanid = request.data.get('dingdanid') if not dingdanid: return Response({ 'code': 400, 'message': '订单ID不能为空' }, status=status.HTTP_400_BAD_REQUEST) # 2. 查询订单(确保是当前用户的订单) try: order = Czjilu.query.get( dingdan_id=dingdanid, UserUID=request.user.UserUID, leixing=4 # 确保是商家充值订单 ) except Czjilu.DoesNotExist: logger.warning(f"商家订单不存在或不属于当前用户: {dingdanid}, 用户{request.user.UserUID}") return Response({ 'code': 400, 'message': '订单不存在或不属于当前用户' }, status=status.HTTP_400_BAD_REQUEST) # 3. 检查订单状态是否为未支付(9) if order.zhuangtai == 9: # 删除订单 order.delete() logger.info(f"商家{request.user.ShopProfile.nicheng}删除未支付订单: {dingdanid}") return Response({ 'code': 200, 'message': '订单已删除' }, status=status.HTTP_200_OK) else: return Response({ 'code': 400, 'message': '订单状态不是未支付,无法删除' }, status=status.HTTP_400_BAD_REQUEST) except UserShangjia.DoesNotExist: return Response({ 'code': 400, 'message': '用户不是商家' }, status=status.HTTP_400_BAD_REQUEST) except Exception as e: logger.error(f"商家支付失败处理异常: {str(e)}", exc_info=True) return Response({ 'code': 500, 'message': '服务器内部错误' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # views.py class ShangjiaChenggong(APIView): """ 商家支付成功轮询接口 路径:/shangpin/sjcg 请求方式:POST 权限:JWT认证 参数:dingdanid(订单ID) 返回:订单状态和当前商家余额 """ permission_classes = [IsAuthenticated] def post(self, request): try: # 1. 获取订单ID dingdanid = request.data.get('dingdanid') if not dingdanid: return Response({ 'code': 400, 'message': '订单ID不能为空' }, status=status.HTTP_400_BAD_REQUEST) # 2. 查询订单(确保是当前用户的商家订单) try: order = Czjilu.query.get( dingdan_id=dingdanid, UserUID=request.user.UserUID, leixing=4 # 商家充值类型 ) except Czjilu.DoesNotExist: logger.warning(f"商家订单不存在: {dingdanid}, 用户{request.user.UserUID}") return Response({ 'code': 400, 'message': '订单不存在或不属于当前用户' }, status=status.HTTP_400_BAD_REQUEST) # 3. 检查订单状态是否为已支付(3) if order.zhuangtai != 3: logger.info(f"商家订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}") return Response({ 'code': 400, 'message': '订单尚未支付完成' }, status=status.HTTP_400_BAD_REQUEST) # 4. 获取商家余额(通过反向查询) try: shangjia = request.user.ShopProfile yue = float(shangjia.yue) # 转为浮点数方便前端处理 logger.info(f"商家轮询成功: 订单{dingdanid}已支付,商家{shangjia.nicheng}余额为{yue}元") return Response({ 'code': 200, 'message': '支付流程全部完成', 'yue': yue # 返回当前商家余额(前端需要更新全局变量) }, status=status.HTTP_200_OK) except UserShangjia.DoesNotExist: logger.error(f"商家信息不存在: 用户{request.user.UserUID}") return Response({ 'code': 400, 'message': '商家信息不存在' }, status=status.HTTP_400_BAD_REQUEST) except Exception as e: logger.error(f"商家轮询异常: {str(e)}", exc_info=True) return Response({ 'code': 500, 'message': '服务器内部错误' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 管事分红记录查询API class GuanshiFenhongListAPI(APIView): """ 管事分红记录查询接口 权限:需要JWT认证 请求方式:POST 分页参数:page, page_size """ permission_classes = [IsAuthenticated] def post(self, request): try: # 获取当前管事用户ID yonghuid = request.user.UserUID # 获取分页参数 page = int(request.data.get('page', 1)) page_size = int(request.data.get('page_size', 50)) if page < 1: page = 1 if page_size < 1 or page_size > 100: # 限制最大100条 page_size = 50 # 计算偏移量 offset = (page - 1) * page_size # 查询条件:管事ID匹配 query = Q(guanshi=yonghuid) # 先查询总数(使用分页查询优化) # 查询总记录数 zong_tiaoshu = Gsfenhong.query.filter(query).count() # 查询去重后的打手数量 chongzhi_dashou_shuliang = Gsfenhong.query.filter(query).values('dashouid').distinct().count() # 查询当前页数据,按创建时间倒序排列 fenhong_list = Gsfenhong.query.filter(query) \ .order_by('-CreateTime') \ .values('dashouid', 'avatar', 'nicheng', 'fenhong', 'CreateTime')[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']), # 分红金额 'CreateTime': item['CreateTime'].strftime('%Y-%m-%d %H:%M:%S') if item['CreateTime'] else '' }) # 返回数据 return Response({ 'code': 200, 'msg': '成功', 'data': { 'list': formatted_list, 'total': zong_tiaoshu, # 总记录数 'chongzhi_dashou_shuliang': chongzhi_dashou_shuliang, # 充值打手数量(去重) 'page': page, 'page_size': page_size, 'has_more': len(formatted_list) == page_size # 是否还有更多数据 } }) except Exception as e: logger.error(f"查询管事分红记录失败: {str(e)}", exc_info=True) return Response({ 'code': 500, 'msg': f'服务器错误: {str(e)}', 'data': {} }) # views.py - 商品模块视图 class AdGetAllDataView(APIView): """ 获取商品类型、商品专区、会员数据的接口 接口地址:/shangpin/adhqlx 请求方法:POST 权限要求:JWT Token认证 + 管理员权限 """ # 需要JWT Token认证 permission_classes = [IsAuthenticated] def post(self, request): """ 处理POST请求,返回商品相关数据 """ try: # 1. 获取请求数据 zhanghao = request.data.get('zhanghao') if not zhanghao: logger.warning(f"管理员接口:未接收到账号参数,请求用户:{request.user.UserUID}") return Response({ 'code': 400, 'message': '缺少账号参数', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 2. 获取当前登录用户 current_user = request.user # 3. 验证用户类型是否为管理员 if current_user.UserType != 'admin': logger.warning( f"权限验证失败:用户 {current_user.UserUID} 的用户类型为 {current_user.UserType},非管理员") return Response({ 'code': 401, 'message': '无管理员权限', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) # 4. 验证手机号与传递的账号是否一致 if current_user.Phone != zhanghao: logger.warning( f"账号验证失败:用户 {current_user.UserUID} 的phone={current_user.Phone},传递的zhanghao={zhanghao}") return Response({ 'code': 401, 'message': '账号验证失败', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) # 5. 验证管理员扩展表是否存在 try: admin_profile = current_user.AdminProfile logger.info(f"管理员验证通过:用户 {current_user.UserUID},手机号 {current_user.Phone}") except AdminProfile.DoesNotExist: logger.error( f"管理员扩展表不存在:用户 {current_user.UserUID} 虽然user_type=admin,但没有AdminProfile记录") return Response({ 'code': 401, 'message': '管理员信息不完整', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) # 6. 查询商品类型数据 try: # 使用select_related或prefetch_related优化查询(如果需要关联查询) # 这里直接查询所有字段,然后提取需要的字段 shangpinleixing_queryset = ShangpinLeixing.query.all() # 转换为前端需要的格式 shangpinleixing_list = [] for leixing in shangpinleixing_queryset: leixing_data = { 'id': leixing.id, 'yaoqiuleixing': leixing.yaoqiuleixing if leixing.yaoqiuleixing is not None else None, 'huiyuan_id': leixing.huiyuan_id if leixing.huiyuan_id else None, 'yongjin': str(leixing.yongjin) if leixing.yongjin is not None else None, 'jieshao': leixing.jieshao if leixing.jieshao else '', 'tupian_url': leixing.tupian_url if leixing.tupian_url else '' } shangpinleixing_list.append(leixing_data) #logger.info(f"成功查询到 {len(shangpinleixing_list)} 条商品类型数据") except Exception as e: logger.error(f"查询商品类型数据时出错:{str(e)}") shangpinleixing_list = [] # 7. 查询商品专区数据 try: # 查询所有商品专区 zhuanqu_queryset = ShangpinZhuanqu.query.all() # 转换为前端需要的格式 zhuanqu_list = [] for zhuanqu in zhuanqu_queryset: zhuanqu_data = { 'id': zhuanqu.id, 'mingzi': zhuanqu.mingzi if zhuanqu.mingzi else '', 'leixing_id': zhuanqu.leixing_id } zhuanqu_list.append(zhuanqu_data) # logger.info(f"成功查询到 {len(zhuanqu_list)} 条商品专区数据") except Exception as e: logger.error(f"查询商品专区数据时出错:{str(e)}") zhuanqu_list = [] # 8. 查询会员数据 try: # 查询所有会员 huiyuan_queryset = Huiyuan.query.all() # 转换为前端需要的格式 huiyuan_list = [] for huiyuan in huiyuan_queryset: huiyuan_data = { 'huiyuan_id': huiyuan.huiyuan_id, 'jieshao': huiyuan.jieshao, 'jiage': str(huiyuan.jiage) # Decimal转为字符串 } huiyuan_list.append(huiyuan_data) # logger.info(f"成功查询到 {len(huiyuan_list)} 条会员数据") except Exception as e: logger.error(f"查询会员数据时出错:{str(e)}") huiyuan_list = [] # 9. 检查会员列表是否为空 if not huiyuan_list: logger.warning("会员列表为空,可能会影响商品发布功能") # 10. 组织返回数据 response_data = { 'code': 0, 'message': '获取数据成功', 'data': { 'shangpinleixingList': shangpinleixing_list, 'zhuanquList': zhuanqu_list, 'huiyuanList': huiyuan_list } } return Response(response_data, status=status.HTTP_200_OK) except Exception as e: # 捕获未预期的异常 logger.error(f"接口处理异常:{str(e)}", exc_info=True) return Response({ 'code': 500, 'message': f'服务器内部错误:{str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # views.py - shangpin模块 class ShangpinLeixingFabuView(APIView): """ 商品类型发布接口 接收图片、类型介绍、抢单要求类型等字段 需要管理员权限 """ permission_classes = [permissions.IsAuthenticated] parser_classes = (MultiPartParser, FormParser) # 需要处理文件上传 def post(self, request): try: # === 1. 权限验证 === # 获取当前用户 user_main = request.user # 1.1 验证前端传递的账号是否匹配(使用phone字段) zhanghao = request.data.get('zhanghao', '').strip() if not zhanghao: return Response( {'code': 401, 'msg': '未提供账号信息', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 验证账号是否匹配(使用phone字段比较) if user_main.Phone != zhanghao: return Response( {'code': 401, 'msg': '账号验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 1.2 验证是否为管理员(检查user_type字段) if user_main.UserType != 'admin': return Response( {'code': 401, 'msg': '权限不足', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 1.3 验证管理员扩展表(使用AdminProfile模型) try: admin_profile = AdminProfile.query.get(user=user_main) except AdminProfile.DoesNotExist: return Response( {'code': 401, 'msg': '管理员信息不存在', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # === 2. 接收并验证字段 === # 2.1 必填字段验证 jieshao = request.data.get('jieshao', '').strip() yaoqiuleixing = request.data.get('yaoqiuleixing') if not jieshao: return Response( {'code': 400, 'msg': '类型介绍不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) if yaoqiuleixing not in ['1', '2']: return Response( {'code': 400, 'msg': '抢单要求类型无效', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 2.2 验证图片 tupian_file = request.FILES.get('file') if not tupian_file: return Response( {'code': 400, 'msg': '请上传类型图片', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 验证图片格式和大小 is_valid, error_msg = validate_image(tupian_file) if not is_valid: return Response( {'code': 400, 'msg': f'图片验证失败: {error_msg}', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 2.3 根据抢单要求类型验证对应字段 huiyuan_id = request.data.get('huiyuan_id', '').strip() yongjin = request.data.get('yongjin', '').strip() # 转换yaoqiuleixing为整数便于处理 yaoqiuleixing_int = int(yaoqiuleixing) if yaoqiuleixing_int == 1: # 会员抢单 if not huiyuan_id: return Response( {'code': 400, 'msg': '会员抢单必须选择会员', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 验证会员ID是否存在 try: huiyuan_obj = Huiyuan.query.get(huiyuan_id=huiyuan_id) except Huiyuan.DoesNotExist: return Response( {'code': 400, 'msg': '会员ID不存在', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 会员ID为字符串,佣金为空 yongjin_value = None huiyuan_id_value = huiyuan_id else: # yaoqiuleixing_int == 2 佣金抢单 if not yongjin: return Response( {'code': 400, 'msg': '佣金抢单必须填写佣金金额', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) try: # 验证佣金为有效数字 yongjin_value = float(yongjin) if yongjin_value <= 0: return Response( {'code': 400, 'msg': '佣金金额必须大于0', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) except ValueError: return Response( {'code': 400, 'msg': '佣金金额格式错误', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) huiyuan_id_value = None # === 3. 创建商品类型记录 === with transaction.atomic(): # 3.1 创建商品类型对象 leixing_obj = ShangpinLeixing( jieshao=jieshao, yaoqiuleixing=yaoqiuleixing_int, huiyuan_id=huiyuan_id_value, yongjin=yongjin_value, # tupian_url 先留空,上传成功后再更新 ) # 保存到数据库获取ID leixing_obj.save() leixing_id = leixing_obj.id # === 4. 处理图片上传 === try: # 4.1 生成文件名和路径 file_extension = os.path.splitext(tupian_file.name)[1].lower() if not file_extension: file_extension = '.jpg' # 生成唯一文件名:商品类型ID + 随机UUID + 后缀 unique_filename = f"{leixing_id}_{uuid.uuid4().hex}{file_extension}" # 构建OSS文件路径:a_long/leixing/{leixing_id}/{filename} oss_folder = 'a_long' # 根文件夹 leixing_folder = 'leixing' # 类型文件夹 # 构建完整路径 oss_file_path = f"{oss_folder}/{leixing_folder}/{leixing_id}/{unique_filename}" # 4.2 上传到OSS tupian_url = upload_to_oss(tupian_file, oss_file_path) if not tupian_url: # 上传失败,抛出异常触发回滚 raise Exception("图片上传到OSS失败") # 4.3 更新商品类型的图片URL(存储相对路径) leixing_obj.tupian_url = oss_file_path # 存相对路径,方便拼接 leixing_obj.save() except Exception as oss_error: # 图片上传失败,事务会回滚,商品类型记录也会被删除 print(f"OSS上传失败: {oss_error}") # 手动抛出异常触发事务回滚 raise Exception(f"图片上传失败: {str(oss_error)}") # === 5. 构建返回数据 === response_data = { 'id': leixing_id, 'jieshao': jieshao, 'yaoqiuleixing': yaoqiuleixing_int, 'tupian_url': leixing_obj.tupian_url, # 相对路径 'huiyuan_id': huiyuan_id_value, 'yongjin': str(yongjin_value) if yongjin_value else None } # 返回成功响应 return Response({ 'code': 0, 'msg': '商品类型添加成功', 'data': response_data }) except Exception as e: # 捕获所有异常,返回500错误 traceback.print_exc() return Response({ 'code': 500, 'msg': f'系统错误: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class ShangpinZhuanquFabuView(APIView): """ 商品专区发布接口 接收专区名称、类型ID 需要管理员权限 """ permission_classes = [permissions.IsAuthenticated] def post(self, request): try: # === 1. 权限验证 === user_main = request.user # 1.1 验证账号(使用phone字段) zhanghao = request.data.get('zhanghao', '').strip() if not zhanghao: return Response( {'code': 401, 'msg': '未提供账号信息', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) if user_main.Phone != zhanghao: return Response( {'code': 401, 'msg': '账号验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 1.2 验证是否为管理员(检查user_type字段) if user_main.UserType != 'admin': return Response( {'code': 401, 'msg': '权限不足', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 1.3 验证管理员扩展表(使用AdminProfile模型) try: admin_profile = AdminProfile.query.get(user=user_main) except AdminProfile.DoesNotExist: return Response( {'code': 401, 'msg': '管理员信息不存在', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # === 2. 接收并验证字段 === mingzi = request.data.get('mingzi', '').strip() leixing_id = request.data.get('leixing_id') # 2.1 验证必填字段 if not mingzi: return Response( {'code': 400, 'msg': '专区名称不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) if not leixing_id: return Response( {'code': 400, 'msg': '类型ID不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 2.2 验证类型ID是否存在 try: # 验证商品类型是否存在 leixing_obj = ShangpinLeixing.query.get(id=leixing_id) except ShangpinLeixing.DoesNotExist: return Response( {'code': 400, 'msg': '商品类型不存在', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 2.3 验证名称长度(15汉字以内) if len(mingzi) > 45: # 数据库字段最大长度 return Response( {'code': 400, 'msg': '专区名称过长', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # === 3. 创建商品专区记录 === zhuanqu_obj = ShangpinZhuanqu( mingzi=mingzi, leixing_id=leixing_id ) zhuanqu_obj.save() # === 4. 构建返回数据 === response_data = { 'id': zhuanqu_obj.id, 'mingzi': mingzi, 'leixing_id': leixing_id } # 返回成功响应 return Response({ 'code': 0, 'msg': '商品专区添加成功', 'data': response_data }) except Exception as e: traceback.print_exc() return Response({ 'code': 500, 'msg': f'系统错误: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class ShangpinFabuView(APIView): """ 商品发布接口 接收商品信息、图片等字段 需要管理员权限 """ permission_classes = [permissions.IsAuthenticated] parser_classes = (MultiPartParser, FormParser) # 需要处理文件上传 def post(self, request): try: # === 1. 权限验证 === user_main = request.user # 1.1 验证账号(使用phone字段) zhanghao = request.data.get('zhanghao', '').strip() if not zhanghao: return Response( {'code': 401, 'msg': '未提供账号信息', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) if user_main.Phone != zhanghao: return Response( {'code': 401, 'msg': '账号验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 1.2 验证是否为管理员(检查user_type字段) if user_main.UserType != 'admin': return Response( {'code': 401, 'msg': '权限不足', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 1.3 验证管理员扩展表(使用AdminProfile模型) try: admin_profile = AdminProfile.query.get(user=user_main) except AdminProfile.DoesNotExist: return Response( {'code': 401, 'msg': '管理员信息不存在', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # === 2. 接收并验证字段 === # 2.1 验证必填字段 biaoti = request.data.get('biaoti', '').strip() jiage = request.data.get('jiage', '').strip() kucun = request.data.get('kucun', '').strip() leixing_id = request.data.get('leixing_id') zhuanqu_id = request.data.get('zhuanqu_id') yaoqiuleixing = request.data.get('yaoqiuleixing') jieshao = request.data.get('jieshao', '').strip() # 验证图片 shangpin_tupian = request.FILES.get('shangpin_tupian') if not shangpin_tupian: return Response( {'code': 400, 'msg': '请上传商品图片', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 验证图片格式和大小 is_valid, error_msg = validate_image(shangpin_tupian) if not is_valid: return Response( {'code': 400, 'msg': f'商品图片验证失败: {error_msg}', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 2.2 验证必填字段是否为空 required_fields = { 'biaoti': biaoti, 'jiage': jiage, 'kucun': kucun, 'leixing_id': leixing_id, 'zhuanqu_id': zhuanqu_id, 'yaoqiuleixing': yaoqiuleixing, 'jieshao': jieshao } for field_name, field_value in required_fields.items(): if not field_value: return Response( {'code': 400, 'msg': f'{field_name}不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 2.3 验证数值字段 try: jiage_value = float(jiage) if jiage_value <= 0: return Response( {'code': 400, 'msg': '商品价格必须大于0', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) except ValueError: return Response( {'code': 400, 'msg': '商品价格格式错误', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) try: kucun_value = int(kucun) if kucun_value < 0: return Response( {'code': 400, 'msg': '库存不能为负数', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) except ValueError: return Response( {'code': 400, 'msg': '库存必须为整数', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 验证抢单要求类型 if yaoqiuleixing not in ['1', '2']: return Response( {'code': 400, 'msg': '抢单要求类型无效', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # === 3. 验证关联数据 === # 3.1 验证商品类型 try: leixing_obj = ShangpinLeixing.query.get(id=leixing_id) except ShangpinLeixing.DoesNotExist: return Response( {'code': 400, 'msg': '商品类型不存在', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 3.2 根据抢单要求类型验证商品类型 yaoqiuleixing_int = int(yaoqiuleixing) if yaoqiuleixing_int == 1: # 会员抢单 # 需要验证商品类型的抢单要求类型是否为1 if leixing_obj.yaoqiuleixing != 1: return Response( {'code': 400, 'msg': '商品类型与抢单要求类型不匹配', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 获取会员ID(从前端或商品类型) huiyuan_id = request.data.get('huiyuan_id', leixing_obj.huiyuan_id) if not huiyuan_id: return Response( {'code': 400, 'msg': '会员ID不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 验证会员是否存在 try: huiyuan_obj = Huiyuan.query.get(huiyuan_id=huiyuan_id) except Huiyuan.DoesNotExist: return Response( {'code': 400, 'msg': '会员不存在', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 设置会员相关字段 huiyuan_id_value = huiyuan_id yongjin_value = None else: # 佣金抢单 # 需要验证商品类型的抢单要求类型是否为2 if leixing_obj.yaoqiuleixing != 2: return Response( {'code': 400, 'msg': '商品类型与抢单要求类型不匹配', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 获取佣金(从前端或商品类型) yongjin = request.data.get('yongjin', leixing_obj.yongjin) if not yongjin: return Response( {'code': 400, 'msg': '佣金金额不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) try: yongjin_value = float(yongjin) if yongjin_value <= 0: return Response( {'code': 400, 'msg': '佣金金额必须大于0', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) except ValueError: return Response( {'code': 400, 'msg': '佣金金额格式错误', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) huiyuan_id_value = None # 3.3 验证商品专区 try: zhuanqu_obj = ShangpinZhuanqu.query.get(id=zhuanqu_id, leixing_id=leixing_id) except ShangpinZhuanqu.DoesNotExist: return Response( {'code': 400, 'msg': '商品专区不存在或与商品类型不匹配', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # === 4. 创建商品记录 === with transaction.atomic(): # 4.1 获取其他字段 xuzhi = request.data.get('xuzhi', '').strip() xiaoliang = request.data.get('xiaoliang', '0').strip() # 转换销量为整数 try: duiwai_xiaoliang_value = int(xiaoliang) if xiaoliang else 0 except ValueError: duiwai_xiaoliang_value = 0 # 4.2 创建商品对象 shangpin_obj = Shangpin( biaoqian=biaoti, jiage=jiage_value, kucun=kucun_value, leixing_id=leixing_id, zhuanqu_id=zhuanqu_id, duiwai_xiaoliang=duiwai_xiaoliang_value, jieshao=jieshao, xiadan_xuzhi=xuzhi, yaoqiuleixing=yaoqiuleixing_int, huiyuan_id=huiyuan_id_value, yongjin=yongjin_value, # tupian_url 先留空,上传成功后再更新 ) # 保存到数据库获取ID shangpin_obj.save() shangpin_id = shangpin_obj.id # === 5. 处理商品图片上传 === try: # 5.1 生成文件名和路径 file_extension = os.path.splitext(shangpin_tupian.name)[1].lower() if not file_extension: file_extension = '.jpg' # 构建OSS文件路径:a_long/Spcc{shangpin_id}/{filename} oss_folder = 'a_long' # 根文件夹 spcc_folder = f"Spcc{shangpin_id}" # 商品图片文件夹 # 生成文件名:Spcc{shangpin_id}{file_extension} unique_filename = f"Spcc{shangpin_id}{file_extension}" # 构建完整路径 oss_file_path = f"{oss_folder}/{spcc_folder}/{unique_filename}" # 5.2 上传到OSS tupian_url = upload_to_oss(shangpin_tupian, oss_file_path) if not tupian_url: # 上传失败,抛出异常触发回滚 raise Exception("商品图片上传到OSS失败") # 5.3 更新商品的图片URL(存储相对路径) shangpin_obj.tupian_url = oss_file_path # 存相对路径 shangpin_obj.save() except Exception as oss_error: # 图片上传失败,事务会回滚,商品记录也会被删除 #print(f"商品图片OSS上传失败: {oss_error}") # 手动抛出异常触发事务回滚 raise Exception(f"商品图片上传失败: {str(oss_error)}") # === 6. 构建返回数据 === response_data = { 'shangpin_id': shangpin_id, 'biaoti': biaoti, 'jiage': str(jiage_value), 'kucun': kucun_value, 'leixing_id': leixing_id, 'zhuanqu_id': zhuanqu_id, 'xiaoliang': duiwai_xiaoliang_value, 'jieshao': jieshao, 'xuzhi': xuzhi, 'yaoqiuleixing': yaoqiuleixing_int, 'huiyuan_id': huiyuan_id_value, 'yongjin': str(yongjin_value) if yongjin_value else None, 'tupian_url': shangpin_obj.tupian_url # 相对路径 } # 返回成功响应 return Response({ 'code': 0, 'msg': '商品发布成功', 'data': response_data }) except Exception as e: # 捕获所有异常,返回500错误 traceback.print_exc() return Response({ 'code': 500, 'msg': f'系统错误: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class GuizeTupianShangchuanView(APIView): """ 规则图片上传接口 接收商品ID和规则图片 需要管理员权限 """ permission_classes = [permissions.IsAuthenticated] parser_classes = (MultiPartParser, FormParser) # 需要处理文件上传 def post(self, request): try: # === 1. 权限验证 === user_main = request.user # 1.1 验证账号(使用phone字段) zhanghao = request.data.get('zhanghao', '').strip() if not zhanghao: return Response( {'code': 401, 'msg': '未提供账号信息', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) if user_main.Phone != zhanghao: return Response( {'code': 401, 'msg': '账号验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 1.2 验证是否为管理员(检查user_type字段) if user_main.UserType != 'admin': return Response( {'code': 401, 'msg': '权限不足', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 1.3 验证管理员扩展表(使用AdminProfile模型) try: admin_profile = AdminProfile.query.get(user=user_main) except AdminProfile.DoesNotExist: return Response( {'code': 401, 'msg': '管理员信息不存在', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # === 2. 接收并验证字段 === # 2.1 验证商品ID shangpin_id = request.data.get('shangpin_id') if not shangpin_id: return Response( {'code': 400, 'msg': '商品ID不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 2.2 验证规则图片 guize_tupian = request.FILES.get('guize_tupian') if not guize_tupian: return Response( {'code': 400, 'msg': '请上传规则图片', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 验证图片格式和大小 is_valid, error_msg = validate_image(guize_tupian) if not is_valid: return Response( {'code': 400, 'msg': f'规则图片验证失败: {error_msg}', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # === 3. 验证商品是否存在 === try: shangpin_obj = Shangpin.query.get(id=shangpin_id) except Shangpin.DoesNotExist: return Response( {'code': 400, 'msg': '商品不存在', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # === 4. 处理规则图片上传 === try: # 4.1 生成文件名和路径 file_extension = os.path.splitext(guize_tupian.name)[1].lower() if not file_extension: file_extension = '.jpg' # 构建OSS文件路径:a_long/Spgz{shangpin_id}/{filename} oss_folder = 'a_long' # 根文件夹 spgz_folder = f"Spgz{shangpin_id}" # 规则图片文件夹 # 生成文件名:Spgz{shangpin_id}{file_extension} unique_filename = f"Spgz{shangpin_id}{file_extension}" # 构建完整路径 oss_file_path = f"{oss_folder}/{spgz_folder}/{unique_filename}" # 4.2 上传到OSS tupian_url = upload_to_oss(guize_tupian, oss_file_path) if not tupian_url: # 上传失败 return Response({ 'code': 500, 'msg': '规则图片上传失败', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 4.3 更新商品的规则图片URL(存储相对路径) shangpin_obj.guize_tupian = oss_file_path # 存相对路径 shangpin_obj.save() except Exception as oss_error: # OSS上传失败 print(f"规则图片OSS上传失败: {oss_error}") return Response({ 'code': 500, 'msg': f'规则图片上传失败: {str(oss_error)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # === 5. 构建返回数据 === response_data = { 'shangpin_id': shangpin_id, 'guize_tupian_url': oss_file_path # 相对路径 } # 返回成功响应 return Response({ 'code': 0, 'msg': '规则图片上传成功', 'data': response_data }) except Exception as e: # 捕获所有异常,返回500错误 traceback.print_exc() return Response({ 'code': 500, 'msg': f'系统错误: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class AdHuiyuanHqView(APIView): """ 管理员获取会员信息和商品类型接口 需要管理员权限 """ permission_classes = [permissions.IsAuthenticated] def post(self, request): try: # === 1. 权限验证 === # 获取当前用户 user_main = request.user # 1.1 验证前端传递的账号是否匹配(使用phone字段) zhanghao = request.data.get('zhanghao', '').strip() if not zhanghao: return Response( {'code': 401, 'msg': '权限验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 验证账号是否匹配(使用phone字段比较) if user_main.Phone != zhanghao: return Response( {'code': 401, 'msg': '权限验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 1.2 验证是否为管理员(检查user_type字段) if user_main.UserType != 'admin': return Response( {'code': 401, 'msg': '权限验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 1.3 验证管理员扩展表(使用AdminProfile模型) try: admin_profile = AdminProfile.query.get(user=user_main) except AdminProfile.DoesNotExist: return Response( {'code': 401, 'msg': '权限验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # === 2. 查询会员表数据 === # 查询所有会员数据,按创建时间倒序排列 huiyuan_queryset = Huiyuan.query.all().order_by('-CreateTime') # 构建会员数据列表 huiyuan_list = [] for huiyuan in huiyuan_queryset: huiyuan_data = { 'huiyuan_id': huiyuan.huiyuan_id, # 会员ID 'jieshao': huiyuan.jieshao, # 会员介绍(会员标题) 'jtjieshao': huiyuan.jtjieshao if huiyuan.jtjieshao else '', # 会员规则介绍(具体介绍) 'jiage': str(huiyuan.jiage), # 会员价格(转换为字符串,避免Decimal序列化问题) 'guanshifc': str(huiyuan.guanshifc), # 管事分成(转换为字符串) 'goumai_cishu': huiyuan.goumai_cishu, # 购买次数(销量) 'CreateTime': huiyuan.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if huiyuan.CreateTime else None, # 创建时间 'UpdateTime': huiyuan.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if huiyuan.UpdateTime else None # 更新时间 } huiyuan_list.append(huiyuan_data) # === 3. 查询商品类型表数据 === # 筛选抢单要求类型等于数字1的所有数据(yaoqiuleixing=1) shangpinleixing_queryset = ShangpinLeixing.query.filter( yaoqiuleixing=1 # 抢单要求类型为1(会员抢单) ).order_by('id') # 按ID排序 # 构建商品类型数据列表 shangpinleixing_list = [] for leixing in shangpinleixing_queryset: # 注意:这里只需要特定的字段 leixing_data = { 'id': leixing.id, # 商品类型ID 'huiyuan_id': leixing.huiyuan_id, # 会员ID(关联字段) 'jieshao': leixing.jieshao, # 类型介绍(商品类型名称) 'tupian_url': leixing.tupian_url if leixing.tupian_url else '' # 商品类型图片相对URL } shangpinleixing_list.append(leixing_data) # === 4. 构建返回数据 === response_data = { 'huiyuanList': huiyuan_list, 'shangpinleixingList': shangpinleixing_list } # 返回成功响应 return Response({ 'code': 0, 'msg': '获取数据成功', 'data': response_data }) except Exception as e: # 捕获所有异常,返回500错误 traceback.print_exc() return Response({ 'code': 500, 'msg': f'系统错误: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class HuiyuanXiugaiView(APIView): """ 会员修改接口 需要管理员权限 """ permission_classes = [permissions.IsAuthenticated] def post(self, request): try: # === 1. 权限验证 === user_main = request.user # 1.1 验证前端传递的账号是否匹配(使用phone字段) zhanghao = request.data.get('zhanghao', '').strip() if not zhanghao: return Response( {'code': 401, 'msg': '权限验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) if user_main.Phone != zhanghao: return Response( {'code': 401, 'msg': '权限验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 1.2 验证是否为管理员(检查user_type字段) if user_main.UserType != 'admin': return Response( {'code': 401, 'msg': '权限验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 1.3 验证管理员扩展表 try: admin_profile = AdminProfile.query.get(user=user_main) except AdminProfile.DoesNotExist: return Response( {'code': 401, 'msg': '权限验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # === 2. 接收并验证必填字段 === huiyuan_id = request.data.get('huiyuan_id', '').strip() if not huiyuan_id: return Response( {'code': 400, 'msg': '会员ID不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # === 3. 查询会员数据 === try: huiyuan_obj = Huiyuan.query.get(huiyuan_id=huiyuan_id) except Huiyuan.DoesNotExist: return Response( {'code': 400, 'msg': '会员不存在', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # === 4. 更新会员信息 === # 4.1 获取前端传递的字段(只更新有值的字段) jieshao = request.data.get('jieshao', '').strip() jtjieshao = request.data.get('jtjieshao', '').strip() jiage = request.data.get('jiage', '').strip() guanshifc = request.data.get('guanshifc', '').strip() # 4.2 更新字段 if jieshao: # 验证名称长度(30个字符以内) if len(jieshao) > 30: return Response( {'code': 400, 'msg': '会员名称不能超过30个字符', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) huiyuan_obj.jieshao = jieshao if jtjieshao: # 验证介绍长度(没有长度限制,但做一下基本检查) if len(jtjieshao) > 2000: return Response( {'code': 400, 'msg': '会员介绍过长', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) huiyuan_obj.jtjieshao = jtjieshao if jiage: try: jiage_value = float(jiage) if jiage_value < 0: return Response( {'code': 400, 'msg': '会员价格不能为负数', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) if jiage_value > 100000: return Response( {'code': 400, 'msg': '会员价格不能超过10万元', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) huiyuan_obj.jiage = jiage_value except ValueError: return Response( {'code': 400, 'msg': '会员价格格式错误', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) if guanshifc: try: guanshifc_value = float(guanshifc) if guanshifc_value < 0: return Response( {'code': 400, 'msg': '管事分成不能为负数', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 检查管事分成是否超过会员价格 if hasattr(huiyuan_obj, 'jiage') and huiyuan_obj.jiage: if guanshifc_value > float(huiyuan_obj.jiage): return Response( {'code': 400, 'msg': '管事分成不能超过会员价格', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) huiyuan_obj.guanshifc = guanshifc_value except ValueError: return Response( {'code': 400, 'msg': '管事分成格式错误', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 4.3 检查是否有任何字段需要更新 if not (jieshao or jtjieshao or jiage or guanshifc): return Response( {'code': 400, 'msg': '没有需要更新的字段', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 4.4 保存更新 huiyuan_obj.save() # === 5. 构建返回数据 === response_data = { 'huiyuan_id': huiyuan_obj.huiyuan_id, 'jieshao': huiyuan_obj.jieshao, 'jtjieshao': huiyuan_obj.jtjieshao if huiyuan_obj.jtjieshao else '', 'jiage': str(huiyuan_obj.jiage), 'guanshifc': str(huiyuan_obj.guanshifc), 'goumai_cishu': huiyuan_obj.goumai_cishu, 'CreateTime': huiyuan_obj.CreateTime.strftime( '%Y-%m-%d %H:%M:%S') if huiyuan_obj.CreateTime else None, 'UpdateTime': huiyuan_obj.UpdateTime.strftime( '%Y-%m-%d %H:%M:%S') if huiyuan_obj.UpdateTime else None } # 返回成功响应 return Response({ 'code': 0, 'msg': '会员信息修改成功', 'data': response_data }) except Exception as e: # 捕获所有异常,返回500错误 traceback.print_exc() return Response({ 'code': 500, 'msg': f'系统错误: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class AdHuiyuanFbView(APIView): """ 会员发布接口 需要管理员权限 """ permission_classes = [permissions.IsAuthenticated] def post(self, request): try: # === 1. 权限验证 === user_main = request.user # 1.1 验证前端传递的账号是否匹配 zhanghao = request.data.get('zhanghao', '').strip() if not zhanghao: return Response( {'code': 401, 'msg': '权限验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) if user_main.Phone != zhanghao: return Response( {'code': 401, 'msg': '权限验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 1.2 验证是否为管理员 if user_main.UserType != 'admin': return Response( {'code': 401, 'msg': '权限验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 1.3 验证管理员扩展表 try: admin_profile = AdminProfile.query.get(user=user_main) except AdminProfile.DoesNotExist: return Response( {'code': 401, 'msg': '权限验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # === 2. 接收并验证字段 === jieshao = request.data.get('jieshao', '').strip() jtjieshao = request.data.get('jtjieshao', '').strip() jiage = request.data.get('jiage', '').strip() guanshifc = request.data.get('guanshifc', '').strip() # 2.1 验证必填字段 if not jieshao: return Response( {'code': 400, 'msg': '会员名称不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) if not jiage: return Response( {'code': 400, 'msg': '会员价格不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) if not guanshifc: return Response( {'code': 400, 'msg': '管事分成不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 2.2 验证字段长度和格式 if len(jieshao) > 30: return Response( {'code': 400, 'msg': '会员名称不能超过30个字符', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) if jtjieshao and len(jtjieshao) > 2000: return Response( {'code': 400, 'msg': '会员介绍过长', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 2.3 验证价格和分成格式 try: jiage_value = float(jiage) if jiage_value <= 0: return Response( {'code': 400, 'msg': '会员价格必须大于0', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) if jiage_value > 100000: return Response( {'code': 400, 'msg': '会员价格不能超过10万元', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) except ValueError: return Response( {'code': 400, 'msg': '会员价格格式错误', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) try: guanshifc_value = float(guanshifc) if guanshifc_value < 0: return Response( {'code': 400, 'msg': '管事分成不能为负数', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) if guanshifc_value > jiage_value: return Response( {'code': 400, 'msg': '管事分成不能超过会员价格', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) except ValueError: return Response( {'code': 400, 'msg': '管事分成格式错误', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # === 3. 生成会员ID === # 生成6位唯一会员ID,格式:V + 5位随机数字字母 def generate_huiyuan_id(): # 使用V开头,后跟5位随机字符(数字和大写字母) chars = string.digits + string.ascii_uppercase while True: random_part = ''.join(random.choices(chars, k=5)) huiyuan_id = f"V{random_part}" # 检查是否已存在 if not Huiyuan.query.filter(huiyuan_id=huiyuan_id).exists(): return huiyuan_id huiyuan_id = generate_huiyuan_id() # === 4. 创建会员记录 === with transaction.atomic(): huiyuan_obj = Huiyuan( huiyuan_id=huiyuan_id, jieshao=jieshao, jtjieshao=jtjieshao if jtjieshao else '', jiage=jiage_value, guanshifc=guanshifc_value, goumai_cishu=0, # 初始购买次数为0 ) huiyuan_obj.save() # === 5. 构建返回数据 === response_data = { 'huiyuan_id': huiyuan_obj.huiyuan_id, 'jieshao': huiyuan_obj.jieshao, 'jtjieshao': huiyuan_obj.jtjieshao, 'jiage': str(huiyuan_obj.jiage), 'guanshifc': str(huiyuan_obj.guanshifc), 'goumai_cishu': huiyuan_obj.goumai_cishu, 'CreateTime': huiyuan_obj.CreateTime.strftime( '%Y-%m-%d %H:%M:%S') if huiyuan_obj.CreateTime else None, 'UpdateTime': huiyuan_obj.UpdateTime.strftime( '%Y-%m-%d %H:%M:%S') if huiyuan_obj.UpdateTime else None } # 返回成功响应 return Response({ 'code': 0, 'msg': '会员发布成功', 'data': response_data }) except Exception as e: # 捕获所有异常,返回500错误 traceback.print_exc() return Response({ 'code': 500, 'msg': f'系统错误: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class AdShangpinHuQu(APIView): """ 后台商品管理页面数据获取接口 路径: /shangpin/adsphq/ 方法: POST 权限: JWT认证 + 管理员权限 功能: 获取商品管理页面需要的所有数据 """ permission_classes = [IsAuthenticated] def post(self, request): try: # 1. 获取前端传递的账号 qianzhanghao = request.data.get('zhanghao', '').strip() if not qianzhanghao: return Response( {'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED ) # 2. 从JWT Token获取当前用户 dangqianyonghu = request.user # 3. 验证账号匹配 if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao: return Response( {'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED ) # 4. 验证用户类型为管理员 if dangqianyonghu.UserType != 'admin': return Response( {'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED ) # 5. 验证管理员扩展表数据 try: guanliyuan = dangqianyonghu.AdminProfile except AdminProfile.DoesNotExist: return Response( {'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED ) # 6. 查询公告与轮播(按俱乐部,默认抢单池) from jituan.services.display_config import get_gonggao_content, get_lunbo_urls shangpingonggao = get_gonggao_content(request, notice_type=1) shangpinlunbo = get_lunbo_urls(request, page_key='order_pool', image_type=1) # 8. 查询商品类型数据 shangpinleixing_data = ShangpinLeixing.query.all().values( 'id', # 类型ID 'jieshao', # 类型介绍(类型名称) 'tupian_url', # 类型图片URL 'yaoqiuleixing', # 抢单要求类型(1=会员抢单,2=佣金抢单) 'huiyuan_id', # 会员ID(如果yaoqiuleixing=1) 'yongjin' # 佣金要求(如果yaoqiuleixing=2) ) # 9. 查询商品专区数据 shangpinzhuanqu_data = ShangpinZhuanqu.query.all().values( 'id', # 专区ID 'mingzi', # 专区名称 'leixing_id' # 所属类型ID ) # 10. 查询会员数据 huiyuan_data = Huiyuan.query.all().values( 'huiyuan_id', # 会员ID 'jieshao', # 会员介绍(会员名称) 'jiage' # 会员价格 ) # 11. 查询商品数据 shangpin_data = Shangpin.query.all().values( 'id', # 商品ID 'biaoqian', # 商品标题/标签 'jiage', # 商品价格 'leixing_id', # 商品类型ID 'zhuanqu_id', # 商品专区ID 'tupian_url', # 商品图片URL 'duiwai_xiaoliang' # 商品对外销量 ) # 12. 构造返回数据 response_data = { 'code': 0, 'message': '获取成功', 'data': { 'shangpingonggao': shangpingonggao, # 商品公告 'shangpinlunbo': shangpinlunbo, # 轮播图相对URL数组 'shangpinleixing': list(shangpinleixing_data), # 商品类型列表 'shangpinzhuanqu': list(shangpinzhuanqu_data), # 商品专区列表 'huiyuanliebiao': list(huiyuan_data), # 会员列表 'shangpinliebiao': list(shangpin_data), # 商品列表 } } return Response(response_data, status=status.HTTP_200_OK) except Exception as e: print(f"获取商品管理数据异常: {e}") return Response( {'code': 500, 'message': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) # shangpin/views.py class AdGonggaoXiuGai(APIView): """ 后台编辑公告接口 路径: /shangpin/adggxg/ 方法: POST 权限: JWT认证 + 管理员权限 功能: 修改公告内容 """ permission_classes = [IsAuthenticated] def post(self, request): try: # 1. 获取前端传递的账号 qianzhanghao = request.data.get('zhanghao', '').strip() if not qianzhanghao: return Response( {'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED ) # 2. 从JWT Token获取当前用户 dangqianyonghu = request.user # 3. 验证账号匹配 if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao: return Response( {'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED ) # 4. 验证用户类型为管理员 if dangqianyonghu.UserType != 'admin': return Response( {'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED ) # 5. 验证管理员扩展表数据 try: guanliyuan = dangqianyonghu.AdminProfile except AdminProfile.DoesNotExist: return Response( {'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED ) # 6. 获取公告内容 gonggao_neirong = request.data.get('gonggao', '').strip() # 7. 验证公告内容 if not gonggao_neirong: return Response( {'code': 400, 'message': '公告内容不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) if len(gonggao_neirong) > 300: return Response( {'code': 400, 'message': '公告内容不能超过300字', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 8. 查询公告表,类型为1的公告 gonggao_obj = Gonggao.query.filter(NoticeType=1).first() # 如果不存在,则创建 if not gonggao_obj: gonggao_obj = Gonggao(NoticeType=1) # 9. 更新公告内容 gonggao_obj.Content = gonggao_neirong gonggao_obj.save() # 10. 返回成功响应 return Response({ 'code': 0, 'message': '公告更新成功', 'data': { 'gonggao': gonggao_neirong } }) except Exception as e: print(f"编辑公告异常: {e}") return Response( {'code': 500, 'message': '服务器内部错误', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) class AdZhuanquXiuGai(APIView): """ 后台修改专区名称接口 路径: /shangpin/adzqxg/ 方法: POST 权限: JWT认证 + 管理员权限 功能: 修改专区名称 """ permission_classes = [IsAuthenticated] def post(self, request): try: # 1. 权限验证 qianzhanghao = request.data.get('zhanghao', '').strip() if not qianzhanghao: return Response( {'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED ) dangqianyonghu = request.user if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao: return Response( {'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED ) if dangqianyonghu.UserType != 'admin': return Response( {'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED ) try: guanliyuan = dangqianyonghu.AdminProfile except AdminProfile.DoesNotExist: return Response( {'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED ) # 2. 获取专区ID和新的专区名称 zhuanqu_id = request.data.get('zhuanqu_id') mingzi = request.data.get('mingzi', '').strip() if not zhuanqu_id: return Response( {'code': 400, 'message': '专区ID不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) if not mingzi: return Response( {'code': 400, 'message': '专区名称不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) if len(mingzi) > 10: return Response( {'code': 400, 'message': '专区名称不能大于10个字', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 3. 查询专区 try: zhuanqu_obj = ShangpinZhuanqu.query.get(id=zhuanqu_id) except ShangpinZhuanqu.DoesNotExist: return Response( {'code': 400, 'message': '专区不存在', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 4. 更新专区名称 zhuanqu_obj.mingzi = mingzi zhuanqu_obj.save() # 5. 返回成功响应 return Response({ 'code': 0, 'message': '专区名称修改成功', 'data': { 'id': zhuanqu_obj.id, 'mingzi': zhuanqu_obj.mingzi, 'leixing_id': zhuanqu_obj.leixing_id } }) except Exception as e: print(f"修改专区名称异常: {e}") return Response( {'code': 500, 'message': '服务器内部错误', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) class AdLunboXiuGai(APIView): """ 后台编辑轮播图接口 路径: /shangpin/adlunboxg/ 方法: POST 权限: JWT认证 + 管理员权限 功能: 添加或删除轮播图 """ permission_classes = [IsAuthenticated] parser_classes = (MultiPartParser, FormParser,JSONParser) def post(self, request): try: # 1. 权限验证 qianzhanghao = request.data.get('zhanghao', '').strip() if not qianzhanghao: return Response( {'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED ) dangqianyonghu = request.user if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao: return Response( {'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED ) if dangqianyonghu.UserType != 'admin': return Response( {'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED ) try: guanliyuan = dangqianyonghu.AdminProfile except AdminProfile.DoesNotExist: return Response( {'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED ) # 2. 获取操作类型 action = request.data.get('action', '').strip() # 'add' 或 'delete' if action not in ['add', 'delete']: return Response( {'code': 400, 'message': '操作类型无效', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 3. 处理删除操作 if action == 'delete': tupian_url = request.data.get('tupian_url', '').strip() if not tupian_url: return Response( {'code': 400, 'message': '图片URL不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 查询轮播图记录 lunbo_obj = Lunbo.query.filter(ImageURL=tupian_url, ImageType=1).first() if not lunbo_obj: return Response( {'code': 400, 'message': '轮播图不存在', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 从OSS删除图片 delete_success = delete_from_oss(tupian_url) if not delete_success: return Response( {'code': 500, 'message': 'OSS删除失败', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) # 从数据库删除记录 lunbo_obj.delete() return Response({ 'code': 0, 'message': '删除成功', 'data': None }) # 4. 处理添加操作 else: # action == 'add' tupian_file = request.FILES.get('file') if not tupian_file: return Response( {'code': 400, 'message': '请上传图片', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 验证图片 is_valid, error_msg = validate_image(tupian_file) if not is_valid: #print(f"[轮播图上传] 图片验证失败详情: {error_msg}") # 还可以记录文件大小、名称等信息 #print(f"[轮播图上传] 问题文件: {tupian_file.name}, Size: {tupian_file.size}") #print(is_valid) return Response( {'code': 400, 'message': f'图片验证失败: {error_msg}', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 生成文件名和路径 file_extension = os.path.splitext(tupian_file.name)[1].lower() if not file_extension: file_extension = '.jpg' # 生成6位随机字符串作为文件名 random_str = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6)) unique_filename = f"{random_str}{file_extension}" # 构建OSS文件路径:a_long/lunbo/{filename} oss_folder = 'a_long' lunbo_folder = 'lunbo' oss_file_path = f"{oss_folder}/{lunbo_folder}/{unique_filename}" # 上传到OSS tupian_url = upload_to_oss(tupian_file, oss_file_path) if not tupian_url: return Response( {'code': 500, 'message': '图片上传失败', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) # 创建轮播图记录 lunbo_obj = Lunbo.query.create( ImageURL=oss_file_path, # 存相对路径 ImageType=1 ) return Response({ 'code': 0, 'message': '添加成功', 'data': { 'tupian_url': lunbo_obj.ImageURL } }) except Exception as e: #print(f"编辑轮播图异常: {e}") return Response( {'code': 500, 'message': '服务器内部错误', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) class AdLeixingXiuGai(APIView): """ 后台编辑商品类型接口 - 优化版 支持:只改图片、只改字段、两者都改 拒绝:两者都不改 """ permission_classes = [IsAuthenticated] parser_classes = (MultiPartParser, FormParser, JSONParser) # 添加JSONParser支持 def post(self, request): try: # 1. 权限验证(保持不变) qianzhanghao = request.data.get('zhanghao', '').strip() if not qianzhanghao: return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) dangqianyonghu = request.user if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao: return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) if dangqianyonghu.UserType != 'admin': return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) try: guanliyuan = dangqianyonghu.AdminProfile except AdminProfile.DoesNotExist: return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) # 2. 获取商品类型ID leixing_id = request.data.get('leixing_id') if not leixing_id: return Response({'code': 400, 'message': '商品类型ID不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST) # 3. 查询商品类型 try: leixing_obj = ShangpinLeixing.query.get(id=leixing_id) except ShangpinLeixing.DoesNotExist: return Response({'code': 400, 'message': '商品类型不存在', 'data': None}, status=status.HTTP_400_BAD_REQUEST) # 4. 获取字段并初始化变更标记 has_changed = False jieshao = request.data.get('jieshao', '').strip() yaoqiuleixing = request.data.get('yaoqiuleixing') huiyuan_id = request.data.get('huiyuan_id', '').strip() yongjin = request.data.get('yongjin', '').strip() tupian_url = request.data.get('tupian_url', '').strip() # 原有图片URL # 5. 检查非图片字段是否有变更 if jieshao and jieshao != leixing_obj.jieshao: leixing_obj.jieshao = jieshao has_changed = True if yaoqiuleixing and int(yaoqiuleixing) != leixing_obj.yaoqiuleixing: leixing_obj.yaoqiuleixing = int(yaoqiuleixing) has_changed = True # 根据类型清空另一个字段 if leixing_obj.yaoqiuleixing == 1: leixing_obj.yongjin = None if huiyuan_id: leixing_obj.huiyuan_id = huiyuan_id # 验证会员存在 try: Huiyuan.query.get(huiyuan_id=huiyuan_id) except Huiyuan.DoesNotExist: return Response({'code': 400, 'msg': '会员ID不存在', 'data': None}, status=status.HTTP_400_BAD_REQUEST) else: # 类型2 leixing_obj.huiyuan_id = None if yongjin: try: leixing_obj.yongjin = float(yongjin) if leixing_obj.yongjin <= 0: return Response({'code': 400, 'message': '佣金金额必须大于0', 'data': None}, status=status.HTTP_400_BAD_REQUEST) except ValueError: return Response({'code': 400, 'message': '佣金金额格式错误', 'data': None}, status=status.HTTP_400_BAD_REQUEST) # ===== 在现有代码的第5步后面添加 ===== # 5.5 检查抢单要求类型不变时,会员ID或佣金是否有变更 if yaoqiuleixing and int(yaoqiuleixing) == leixing_obj.yaoqiuleixing: # 类型没有变化,但需要检查对应字段的变化 # 情况1:会员抢单 (yaoqiuleixing == 1) if leixing_obj.yaoqiuleixing == 1: # 检查会员ID是否变更 if huiyuan_id and huiyuan_id != leixing_obj.huiyuan_id: # 验证会员存在 try: Huiyuan.query.get(huiyuan_id=huiyuan_id) except Huiyuan.DoesNotExist: return Response({'code': 400, 'msg': '会员ID不存在', 'data': None}, status=status.HTTP_400_BAD_REQUEST) leixing_obj.huiyuan_id = huiyuan_id has_changed = True # print(f"会员ID变更: {leixing_obj.huiyuan_id} -> {huiyuan_id}") # 情况2:佣金抢单 (yaoqiuleixing == 2) elif leixing_obj.yaoqiuleixing == 2: # 检查佣金是否变更 if yongjin: try: new_yongjin = float(yongjin) if new_yongjin <= 0: return Response({'code': 400, 'message': '佣金金额必须大于0', 'data': None}, status=status.HTTP_400_BAD_REQUEST) # 判断是否有变化(考虑None的情况) current_yongjin = leixing_obj.yongjin if (current_yongjin is None and new_yongjin != 0) or \ (current_yongjin is not None and abs( float(current_yongjin) - new_yongjin) > 0.001): leixing_obj.yongjin = new_yongjin has_changed = True # print(f"佣金变更: {current_yongjin} -> {new_yongjin}") except ValueError: return Response({'code': 400, 'message': '佣金金额格式错误', 'data': None}, status=status.HTTP_400_BAD_REQUEST) # ===== 添加结束 ===== # 6. 处理图片上传(如果有新图片) tupian_file = request.FILES.get('file') if tupian_file: # 验证图片 is_valid, error_msg = validate_image(tupian_file) if not is_valid: return Response({'code': 400, 'message': f'图片验证失败: {error_msg}', 'data': None}, status=status.HTTP_400_BAD_REQUEST) # 删除旧图片(如果存在) if leixing_obj.tupian_url: delete_from_oss(leixing_obj.tupian_url) # 生成文件名和路径 file_extension = os.path.splitext(tupian_file.name)[1].lower() or '.jpg' oss_folder = 'a_long' leixing_folder = 'leixing' # 使用原文件名或生成新文件名 if leixing_obj.tupian_url: original_filename = leixing_obj.tupian_url.split('/')[-1] else: original_filename = f"{leixing_id}_{uuid.uuid4().hex}{file_extension}" oss_file_path = f"{oss_folder}/{leixing_folder}/{leixing_id}/{original_filename}" # 上传到OSS new_tupian_url = upload_to_oss(tupian_file, oss_file_path) if not new_tupian_url: return Response({'code': 500, 'message': '图片上传失败', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) leixing_obj.tupian_url = oss_file_path # 存相对路径 has_changed = True elif tupian_url and tupian_url != leixing_obj.tupian_url: # 前端传了新URL(可能是从其他地方选择的) leixing_obj.tupian_url = tupian_url has_changed = True # 7. 验证是否至少有变更 if not has_changed: return Response({ 'code': 400, 'message': '没有检测到任何更改', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 8. 保存并返回 leixing_obj.save() # 8. 保存商品类型 leixing_obj.save() # 🔴 ===========【新增:同步更新关联商品】=========== try: # 1. 准备商品更新字典 shangpin_update_fields = {} # 始终同步抢单要求类型 shangpin_update_fields['yaoqiuleixing'] = leixing_obj.yaoqiuleixing # 根据抢单类型,同步对应字段并清空另一个 if leixing_obj.yaoqiuleixing == 1: # 会员抢单 if leixing_obj.huiyuan_id: # 仅当会员ID非空时更新 shangpin_update_fields['huiyuan_id'] = leixing_obj.huiyuan_id # 清空佣金字段 shangpin_update_fields['yongjin'] = None elif leixing_obj.yaoqiuleixing == 2: # 佣金抢单 if leixing_obj.yongjin: # 仅当佣金非空时更新 shangpin_update_fields['yongjin'] = leixing_obj.yongjin # 清空会员字段 shangpin_update_fields['huiyuan_id'] = None # 2. 执行批量更新(确保只有实际有变化的字段才会执行) if shangpin_update_fields: # 查询所有属于该类型的商品(使用filter而非all以提高效率) affected_count = Shangpin.query.filter( leixing_id=leixing_obj.id ).update(**shangpin_update_fields) # 可选:记录日志以便追踪 # logger.info(f"商品类型{leixing_obj.id}修改成功,已同步更新{affected_count}个关联商品") except Exception as sync_error: # 商品同步失败不影响商品类型本身的修改,但记录错误 # 可根据需求决定是否抛出异常回滚整个事务 # 当前设计:记录错误但允许商品类型修改成功 logger.error(f"修改十五") # 可在此添加更详细的错误日志记录 # 🔴 ===========【新增结束】=========== response_data = { 'id': leixing_obj.id, 'jieshao': leixing_obj.jieshao, 'tupian_url': leixing_obj.tupian_url, 'yaoqiuleixing': leixing_obj.yaoqiuleixing, 'huiyuan_id': leixing_obj.huiyuan_id, 'yongjin': str(leixing_obj.yongjin) if leixing_obj.yongjin else None } return Response({ 'code': 0, 'message': '商品类型修改成功', 'data': response_data }) except Exception as e: print(f"编辑商品类型异常: {e}") return Response({'code': 500, 'message': '服务器内部错误', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) logger = logging.getLogger(__name__) class AdShangpinNeiRongHuoQu(APIView): """ 后台获取商品详情接口 路径: /shangpin/adspnrhq/ 方法: POST 权限: JWT认证 + 管理员权限 功能: 获取指定商品的详细信息 """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): try: # 1. 获取前端数据 qianzhanghao = request.data.get('zhanghao', '').strip() shangpin_id = request.data.get('shangpin_id') if not qianzhanghao or not shangpin_id: return Response( {'code': 400, 'message': '参数不完整', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 2. 权限验证 dangqianyonghu = request.user # 验证phone字段匹配 if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao: logger.warning( f"账号不匹配: 当前用户phone={getattr(dangqianyonghu, 'Phone', '无')}, 前端账号={qianzhanghao}") return Response( {'code': 401, 'message': '身份验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 验证用户类型 if dangqianyonghu.UserType != 'admin': logger.warning(f"非管理员访问: user_type={dangqianyonghu.UserType}") return Response( {'code': 401, 'message': '身份验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 3. 验证管理员扩展表 try: guanliyuan = dangqianyonghu.AdminProfile except AdminProfile.DoesNotExist: logger.warning(f"管理员扩展表不存在: user_id={dangqianyonghu.id}") return Response( {'code': 401, 'message': '身份验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 4. 查询商品数据 try: shangpin_obj = Shangpin.query.get(id=shangpin_id) except Shangpin.DoesNotExist: return Response( {'code': 404, 'message': '商品不存在', 'data': None}, status=status.HTTP_404_NOT_FOUND ) # 5. 构建返回数据(与前端字段对应) response_data = { 'tupian_url': shangpin_obj.tupian_url or '', # 商品图片相对URL 'biaoti': shangpin_obj.biaoqian or '', # 商品标题(前端用biaoti,对应biaoqian) 'jieshao': shangpin_obj.jieshao or '', # 商品介绍 'jiage': str(shangpin_obj.jiage) if shangpin_obj.jiage else '0.00', # 商品价格 'kucun': shangpin_obj.kucun, # 商品库存 'zhenshixiaoliang': shangpin_obj.zhenshi_xiaoliang, # 商品真实销量 'duiwaixiaoliang': shangpin_obj.duiwai_xiaoliang, # 商品对外销量 'xiadanxuzhi': shangpin_obj.xiadan_xuzhi or '', # 下单须知 'guizetupian_url': shangpin_obj.guize_tupian or '', # 规则图片相对URL 'yajin': str(shangpin_obj.yajin) if hasattr(shangpin_obj, 'yajin') and shangpin_obj.yajin else None, # 押金(可能不存在) 'chuangjianshijian': shangpin_obj.CreateTime.strftime( '%Y-%m-%d %H:%M:%S') if shangpin_obj.CreateTime else '', 'genggaishijian': shangpin_obj.UpdateTime.strftime( '%Y-%m-%d %H:%M:%S') if shangpin_obj.UpdateTime else '', # 额外字段,前端可能不需要但可以保留 'leixing_id': shangpin_obj.leixing_id, 'zhuanqu_id': shangpin_obj.zhuanqu_id, 'yaoqiuleixing': shangpin_obj.yaoqiuleixing, 'huiyuan_id': shangpin_obj.huiyuan_id, 'yongjin': str(shangpin_obj.yongjin) if shangpin_obj.yongjin else None, } return Response({ 'code': 0, 'message': '获取成功', 'data': response_data }) except Exception as e: logger.error(f"获取商品详情异常: {str(e)}", exc_info=True) return Response( {'code': 500, 'message': '服务器内部错误', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) class AdShangpinXiuGai(APIView): """ 后台更改商品数据接口 - 修复版 修复:1. 每次上传生成新文件名 2. 解决浏览器缓存问题 """ permission_classes = [IsAuthenticated] parser_classes = (MultiPartParser, FormParser, JSONParser) def post(self, request): try: # 1. 权限验证 qianzhanghao = request.data.get('zhanghao', '').strip() shangpin_id = request.data.get('shangpin_id') if not qianzhanghao or not shangpin_id: return Response( {'code': 400, 'message': '参数不完整', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 验证身份 dangqianyonghu = request.user if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao: return Response( {'code': 401, 'message': '身份验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) if dangqianyonghu.UserType != 'admin': return Response( {'code': 401, 'message': '身份验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) try: guanliyuan = dangqianyonghu.AdminProfile except AdminProfile.DoesNotExist: return Response( {'code': 401, 'message': '身份验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 2. 判断操作类型:删除还是修改 action = request.data.get('action', 'update').strip().lower() if action == 'delete': return self._delete_shangpin(shangpin_id) else: return self._update_shangpin(request, shangpin_id) except Exception as e: logger.error(f"更改商品数据异常: {str(e)}", exc_info=True) return Response( {'code': 500, 'message': '服务器内部错误', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) def _delete_shangpin(self, shangpin_id): """删除商品逻辑""" try: # 查询商品 try: shangpin_obj = Shangpin.query.get(id=shangpin_id) except Shangpin.DoesNotExist: return Response( {'code': 404, 'message': '商品不存在', 'data': None}, status=status.HTTP_404_NOT_FOUND ) # 获取图片URL(如果有) tupian_url = shangpin_obj.tupian_url guizetupian_url = shangpin_obj.guize_tupian # 异步或同步删除OSS图片 if tupian_url: try: delete_from_oss(tupian_url) except Exception as e: logger.warning(f"删除商品图片失败: {tupian_url}, 错误: {e}") if guizetupian_url: try: delete_from_oss(guizetupian_url) except Exception as e: logger.warning(f"删除规则图片失败: {guizetupian_url}, 错误: {e}") # 删除数据库记录 shangpin_obj.delete() return Response({ 'code': 0, 'message': '删除成功', 'data': { 'shangpin_id': shangpin_id, 'deleted': True } }) except Exception as e: logger.error(f"删除商品异常: {str(e)}", exc_info=True) return Response( {'code': 500, 'message': '删除失败', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) def _update_shangpin(self, request, shangpin_id): """修改商品逻辑 - 修复:每次上传生成新文件名""" try: # 查询商品 try: shangpin_obj = Shangpin.query.get(id=shangpin_id) except Shangpin.DoesNotExist: return Response( {'code': 404, 'message': '商品不存在', 'data': None}, status=status.HTTP_404_NOT_FOUND ) has_changes = False # 获取原图片URL(如果有) old_tupian_url = shangpin_obj.tupian_url # 3. 处理商品图片上传(如果有新商品图片) tupian_file = request.FILES.get('tupian_file') new_tupian_url = None if tupian_file: # 验证图片 is_valid, error_msg = validate_image(tupian_file) if not is_valid: return Response( {'code': 400, 'message': f'图片验证失败: {error_msg}', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 生成新文件名:时间戳 + 随机数 + 原扩展名 timestamp = int(time.time() * 1000) % 100000000 # 8位时间戳 random_num = random.randint(1000, 9999) file_extension = os.path.splitext(tupian_file.name)[1].lower() or '.jpg' new_filename = f"sp_{timestamp}_{random_num}{file_extension}" # 确定OSS路径 oss_folder = 'a_long' shangpin_folder = 'shangpin' oss_file_path = f"{oss_folder}/{shangpin_folder}/{new_filename}" # 删除旧图片(如果存在) if old_tupian_url: try: delete_from_oss(old_tupian_url) logger.info(f"已删除旧商品图片: {old_tupian_url}") except Exception as e: logger.warning(f"删除旧商品图片失败: {old_tupian_url}, 错误: {e}") # 继续上传新图片,不中断流程 # 上传到OSS new_tupian_url = upload_to_oss(tupian_file, oss_file_path) if not new_tupian_url: return Response( {'code': 500, 'message': '商品图片上传失败', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) # 更新数据库中的图片URL shangpin_obj.tupian_url = oss_file_path # 存相对路径 has_changes = True logger.info(f"商品图片更新成功: {oss_file_path}") # 4. 处理其他字段(只更新有变化的字段) # 注意:这里同时处理 request.data(文本字段)和可能有文件的情况 field_mapping = { 'biaoti': 'biaoqian', 'jieshao': 'jieshao', 'jiage': 'jiage', 'kucun': 'kucun', 'duiwaixiaoliang': 'duiwai_xiaoliang', 'xiadanxuzhi': 'xiadan_xuzhi', 'yajin': 'yajin', } for front_field, db_field in field_mapping.items(): # 注意:request.data 可能包含文件和其他字段,但DRF会处理好 if front_field in request.data: new_value = request.data.get(front_field) # 空字符串处理 if new_value == '': new_value = None if new_value is not None: # 获取当前值 current_value = getattr(shangpin_obj, db_field) # 类型转换 try: if db_field in ['jiage', 'yajin']: # 价格和押金 if new_value: new_value = Decimal(str(new_value)) else: new_value = None elif db_field in ['kucun', 'duiwai_xiaoliang']: # 库存和对外销量 if new_value: new_value = int(float(new_value)) else: new_value = 0 except (ValueError, TypeError, InvalidOperation) as e: logger.warning(f"字段{front_field}转换失败: {new_value}, 错误: {e}") continue # 检查是否有变化(处理None值) if current_value != new_value: setattr(shangpin_obj, db_field, new_value) has_changes = True # 5. 如果没有检测到任何更改 if not has_changes: return Response({ 'code': 400, 'message': '没有检测到任何更改', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 6. 保存更改 shangpin_obj.save() # 7. 返回更新后的数据 response_data = { 'tupian_url': shangpin_obj.tupian_url or '', 'biaoti': shangpin_obj.biaoqian or '', 'jieshao': shangpin_obj.jieshao or '', 'jiage': str(shangpin_obj.jiage) if shangpin_obj.jiage else '0.00', 'kucun': shangpin_obj.kucun, 'duiwaixiaoliang': shangpin_obj.duiwai_xiaoliang, 'xiadanxuzhi': shangpin_obj.xiadan_xuzhi or '', 'yajin': str(shangpin_obj.yongjin) if shangpin_obj.yongjin else None, 'genggaishijian': shangpin_obj.UpdateTime.strftime( '%Y-%m-%d %H:%M:%S') if shangpin_obj.UpdateTime else '', } return Response({ 'code': 0, 'message': '更新成功', 'data': response_data }) except Exception as e: logger.error(f"更新商品异常: {str(e)}", exc_info=True) return Response( {'code': 500, 'message': '更新失败', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) class AdShangpinGuizeXiuGai(APIView): """ 后台规则图片更换接口 - 修复版 修复:每次上传生成新文件名,避免浏览器缓存 """ permission_classes = [IsAuthenticated] parser_classes = (MultiPartParser, FormParser, JSONParser) def post(self, request): try: # 1. 权限验证 qianzhanghao = request.data.get('zhanghao', '').strip() shangpin_id = request.data.get('shangpin_id') action = request.data.get('action', 'update').strip().lower() if not qianzhanghao or not shangpin_id: return Response( {'code': 400, 'message': '参数不完整', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 验证身份 dangqianyonghu = request.user if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao: return Response( {'code': 401, 'message': '身份验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) if dangqianyonghu.UserType != 'admin': return Response( {'code': 401, 'message': '身份验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) try: guanliyuan = dangqianyonghu.AdminProfile except AdminProfile.DoesNotExist: return Response( {'code': 401, 'message': '身份验证失败', 'data': None}, status=status.HTTP_401_UNAUTHORIZED ) # 2. 查询商品 try: shangpin_obj = Shangpin.query.get(id=shangpin_id) except Shangpin.DoesNotExist: return Response( {'code': 404, 'message': '商品不存在', 'data': None}, status=status.HTTP_404_NOT_FOUND ) # 3. 处理不同操作 if action == 'delete': return self._delete_guize_image(shangpin_obj) elif action == 'update': return self._update_guize_image(request, shangpin_obj) else: return Response( {'code': 400, 'message': '操作类型无效', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) except Exception as e: logger.error(f"规则图片操作异常: {str(e)}", exc_info=True) return Response( {'code': 500, 'message': '服务器内部错误', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) def _delete_guize_image(self, shangpin_obj): """删除规则图片""" guizetupian_url = shangpin_obj.guize_tupian if guizetupian_url: # 从OSS删除 try: delete_success = delete_from_oss(guizetupian_url) '''if delete_success: logger.info(f"已删除规则图片: {guizetupian_url}") else: logger.warning(f"OSS删除失败,但继续删除数据库记录: {guizetupian_url}")''' except Exception as e: logger.warning(f"删除规则图片异常: {e}") # 清除数据库记录 shangpin_obj.guize_tupian = '' shangpin_obj.save() return Response({ 'code': 0, 'message': '删除成功', 'data': { 'guizetupian_url': '' } }) def _update_guize_image(self, request, shangpin_obj): """更新规则图片 - 修复:每次上传生成新文件名""" guize_file = request.FILES.get('guize_file') if not guize_file: return Response( {'code': 400, 'message': '请上传规则图片', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 验证图片 is_valid, error_msg = validate_image(guize_file) if not is_valid: return Response( {'code': 400, 'message': f'图片验证失败: {error_msg}', 'data': None}, status=status.HTTP_400_BAD_REQUEST ) # 获取原规则图片URL old_guizetupian_url = shangpin_obj.guize_tupian # 生成新文件名:时间戳 + 随机数 + 原扩展名 timestamp = int(time.time() * 1000) % 100000000 # 8位时间戳 random_num = random.randint(1000, 9999) file_extension = os.path.splitext(guize_file.name)[1].lower() or '.jpg' new_filename = f"gz_{timestamp}_{random_num}{file_extension}" # 确定OSS路径 oss_folder = 'a_long' guize_folder = 'guize' # 修改为你的要求:a_long/guize/ oss_file_path = f"{oss_folder}/{guize_folder}/{new_filename}" # 删除旧图片(如果存在) if old_guizetupian_url: try: delete_from_oss(old_guizetupian_url) #logger.info(f"已删除旧规则图片: {old_guizetupian_url}") except Exception as e: logger.warning(f"删除旧规则图片失败: {old_guizetupian_url}, 错误: {e}") # 继续上传新图片,不中断流程 # 上传到OSS new_guizetupian_url = upload_to_oss(guize_file, oss_file_path) if not new_guizetupian_url: return Response( {'code': 500, 'message': '规则图片上传失败', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) # 更新数据库(存相对路径) shangpin_obj.guize_tupian = oss_file_path shangpin_obj.save() #logger.info(f"规则图片更新成功: {oss_file_path}") return Response({ 'code': 0, 'message': '规则图片更新成功', 'data': { 'guizetupian_url': oss_file_path # 返回相对路径 } }) # views.py class HuiyuanTypeListView(APIView): """ 获取会员类型列表接口 路径:/shangpin/adhyjllxcx 方法:POST 权限:JWT Token + 管理员验证 """ permission_classes = [IsAuthenticated] def post(self, request): try: # 🔥 1. 身份验证 user = request.user # 检查是否是管理员 if not hasattr(user, 'AdminProfile'): return Response({ 'code': 401, 'message': '无权限访问', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) # 🔥 2. 获取所有会员类型 huiyuan_list = Huiyuan.query.all().order_by('CreateTime') # 🔥 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, # 购买次数 'CreateTime': huiyuan.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if huiyuan.CreateTime else None }) return Response({ 'code': 0, 'message': '获取成功', 'data': huiyuan_data }, status=status.HTTP_200_OK) except Exception as e: print(f"获取会员类型列表失败: {str(e)}") return Response({ 'code': 500, 'message': '系统错误', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class HuiyuanRecordListView(APIView): """ 获取会员记录列表接口 路径:/shangpin/adhyjlcxhq 方法:POST 权限:JWT Token + 管理员验证 参数: - zhanghao: 管理员账号 - huiyuan_type: 会员类型ID - jiluzhuangtai: 记录状态(1=未过期, 2=已过期) - page: 页码 - pagesize: 每页条数 """ permission_classes = [IsAuthenticated] def post(self, request): try: # 🔥 1. 身份验证 user = request.user # 检查是否是管理员 if not hasattr(user, 'AdminProfile'): return Response({ 'code': 401, 'message': '无权限访问', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) # 🔥 2. 获取参数 zhanghao = request.data.get('zhanghao', '').strip() huiyuan_type = request.data.get('huiyuan_type', '').strip() jiluzhuangtai = request.data.get('jiluzhuangtai', 1) # 默认未过期 page = int(request.data.get('page', 1)) pagesize = int(request.data.get('pagesize', 5)) # 🔥 3. 参数验证 if not zhanghao: return Response({ 'code': 400, 'message': '管理员账号不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) if not huiyuan_type: return Response({ 'code': 400, 'message': '会员类型不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 🔥 4. 检查会员类型是否存在 huiyuan_obj = Huiyuan.query.filter(huiyuan_id=huiyuan_type).first() if not huiyuan_obj: return Response({ 'code': 404, 'message': '会员类型不存在', 'data': None }, status=status.HTTP_404_NOT_FOUND) # 🔥 5. 构建基础查询(一次性获取所有符合条件的记录ID) now = timezone.now() # 🔥 使用原生SQL优化查询(避免N+1问题) with connection.cursor() as cursor: # 先查询符合条件的总记录数 count_sql = """ SELECT COUNT(*) FROM huiyuangoumai hg WHERE hg.huiyuan_id = %s """ params = [huiyuan_type] if jiluzhuangtai == 1: # 未过期 count_sql += " AND hg.daoqi_time > %s" params.append(now) else: # 已过期 count_sql += " AND hg.daoqi_time <= %s" params.append(now) cursor.execute(count_sql, params) total_count = cursor.fetchone()[0] # 🔥 6. 分页查询(使用窗口函数提高性能) offset = (page - 1) * pagesize record_sql = """ SELECT hg.id, hg.yonghu_id, hg.huiyuan_id, hg.jieshao, hg.daoqi_time, hg.CreateTime, 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.CreateTime 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, 'CreateTime': record[5].strftime('%Y-%m-%d %H:%M:%S') if record[5] else None, 'touxiang': record[6], # 头像相对URL 'nicheng': record[7] or record[1], # 昵称或用户ID 'yonghu_type': 2 # 🔥 默认打手类型(实际应该根据user_type判断) }) # 🔥 8. 判断是否还有更多数据 has_more = (page * pagesize) < total_count # 🔥 9. 获取会员类型统计信息(所有会员类型的当前状态总数) huiyuan_types = Huiyuan.query.all() huiyuan_stats = [] for huiyuan in huiyuan_types: # 使用COUNT查询统计 stats_sql = """ SELECT COUNT(*) FROM huiyuangoumai WHERE huiyuan_id = %s """ stats_params = [huiyuan.huiyuan_id] if jiluzhuangtai == 1: # 未过期 stats_sql += " AND daoqi_time > %s" stats_params.append(now) else: # 已过期 stats_sql += " AND daoqi_time <= %s" stats_params.append(now) with connection.cursor() as cursor: cursor.execute(stats_sql, stats_params) count = cursor.fetchone()[0] huiyuan_stats.append({ 'id': huiyuan.huiyuan_id, 'jieshao': huiyuan.jieshao, 'count': count }) return Response({ 'code': 0, 'message': '获取成功', 'data': { 'list': record_list, 'total': total_count, 'current_page': page, 'page_size': pagesize, 'has_more': has_more, 'huiyuan_stats': huiyuan_stats, # 🔥 所有会员类型的统计 'current_huiyuan': { 'id': huiyuan_obj.huiyuan_id, 'jieshao': huiyuan_obj.jieshao, 'count': total_count } } }, status=status.HTTP_200_OK) except ValueError as e: print(f"参数错误: {str(e)}") return Response({ 'code': 400, 'message': '参数错误', 'data': None }, status=status.HTTP_400_BAD_REQUEST) except Exception as e: print(f"获取会员记录失败: {str(e)}") traceback.print_exc() return Response({ 'code': 500, 'message': '系统错误', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class ZuzhangFenhongView(APIView): """ 组长分红记录接口 路径:/shangpin/zuzhangfenhong 方法:POST 权限:JWT认证,仅限组长身份 请求参数: { "page": 1, # 页码,默认1 "page_size": 5 # 每页条数,默认5 } 返回: { "code": 200, "msg": "success", "data": { "fenhong_zonge": "1234.56", # 分红总额 "yaoqing_guanshi": 10, # 邀请管事数 "fenhong_cishu": 50, # 分红次数 "total": 50, # 总记录数 "has_more": false, # 是否有下一页 "list": [...] # 记录列表 } } """ permission_classes = [permissions.IsAuthenticated] def post(self, request): current_user = request.user # 1. 验证组长身份 try: zuzhang = UserZuzhang.query.get(user=current_user) except UserZuzhang.DoesNotExist: return Response({ 'code': 403, 'msg': '当前用户不是组长身份', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 2. 请求参数 page = int(request.data.get('page', 1)) page_size = int(request.data.get('page_size', 5)) page = max(page, 1) page_size = max(page_size, 1) # 3. 统计数据 # 3.1 分红总额(从组长扩展表) fenhong_zonge = zuzhang.fenyong_zonge # 3.2 邀请管事数(统计邀请人等于当前用户ID的管事记录) yaoqing_guanshi = UserGuanshi.query.filter(yaoqingren=current_user.UserUID).count() # 3.3 分红次数(总记录数) base_queryset = Gsfenhong.query.filter(zuzhang_id=current_user.UserUID).order_by('-CreateTime') 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'))) # 时间格式化 CreateTime = rec.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if rec.CreateTime 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, 'CreateTime': CreateTime, }) # 7. 是否有更多数据 has_more = (end < fenhong_cishu) return Response({ 'code': 200, 'msg': 'success', 'data': { 'fenhong_zonge': str(fenhong_zonge.quantize(decimal.Decimal('0.00'))), 'yaoqing_guanshi': yaoqing_guanshi, 'fenhong_cishu': fenhong_cishu, 'total': fenhong_cishu, 'has_more': has_more, 'list': list_data, } }) def _get_guanshi_info(self, guanshi_ids): """ 批量获取管事用户的头像和昵称 昵称从老板扩展表(boss_profile)获取,头像从主表获取 返回字典:{用户ID: {'avatar': 相对路径, 'nicheng': 昵称}} """ if not guanshi_ids: return {} # 查询用户主表,并预加载 boss_profile users = User.query.filter(UserUID__in=guanshi_ids).select_related( 'BossProfile' ).only( 'UserUID', 'Avatar', 'BossProfile__nickname' ) info = {} for user in users: avatar = user.Avatar or '' # 从 boss_profile 获取昵称 if hasattr(user, 'BossProfile') and user.BossProfile: nicheng = user.BossProfile.nickname or '' else: # 如果没有 boss_profile,使用默认昵称 nicheng = f"用户{user.UserUID[-4:]}" info[user.UserUID] = {'avatar': avatar, 'nicheng': nicheng} # 对于未查询到的用户,提供默认值 for gid in guanshi_ids: if gid not in info: info[gid] = {'avatar': '', 'nicheng': f"用户{gid[-4:]}"} return info # 常量定义(可根据需要调整) INTEGRAL_PRICE = Decimal('5.00') # 积分购买固定价格(元) class CzhqdyView(APIView): """ 获取可用抵扣身份接口 路径:/shangpin/czhqdy 方法:POST 权限:JWT认证 请求参数: leixing: int # 1=会员,2=押金,3=积分,4=罚款 huiyuan_id: str # 当 leixing=1 时必传,会员ID yajin_jine: decimal # 当 leixing=2 时必传,押金金额(元) fadan_id: int # 当 leixing=4 时必传,罚单ID 返回: { "code": 200, "msg": "success", "data": [ { "id": 1, # 身份ID:1打手佣金 2管事分红 3组长分红 4打手押金 "jieshao": "打手佣金抵扣", "xuyao": "22.22" # 所需抵扣金额(保留两位小数) }, ... ] } """ permission_classes = [permissions.IsAuthenticated] # 身份ID与对应扩展表模型、余额字段、费率查询条件的映射 IDENTITY_CONFIG = [ { 'id': 1, 'name': '打手佣金抵扣', 'profile_attr': 'DashouProfile', # User 的反向 related_name 'balance_field': 'yue', 'rate_key': '5', 'model': UserDashou, }, { 'id': 2, 'name': '管事分红抵扣', 'profile_attr': 'GuanshiProfile', 'balance_field': 'yue', 'rate_key': '6', 'model': UserGuanshi, }, { 'id': 3, 'name': '组长分红抵扣', 'profile_attr': 'ZuzhangProfile', 'balance_field': 'ketixian_jine', 'rate_key': '8', 'model': UserZuzhang, }, # 新增打手押金抵扣 { 'id': 4, 'name': '打手押金抵扣', 'profile_attr': 'DashouProfile', # 与打手佣金共用扩展表 'balance_field': 'yajin', # 押金字段 'rate_key': '11', # 押金提现费率对应的 Platform 'model': UserDashou, }, ] def post(self, request): # 获取参数 leixing = request.data.get('leixing') huiyuan_id = request.data.get('huiyuan_id') yajin_jine = request.data.get('yajin_jine') fadan_id = request.data.get('fadan_id') # 参数校验 if leixing not in [1, 2, 3, 4]: return Response({'code': 400, 'msg': 'leixing 必须为 1,2,3,4'}, status=status.HTTP_400_BAD_REQUEST) if leixing == 1 and not huiyuan_id: return Response({'code': 400, 'msg': '购买会员时必须提供 huiyuan_id'}, status=status.HTTP_400_BAD_REQUEST) if leixing == 2: if not yajin_jine: return Response({'code': 400, 'msg': '购买押金时必须提供 yajin_jine'}, status=status.HTTP_400_BAD_REQUEST) try: yajin_jine = Decimal(str(yajin_jine)) if yajin_jine <= 0: raise ValueError except: return Response({'code': 400, 'msg': 'yajin_jine 必须为正数'}, status=status.HTTP_400_BAD_REQUEST) if leixing == 4 and not fadan_id: return Response({'code': 400, 'msg': '缴纳罚款时必须提供 fadan_id'}, status=status.HTTP_400_BAD_REQUEST) # 获取当前用户 user = request.user # 获取目标价格(需支付的原价) target_price = None if leixing == 1: # 会员 try: huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id) club_id = resolve_club_id_from_request(request) target_price = get_huiyuan_price(club_id, huiyuan_id, huiyuan) except Huiyuan.DoesNotExist: return Response({'code': 404, 'msg': '会员不存在'}, status=status.HTTP_404_NOT_FOUND) elif leixing == 2: # 押金 target_price = yajin_jine elif leixing == 3: # 积分(固定价格) target_price = INTEGRAL_PRICE elif leixing == 4: # 罚款缴纳 try: fadan = Penalty.query.get(id=fadan_id, PenalizedUserID=user.UserUID) target_price = fadan.FineAmount except Penalty.DoesNotExist: return Response({'code': 404, 'msg': '罚单不存在或不属于当前用户'}, status=status.HTTP_404_NOT_FOUND) # 获取所有费率(打手佣金、管事分红、组长分红、打手押金) rates = {} club_id = resolve_club_id_from_request(request) for key in ['5', '6', '8', '11']: rates[key] = get_commission_rate(club_id, key) # 构建可用身份列表 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 club_id = resolve_club_id_from_request(request) if leixing == 1: try: huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id) target_price = get_huiyuan_price(club_id, huiyuan_id, huiyuan) except Huiyuan.DoesNotExist: return Response({'code': 404, 'msg': '会员不存在'}, status=status.HTTP_404_NOT_FOUND) elif leixing == 2: target_price = yajin_jine elif leixing == 3: target_price = INTEGRAL_PRICE elif leixing == 4: if not fadan_id: return Response({'code': 400, 'msg': '缺少罚单ID'}, status=status.HTTP_400_BAD_REQUEST) try: fadan_obj = Penalty.query.get(id=fadan_id, PenalizedUserID=request.user.UserUID) target_price = fadan_obj.FineAmount except Penalty.DoesNotExist: return Response({'code': 404, 'msg': '罚单不存在或不属于当前用户'}, status=status.HTTP_404_NOT_FOUND) identity = IDENTITY_CONFIG.get(shenfen_id) try: rate_obj = CommissionRate.query.get(Platform=identity['rate_key']) rate = rate_obj.Rate if rate >= 1: return Response({'code': 400, 'msg': f"{identity['name']}费率异常"}, status=status.HTTP_400_BAD_REQUEST) except CommissionRate.DoesNotExist: return Response({'code': 400, 'msg': f"{identity['name']}费率未配置"}, status=status.HTTP_400_BAD_REQUEST) logger.info(f"目标价格={target_price}, 费率={rate}") try: required = target_price / (Decimal('1.0') - rate) required = required.quantize(Decimal('0.01'), rounding=ROUND_CEILING) logger.info(f"所需抵扣金额={required}") except Exception as e: logger.error(f"金额计算错误: {str(e)}", exc_info=True) return Response({'code': 500, 'msg': f'金额计算错误: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) user = request.user yonghuid = user.UserUID try: profile = identity['model'].query.get(user=user) except identity['model'].DoesNotExist: return Response({'code': 403, 'msg': f'您不是{identity["name"]}身份'}) status_value = getattr(profile, identity['status_field']) if status_value != 1: return Response({'code': 403, 'msg': f'{identity["name"]}账号状态异常,无法使用'}) balance = getattr(profile, identity['balance_field']) if balance < required: return Response({'code': 400, 'msg': f'{identity["name"]}余额不足,需要{required}元,当前{balance}元'}, status=status.HTTP_400_BAD_REQUEST) try: with transaction.atomic(): locked_profile = identity['model'].objects.select_for_update().get(id=profile.id) current_balance = getattr(locked_profile, identity['balance_field']) if current_balance < required: raise ValueError(f"{identity['name']}余额不足,需要{required}元,当前{current_balance}元") setattr(locked_profile, identity['balance_field'], current_balance - required) locked_profile.save(update_fields=[identity['balance_field']]) logger.info(f"扣款成功: {identity['name']} 扣除 {required} 元,剩余 {current_balance - required} 元") timestamp = str(int(time.time() * 1000))[-12:] rand_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4)) dingdan_id = f"CZ{timestamp}{rand_str}" if leixing == 1: action_desc = f"购买会员 {huiyuan.jieshao}" elif leixing == 2: action_desc = f"充值押金 {yajin_jine}元" elif leixing == 3: action_desc = "补充积分" elif leixing == 4: action_desc = f"缴纳罚款 {target_price}元" shuoming = f"{identity['name']}抵扣 {action_desc},抵扣金额{required}元" czjilu = Czjilu.query.create( dingdan_id=dingdan_id, huiyuan_id=huiyuan_id if leixing == 1 else None, zhuangtai=1, yonghuid=yonghuid, jine=required, leixing=leixing, shuoming=shuoming, club_id=club_id, ) 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.Status not in [1, 3]: return Response({'code': 400, 'msg': '罚单状态不可缴纳'}, status=status.HTTP_400_BAD_REQUEST) fadan_obj.Status = 2 fadan_obj.save(update_fields=['Status']) response_data['fadan_id'] = fadan_obj.id response_data['fadan_status'] = fadan_obj.Status # ========== 修改点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), club_id=get_user_club_id(User.query.filter(UserUID=yonghuid).first()), ) 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