import io import os import json import re import time import random import secrets import hashlib import threading import urllib.parse from decimal import Decimal, InvalidOperation from django.conf import settings from django.db import models, transaction, connection from django.db.models import Q, F, Max, Prefetch from django.core.cache import cache from django.core.paginator import Paginator from django.utils import timezone from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status, permissions from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.parsers import JSONParser, MultiPartParser, FormParser from rest_framework.throttling import AnonRateThrottle, SimpleRateThrottle from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image from utils.weixin_broadcast import WeixinBroadcastSender # from utils.ip_security import * from Django.utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode from backend.utils import update_shangjia_daily from orders.utils import sync_order_to_partners from .models import ( Gonggao, Lunbo, ClubConfig, Tupianpeizhi, Qunpeizhi, ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig ) from users.models import ( UserMain, AdminProfile, UserDashou, UserBoss, UserShangjia, UserGuanshi, UserZuzhang ) from orders.models import Lilubiao, Dingdan, DingdanShangjia from orders.notice_tasks import dingdan_guangbo from rank.models import Chenghao, DingdanBiaoqian from products.models import ShangpinLeixing, Huiyuangoumai from .serializers import PopupConfigSerializer import traceback import requests import logging logger = logging.getLogger(__name__) class GetDynamicConfigView(APIView): permission_classes = [AllowAny] parser_classes = [JSONParser] def post(self, request): try: # 查询我方俱乐部配置(is_self=1),取第一条(通常只有一条) club_config = ClubConfig.objects.filter(is_self=1).only( 'storage_bucket_domain', 'storage_bucket_name', 'chat_api_key', 'customer_service_api_url', 'customer_service_corp_id' ).first() # 默认值(防止空数据导致前端报错) cos_bucket = '' cos_region = 'ap-shanghai' cos_oss_url = '' goeasy_host = 'hangzhou.goeasy.io' goeasy_appkey = '' kefu_link = '' kefu_enterprise_id = '' morentouxiang = 'beijing/morentouxiang.jpg' dashouguize = 'a_long/dashouguize.jpg' if club_config: # 存储桶配置 bucket = club_config.storage_bucket_name or '' region = 'ap-shanghai' # 若需要动态,可单独存储 cos_oss_url = club_config.storage_bucket_domain or '' if cos_oss_url and not cos_oss_url.endswith('/'): cos_oss_url += '/' cos_bucket = bucket cos_region = region # GoEasy 配置 if club_config.chat_api_key: goeasy_appkey = club_config.chat_api_key # host 如果需要可动态,这里固定或从其他字段获取 # 客服配置 if club_config.customer_service_api_url: kefu_link = club_config.customer_service_api_url if club_config.customer_service_corp_id: kefu_enterprise_id = club_config.customer_service_corp_id # 其他配置可在此扩展 # 构造返回数据 data = { "code": 0, "data": { "cos": { "bucket": cos_bucket, "region": cos_region, "ossImageUrl": cos_oss_url, "uploadPathPrefix": "order/" }, "goEasy": { "host": goeasy_host, "appkey": goeasy_appkey }, "otherConfig": { "morentouxiang": morentouxiang, "dashouguize": dashouguize }, "kefu": { "link": kefu_link, "enterpriseId": kefu_enterprise_id } } } return Response(data) except Exception as e: logger.error(f"获取动态配置失败: {str(e)}", exc_info=True) return Response({"code": 500, "msg": "服务器内部错误"}, status=500) class ShangpinGonggaoView(APIView): """ 商品公告和轮播图接口 获取类型为1的公告和轮播图 """ # 应用节流(限制频率) throttle_classes = [AnonRateThrottle] # 新增:允许所有用户访问(不需要认证) permission_classes = [AllowAny] def post(self, request): """ 处理POST请求,返回公告和轮播图数据 请求: POST /api/peizhi/shangpingonggao/ 返回: { "shangpingonggao": "公告内容字符串", "shangpinlunbo": ["轮播图URL1", "轮播图URL2"] } """ try: logger.info("收到商品公告和轮播图请求") # 1. 查询公告(类型为1) gonggao_obj = Gonggao.objects.filter(leixing=1).first() # 获取公告内容,如果没有则返回空字符串 gonggao_content = gonggao_obj.jieshao if gonggao_obj else "" # 2. 查询轮播图(类型为1,按显示顺序排序) lunbo_queryset = Lunbo.objects.filter(leixing=1) # 提取轮播图URL列表 lunbo_urls = [lunbo.tupian_url for lunbo in lunbo_queryset] # 3. 构建返回数据 response_data = { "shangpingonggao": gonggao_content, "shangpinlunbo": lunbo_urls } logger.info(f"返回数据:公告长度{len(gonggao_content)},轮播图数量{len(lunbo_urls)}") # 4. 返回响应 return Response(response_data, status=status.HTTP_200_OK) except Exception as e: # 记录异常 logger.error(f"获取商品公告和轮播图时发生错误:{str(e)}") # 返回错误响应 error_data = { "error": "服务器内部错误", "detail": "获取数据失败,请稍后重试" } return Response(error_data, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class AdminConfigQueryView(APIView): """ 管理员获取系统配置接口 URL: /peizhi/adpzhq 方法: POST 权限: JWT Token认证 + 管理员权限 参数: zhanghao (管理员账号) """ permission_classes = [permissions.IsAuthenticated] def post(self, request): try: # 获取前端传递的账号 zhanghao = request.data.get('zhanghao') if not zhanghao: return Response({ 'code': 1, 'msg': '账号不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 获取当前用户(UserMain实例) user_main = request.user # 验证账号:比较user_main的phone与前端传递的zhanghao if user_main.phone != zhanghao: return Response({ 'code': 1, 'msg': '账号不匹配', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) # 验证用户类型是否为管理员 if user_main.user_type != 'admin': return Response({ 'code': 1, 'msg': '无权限', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) # 验证管理员扩展表是否存在 try: admin_profile = user_main.admin_profile except AdminProfile.DoesNotExist: return Response({ 'code': 1, 'msg': '无权限', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) # 查询图片配置 rule_image = Tupianpeizhi.objects.filter(leixing=1).first() default_avatar = Tupianpeizhi.objects.filter(leixing=2).first() # 查询群配置 qun_configs = Qunpeizhi.objects.all() qq_groups = [] for config in qun_configs: qq_groups.append({ 'peizhiid': config.peizhiid, 'neirong': config.neirong if config.neirong else '', 'qunid': config.qunid if config.qunid else '', 'jieshao': config.jieshao if config.jieshao else '' }) # 查询利率配置 platform_rate_obj = Lilubiao.objects.filter(fadanpingtai='1').first() shop_rate_obj = Lilubiao.objects.filter(fadanpingtai='3').first() # 🔴【新增】打手提现抽成利率(字符5) dashou_withdraw_obj = Lilubiao.objects.filter(fadanpingtai='5').first() # 🔴【新增】管事提现抽成利率(字符6) guanshi_withdraw_obj = Lilubiao.objects.filter(fadanpingtai='6').first() # 构建返回数据 data = { 'images': { 'rule_image': rule_image.tupian if rule_image and rule_image.tupian else '', 'default_avatar': default_avatar.tupian if default_avatar and default_avatar.tupian else '' }, 'qq_groups': qq_groups, 'rates': { 'platform_rate': float(platform_rate_obj.lilu) if platform_rate_obj and platform_rate_obj.lilu else 0.0, 'shop_rate': float(shop_rate_obj.lilu) if shop_rate_obj and shop_rate_obj.lilu else 0.0, # 🔴【新增】打手提现抽成利率 'dashou_withdraw_rate': float( dashou_withdraw_obj.lilu) if dashou_withdraw_obj and dashou_withdraw_obj.lilu else 0.0, # 🔴【新增】管事提现抽成利率 'guanshi_withdraw_rate': float( guanshi_withdraw_obj.lilu) if guanshi_withdraw_obj and guanshi_withdraw_obj.lilu else 0.0 } } return Response({ 'code': 0, 'msg': 'success', 'data': data }) except Exception as e: return Response({ 'code': 99, 'msg': f'系统错误: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class AdminUploadImageView(APIView): """ 管理员上传配置图片接口 URL: /peizhi/adxgtp 方法: POST 权限: JWT Token认证 + 管理员权限 参数: zhanghao, leixing (1:打手规则图片, 2:默认头像图片), image (文件) """ permission_classes = [permissions.IsAuthenticated] parser_classes = (MultiPartParser, FormParser) def post(self, request): try: zhanghao = request.data.get('zhanghao') leixing = request.data.get('leixing') image_file = request.FILES.get('image') if not zhanghao: return Response({ 'code': 1, 'msg': '账号不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) if not leixing: return Response({ 'code': 2, 'msg': '图片类型不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) if not image_file: return Response({ 'code': 3, 'msg': '请选择图片文件', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 验证用户权限 user_main = request.user if user_main.phone != zhanghao: return Response({ 'code': 1, 'msg': '账号不匹配', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) if user_main.user_type != 'admin': return Response({ 'code': 1, 'msg': '无权限', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) try: admin_profile = user_main.admin_profile except AdminProfile.DoesNotExist: return Response({ 'code': 1, 'msg': '无权限', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) # 验证图片类型 if leixing not in ['1', '2']: return Response({ 'code': 4, 'msg': '图片类型错误', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 验证图片文件 is_valid, error_msg = validate_image(image_file) if not is_valid: return Response({ 'code': 5, 'msg': f'图片验证失败: {error_msg}', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 根据类型确定固定相对URL和文件名 if leixing == '1': # 打手规则图片 fixed_relative_url = 'a_long/dashouguize.jpg' oss_file_name = 'dashouguize.jpg' else: # 默认头像图片 fixed_relative_url = 'a_long/morentouxiang.jpg' oss_file_name = 'morentouxiang.jpg' # 构建OSS文件路径 oss_file_path = fixed_relative_url # a_long/dashouguize.jpg 或 a_long/morentouxiang.jpg # 上传到OSS(覆盖原有文件) image_file.seek(0) # 重置文件指针 full_url = upload_to_oss(image_file, oss_file_path) if not full_url: return Response({ 'code': 6, 'msg': '图片上传到存储桶失败', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 更新数据库 with transaction.atomic(): # 获取或创建图片配置记录 tupian_peizhi, created = Tupianpeizhi.objects.get_or_create(leixing=int(leixing)) tupian_peizhi.tupian = fixed_relative_url tupian_peizhi.save() return Response({ 'code': 0, 'msg': '图片上传成功', 'data': { 'image_url': fixed_relative_url } }) except Exception as e: return Response({ 'code': 99, 'msg': f'系统错误: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class AdminUpdateConfigView(APIView): """ 管理员更新系统配置接口(QQ群、利率)- 已修复 URL: /peizhi/adupdate 方法: POST 权限: JWT Token认证 + 管理员权限 参数: - zhanghao: 管理员账号 - qq_groups: [{"peizhiid": 1, "neirong": "", "qunid": "", "jieshao": ""}, ...] (可选) - rates: {"platform_rate": 0.1, "shop_rate": 0.2} (可选) """ permission_classes = [permissions.IsAuthenticated] def post(self, request): try: zhanghao = request.data.get('zhanghao') if not zhanghao: return Response({ 'code': 1, 'msg': '账号不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 验证用户权限 user_main = request.user if user_main.phone != zhanghao: return Response({ 'code': 1, 'msg': '账号不匹配', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) if user_main.user_type != 'admin': return Response({ 'code': 1, 'msg': '无权限', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) try: admin_profile = user_main.admin_profile except AdminProfile.DoesNotExist: return Response({ 'code': 1, 'msg': '无权限', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) response_data = {} # 🔴 修复:更新QQ群配置 - 确保根据peizhiid正确更新对应的记录 qq_groups = request.data.get('qq_groups') if qq_groups and isinstance(qq_groups, list): updated_groups = [] for group_data in qq_groups: peizhiid = group_data.get('peizhiid') if not peizhiid: continue # 🔴 修复:使用filter()确保查询到正确的记录 qun_configs = Qunpeizhi.objects.filter(peizhiid=peizhiid) if qun_configs.exists(): # 如果存在多个相同peizhiid的记录,取第一个(应该只有一个) qun_config = qun_configs.first() # 更新字段 neirong = group_data.get('neirong') qunid = group_data.get('qunid') jieshao = group_data.get('jieshao') if neirong is not None: qun_config.neirong = neirong or '' if qunid is not None: qun_config.qunid = qunid or '' if jieshao is not None: qun_config.jieshao = jieshao or '' qun_config.save() else: # 如果不存在,创建新的(使用提供的peizhiid) qun_config = Qunpeizhi.objects.create( peizhiid=peizhiid, neirong=group_data.get('neirong', '') or '', qunid=group_data.get('qunid', '') or '', jieshao=group_data.get('jieshao', '') or '' ) updated_groups.append({ 'peizhiid': qun_config.peizhiid, 'neirong': qun_config.neirong, 'qunid': qun_config.qunid, 'jieshao': qun_config.jieshao }) if updated_groups: response_data['qq_groups'] = updated_groups # 🔴 修复:更新利率配置 - 确保更新现有记录而不是创建新记录 rates = request.data.get('rates') if rates and isinstance(rates, dict): updated_rates = {} # 平台发单利率(类型1,fadanpingtai='1') platform_rate = rates.get('platform_rate') if platform_rate is not None: try: platform_rate_float = float(platform_rate) if 0 <= platform_rate_float <= 1: # 🔴 修复:查询现有记录,如果不存在则创建 platform_rate_obj = Lilubiao.objects.filter(fadanpingtai='1').first() if platform_rate_obj: # 更新现有记录 platform_rate_obj.lilu = platform_rate_float platform_rate_obj.save() else: # 创建新记录 platform_rate_obj = Lilubiao.objects.create( lilu=platform_rate_float, fadanpingtai='1' ) updated_rates['platform_rate'] = platform_rate_float else: return Response({ 'code': 3, 'msg': '平台发单利率必须在0-1之间', 'data': None }, status=status.HTTP_400_BAD_REQUEST) except ValueError: return Response({ 'code': 4, 'msg': '平台发单利率格式错误', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 商家发单利率(类型2,fadanpingtai='2') shop_rate = rates.get('shop_rate') if shop_rate is not None: try: shop_rate_float = float(shop_rate) if 0 <= shop_rate_float <= 1: # 🔴 修复:查询现有记录,如果不存在则创建 shop_rate_obj = Lilubiao.objects.filter(fadanpingtai='3').first() if shop_rate_obj: # 更新现有记录 shop_rate_obj.lilu = shop_rate_float shop_rate_obj.save() else: # 创建新记录 shop_rate_obj = Lilubiao.objects.create( lilu=shop_rate_float, fadanpingtai='3' ) updated_rates['shop_rate'] = shop_rate_float else: return Response({ 'code': 5, 'msg': '商家发单利率必须在0-1之间', 'data': None }, status=status.HTTP_400_BAD_REQUEST) except ValueError: return Response({ 'code': 6, 'msg': '商家发单利率格式错误', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 🔴【新增】打手提现抽成利率(类型5,fadanpingtai='5') dashou_withdraw_rate = rates.get('dashou_withdraw_rate') if dashou_withdraw_rate is not None: try: dashou_withdraw_rate_float = float(dashou_withdraw_rate) if 0 <= dashou_withdraw_rate_float <= 1: # 查询现有记录,如果不存在则创建 dashou_withdraw_obj = Lilubiao.objects.filter(fadanpingtai='5').first() if dashou_withdraw_obj: # 更新现有记录 dashou_withdraw_obj.lilu = dashou_withdraw_rate_float dashou_withdraw_obj.save() else: # 创建新记录 dashou_withdraw_obj = Lilubiao.objects.create( lilu=dashou_withdraw_rate_float, fadanpingtai='5' ) updated_rates['dashou_withdraw_rate'] = dashou_withdraw_rate_float else: return Response({ 'code': 7, # 注意:这里需要调整错误码,不要重复 'msg': '打手提现抽成利率必须在0-1之间', 'data': None }, status=status.HTTP_400_BAD_REQUEST) except ValueError: return Response({ 'code': 8, 'msg': '打手提现抽成利率格式错误', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 🔴【新增】管事提现抽成利率(类型6,fadanpingtai='6') guanshi_withdraw_rate = rates.get('guanshi_withdraw_rate') if guanshi_withdraw_rate is not None: try: guanshi_withdraw_rate_float = float(guanshi_withdraw_rate) if 0 <= guanshi_withdraw_rate_float <= 1: # 查询现有记录,如果不存在则创建 guanshi_withdraw_obj = Lilubiao.objects.filter(fadanpingtai='6').first() if guanshi_withdraw_obj: # 更新现有记录 guanshi_withdraw_obj.lilu = guanshi_withdraw_rate_float guanshi_withdraw_obj.save() else: # 创建新记录 guanshi_withdraw_obj = Lilubiao.objects.create( lilu=guanshi_withdraw_rate_float, fadanpingtai='6' ) updated_rates['guanshi_withdraw_rate'] = guanshi_withdraw_rate_float else: return Response({ 'code': 9, 'msg': '管事提现抽成利率必须在0-1之间', 'data': None }, status=status.HTTP_400_BAD_REQUEST) except ValueError: return Response({ 'code': 10, 'msg': '管事提现抽成利率格式错误', 'data': None }, status=status.HTTP_400_BAD_REQUEST) if updated_rates: response_data['rates'] = updated_rates # 如果没有任何更新 if not response_data: return Response({ 'code': 7, 'msg': '没有需要更新的数据', 'data': None }, status=status.HTTP_400_BAD_REQUEST) return Response({ 'code': 0, 'msg': '更新成功', 'data': response_data }) except Exception as e: return Response({ 'code': 99, 'msg': f'系统错误: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class DashouPaihangView(APIView): """ 排行榜接口 路径:/peizhi/dsph 方法:POST 认证:JWT Token认证 请求参数: - leixing: 1=今日收入,2=今月收入 - yonghu_leixing: 1=打手,2=管事,3=商家 返回数据: - code: 0=成功,其他=失败 - msg: 提示信息 - data: { xianshi: True/False, # 是否展示真实数据 paihang_list: [ # 排行榜列表 { uid: '用户ID', touxiang: '头像相对URL', nicheng: '用户昵称', jine: 收入金额, mingci: 排名 }, ... ] } """ # =============== 可配置参数 =============== # 每次查询返回的最大记录数(可修改) ZUI_DAO_CHA_XUN_SHU_LIANG = 50 # 最少需要多少人才能展示(可修改) ZUI_SHAO_REN_SHU_YAO_QIU = 1 # 第一名最低金额要求(单位:元)(可修改) DI_YI_MING_ZUI_DI_JIN_E = Decimal('10.00') # 管事第一名最低数量要求(可修改) DI_YI_MING_ZUI_DI_SHU_LIANG = 1 # =============== 可配置参数结束 =============== permission_classes = [IsAuthenticated] def post(self, request): try: # 获取请求参数 - 修复参数类型问题 leixing_str = request.data.get('leixing', '1') yonghu_leixing_str = request.data.get('yonghu_leixing', '1') # 转换为整数 try: leixing = int(leixing_str) except (ValueError, TypeError): leixing = 1 try: yonghu_leixing = int(yonghu_leixing_str) except (ValueError, TypeError): yonghu_leixing = 1 # 参数验证 if leixing not in [1, 2]: return Response({ 'code': 400, 'msg': '参数错误:leixing必须为1(今日)或2(今月)', 'data': {'xianshi': False} }) if yonghu_leixing not in [1, 2, 3]: return Response({ 'code': 400, 'msg': '参数错误:yonghu_leixing必须为1(打手)、2(管事)或3(商家)', 'data': {'xianshi': False} }) # 根据用户类型调用不同的查询方法 if yonghu_leixing == 1: # 打手排行榜 paihang_list, xianshi_flag = self.chaxun_dashou_paihang(leixing) elif yonghu_leixing == 2: # 管事排行榜 paihang_list, xianshi_flag = self.chaxun_guanshi_paihang(leixing) else: # 商家排行榜 paihang_list, xianshi_flag = self.chaxun_shangjia_paihang(leixing) # 返回结果 return Response({ 'code': 0, 'msg': '获取成功', 'data': { 'xianshi': xianshi_flag, 'paihang_list': paihang_list } }) except Exception as e: logger.error(f"排行榜查询失败: {str(e)}", exc_info=True) return Response({ 'code': 500, 'msg': '服务器内部错误', 'data': {'xianshi': False} }) def chaxun_dashou_paihang(self, leixing): """ 查询打手排行榜 :param leixing: 1=今日收入,2=今月收入 :return: (排行榜列表, 是否展示标志) """ # 确定查询的字段 if leixing == 1: # 今日收入 shouyi_ziduan = 'jinrishouyi' else: # 今月收入 shouyi_ziduan = 'jinyueshouyi' # 查询条件:收入大于0,打手账号状态正常(1=正常) # 注意:打手表中的zhanghaozhuangtai字段 chaxun_tiaojian = Q(**{f'{shouyi_ziduan}__gt': 0}) & Q(zhanghaozhuangtai=1) # 查询打手扩展表,并关联用户主表 dashou_list = UserDashou.objects.select_related('user').filter( chaxun_tiaojian ).order_by(f'-{shouyi_ziduan}')[:self.ZUI_DAO_CHA_XUN_SHU_LIANG] # 检查是否满足展示条件 if len(dashou_list) < self.ZUI_SHAO_REN_SHU_YAO_QIU: # 人数不足,不展示 return [], False # 检查第一名金额是否达到最低要求 if leixing == 1: di_yi_ming_jine = dashou_list[0].jinrishouyi else: di_yi_ming_jine = dashou_list[0].jinyueshouyi if di_yi_ming_jine < self.DI_YI_MING_ZUI_DI_JIN_E: # 第一名金额不足,不展示 return [], False # 构建返回数据 paihang_list = [] for index, dashou in enumerate(dashou_list): # 获取用户主表信息 user_main = dashou.user # 打手的昵称直接使用打手扩展表的nicheng字段 dashou_nicheng = dashou.nicheng or '未设置昵称' # 获取头像(相对URL) touxiang_url = user_main.avatar or '' # 获取收入金额 if leixing == 1: shouyi_jine = dashou.jinrishouyi else: shouyi_jine = dashou.jinyueshouyi paihang_list.append({ 'uid': user_main.yonghuid, 'touxiang': touxiang_url, 'nicheng': dashou_nicheng, 'jine': float(shouyi_jine), # 转换为float方便前端处理 'mingci': index + 1 }) return paihang_list, True def chaxun_guanshi_paihang(self, leixing): """ 查询管事排行榜 :param leixing: 1=今日,2=今月 :return: (排行榜列表, 是否展示标志) """ # 确定查询的字段 if leixing == 1: # 今日充值打手数量 chaxun_ziduan = 'jinrichongzhi' else: # 今月充值打手数量 chaxun_ziduan = 'jinyuechongzhi' # 查询条件:数量大于0,管事状态正常(状态1为正常) # 注意:管手表中的zhuangtai字段 chaxun_tiaojian = Q(**{f'{chaxun_ziduan}__gt': 0}) & Q(zhuangtai=1) # 查询管事扩展表,并关联用户主表 guanshi_list = UserGuanshi.objects.select_related('user').filter( chaxun_tiaojian ).order_by(f'-{chaxun_ziduan}')[:self.ZUI_DAO_CHA_XUN_SHU_LIANG] # 检查是否满足展示条件 if len(guanshi_list) < self.ZUI_SHAO_REN_SHU_YAO_QIU: # 人数不足,不展示 return [], False # 检查第一名数量是否达到最低要求 if leixing == 1: di_yi_ming_shuliang = guanshi_list[0].jinrichongzhi else: di_yi_ming_shuliang = guanshi_list[0].jinyuechongzhi if di_yi_ming_shuliang < self.DI_YI_MING_ZUI_DI_SHU_LIANG: # 第一名数量不足,不展示 return [], False # 构建返回数据 paihang_list = [] for index, guanshi in enumerate(guanshi_list): # 获取用户主表信息 user_main = guanshi.user # 管事的昵称需要查询老板扩展表 boss_profile = UserBoss.objects.filter(user=user_main).first() guanshi_nicheng = boss_profile.nickname if boss_profile else '未设置昵称' # 获取头像(相对URL) touxiang_url = user_main.avatar or '' # 获取数量 if leixing == 1: chongzhi_shuliang = guanshi.jinrichongzhi else: chongzhi_shuliang = guanshi.jinyuechongzhi paihang_list.append({ 'uid': user_main.yonghuid, 'touxiang': touxiang_url, 'nicheng': guanshi_nicheng, 'jine': float(chongzhi_shuliang), # 这里jine字段实际存储的是数量 'mingci': index + 1 }) return paihang_list, True def chaxun_shangjia_paihang(self, leixing): """ 查询商家排行榜 :param leixing: 1=今日流水,2=今月流水 :return: (排行榜列表, 是否展示标志) """ # 确定查询的字段 if leixing == 1: # 今日流水 liushui_ziduan = 'jinriliushui' else: # 今月流水 liushui_ziduan = 'jinyueliushui' # 查询条件:流水大于0,商家状态正常(状态1为正常) # 注意:商家表中的zhuangtai字段 chaxun_tiaojian = Q(**{f'{liushui_ziduan}__gt': 0}) & Q(zhuangtai=1) # 查询商家扩展表,并关联用户主表 shangjia_list = UserShangjia.objects.select_related('user').filter( chaxun_tiaojian ).order_by(f'-{liushui_ziduan}')[:self.ZUI_DAO_CHA_XUN_SHU_LIANG] # 检查是否满足展示条件 if len(shangjia_list) < self.ZUI_SHAO_REN_SHU_YAO_QIU: # 人数不足,不展示 return [], False # 检查第一名金额是否达到最低要求 if leixing == 1: di_yi_ming_liushui = shangjia_list[0].jinriliushui else: di_yi_ming_liushui = shangjia_list[0].jinyueliushui if di_yi_ming_liushui < self.DI_YI_MING_ZUI_DI_JIN_E: # 第一名流水不足,不展示 return [], False # 构建返回数据 paihang_list = [] for index, shangjia in enumerate(shangjia_list): # 获取用户主表信息 user_main = shangjia.user # 商家的昵称需要查询老板扩展表 boss_profile = UserBoss.objects.filter(user=user_main).first() shangjia_nicheng = boss_profile.nickname if boss_profile else '未设置昵称' # 获取头像(相对URL) touxiang_url = user_main.avatar or '' # 获取流水金额 if leixing == 1: liushui_jine = shangjia.jinriliushui else: liushui_jine = shangjia.jinyueliushui paihang_list.append({ 'uid': user_main.yonghuid, 'touxiang': touxiang_url, 'nicheng': shangjia_nicheng, 'jine': float(liushui_jine), 'mingci': index + 1 }) return paihang_list, True class GetAdminZhanghaoView(APIView): """ 获取管理员账号列表接口 路径:/peizhi/adzhhq 方法:POST 权限:JWT Token + 管理员验证 参数: - zhanghao: 当前登录的管理员账号(字符串) """ permission_classes = [IsAuthenticated] def post(self, request): try: # 🔥 1. 身份验证(100%按表模型) user = request.user # 获取当前用户的手机号(CharField,字符串类型) current_phone = getattr(user, 'phone', '') if not current_phone: return Response({ 'code': 401, 'message': '用户手机号信息不完整', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) # 检查前端传递的账号是否匹配(都是字符串) frontend_zhanghao = request.data.get('zhanghao', '').strip() if frontend_zhanghao != current_phone: return Response({ 'code': 403, 'message': '账号不匹配,无权限访问', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 🔥 2. 验证是否为管理员(user_type是CharField,值是'admin'字符串) user_type = getattr(user, 'user_type', '') if user_type != 'admin': # 检查是否有管理员扩展表 if not hasattr(user, 'admin_profile'): return Response({ 'code': 403, 'message': '无管理员权限', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 🔥 3. 查询管理员账号列表(全部按字符串查询) with connection.cursor() as cursor: # 🔥 查询所有管理员账号(user_type='admin' 字符串) # 🔥 phone和yonghuid都是CharField,xiugaiid也是CharField sql = """ SELECT um.id, um.phone, um.yonghuid, um.user_type, COALESCE(COUNT(xjl.id), 0) as jilushuliang FROM user_main um LEFT JOIN xiugaijilu xjl ON um.phone = xjl.xiugaiid WHERE um.user_type = 'admin' GROUP BY um.id, um.phone, um.yonghuid, um.user_type ORDER BY um.create_time DESC """ cursor.execute(sql) results = cursor.fetchall() # 🔥 4. 处理查询结果(全部保持字符串类型) zhanghao_list = [] for row in results: zhanghao_list.append({ #'id': row[0], 'id': str(row[1]) if row[1] else str(row[2]), # 🔥 保持字符串 'zhanghao':str(row[1]) if row[1] else str(row[2]), # 🔥 保持字符串 'yonghuid': str(row[2]) if row[2] else '', # 🔥 保持字符串 'user_type': str(row[3]) if row[3] else '', # 🔥 保持字符串 'jilushuliang': int(row[4]) # 只有这个是数字 }) return Response({ 'code': 0, 'message': '获取成功', 'data': zhanghao_list }, status=status.HTTP_200_OK) 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 GetXiugaiJiluView(APIView): """ 获取修改记录列表接口 路径:/peizhi/adhqxg 方法:POST 权限:JWT Token + 管理员验证 参数: - zhanghao: 当前登录的管理员账号(字符串) - xiugaizhe_zhanghaoid: 选中的管理员账号ID(字符串,对应phone或yonghuid) - page: 页码(整数) - pagesize: 每页条数(整数) """ permission_classes = [IsAuthenticated] def post(self, request): try: # 🔥 1. 身份验证(100%按表模型) user = request.user # 获取当前用户的手机号(CharField,字符串) current_phone = getattr(user, 'phone', '') if not current_phone: return Response({ 'code': 401, 'message': '用户手机号信息不完整', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) # 检查前端传递的账号是否匹配(字符串比较) frontend_zhanghao = request.data.get('zhanghao', '') if not isinstance(frontend_zhanghao, str): frontend_zhanghao = str(frontend_zhanghao) frontend_zhanghao = frontend_zhanghao.strip() if frontend_zhanghao != current_phone: return Response({ 'code': 403, 'message': '账号不匹配,无权限访问', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 🔥 2. 验证是否为管理员(user_type='admin'字符串) user_type = getattr(user, 'user_type', '') if user_type != 'admin': if not hasattr(user, 'admin_profile'): return Response({ 'code': 403, 'message': '无管理员权限', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 🔥 3. 获取参数(100%按实际类型) # 🔥 xiugaizhe_zhanghaoid 是字符串(对应xiugaiid字段,CharField) xiugaizhe_zhanghaoid_param = request.data.get('xiugaizhe_zhanghaoid', '') # 🔥 强制转为字符串(因为数据库是CharField) if xiugaizhe_zhanghaoid_param is None: xiugaizhe_zhanghaoid = '' else: # 无论如何都转为字符串 xiugaizhe_zhanghaoid = str(xiugaizhe_zhanghaoid_param).strip() # 🔥 分页参数转为整数 try: page = int(request.data.get('page', 1)) except: page = 1 try: pagesize = int(request.data.get('pagesize', 10)) except: pagesize = 10 # 🔥 4. 参数验证 if not xiugaizhe_zhanghaoid: return Response({ 'code': 400, 'message': '修改者账号ID不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) if page < 1: page = 1 if pagesize < 1 or pagesize > 50: pagesize = 10 # 🔥 5. 高效查询(100%按表字段类型) with connection.cursor() as cursor: # 🔥 5.1 查询总记录数(xiugaiid是CharField,用字符串查询) count_sql = """ SELECT COUNT(*) FROM xiugaijilu WHERE xiugaiid = %s """ cursor.execute(count_sql, [xiugaizhe_zhanghaoid]) total_count = cursor.fetchone()[0] # 🔥 5.2 计算分页 offset = (page - 1) * pagesize # 🔥 5.3 查询当前页数据(使用参数化查询,防止SQL注入) data_sql = """ SELECT xjl.id, xjl.yonghuid, -- CharField xjl.xiugaiid, -- CharField xjl.leixing, -- IntegerField xjl.xiugaitijiao, -- DecimalField xjl.xiugaitijiaoq, -- DecimalField xjl.jifen, -- IntegerField xjl.yjifen, -- IntegerField xjl.yajin, -- DecimalField xjl.yyajin, -- DecimalField xjl.shangjiayue, -- DecimalField xjl.shangjiayueq, -- DecimalField xjl.guanshiyue, -- DecimalField xjl.guanshiyueq, -- DecimalField xjl.huiyuants, -- IntegerField xjl.huiyuan_id, -- CharField xjl.qitashuoming, -- TextField xjl.create_time, -- DateTimeField um.avatar -- CharField FROM xiugaijilu xjl LEFT JOIN user_main um ON xjl.yonghuid = um.yonghuid WHERE xjl.xiugaiid = %s ORDER BY xjl.create_time DESC LIMIT %s OFFSET %s """ cursor.execute(data_sql, [xiugaizhe_zhanghaoid, pagesize, offset]) records = cursor.fetchall() # 🔥 6. 处理查询结果(严格按照字段类型处理) jilu_list = [] for record in records: # 获取修改者账号信息(都是字符串) xiugaizhe_zhanghao = '' with connection.cursor() as cursor2: # 🔥 phone和yonghuid都是CharField,用字符串查询 cursor2.execute( "SELECT phone, yonghuid FROM user_main WHERE phone = %s OR yonghuid = %s LIMIT 1", [xiugaizhe_zhanghaoid, xiugaizhe_zhanghaoid] ) xiugaizhe_info = cursor2.fetchone() if xiugaizhe_info: # 🔥 都是字符串 xiugaizhe_zhanghao = str(xiugaizhe_info[0]) if xiugaizhe_info[0] else str(xiugaizhe_info[1]) if \ xiugaizhe_info[1] else xiugaizhe_zhanghaoid # 获取会员名称(如果有) huiyuan_mingzi = '' if record[15]: # huiyuan_id是CharField with connection.cursor() as cursor3: cursor3.execute( "SELECT jieshao FROM huiyuan WHERE huiyuan_id = %s LIMIT 1", [str(record[15])] # 🔥 转为字符串查询 ) huiyuan_info = cursor3.fetchone() if huiyuan_info: huiyuan_mingzi = str(huiyuan_info[0]) if huiyuan_info[0] else '' # 🔥 严格按照前端字段名映射,保持正确类型 jilu_data = { # 基础字段(字符串) 'id': record[0], 'beixiugai_yonghuid': str(record[1]) if record[1] else '', # yonghuid 'xiugaizhe_id': str(record[2]) if record[2] else '', # xiugaiid 'xiugaizhe_zhanghao': xiugaizhe_zhanghao, # 字符串 'beixiugai_yonghuleixing': record[3], # leixing是整数 # 打手字段(Decimal转float,Integer转int) 'xiugaihou_dashoutixianjine': float(record[4]) if record[4] is not None else 0.00, 'xiugaiqian_dashoutixianjine': float(record[5]) if record[5] is not None else 0.00, 'xiugaihou_dashoujifen': int(record[6]) if record[6] is not None else 0, 'xiugaiqian_dashoujifen': int(record[7]) if record[7] is not None else 0, 'xiugaihou_dashouyajin': float(record[8]) if record[8] is not None else 0.00, 'xiugaiqian_dashouyajin': float(record[9]) if record[9] is not None else 0.00, # 商家字段 'xiugaihou_shangjiayue': float(record[10]) if record[10] is not None else 0.00, 'xiugaiqian_shangjiayue': float(record[11]) if record[11] is not None else 0.00, # 管事字段 'xiugaihou_guanshiyue': float(record[12]) if record[12] is not None else 0.00, 'xiugaiqian_guanshiyue': float(record[13]) if record[13] is not None else 0.00, # 会员字段 'tianjia_huiyuantianshu': int(record[14]) if record[14] is not None else 0, 'huiyuan_id': str(record[15]) if record[15] else '', # 字符串 'huiyuan_mingzi': huiyuan_mingzi, # 字符串 # 其他字段 'qita_shuoming': str(record[16]) if record[16] else '', # 字符串 'xiugai_time': record[17].strftime('%Y-%m-%d %H:%M:%S') if record[17] else None, 'touxiang': str(record[18]) if record[18] else '' # avatar是字符串 } jilu_list.append(jilu_data) # 🔥 7. 分页控制 queried_records = len(jilu_list) has_more = False if queried_records == pagesize: total_queried = (page - 1) * pagesize + queried_records has_more = total_queried < total_count return Response({ 'code': 0, 'message': '获取成功', 'data': { 'list': jilu_list, 'zongshuliang': total_count, 'dangqian_page': page, 'page_size': pagesize, 'has_more': has_more, 'shijichaxundediaoshuliang': queried_records } }, 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 ShangjiaMobanListView(APIView): """ 商家订单模板列表接口 POST /peizhi/sjddmblb 权限: JWT认证 """ permission_classes = [IsAuthenticated] def post(self, request): try: user = request.user yonghu_id = user.yonghuid leixing_id = request.data.get('shangpinTypeId') if not leixing_id: return Response({'code': 400, 'msg': '商品类型ID不能为空', 'data': {}}, status=400) get_type = int(request.data.get('getType', 2)) page = int(request.data.get('page', 1)) page_size = int(request.data.get('pageSize', 50)) keyword = request.data.get('keyword', '') min_price = request.data.get('minPrice', '') label_id = request.data.get('labelId', None) if label_id is not None: try: label_id = int(label_id) except (ValueError, TypeError): label_id = None query = Q(yonghu_id=yonghu_id, leixing_id=leixing_id) if get_type == 1: # 搜索模式 if keyword: keyword = str(keyword).strip() try: mid = int(keyword) query &= Q(id=mid) | Q(moban_jieshao__icontains=keyword) except ValueError: query &= Q(moban_jieshao__icontains=keyword) if min_price: try: query &= Q(jiage__gte=Decimal(str(min_price))) except: pass if label_id: query &= Q(chenghao_id=label_id) mobans = ShangjiaMoban.objects.filter(query).order_by('-id') formatted = [] for moban in mobans: texiao_json = '' label_name = '' if moban.chenghao_id: try: ch = Chenghao.objects.get(id=moban.chenghao_id) label_name = ch.mingcheng texiao_json = ch.texiao_miaoshu or '' except Chenghao.DoesNotExist: pass formatted.append({ 'mobanId': moban.id, 'shangpinTypeId': moban.leixing_id, 'jieshao': moban.moban_jieshao, 'jiage': str(moban.jiage), 'fabushuliang': moban.fabu_shuliang, 'commissionEnabled': moban.yongjin_qiangdan, 'commissionValue': str(moban.yongjin_jiage) if moban.yongjin_jiage is not None else None, 'labelId': moban.chenghao_id, 'labelName': label_name, 'texiaoJson': texiao_json, 'createTime': moban.create_time.strftime('%Y-%m-%d %H:%M:%S') if moban.create_time else '' }) return Response({'code': 200, 'msg': '搜索成功', 'data': {'list': formatted, 'total': len(formatted), 'hasMore': False}}) # 分页模式 queryset = ShangjiaMoban.objects.filter(query).order_by('-id') paginator = Paginator(queryset, page_size) if page < 1 or page > paginator.num_pages: return Response({'code': 400, 'msg': '页码超出范围', 'data': {}}, status=400) current_page = paginator.page(page) formatted = [] for moban in current_page.object_list: texiao_json = '' label_name = '' if moban.chenghao_id: try: ch = Chenghao.objects.get(id=moban.chenghao_id) label_name = ch.mingcheng texiao_json = ch.texiao_miaoshu or '' except Chenghao.DoesNotExist: pass formatted.append({ 'mobanId': moban.id, 'shangpinTypeId': moban.leixing_id, 'jieshao': moban.moban_jieshao, 'jiage': str(moban.jiage), 'fabushuliang': moban.fabu_shuliang, 'commissionEnabled': moban.yongjin_qiangdan, 'commissionValue': str(moban.yongjin_jiage) if moban.yongjin_jiage is not None else None, 'labelId': moban.chenghao_id, 'labelName': label_name, 'texiaoJson': texiao_json, 'createTime': moban.create_time.strftime('%Y-%m-%d %H:%M:%S') if moban.create_time else '' }) has_more = current_page.has_next() if len(formatted) < page_size: has_more = False return Response({ 'code': 200, 'msg': '获取成功', 'data': { 'list': formatted, 'total': paginator.count, 'hasMore': has_more, 'currentPage': page, 'pageSize': page_size, 'totalPages': paginator.num_pages } }) except Exception as e: logger.error(f"获取模板列表失败: {str(e)}", exc_info=True) return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500) class ShangjiaTianjiaMobanView(APIView): """ 商家添加订单模板接口 POST /peizhi/sjtjddmb 权限: JWT认证 """ permission_classes = [IsAuthenticated] def post(self, request): try: # 1. 获取当前商家用户ID user = request.user yonghu_id = user.yonghuid # 2. 接收并清洗参数,避免类型错误 leixing_id = request.data.get('shangpinTypeId') jieshao = request.data.get('jieshao', '').strip() if request.data.get('jieshao') else '' jiage = request.data.get('jiage', '') # 佣金相关 commission_enabled = request.data.get('commissionEnabled', False) commission_value = request.data.get('commissionValue', None) # 标签ID(称号ID) label_id = request.data.get('labelId', None) # 3. 必填验证 if not leixing_id or not jieshao or not jiage: logger.warning(f"参数缺失 - leixing_id:{leixing_id}, jieshao:{jieshao}, jiage:{jiage}") return Response({ 'code': 400, 'msg': '缺少必填字段(订单类型、介绍、价格)', 'data': {} }, status=400) # 4. 价格格式验证 try: jiage_decimal = Decimal(str(jiage)) if jiage_decimal <= 0: return Response({'code': 400, 'msg': '价格必须大于0', 'data': {}}, status=400) if jiage_decimal > 10000: return Response({'code': 400, 'msg': '价格不能超过10000元', 'data': {}}, status=400) except (InvalidOperation, ValueError, TypeError): return Response({'code': 400, 'msg': '价格格式错误', 'data': {}}, status=400) # 5. 介绍长度验证 if len(jieshao) > 100: return Response({'code': 400, 'msg': '订单介绍不能超过100字', 'data': {}}, status=400) # 6. 佣金验证(若启用) yongjin_qiangdan = False yongjin_jiage = None if commission_enabled: yongjin_qiangdan = True if not commission_value: return Response({'code': 400, 'msg': '开启佣金时,佣金金额不能为空', 'data': {}}, status=400) try: comm_decimal = Decimal(str(commission_value)) if comm_decimal <= 0: return Response({'code': 400, 'msg': '佣金必须大于0', 'data': {}}, status=400) yongjin_jiage = comm_decimal except (InvalidOperation, ValueError, TypeError): return Response({'code': 400, 'msg': '佣金格式错误', 'data': {}}, status=400) # 7. 标签ID清洗(前端可能传数字或字符串) cleaned_label_id = None if label_id is not None and str(label_id).strip(): try: cleaned_label_id = int(label_id) except (ValueError, TypeError): return Response({'code': 400, 'msg': '标签ID格式错误', 'data': {}}, status=400) # 8. 验证商品类型是否存在且可用 try: shangpin_obj = ShangpinLeixing.objects.get( id=leixing_id, shenhezhuangtai=1 # 审核通过 ) except ShangpinLeixing.DoesNotExist: return Response({'code': 404, 'msg': '商品类型不存在或已禁用', 'data': {}}, status=404) # 9. 检查同一用户、同一类型下是否已存在相同介绍的模板 if ShangjiaMoban.objects.filter( yonghu_id=yonghu_id, leixing_id=leixing_id, moban_jieshao=jieshao ).exists(): return Response({'code': 400, 'msg': '已存在相同介绍的模板', 'data': {}}, status=400) # 10. 执行数据库创建(事务) with transaction.atomic(): moban = ShangjiaMoban.objects.create( yonghu_id=yonghu_id, leixing_id=leixing_id, moban_jieshao=jieshao, jiage=jiage_decimal, fabu_shuliang=0, yongjin_qiangdan=yongjin_qiangdan, yongjin_jiage=yongjin_jiage, chenghao_id=cleaned_label_id ) # 11. 返回成功数据(字段与前端对齐) return Response({ 'code': 200, 'msg': '模板创建成功', 'data': { 'mobanId': moban.id, 'shangpinTypeId': leixing_id, 'jieshao': moban.moban_jieshao, 'jiage': str(moban.jiage), 'fabushuliang': moban.fabu_shuliang, 'createTime': moban.create_time.strftime('%Y-%m-%d %H:%M:%S'), 'commissionEnabled': moban.yongjin_qiangdan, 'commissionValue': str(moban.yongjin_jiage) if moban.yongjin_jiage is not None else None, 'labelId': moban.chenghao_id } }) except Exception as e: # 打印完整错误堆栈到日志 logger.error(f"添加模板失败: {str(e)}", exc_info=True) return Response({ 'code': 500, 'msg': '服务器内部错误,添加模板失败,请联系管理员', 'data': {} }, status=500) class ShangjiaShanchuMobanView(APIView): """ 删除模板 POST /peizhi/sjddmbsc """ permission_classes = [IsAuthenticated] def post(self, request): try: user = request.user yonghu_id = user.yonghuid moban_id = request.data.get('mobanId') leixing_id = request.data.get('shangpinTypeId') if not all([moban_id, leixing_id]): return Response({'code': 400, 'msg': '参数缺失', 'data': {}}, status=400) try: moban = ShangjiaMoban.objects.get(id=moban_id, yonghu_id=yonghu_id, leixing_id=leixing_id) except ShangjiaMoban.DoesNotExist: return Response({'code': 404, 'msg': '模板不存在', 'data': {}}, status=404) # 强制硬删除 moban.delete() return Response({'code': 200, 'msg': '删除成功', 'data': {'mobanId': moban_id, 'shangpinTypeId': leixing_id}}) except Exception as e: logger.error(f"删除模板失败: {str(e)}", exc_info=True) return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500) class ShangjiaGengxinMobanView(APIView): """ 商家更新订单模板接口 POST /peizhi/sjddmbgx 权限: JWT认证 """ permission_classes = [IsAuthenticated] def post(self, request): try: user = request.user yonghu_id = user.yonghuid moban_id = request.data.get('mobanId') leixing_id = request.data.get('shangpinTypeId') new_jieshao = request.data.get('jieshao', '').strip() new_jiage = request.data.get('jiage', '').strip() # 新增字段 commission_enabled = request.data.get('commissionEnabled') commission_value = request.data.get('commissionValue') label_id = request.data.get('labelId') if not all([moban_id, leixing_id]): return Response({'code': 400, 'msg': '模板ID和商品类型ID不能为空', 'data': {}}, status=400) # 查询模板 try: moban = ShangjiaMoban.objects.get( id=moban_id, yonghu_id=yonghu_id, leixing_id=leixing_id ) except ShangjiaMoban.DoesNotExist: return Response({'code': 404, 'msg': '模板不存在或无权修改', 'data': {}}, status=404) update_fields = [] # 修改介绍 if new_jieshao: if len(new_jieshao) > 100: return Response({'code': 400, 'msg': '订单介绍不能超过100字', 'data': {}}, status=400) if new_jieshao != moban.moban_jieshao: moban.moban_jieshao = new_jieshao update_fields.append('moban_jieshao') # 修改价格 if new_jiage: try: jiage_decimal = Decimal(new_jiage) if jiage_decimal <= 0 or jiage_decimal > 10000: return Response({'code': 400, 'msg': '价格必须在0.1-10000元之间', 'data': {}}, status=400) if jiage_decimal != moban.jiage: moban.jiage = jiage_decimal update_fields.append('jiage') except (InvalidOperation, ValueError, TypeError): return Response({'code': 400, 'msg': '价格格式错误', 'data': {}}, status=400) # 修改标签 if label_id is not None: # 允许传空字符串或不传,传0表示清空 new_label = int(label_id) if label_id else None if new_label != moban.chenghao_id: moban.chenghao_id = new_label update_fields.append('chenghao_id') # 修改佣金设置 if commission_enabled is not None: enabled = bool(commission_enabled) if enabled: if not commission_value: return Response({'code': 400, 'msg': '佣金价格不能为空', 'data': {}}, status=400) try: comm_decimal = Decimal(commission_value) if comm_decimal < 0.1 or comm_decimal > 50: return Response({'code': 400, 'msg': '佣金需在0.1-50元之间', 'data': {}}, status=400) if moban.yongjin_qiangdan != True or moban.yongjin_jiage != comm_decimal: moban.yongjin_qiangdan = True moban.yongjin_jiage = comm_decimal update_fields.extend(['yongjin_qiangdan', 'yongjin_jiage']) except (InvalidOperation, ValueError, TypeError): return Response({'code': 400, 'msg': '佣金格式错误', 'data': {}}, status=400) else: if moban.yongjin_qiangdan != False or moban.yongjin_jiage is not None: moban.yongjin_qiangdan = False moban.yongjin_jiage = None update_fields.extend(['yongjin_qiangdan', 'yongjin_jiage']) if not update_fields: return Response({'code': 400, 'msg': '没有修改内容', 'data': {}}, status=400) # 检查介绍唯一性 if 'moban_jieshao' in update_fields: if ShangjiaMoban.objects.filter( yonghu_id=yonghu_id, leixing_id=leixing_id, moban_jieshao=moban.moban_jieshao ).exclude(id=moban_id).exists(): return Response({'code': 400, 'msg': '已存在相同介绍的模板', 'data': {}}, status=400) with transaction.atomic(): moban.save(update_fields=update_fields) # 返回最新数据 return Response({ 'code': 200, 'msg': '模板修改成功', 'data': { 'mobanId': moban.id, 'shangpinTypeId': moban.leixing_id, 'jieshao': moban.moban_jieshao, 'jiage': str(moban.jiage), 'fabushuliang': moban.fabu_shuliang, 'commissionEnabled': moban.yongjin_qiangdan, 'commissionValue': str(moban.yongjin_jiage) if moban.yongjin_jiage else None, 'labelId': moban.chenghao_id, 'updateTime': moban.update_time.strftime('%Y-%m-%d %H:%M:%S') } }) except Exception as e: logger = logging.getLogger(__name__) logger.error(f"修改商家模板失败: {str(e)}", exc_info=True) return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500) class ShangjiaGenerateLinkView(APIView): """商家生成发单链接接口(支持模板佣金/标签)""" permission_classes = [IsAuthenticated] def post(self, request): try: user = request.user # 获取参数 shangpin_type_id = request.data.get('shangpinTypeId') moban_id = request.data.get('mobanId') if not all([shangpin_type_id, moban_id]): return Response({'code': 400, 'msg': '缺少参数', 'data': {}}, status=400) try: shangpin_type_id = int(shangpin_type_id) moban_id = int(moban_id) except ValueError: return Response({'code': 400, 'msg': '参数格式错误', 'data': {}}, status=400) # 商家信息 try: shangjia = user.shop_profile except UserShangjia.DoesNotExist: return Response({'code': 403, 'msg': '商家信息未完善', 'data': {}}, status=403) if shangjia.zhuangtai != 1: return Response({'code': 403, 'msg': '商家状态异常', 'data': {}}, status=403) # 商品类型 try: shangpin_leixing = ShangpinLeixing.objects.get(id=shangpin_type_id, shenhezhuangtai=1) except ShangpinLeixing.DoesNotExist: return Response({'code': 400, 'msg': '商品类型不存在或已禁用', 'data': {}}, status=400) # 查询模板 try: moban = ShangjiaMoban.objects.get(id=moban_id, yonghu_id=user.yonghuid, leixing_id=shangpin_type_id) except ShangjiaMoban.DoesNotExist: return Response({'code': 400, 'msg': '模板不存在', 'data': {}}, status=400) jiage = moban.jiage if jiage > shangjia.yue: return Response({'code': 400, 'msg': f'余额不足,当前余额{shangjia.yue}元', 'data': {}}, status=400) # 利率 try: lilu = float(Lilubiao.objects.get(fadanpingtai='3').lilu) or 1.0 except Lilubiao.DoesNotExist: lilu = 1.0 dashou_fencheng = Decimal(str(round(float(jiage) * lilu, 2))) # 抢单要求:根据模板是否启用佣金 chenghao_id = moban.chenghao_id if moban.yongjin_qiangdan and moban.yongjin_jiage is not None: final_yaoqiuleixing = 2 # 佣金模式 final_yongjin = moban.yongjin_jiage final_huiyuan_id = '' else: final_yaoqiuleixing = shangpin_leixing.yaoqiuleixing or 1 final_yongjin = None final_huiyuan_id = shangpin_leixing.huiyuan_id or '' # 获取标签名称 label_name = '' if chenghao_id: try: label_name = Chenghao.objects.get(id=chenghao_id).mingcheng except Chenghao.DoesNotExist: pass # 生成ID和Token def generate_dingdan_id(): timestamp = int(time.time() * 1000) process_id = os.getpid() % 1000 random_num = random.randint(1000, 9999) return f"SJ{timestamp}{process_id:03d}{random_num}" def generate_secure_token(dd_id, uid): timestamp = int(time.time() * 1_000_000) salt = secrets.token_hex(8) raw = f"{timestamp}:{dd_id}:{uid}:{salt}" token = hashlib.sha256(raw.encode()).hexdigest()[:24] # 确保唯一性(极小概率重复再试一次) if ShangjiaLianjie.objects.filter(lianjie_token=token).exists(): return generate_secure_token(dd_id, uid) return token def generate_link_url(token): base = getattr(settings, 'H5_DOMAIN', 'https://h5.yourdomain.com').rstrip('/') return f"{base}/order/{token}" with transaction.atomic(): dingdan_id = generate_dingdan_id() secure_token = generate_secure_token(dingdan_id, user.yonghuid) # 创建订单主表 dingdan = Dingdan.objects.create( dingdan_id=dingdan_id, zhuangtai=13, fadan_pingtai=2, jine=jiage, dashou_fencheng=dashou_fencheng, leixing_id=shangpin_type_id, yaoqiuleixing=final_yaoqiuleixing, huiyuan_id=final_huiyuan_id, yongjin=final_yongjin, jieshao=moban.moban_jieshao[:2000], user1_id=f"Sj{user.yonghuid}", tupian=user.avatar or '', ) # 商家扩展表 DingdanShangjia.objects.create( dingdan=dingdan, shangjia_id=user.yonghuid, sjnicheng=shangjia.nicheng or f"商家{user.yonghuid}", ) # 关联标签 if chenghao_id: try: ch = Chenghao.objects.get(id=chenghao_id) DingdanBiaoqian.objects.create(dingdan=dingdan, chenghao=ch) except Chenghao.DoesNotExist: logger.warning(f"称号{chenghao_id}不存在,跳过关联") # 更新商家余额和统计 UserShangjia.objects.filter(id=shangjia.id).update( fabu=F('fabu') + 1, yue=F('yue') - jiage, jinridingdan=F('jinridingdan') + 1, jinriliushui=F('jinriliushui') + jiage, jinyuedingdan=F('jinyuedingdan') + 1, jinyueliushui=F('jinyueliushui') + jiage, ) # 生成链接并保存 link_url = generate_link_url(secure_token) lianjie_obj = ShangjiaLianjie.objects.create( yonghu_id=user.yonghuid, leixing_id=shangpin_type_id, moban_id=moban_id, lianjie=link_url, lianjie_token=secure_token, dingdan_id=dingdan_id, is_used=False, youxiao_shijian=timezone.now() + timezone.timedelta(days=1) ) # 更新模板发布数量 ShangjiaMoban.objects.filter(id=moban_id).update(fabu_shuliang=F('fabu_shuliang') + 1) shangjia.refresh_from_db() logger.info(f"链接生成成功 - 订单ID: {dingdan_id}, 标签: {label_name}") return Response({ 'code': 200, 'msg': '链接生成成功', 'data': { 'linkUrl': link_url, 'newBalance': str(shangjia.yue), 'dingdanId': dingdan_id, 'dingdanJieshao': moban.moban_jieshao, 'jiage': str(jiage), 'labelId': chenghao_id, 'labelName': label_name, 'youxiaoShijian': lianjie_obj.youxiao_shijian.strftime('%Y-%m-%d %H:%M:%S'), } }) except Exception as e: logger.error(f"生成链接事务失败: {str(e)}", exc_info=True) return Response({'code': 500, 'msg': '系统错误,生成链接失败', 'data': {}}, status=500) class ShangjiaLianjieListView(APIView): """获取已生成的链接列表(支持筛选是否使用)""" permission_classes = [IsAuthenticated] def post(self, request): try: user = request.user yonghu_id = user.yonghuid moban_id = request.data.get('mobanId') used = int(request.data.get('used', 0)) # 0未使用,1已使用 page = int(request.data.get('page', 1)) page_size = int(request.data.get('pageSize', 5)) page_size = min(page_size, 50) if not moban_id: return Response({'code': 400, 'msg': '模板ID不能为空', 'data': {}}, status=400) # 校验模板归属 try: moban = ShangjiaMoban.objects.get(id=moban_id, yonghu_id=yonghu_id) except ShangjiaMoban.DoesNotExist: return Response({'code': 404, 'msg': '模板不存在或无权查看', 'data': {}}, status=404) links = ShangjiaLianjie.objects.filter( yonghu_id=yonghu_id, moban_id=moban_id, is_used=(used == 1) ).order_by('-create_time') paginator = Paginator(links, page_size) if page < 1 or page > paginator.num_pages: return Response({'code': 400, 'msg': '页码超出范围', 'data': {}}, status=400) current_page = paginator.page(page) data_list = [] for link in current_page.object_list: data_list.append({ 'id': link.id, 'lianjie': link.lianjie, 'lianjie_token': link.lianjie_token, 'is_used': link.is_used, 'dingdan_id': link.dingdan_id, 'youxiao_shijian': link.youxiao_shijian.strftime('%Y-%m-%d %H:%M:%S') if link.youxiao_shijian else '', 'create_time': link.create_time.strftime('%Y-%m-%d %H:%M:%S') }) return Response({ 'code': 200, 'msg': '获取成功', 'data': { 'list': data_list, 'total': paginator.count, 'hasMore': current_page.has_next(), 'currentPage': page, 'pageSize': page_size } }) except Exception as e: logger.error(f"获取链接列表失败: {str(e)}", exc_info=True) return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500) class IPUtils: """IP工具类""" @staticmethod def get_client_ip(request): """ 获取客户端真实IP,处理代理情况 优先级: X-Real-IP > X-Forwarded-For > remote_addr """ ip = None try: # 1. 尝试从 X-Real-IP 获取 x_real_ip = request.META.get('HTTP_X_REAL_IP') if x_real_ip: ip = x_real_ip.strip() logger.info(f"从 X-Real-IP 获取IP: {ip}") return ip # 2. 尝试从 X-Forwarded-For 获取(处理代理链) x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') if x_forwarded_for: # 可能有多个代理IP,第一个是真实IP ips = x_forwarded_for.split(',') for possible_ip in ips: possible_ip = possible_ip.strip() if possible_ip and possible_ip.lower() != 'unknown': ip = possible_ip logger.info(f"从 X-Forwarded-For 获取IP: {ip}") return ip # 3. 使用 remote_addr ip = request.META.get('REMOTE_ADDR', '') logger.info(f"从 REMOTE_ADDR 获取IP: {ip}") except Exception as e: logger.error(f"获取IP失败: {str(e)}", exc_info=True) ip = '0.0.0.0' return ip @staticmethod def validate_ip(ip): """验证IP格式是否合法""" if not ip: return False # IPv4正则 ipv4_pattern = r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$' # IPv6正则(简化版) ipv6_pattern = r'^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$' return bool(re.match(ipv4_pattern, ip) or re.match(ipv6_pattern, ip)) @staticmethod def is_private_ip(ip): """判断是否为内网IP""" if not ip: return False # 内网IP段 private_ranges = [ ('10.0.0.0', '10.255.255.255'), ('172.16.0.0', '172.31.255.255'), ('192.168.0.0', '192.168.255.255'), ('127.0.0.0', '127.255.255.255'), ] try: # 将IP转换为整数比较 def ip_to_int(ip): parts = ip.split('.') return (int(parts[0]) << 24) + (int(parts[1]) << 16) + (int(parts[2]) << 8) + int(parts[3]) ip_num = ip_to_int(ip) for start, end in private_ranges: if ip_to_int(start) <= ip_num <= ip_to_int(end): return True except: pass return False class OrderLinkThrottle(SimpleRateThrottle): """订单链接访问频率限制""" scope = 'order_link' rate = '10/minute' # 每分钟最多10次 def get_cache_key(self, request, view): # 基于IP和接口路径进行限制 ip = IPUtils.get_client_ip(request) if not ip or IPUtils.is_private_ip(ip): # 内网IP或不合法IP,使用默认 ip = 'default' return f'throttle_{view.__class__.__name__}_{ip}' class KehuGetDingdanLianjieView(APIView): """ 客户获取订单链接信息接口 路径:GET /peizhi/khhqddlj?token=xxx 返回字段包括:订单信息、打手标识、商家标识、俱乐部配置(客服链接、存储桶、GoEasy AppKey等) """ throttle_classes = [OrderLinkThrottle] permission_classes = [] # 无需认证,任何人都可访问 def get(self, request): """ 处理GET请求,获取订单链接信息 """ # 1. 记录请求开始 logger.info("=== 客户获取订单链接信息接口开始 ===") # 2. 获取客户端真实IP client_ip = IPUtils.get_client_ip(request) logger.info(f"客户端IP: {client_ip}") # 3. 验证IP合法性 if not IPUtils.validate_ip(client_ip): logger.warning(f"IP格式不合法: {client_ip}") return Response({ 'code': 400, 'msg': '网络连接异常', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 4. 获取Token参数 token = request.query_params.get('token', '') if not token: logger.warning(f"Token参数缺失 - IP: {client_ip}") return Response({ 'code': 400, 'msg': '缺少链接参数', 'data': None }, status=status.HTTP_400_BAD_REQUEST) logger.info(f"接收到Token: {token[:8]}...") # 5. 查询链接记录 try: lianjie_obj = ShangjiaLianjie.objects.select_related().get(lianjie_token=token) logger.info(f"找到链接记录 - 链接ID: {lianjie_obj.id}, 商家ID: {lianjie_obj.yonghu_id}") except ShangjiaLianjie.DoesNotExist: logger.warning(f"链接不存在 - Token: {token[:8]}..., IP: {client_ip}") return Response({ 'code': 404, 'msg': '链接不存在或已失效', 'data': None }, status=status.HTTP_404_NOT_FOUND) except Exception as e: logger.error(f"查询链接异常: {str(e)}", exc_info=True) return Response({ 'code': 500, 'msg': '系统错误,请稍后重试', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 6. 更新访问者IP(无论是否已使用,都记录最新访问IP) try: lianjie_obj.shiyongzhe_ip = client_ip lianjie_obj.save(update_fields=['shiyongzhe_ip']) logger.info(f"更新访问者IP成功: {client_ip}") except Exception as e: logger.warning(f"更新IP失败: {str(e)}") # 继续处理,不影响主要逻辑 # 7. 检查链接有效期 '''now = timezone.now() if lianjie_obj.youxiao_shijian and lianjie_obj.youxiao_shijian < now: logger.warning(f"链接已过期 - 过期时间: {lianjie_obj.youxiao_shijian}, 当前: {now}") return Response({ 'code': 400, 'msg': '链接已过期', 'data': None }, status=status.HTTP_400_BAD_REQUEST)''' # 8. 准备返回数据 response_data = { 'is_used': lianjie_obj.is_used, 'dingdan_id': lianjie_obj.dingdan_id or '', 'youxiao_shijian': lianjie_obj.youxiao_shijian.strftime( '%Y-%m-%d %H:%M:%S') if lianjie_obj.youxiao_shijian else '', } # 9. 获取订单信息 if lianjie_obj.dingdan_id: try: # 查询订单主表 dingdan_obj = Dingdan.objects.get(dingdan_id=lianjie_obj.dingdan_id) # 获取商家昵称 shangjia_nicheng = '' try: user_main = UserMain.objects.get(yonghuid=lianjie_obj.yonghu_id) shangjia_obj = user_main.shop_profile shangjia_nicheng = shangjia_obj.nicheng except (UserMain.DoesNotExist, UserShangjia.DoesNotExist): logger.warning(f"商家信息不存在 - 用户ID: {lianjie_obj.yonghu_id}") # ========== 新增:确定俱乐部配置 ========== # 默认头像(相对路径) default_avatar = 'a_long/morentouxiang.jpg' # 初始化配置字段默认值 kefu_link = '' oss_url = '' goeasy_appkey = '' # 判断是否为跨平台订单(保留,但不影响配置选择) is_cross = dingdan_obj.is_cross if hasattr(dingdan_obj, 'is_cross') else 0 # 始终使用我方俱乐部配置 try: club_config = ClubConfig.objects.filter(is_self=1).first() if club_config: logger.info(f"使用我方俱乐部配置,club_id={club_config.club_id}") kefu_link = club_config.customer_service_api_url or '' oss_url = club_config.storage_bucket_domain or '' goeasy_appkey = club_config.chat_api_key or '' else: logger.warning("未找到我方俱乐部配置(is_self=1)") except Exception as e: logger.error(f"查询我方俱乐部配置异常: {str(e)}", exc_info=True) # ========== 构建订单信息 ========== dingdan_info = { 'jieshao': dingdan_obj.jieshao or '', 'jiage': str(dingdan_obj.jine) if dingdan_obj.jine is not None else '0.00', # 金额字段名是 jine 'shangjia_mingcheng': shangjia_nicheng, 'zhuangtai': dingdan_obj.zhuangtai, 'is_cross': is_cross, # 新增配置字段 'kefu_link': kefu_link, 'oss_url': oss_url, 'default_avatar': default_avatar, 'goeasy_appkey': goeasy_appkey, # 前端用于连接GoEasy的自身用户ID(固定B前缀+商家ID) 'self_user_id': f"B{lianjie_obj.yonghu_id}" if lianjie_obj.yonghu_id else '', } # 如果已使用,返回已填写的信息 if lianjie_obj.is_used: dingdan_info.update({ 'youxi_nicheng': dingdan_obj.nicheng or '', 'zhiding_dashou': dingdan_obj.zhiding_id or '', 'beizhu': dingdan_obj.beizhu or '', }) # 获取聊天相关标识 # 9.1 获取接单打手ID jiedan_dashou_id = dingdan_obj.jiedan_dashou_id dashou_biaoshi = f'Ds{jiedan_dashou_id}' if jiedan_dashou_id else '' # 9.2 获取商家标识 shangjia_biaoshi = '' try: shangjia_kuozhan = DingdanShangjia.objects.select_related('dingdan').filter( dingdan__dingdan_id=lianjie_obj.dingdan_id ).first() if shangjia_kuozhan and shangjia_kuozhan.shangjia_id: shangjia_biaoshi = f'Sj{shangjia_kuozhan.shangjia_id}' logger.info(f"生成商家标识: {shangjia_biaoshi}") else: logger.info(f"订单 {lianjie_obj.dingdan_id} 没有商家订单扩展信息") except Exception as e: logger.error(f"查询商家订单扩展异常: {str(e)}", exc_info=True) # 添加聊天标识 dingdan_info.update({ 'dashou_biaoshi': dashou_biaoshi, 'shangjia_biaoshi': shangjia_biaoshi, 'you_dashou': bool(dashou_biaoshi), }) response_data.update(dingdan_info) except Dingdan.DoesNotExist: logger.error(f"订单不存在 - 订单ID: {lianjie_obj.dingdan_id}") response_data.update({ 'jieshao': '', 'jiage': '0.00', 'shangjia_mingcheng': '', 'zhuangtai': None, 'dashou_biaoshi': '', 'shangjia_biaoshi': '', 'you_dashou': False, 'is_cross': 0, 'kefu_link': '', 'oss_url': '', 'default_avatar': 'a_long/morentouxiang.jpg', 'goeasy_appkey': '', 'self_user_id': '', }) except Exception as e: logger.error(f"获取订单信息异常: {str(e)}", exc_info=True) response_data.update({ 'jieshao': '', 'jiage': '0.00', 'shangjia_mingcheng': '', 'zhuangtai': None, 'dashou_biaoshi': '', 'shangjia_biaoshi': '', 'you_dashou': False, 'is_cross': 0, 'kefu_link': '', 'oss_url': '', 'default_avatar': 'a_long/morentouxiang.jpg', 'goeasy_appkey': '', 'self_user_id': '', }) else: # 如果订单ID为空 logger.warning(f"链接记录中订单ID为空 - 链接ID: {lianjie_obj.id}") response_data.update({ 'jieshao': '', 'jiage': '0.00', 'shangjia_mingcheng': '', 'zhuangtai': None, 'dashou_biaoshi': '', 'shangjia_biaoshi': '', 'you_dashou': False, 'is_cross': 0, 'kefu_link': '', 'oss_url': '', 'default_avatar': 'a_long/morentouxiang.jpg', 'goeasy_appkey': '', 'self_user_id': '', }) # 10. 判断订单状态是否允许修改 if not lianjie_obj.is_used and response_data.get('zhuangtai') == 13: response_data['allow_modify'] = True else: response_data['allow_modify'] = False # 11. 记录成功日志并返回 logger.info( f"返回订单信息成功 - Token: {token[:8]}..., IP: {client_ip}, " f"是否允许修改: {response_data['allow_modify']}, " f"是否有打手: {response_data.get('you_dashou', False)}" ) return Response({ 'code': 200, 'msg': '成功', 'data': response_data }, status=status.HTTP_200_OK) class KehuTianxieDingdanView(APIView): """ 客户填写订单信息接口 路径:POST /peizhi/kehutx """ # 添加频率限制(比查询接口更严格) throttle_classes = [OrderLinkThrottle] permission_classes = [] # 无需认证 def post(self, request): """ 处理POST请求,填写订单信息 """ # 1. 记录请求开始 logger.info("=== 客户填写订单信息接口开始 ===") # 2. 获取客户端真实IP client_ip = IPUtils.get_client_ip(request) logger.info(f"客户端IP: {client_ip}") # 3. 验证IP合法性 if not IPUtils.validate_ip(client_ip): logger.warning(f"IP格式不合法: {client_ip}") return Response({ 'code': 400, 'msg': '网络连接异常', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 4. 获取请求参数 data = request.data token = data.get('token', '') youxi_nicheng = data.get('youxi_nicheng', '').strip() zhiding_dashou = data.get('zhiding_dashou', '').strip() # 可选 beizhu = data.get('beizhu', '').strip() # 可选 # 4.0 临时关闭指定功能(如需开启,请删除或注释掉下面两行) #if zhiding_dashou: #return Response({'code': 8, 'msg': '指定功能尚未开启,请不要填写指定打手ID', 'data': None}) #if beizhu: #return Response({'code': 8, 'msg': '备注功能尚未开启,请不要填写备注', 'data': None}) logger.info( f"请求参数 - Token: {token[:8]}..., 昵称: {youxi_nicheng[:10]}..., 指定打手: {zhiding_dashou}, 备注长度: {len(beizhu)}") # 5. 验证必填参数 if not token: logger.warning(f"Token参数缺失 - IP: {client_ip}") return Response({ 'code': 400, 'msg': '缺少链接参数', 'data': None }, status=status.HTTP_400_BAD_REQUEST) if not youxi_nicheng: logger.warning(f"游戏昵称为空 - IP: {client_ip}") return Response({ 'code': 400, 'msg': '游戏昵称不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 6. 验证游戏昵称格式 if len(youxi_nicheng) > 50: logger.warning(f"游戏昵称过长 - IP: {client_ip}, 长度: {len(youxi_nicheng)}") return Response({ 'code': 400, 'msg': '游戏昵称不能超过50个字符', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 过滤危险字符 danger_chars = ['<', '>', "'", '"', '&', ';', '|'] for char in danger_chars: if char in youxi_nicheng: logger.warning(f"游戏昵称包含危险字符 - IP: {client_ip}, 字符: {char}") return Response({ 'code': 400, 'msg': '昵称包含非法字符', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 7. 验证指定打手ID格式(如果提供) if zhiding_dashou and len(zhiding_dashou) > 32: logger.warning(f"指定打手ID过长 - IP: {client_ip}, 长度: {len(zhiding_dashou)}") return Response({ 'code': 400, 'msg': '指定打手ID不能超过32个字符', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 8. 验证备注长度 if len(beizhu) > 500: logger.warning(f"备注过长 - IP: {client_ip}, 长度: {len(beizhu)}") return Response({ 'code': 400, 'msg': '备注不能超过500个字符', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 9. 查询链接记录 try: lianjie_obj = ShangjiaLianjie.objects.get(lianjie_token=token) logger.info( f"找到链接记录 - 链接ID: {lianjie_obj.id}, 商家ID: {lianjie_obj.yonghu_id}, 是否已使用: {lianjie_obj.is_used}") except ShangjiaLianjie.DoesNotExist: logger.warning(f"链接不存在 - Token: {token[:8]}..., IP: {client_ip}") return Response({ 'code': 404, 'msg': '链接不存在或已失效', 'data': None }, status=status.HTTP_404_NOT_FOUND) # 10. 检查链接有效期 now = timezone.now() if lianjie_obj.youxiao_shijian and lianjie_obj.youxiao_shijian < now: logger.warning(f"链接已过期 - 过期时间: {lianjie_obj.youxiao_shijian}") return Response({ 'code': 400, 'msg': '链接已过期', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 11. 检查链接是否已使用 if lianjie_obj.is_used: logger.warning(f"链接已使用 - 链接ID: {lianjie_obj.id}, IP: {client_ip}") return Response({ 'code': 400, 'msg': '链接已被使用,无法重复提交', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 12. 验证指定打手(如果需要) zhuangtai_xin = 1 # 默认普通订单状态 if zhiding_dashou: # 验证打手是否存在且有效 try: # 先查询用户主表 dashou_user = UserMain.objects.get(yonghuid=zhiding_dashou) logger.info(f"找到打手用户 - 用户ID: {zhiding_dashou}") # 通过反向关系获取打手扩展信息 try: dashou_profile = dashou_user.dashou_profile # 检查打手状态(zhuangtai) if dashou_profile.zhuangtai != 1: logger.warning(f"打手状态异常 - 打手ID: {zhiding_dashou}, 状态: {dashou_profile.zhuangtai}") return Response({ 'code': 400, 'msg': '指定打手当前无法接单', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 检查打手账号状态(zhanghaozhuangtai) if dashou_profile.zhanghaozhuangtai != 1: logger.warning( f"打手账号状态异常 - 打手ID: {zhiding_dashou}, 账号状态: {dashou_profile.zhanghaozhuangtai}") return Response({ 'code': 400, 'msg': '指定打手账号异常', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 验证通过,设置为指定订单状态 zhuangtai_xin = 7 # 指定打手订单状态 logger.info(f"指定打手验证通过 - 打手ID: {zhiding_dashou}, 昵称: {dashou_profile.nicheng}") except UserDashou.DoesNotExist: logger.warning(f"打手扩展信息不存在 - 用户ID: {zhiding_dashou}") return Response({ 'code': 400, 'msg': '指定打手信息不完整', 'data': None }, status=status.HTTP_400_BAD_REQUEST) except UserMain.DoesNotExist: logger.warning(f"指定打手用户不存在 - 用户ID: {zhiding_dashou}") return Response({ 'code': 400, 'msg': '指定打手不存在', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 13. 使用事务确保数据一致性 try: with transaction.atomic(): # 更新链接记录 lianjie_obj.shiyongzhe_ip = client_ip lianjie_obj.is_used = True lianjie_obj.save(update_fields=['shiyongzhe_ip', 'is_used']) logger.info(f"更新链接记录成功 - 链接ID: {lianjie_obj.id}, 设置为已使用") # 查询订单 if not lianjie_obj.dingdan_id: logger.error(f"链接记录中订单ID为空 - 链接ID: {lianjie_obj.id}") raise ValueError("订单ID为空") dingdan_obj = Dingdan.objects.get(dingdan_id=lianjie_obj.dingdan_id) # 检查订单状态是否为13(灰色状态) if dingdan_obj.zhuangtai != 13: logger.warning( f"订单状态不允许修改 - 订单ID: {dingdan_obj.dingdan_id}, 当前状态: {dingdan_obj.zhuangtai}") # 回滚事务 transaction.set_rollback(True) return Response({ 'code': 400, 'msg': '订单状态异常,无法修改', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 更新订单信息 dingdan_obj.nicheng = youxi_nicheng # 游戏昵称 dingdan_obj.beizhu = beizhu if beizhu else '' # 备注 dingdan_obj.zhuangtai = zhuangtai_xin # 新状态:1或7 # 如果是指定打手,设置zhiding_id if zhiding_dashou and zhuangtai_xin == 7: dingdan_obj.zhiding_id = zhiding_dashou dingdan_obj.save() logger.info( f"更新订单成功 - 订单ID: {dingdan_obj.dingdan_id}, 新状态: {zhuangtai_xin}, 指定打手: {zhiding_dashou or '无'}") # 构建通知数据(你的 _get_game_type_name 方法保留在视图里直接用) order_info = { 'dingdan_id': dingdan_obj.dingdan_id, 'game_type': self._get_game_type_name(dingdan_obj.leixing_id), 'amount': str(dingdan_obj.jine), 'order_desc': (dingdan_obj.jieshao or '')[:50], } dingdan_guangbo.delay(order_info) # 瞬间返回,不阻塞请求 # 获取商家信息用于返回 try: user_main = UserMain.objects.get(yonghuid=lianjie_obj.yonghu_id) shangjia_obj = user_main.shop_profile shangjia_mingcheng = shangjia_obj.nicheng except: shangjia_mingcheng = f"商家{lianjie_obj.yonghu_id}" logger.warning(f"获取商家昵称失败,使用默认 - 用户ID: {lianjie_obj.yonghu_id}") # ========== 新增:判断是否需要跨平台同步 ========== # 获取商家ID(从链接记录) shangjia_user_id = lianjie_obj.yonghu_id # 跨平台条件:商家ID在白名单、订单为普通状态(zhuangtai=1)、要求类型为1、会员ID等于配置值 sync_order_to_partners(dingdan_obj.dingdan_id) # ========== 新增结束 ========== # 14. 返回成功响应 response_data = { #'dingdan_id': dingdan_obj.dingdan_id, #'create_time': dingdan_obj.create_time.strftime('%Y-%m-%d %H:%M:%S'), #'shangpin_mingcheng': dingdan_obj.jieshao[:50] if dingdan_obj.jieshao else '', # 截取前50字符 #'jiage': str(dingdan_obj.jine) if dingdan_obj.jine else '0.00', 'shangjia_mingcheng': shangjia_mingcheng, 'youxi_nicheng': youxi_nicheng, 'zhuangtai': zhuangtai_xin, 'zhuangtai_text': '普通订单' if zhuangtai_xin == 1 else '指定打手订单' } logger.info(f"订单提交成功 - 订单ID: {dingdan_obj.dingdan_id}, IP: {client_ip}") # 新方法(行为驱动,更安全) update_shangjia_daily( yonghuid=lianjie_obj.yonghu_id, amount=Decimal(str(dingdan_obj.jine)), action=1 # 1 = 派发 ) return Response({ 'code': 200, 'msg': '提交成功', 'data': response_data }, status=status.HTTP_200_OK) except Dingdan.DoesNotExist: logger.error(f"订单不存在 - 订单ID: {lianjie_obj.dingdan_id}") return Response({ 'code': 500, 'msg': '订单数据异常', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) except Exception as e: logger.error(f"处理订单异常: {str(e)}", exc_info=True) return Response({ 'code': 500, 'msg': '系统错误,请稍后重试', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) def _get_game_type_name(self, leixing_id): try: gt = ShangpinLeixing.objects.filter(id=leixing_id).first() return gt.jieshao if gt else '游戏订单' except Exception as e: logger.error(f"获取游戏类型名称异常: {e}") return '游戏订单' def send_order_notification_async(self, dingdan, youxi_nicheng, zhiding_dashou, zhuangtai): """异步发送订单通知(客户填写订单后)""" try: # 避免阻塞主线程,使用线程异步执行 def notification_task(): try: # 获取游戏类型名称 try: leixing_obj = ShangpinLeixing.objects.get(id=dingdan.leixing_id) game_type = leixing_obj.jieshao except: game_type = '未知游戏' # 构建订单信息 order_info = { 'dingdan_id': dingdan.dingdan_id, 'game_type': game_type, 'amount': str(dingdan.jine), 'order_desc': dingdan.jieshao[:50] if dingdan.jieshao else '新订单', 'youxi_nicheng': youxi_nicheng, 'order_type': '指定订单' if zhuangtai == 7 else '普通订单', 'zhiding_dashou': zhiding_dashou or '' } # 调用微信广播发送器 sender = WeixinBroadcastSender() result = sender.broadcast_order(order_info) if result['success']: logger.info(f"【客户填写订单】微信通知发送成功: {result['msg']}") else: logger.warning(f"【客户填写订单】微信通知发送失败: {result['msg']}") except Exception as e: logger.error(f"【客户填写订单】发送通知任务异常: {str(e)}") # 启动异步线程 thread = threading.Thread(target=notification_task) thread.daemon = True thread.start() except Exception as e: logger.error(f"【客户填写订单】启动通知线程异常: {str(e)}") class GuanZhuAListView(APIView): """ 获取关注阿龙页面图片列表 POST /peizhi/gzal 需要JWT认证 返回: { "code": 0, "msg": "success", "data": { "images": ["相对URL1", "相对URL2", ...], "last_update": "2025-03-16 12:34:56", "visit_count": 123 } } """ permission_classes = [IsAuthenticated] def post(self, request): try: # 查询类型为3的所有记录 qs = Tupianpeizhi.objects.filter(leixing=3) if not qs.exists(): return Response({ 'code': 0, 'msg': 'success', 'data': { 'images': [], 'last_update': None, 'visit_count': 0 } }, status=status.HTTP_200_OK) # 获取所有图片的相对URL images = list(qs.values_list('tupian', flat=True)) # 获取最新更新时间(取所有记录的update_time最大值) last_update = qs.aggregate(Max('update_time'))['update_time__max'] last_update_str = last_update.strftime('%Y-%m-%d %H:%M:%S') if last_update else None # 原子性增加访问次数 # 先更新所有记录,再获取更新后的最大次数 qs.update(fangwen_cishu=F('fangwen_cishu') + 1) visit_count = qs.aggregate(Max('fangwen_cishu'))['fangwen_cishu__max'] or 0 return Response({ 'code': 0, 'msg': 'success', 'data': { 'images': images, 'last_update': last_update_str, 'visit_count': visit_count } }, status=status.HTTP_200_OK) except Exception as e: logger.error(f"获取关注阿龙图片列表失败: {e}", exc_info=True) return Response({ 'code': 1, 'msg': '服务器内部错误', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) #不用缓存,一天2000次 class GuanshiQRCodeView(APIView): permission_classes = [permissions.IsAuthenticated] def post(self, request): logger.info("===== 开始生成管事二维码 =====") current_user = request.user logger.info(f"当前用户ID: {current_user.yonghuid}") try: try: guanshi = current_user.guanshi_profile logger.info(f"管事扩展表查询成功,状态: {guanshi.zhuangtai}") except UserGuanshi.DoesNotExist: logger.warning(f"用户 {current_user.yonghuid} 不是管事身份") return Response( {'code': 403, 'message': '当前用户不是管事身份', 'data': None}, status=status.HTTP_403_FORBIDDEN ) if guanshi.zhuangtai != 1: logger.warning(f"用户 {current_user.yonghuid} 管事状态异常: {guanshi.zhuangtai}") return Response( {'code': 403, 'message': '管事账号状态异常', 'data': None}, status=status.HTTP_403_FORBIDDEN ) invite_code = guanshi.yaoqingma if not invite_code: logger.info(f"用户 {current_user.yonghuid} 无邀请码,开始生成") invite_code = CreateInvitationCode(str(current_user.yonghuid)) if not VerifyInvitationCode(invite_code): logger.error(f"邀请码唯一性校验失败,用户: {current_user.yonghuid}") return Response( {'code': 500, 'message': '邀请码生成失败', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) guanshi.yaoqingma = invite_code guanshi.save(update_fields=['yaoqingma']) logger.info(f"邀请码生成成功: {invite_code}") else: logger.info(f"使用已有邀请码: {invite_code}") # 直接获取新的 access_token,不缓存 access_token = self.get_fresh_access_token() if not access_token: logger.error("获取 access_token 失败") return Response( {'code': 500, 'message': '获取微信access_token失败', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) logger.info(f"获取到 access_token (前10位): {access_token[:10]}...") encoded_invite = urllib.parse.quote(invite_code, safe='') page_path = f'pages/dashouduan/dashouduan?inviteCode={encoded_invite}' wx_url = f'https://api.weixin.qq.com/wxa/getwxacode?access_token={access_token}' post_data = { 'path': page_path, 'width': 430, 'auto_color': False, 'line_color': {'r': 0, 'g': 0, 'b': 0} } logger.info(f"调用微信接口生成小程序码,path: {page_path}") try: wx_resp = requests.post(wx_url, json=post_data, timeout=10) logger.info(f"微信接口响应状态码: {wx_resp.status_code}") 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 ) if wx_resp.status_code != 200: error_info = {} if wx_resp.headers.get('content-type') == 'application/json': try: error_info = wx_resp.json() except: pass errmsg = error_info.get('errmsg', '未知错误') logger.error(f"微信接口返回错误 (HTTP {wx_resp.status_code}): {errmsg}") if 'errcode' in error_info: logger.error(f"微信错误码: {error_info.get('errcode')}") return Response( {'code': 500, 'message': f'生成二维码失败: {errmsg}', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) content_type = wx_resp.headers.get('content-type', '') if 'image' not in content_type: try: error_info = wx_resp.json() errmsg = error_info.get('errmsg', '未知错误') logger.error(f"微信接口返回非图片内容: {errmsg}") return Response( {'code': 500, 'message': f'生成二维码失败: {errmsg}', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) except: logger.error("微信接口返回未知内容,无法解析为JSON") return Response( {'code': 500, 'message': '生成二维码失败', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) timestamp = int(time.time()) filename = f"{current_user.yonghuid}_{timestamp}.png" oss_path = f"erweima/guanshi/{filename}" file_obj = io.BytesIO(wx_resp.content) logger.info(f"开始上传图片到COS: {oss_path}") try: full_url = upload_to_oss(file_obj, oss_path) logger.info(f"上传成功,完整URL: {full_url}") except Exception as e: logger.error(f"上传到COS异常: {str(e)}", exc_info=True) return Response( {'code': 500, 'message': '图片上传失败', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) if not full_url: logger.error("upload_to_oss 返回了空 URL") return Response( {'code': 500, 'message': '图片上传失败', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) cos_domain = getattr(settings, 'COS_DOMAIN', '') if cos_domain and full_url.startswith(cos_domain): relative_path = full_url[len(cos_domain):].lstrip('/') else: relative_path = oss_path logger.info(f"相对路径: {relative_path}") old_url = guanshi.invite_qrcode_url if old_url: logger.info(f"删除旧二维码: {old_url}") try: delete_from_oss(old_url) except Exception as e: logger.warning(f"删除旧二维码失败(不影响主流程): {str(e)}") with transaction.atomic(): guanshi.invite_qrcode_url = relative_path guanshi.save(update_fields=['invite_qrcode_url']) logger.info("数据库更新成功") return Response({ 'code': 0, 'message': 'success', 'data': {'url': relative_path} }) 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 get_fresh_access_token(self): """ 直接请求微信接口获取新的 access_token,不缓存,保证最新。 """ appid = getattr(settings, 'WEIXIN_APPID', '') secret = getattr(settings, 'WEIXIN_SECRET', '') if not appid or not secret: logger.error("微信小程序配置缺失: WEIXIN_APPID 或 WEIXIN_SECRET 未设置") return None url = 'https://api.weixin.qq.com/cgi-bin/token' params = { 'grant_type': 'client_credential', 'appid': appid, 'secret': secret } try: logger.info("正在请求微信接口获取最新 access_token...") resp = requests.get(url, params=params, timeout=5) data = resp.json() new_token = data.get('access_token') if new_token: logger.info("成功获取最新 access_token") return new_token else: errcode = data.get('errcode', '未知') errmsg = data.get('errmsg', '未知错误') logger.error(f"获取 access_token 失败,errcode={errcode}, errmsg={errmsg}") return None except Exception as e: logger.error(f"请求微信 access_token 接口异常: {str(e)}", exc_info=True) return None class ZuzhangHaibaoView(APIView): """ 组长专属海报二维码生成接口 路径:/peizhi/zuzhanghb 方法:POST 权限:JWT认证,仅限组长身份 返回: { "code": 0, "message": "success", "data": { "url": "erweima/zuzhang/1000001_1234567890.png", "yaoqingma": "ABC123XYZ" } } """ permission_classes = [permissions.IsAuthenticated] def post(self, request): current_user = request.user # 1. 验证组长身份 try: zuzhang = current_user.zuzhang_profile except UserZuzhang.DoesNotExist: return Response({ 'code': 403, 'message': '当前用户不是组长身份', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 2. 验证组长账号状态 if zuzhang.zhuangtai != 1: return Response({ 'code': 403, 'message': '组长账号状态异常,无法生成海报', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 3. 获取或生成邀请码 invite_code = zuzhang.yaoqingma if not invite_code: invite_code = CreateInvitationCode(str(current_user.yonghuid)) if not VerifyInvitationCode(invite_code): logger.error(f"组长邀请码生成失败,用户: {current_user.yonghuid}") return Response({ 'code': 500, 'message': '邀请码生成失败', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) zuzhang.yaoqingma = invite_code zuzhang.save(update_fields=['yaoqingma']) # 4. 获取微信 access_token access_token = self.get_cached_access_token() if not access_token: return Response({ 'code': 500, 'message': '获取微信access_token失败', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # ===== 修改点:使用 A 接口(getwxacode) ===== # 对邀请码进行 URL 编码,防止特殊字符破坏路径 encoded_invite = urllib.parse.quote(invite_code, safe='') # 构造带参数的完整路径(请根据实际页面调整) page_path = f'pages/guanshiduan/guanshiduan?inviteCode={encoded_invite}' wx_url = f'https://api.weixin.qq.com/wxa/getwxacode?access_token={access_token}' post_data = { 'path': page_path, 'width': 430, 'auto_color': False, 'line_color': {'r': 0, 'g': 0, 'b': 0} } try: wx_resp = requests.post(wx_url, json=post_data, timeout=10) except Exception as e: logger.error(f"请求微信接口失败: {e}") return Response({ 'code': 500, 'message': '调用微信服务失败', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 处理微信返回结果 if wx_resp.status_code != 200: error_info = wx_resp.json() if wx_resp.headers.get('content-type') == 'application/json' else {} errmsg = error_info.get('errmsg', '未知错误') logger.error(f"微信接口返回错误 (HTTP {wx_resp.status_code}): {errmsg}") return Response({ 'code': 500, 'message': f'生成二维码失败: {errmsg}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) content_type = wx_resp.headers.get('content-type', '') if 'image' not in content_type: try: error_info = wx_resp.json() errmsg = error_info.get('errmsg', '未知错误') logger.error(f"微信接口返回非图片内容: {errmsg}") return Response({ 'code': 500, 'message': f'生成二维码失败: {errmsg}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) except: logger.error("微信接口返回未知内容") return Response({ 'code': 500, 'message': '生成二维码失败', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 5. 上传到腾讯云 COS timestamp = int(time.time()) filename = f"{current_user.yonghuid}_{timestamp}.png" oss_path = f"erweima/zuzhang/{filename}" file_obj = io.BytesIO(wx_resp.content) try: full_url = upload_to_oss(file_obj, oss_path) except Exception as e: logger.error(f"上传到COS失败: {e}") return Response({ 'code': 500, 'message': '图片上传失败', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) if not full_url: return Response({ 'code': 500, 'message': '图片上传失败', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 6. 获取相对路径 cos_domain = getattr(settings, 'COS_DOMAIN', '') if cos_domain and full_url.startswith(cos_domain): relative_path = full_url[len(cos_domain):].lstrip('/') else: relative_path = oss_path # 7. 删除旧二维码文件 old_url = zuzhang.haibao_url if old_url: delete_from_oss(old_url) # 8. 更新数据库中的 haibao_url 字段 with transaction.atomic(): zuzhang.haibao_url = relative_path zuzhang.save(update_fields=['haibao_url']) # 9. 返回邀请码和二维码相对路径 return Response({ 'code': 0, 'message': 'success', 'data': { 'url': relative_path, 'yaoqingma': invite_code } }) def get_cached_access_token(self): """从缓存获取微信 access_token""" cache_key = 'wx_mini_access_token' token = cache.get(cache_key) if token: return token appid = getattr(settings, 'WEIXIN_APPID', '') secret = getattr(settings, 'WEIXIN_SECRET', '') if not appid or not secret: logger.error("微信小程序配置缺失") return None url = f'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={appid}&secret={secret}' try: resp = requests.get(url, timeout=5) data = resp.json() token = data.get('access_token') expires_in = data.get('expires_in', 7200) if token: cache.set(cache_key, token, 7000) return token except Exception as e: logger.error(f"获取微信access_token失败: {e}") return None class PopupConfigView(APIView): """ 获取指定页面的弹窗配置 请求格式:POST { "pageKey": "dashouzhongxin" } 返回格式:{ "code": 0, "data": { "serverTime": "...", "popups": [...] } } """ permission_classes = [IsAuthenticated] # JWT 认证 def post(self, request): # 1. 获取当前用户(已通过JWT认证,可直接使用) user = request.user if not user or not user.id: return Response({ 'code': 401, 'msg': '用户未登录或认证失败' }, status=status.HTTP_401_UNAUTHORIZED) # 2. 获取参数 pageKey page_key = request.data.get('pageKey') if not page_key: return Response({ 'code': 400, 'msg': '缺少参数 pageKey' }, status=status.HTTP_400_BAD_REQUEST) # 3. 查询页面配置 try: popup_page = PopupPage.objects.get(page_key=page_key, is_active=True) except PopupPage.DoesNotExist: # 该页面没有配置弹窗,返回空 return Response({ 'code': 0, 'data': { 'serverTime': timezone.now().isoformat(), 'popups': [] } }) # 4. 查询该页面下所有启用的弹窗(且在有效时间范围内) now = timezone.now() popups = PopupConfig.objects.filter( page=popup_page, is_active=True, start_time__lte=now, end_time__gte=now ).prefetch_related('images') # 预加载图片,避免N+1查询 # 按 sort_order 排序(model Meta 已定义) # 5. 序列化 serializer = PopupConfigSerializer(popups, many=True) # 6. 返回数据 return Response({ 'code': 0, 'data': { 'serverTime': now.isoformat(), 'ignore_user_mute': popup_page.ignore_user_mute, # 新增 'popups': serializer.data } }) class GetWithdrawModeView(APIView): """ 获取提现模式配置 前端请求 /peizhi/hqtxzsym 返回格式: { "code": 0, "data": { "mode": 1 or 2 } } """ permission_classes = [IsAuthenticated] # JWT 认证 def post(self, request, *args, **kwargs): # 尝试获取 ID=1 的记录 config = WithdrawConfig.objects.filter(id=1).first() # 如果存在则取实际值,否则默认 2(手动提现) mode = config.mode if config else 2 return Response({ 'code': 0, 'data': { 'mode': mode } }) class CheckPhoneAuthView(APIView): """ 判断当前登录用户是否需要强制手机号认证 规则: - 已认证过手机号(shoujihao_renzheng=True) → 不需要 - 未认证时,检查用户角色及其业务指标是否达到阈值: 打手:有会员购买记录(Huiyuangoumai 中存在记录) 商家:发布订单总数(fabu)>= 5 管事:充值分佣总额(chongzhifenrun)>= 15 组长:分佣总额(fenyong_zonge)>= 2 老板:消费总额(zonge)>= 20 - 满足任一角色的阈值 → 需要认证 - 否则 → 不需要认证 """ permission_classes = [IsAuthenticated] def post(self, request): user = request.user # UserMain 实例 # 已经认证过手机号,直接放行 if user.shoujihao_renzheng: return Response({'need_auth': False}) # 检查各角色是否满足阈值条件 if self._dashou_meets_threshold(user): return Response({'need_auth': True}) if self._shangjia_meets_threshold(user): return Response({'need_auth': True}) if self._guanshi_meets_threshold(user): return Response({'need_auth': True}) if self._zuzhang_meets_threshold(user): return Response({'need_auth': True}) if self._boss_meets_threshold(user): return Response({'need_auth': True}) # 都不满足,无需认证 return Response({'need_auth': False}) # ==================== 各角色阈值判断方法 ==================== def _dashou_meets_threshold(self, user): """ 打手条件:存在会员购买记录(Huiyuangoumai 表中有该用户的记录) """ try: # 先确认用户是打手 UserDashou.objects.get(user=user) except UserDashou.DoesNotExist: return False # 检查是否有任意会员购买记录 return Huiyuangoumai.objects.filter(yonghu_id=user.yonghuid).exists() def _shangjia_meets_threshold(self, user): """商家条件:发布订单总数 fabu >= 5""" try: shop = UserShangjia.objects.get(user=user) except UserShangjia.DoesNotExist: return False return shop.fabu >= 5 def _guanshi_meets_threshold(self, user): """管事条件:充值分佣总额 chongzhifenrun >= 15""" try: guanshi = UserGuanshi.objects.get(user=user) except UserGuanshi.DoesNotExist: return False return guanshi.chongzhifenrun >= 15 def _zuzhang_meets_threshold(self, user): """组长条件:分佣总额 fenyong_zonge >= 2""" try: zuzhang = UserZuzhang.objects.get(user=user) except UserZuzhang.DoesNotExist: return False return zuzhang.fenyong_zonge >= 2 def _boss_meets_threshold(self, user): """老板条件:消费总额 zonge >= 20""" try: boss = UserBoss.objects.get(user=user) except UserBoss.DoesNotExist: return False return boss.zonge >= 20