# peizhi/views.py from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from rest_framework.throttling import AnonRateThrottle from django.db import models from houtai.utils import update_shangjia_daily from peizhi.models import Gonggao, Lunbo import logging from rest_framework.permissions import AllowAny # 新增这行 # 配置日志 logger = logging.getLogger(__name__) # peizhi/views.py 或新建 views_config.py import logging from rest_framework.views import APIView from rest_framework.permissions import AllowAny from rest_framework.parsers import JSONParser from .models import ClubConfig 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 = 'a_long/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) # peizhi/views.py from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status, permissions from rest_framework.parsers import MultiPartParser, FormParser from django.db import transaction from yonghu.models import UserMain, AdminProfile from .models import Tupianpeizhi, Qunpeizhi from dingdan.models import Lilubiao import json import os import uuid from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image 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) # AdminUpdateConfigView 类中的 post 方法,在 rates 部分 # 🔴【新增】打手提现抽成利率(类型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 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群配置 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 # 查找配置 qun_config = Qunpeizhi.objects.filter(peizhiid=peizhiid).first() if not qun_config: # 如果不存在,创建新的 qun_config = Qunpeizhi(peizhiid=peizhiid) # 更新字段(peizhiid不可改) qun_config.neirong = group_data.get('neirong', qun_config.neirong) or '' qun_config.qunid = group_data.get('qunid', qun_config.qunid) or '' qun_config.jieshao = group_data.get('jieshao', qun_config.jieshao) or '' qun_config.save() 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) 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 not platform_rate_obj: platform_rate_obj = Lilubiao.objects.create( lilu=platform_rate_float, fadanpingtai='1' ) else: platform_rate_obj.lilu = platform_rate_float platform_rate_obj.save() 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) 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='2').first() if not shop_rate_obj: shop_rate_obj = Lilubiao.objects.create( lilu=shop_rate_float, fadanpingtai='3' ) else: shop_rate_obj.lilu = shop_rate_float shop_rate_obj.save() 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) 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)''' from rest_framework.views import APIView from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from django.db.models import Q from decimal import Decimal import logging from yonghu.models import UserMain, UserDashou, UserBoss, UserShangjia, UserGuanshi logger = logging.getLogger(__name__) 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 # views.py from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from rest_framework.permissions import IsAuthenticated from django.db import connection from django.db.models import Q import time # views.py from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from rest_framework.permissions import IsAuthenticated from django.db import connection from django.db.models import Count import time # views.py from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from rest_framework.permissions import IsAuthenticated from django.db import connection import time # views.py from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from rest_framework.permissions import IsAuthenticated from django.db import connection from django.db.models import Q import time 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)}") import traceback 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)}") import traceback traceback.print_exc() return Response({ 'code': 500, 'message': '系统错误', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # views/shangjia_moban.py from rest_framework.views import APIView from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from django.db import transaction from decimal import Decimal import re from .models import ShangjiaMoban from shangpin.models import ShangpinLeixing # 假设商品模型路径 '''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() jiage = request.data.get('jiage', '') # 3. 验证必填字段 if not all([leixing_id, jieshao, jiage]): return Response({ 'code': 400, 'msg': '缺少必填字段', 'data': {} }, status=400) # 4. 验证价格格式 try: jiage_decimal = Decimal(jiage) if jiage_decimal <= 0 or jiage_decimal > 10000: return Response({ 'code': 400, 'msg': '价格必须在0.1-10000元之间', 'data': {} }, status=400) except (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. 验证商品类型是否存在且可用 try: # 使用select_related优化查询,只获取需要的字段 shangpin_obj = ShangpinLeixing.objects.filter( id=leixing_id, yaoqiuleixing=1 # 假设1为正常状态 ).only('id', 'yaoqiuleixing').first() if not shangpin_obj: return Response({ 'code': 404, 'msg': '商品类型不存在或已禁用', 'data': {} }, status=404) # 检查抢单要求类型 if shangpin_obj.yaoqiuleixing != 1: return Response({ 'code': 400, 'msg': '该商品类型不支持创建模板', 'data': {} }, status=400) except ShangpinLeixing.DoesNotExist: return Response({ 'code': 404, 'msg': '商品类型不存在', 'data': {} }, status=404) # 7. 创建模板(使用事务保证数据一致性) with transaction.atomic(): # 检查是否已存在相同介绍(根据unique_together约束) existing_moban = ShangjiaMoban.objects.filter( yonghu_id=yonghu_id, leixing_id=leixing_id, moban_jieshao=jieshao, #zhuangtai=1 # 只检查正常状态的模板 ).exists() if existing_moban: return Response({ 'code': 400, 'msg': '已存在相同介绍的模板', 'data': {} }, status=400) # 创建新模板 moban = ShangjiaMoban.objects.create( yonghu_id=yonghu_id, leixing_id=leixing_id, moban_jieshao=jieshao, jiage=jiage_decimal, fabu_shuliang=0, #zhuangtai=1 ) # 8. 返回成功响应(与前端字段对应) return Response({ 'code': 200, 'msg': '模板创建成功', 'data': { 'mobanId': moban.id, # 模板ID 'shangpinTypeId': leixing_id, # 商品类型ID 'jieshao': moban.moban_jieshao, # 模板介绍 'jiage': str(moban.jiage), # 价格(转为字符串) 'fabushuliang': moban.fabu_shuliang, # 发布数量 'createTime': moban.create_time.strftime('%Y-%m-%d %H:%M:%S') # 创建时间 } }) except Exception as e: # 记录错误日志 import logging logger = logging.getLogger(__name__) logger.error(f"创建商家模板失败: {str(e)}", exc_info=True) return Response({ 'code': 500, 'msg': '服务器内部错误', 'data': {} }, status=500) # views/shangjia_moban.py from django.core.paginator import Paginator from django.db.models import Q class ShangjiaMobanListView(APIView): """ 商家订单模板列表接口 POST /peizhi/sjddmblb 权限: 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') get_type = request.data.get('getType', 2) # 1=搜索,2=普通获取 page = int(request.data.get('page', 1)) page_size = int(request.data.get('pageSize', 50)) search_keyword = request.data.get('searchKeyword', '').strip() # 3. 验证必填字段 if not leixing_id: return Response({ 'code': 400, 'msg': '商品类型ID不能为空', 'data': {} }, status=400) if get_type not in [1, 2]: return Response({ 'code': 400, 'msg': '获取类型参数错误', 'data': {} }, status=400) # 4. 构建查询条件 query_conditions = Q( yonghu_id=yonghu_id, leixing_id=leixing_id, #zhuangtai=1 # 只查询正常状态的模板 ) # 5. 搜索模式处理 if get_type == 1 and search_keyword: # 搜索模式:按模板ID搜索 try: moban_id = int(search_keyword) query_conditions &= Q(id=moban_id) except ValueError: return Response({ 'code': 400, 'msg': '搜索关键词必须是有效的模板ID', 'data': {} }, status=400) # 6. 执行查询(使用values优化性能) queryset = ShangjiaMoban.objects.filter(query_conditions) # 7. 计算总数和分页 total_count = queryset.count() # 搜索模式不进行分页,直接返回所有匹配结果 if get_type == 1: moban_list = queryset.values( 'id', # 模板ID 'leixing_id', # 商品类型ID 'moban_jieshao', # 模板介绍 'jiage', # 价格 'fabu_shuliang', # 发布数量 'create_time' # 创建时间 ) # 格式化数据(与前端字段对应) formatted_list = [] for item in moban_list: formatted_list.append({ 'mobanId': item['id'], 'shangpinTypeId': item['leixing_id'], 'jieshao': item['moban_jieshao'], 'jiage': str(item['jiage']), # 转为字符串 'fabushuliang': item['fabu_shuliang'], 'createTime': item['create_time'].strftime('%Y-%m-%d %H:%M:%S') if item['create_time'] else '' }) return Response({ 'code': 200, 'msg': '搜索成功', 'data': { 'list': formatted_list, 'total': len(formatted_list), 'hasMore': False, # 搜索模式不分页,没有更多数据 'currentPage': 1, 'pageSize': len(formatted_list) } }) # 8. 普通获取模式:分页处理 else: # 使用Django分页器 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) # 获取数据并格式化 moban_list = current_page.object_list.values( 'id', # 模板ID 'leixing_id', # 商品类型ID 'moban_jieshao', # 模板介绍 'jiage', # 价格 'fabu_shuliang', # 发布数量 'create_time' # 创建时间 ) # 格式化数据(与前端字段对应) formatted_list = [] for item in moban_list: formatted_list.append({ 'mobanId': item['id'], 'shangpinTypeId': item['leixing_id'], 'jieshao': item['moban_jieshao'], 'jiage': str(item['jiage']), # 转为字符串 'fabushuliang': item['fabu_shuliang'], 'createTime': item['create_time'].strftime('%Y-%m-%d %H:%M:%S') if item['create_time'] else '' }) # 判断是否有更多数据 has_more = current_page.has_next() # 特殊逻辑:如果当前页数据量不足page_size,说明没有更多数据 if len(formatted_list) < page_size: has_more = False return Response({ 'code': 200, 'msg': '获取成功', 'data': { 'list': formatted_list, 'total': total_count, 'hasMore': has_more, # 是否允许上拉刷新 'currentPage': page, 'pageSize': page_size, 'totalPages': paginator.num_pages } }) except Exception as e: # 记录错误日志 import logging logger = logging.getLogger(__name__) logger.error(f"获取商家模板列表失败: {str(e)}", exc_info=True) return Response({ 'code': 500, 'msg': '服务器内部错误', 'data': {} }, status=500) # views/shangjia_moban.py class ShangjiaGengxinMobanView(APIView): """ 商家更新订单模板接口 POST /peizhi/sjddmbgx 权限: JWT认证用户 """ permission_classes = [IsAuthenticated] def post(self, request): try: # 1. 获取用户ID user = request.user yonghu_id = user.yonghuid # 2. 获取请求参数 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', '') # 3. 验证必填字段 if not all([moban_id, leixing_id]): return Response({ 'code': 400, 'msg': '模板ID和商品类型ID不能为空', 'data': {} }, status=400) # 4. 检查是否有修改内容 if not new_jieshao and not new_jiage: return Response({ 'code': 400, 'msg': '没有修改内容', 'data': {} }, status=400) # 5. 验证修改内容 update_fields = {} # 验证介绍 if new_jieshao: if len(new_jieshao) > 100: return Response({ 'code': 400, 'msg': '订单介绍不能超过100字', 'data': {} }, status=400) update_fields['moban_jieshao'] = new_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) update_fields['jiage'] = jiage_decimal except (ValueError, TypeError): return Response({ 'code': 400, 'msg': '价格格式错误', 'data': {} }, status=400) # 6. 查询模板是否存在且属于当前用户 try: moban = ShangjiaMoban.objects.get( id=moban_id, yonghu_id=yonghu_id, leixing_id=leixing_id, #zhuangtai=1 # 只修改正常状态的模板 ) except ShangjiaMoban.DoesNotExist: return Response({ 'code': 404, 'msg': '模板不存在或无权修改', 'data': {} }, status=404) # 7. 检查介绍是否重复(如果修改了介绍) if 'moban_jieshao' in update_fields: existing_moban = ShangjiaMoban.objects.filter( yonghu_id=yonghu_id, leixing_id=leixing_id, moban_jieshao=update_fields['moban_jieshao'], #zhuangtai=1 ).exclude(id=moban_id).exists() # 排除自身 if existing_moban: return Response({ 'code': 400, 'msg': '已存在相同介绍的模板', 'data': {} }, status=400) # 8. 更新模板(使用update_fields优化,只更新修改的字段) with transaction.atomic(): for field, value in update_fields.items(): setattr(moban, field, value) moban.save(update_fields=list(update_fields.keys())) # 9. 获取更新后的数据 updated_moban = ShangjiaMoban.objects.filter(id=moban_id).values( 'id', 'leixing_id', 'moban_jieshao', 'jiage', 'fabu_shuliang', 'update_time' ).first() # 10. 返回成功响应 return Response({ 'code': 200, 'msg': '模板修改成功', 'data': { 'mobanId': updated_moban['id'], 'shangpinTypeId': updated_moban['leixing_id'], 'jieshao': updated_moban['moban_jieshao'], 'jiage': str(updated_moban['jiage']), 'fabushuliang': updated_moban['fabu_shuliang'], 'updateTime': updated_moban['update_time'].strftime('%Y-%m-%d %H:%M:%S') if updated_moban[ 'update_time'] else '' } }) except Exception as e: # 记录错误日志 import logging logger = logging.getLogger(__name__) logger.error(f"修改商家模板失败: {str(e)}", exc_info=True) return Response({ 'code': 500, 'msg': '服务器内部错误', 'data': {} }, status=500) # views/shangjia_moban.py class ShangjiaShanchuMobanView(APIView): """ 商家删除订单模板接口 POST /peizhi/sjddmbsc 权限: JWT认证用户 """ permission_classes = [IsAuthenticated] def post(self, request): try: # 1. 获取用户ID user = request.user yonghu_id = user.yonghuid # 2. 获取请求参数 moban_id = request.data.get('mobanId') leixing_id = request.data.get('shangpinTypeId') # 3. 验证必填字段 if not all([moban_id, leixing_id]): return Response({ 'code': 400, 'msg': '模板ID和商品类型ID不能为空', 'data': {} }, status=400) # 4. 查询模板是否存在且属于当前用户 try: moban = ShangjiaMoban.objects.get( id=moban_id, yonghu_id=yonghu_id, leixing_id=leixing_id, #zhuangtai=1 # 只删除正常状态的模板 ) except ShangjiaMoban.DoesNotExist: return Response({ 'code': 404, 'msg': '模板不存在或无权删除', 'data': {} }, status=404) # 5. 检查模板是否已被使用(有发布数量) if moban.fabu_shuliang > 0: # 可以选择软删除,将状态改为已删除 moban.zhuangtai = 4 # 4=已删除状态 moban.save() delete_type = 'soft' else: # 硬删除 moban.delete() delete_type = 'hard' # 6. 返回成功响应 return Response({ 'code': 200, 'msg': '模板删除成功', 'data': { 'mobanId': moban_id, 'deleteType': delete_type, 'shangpinTypeId': leixing_id } }) except Exception as e: # 记录错误日志 import logging logger = logging.getLogger(__name__) logger.error(f"删除商家模板失败: {str(e)}", exc_info=True) return Response({ 'code': 500, 'msg': '服务器内部错误', 'data': {} }, status=500)''' from django.core.paginator import Paginator from django.db.models import Q, Prefetch from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated from peizhi.models import ShangjiaMoban from dengji.models import Chenghao 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) from dengji.models import Chenghao from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated from django.db.models import Prefetch 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) from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated from peizhi.models import ShangjiaMoban 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: import logging logger = logging.getLogger(__name__) logger.error(f"删除模板失败: {str(e)}", exc_info=True) return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500) from decimal import Decimal, InvalidOperation from django.db import transaction from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated from peizhi.models import ShangjiaMoban 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: import logging logger = logging.getLogger(__name__) logger.error(f"修改商家模板失败: {str(e)}", exc_info=True) return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500) from dengji.models import DingdanBiaoqian import random # 必须导入 import secrets import os import time import random # 必须导入 import hashlib import secrets import logging from decimal import Decimal from django.conf import settings from django.db import transaction from django.db.models import F from django.utils import timezone from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated from rest_framework import status from yonghu.models import UserShangjia from shangpin.models import ShangpinLeixing from dengji.models import Chenghao logger = logging.getLogger(__name__) 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) # 注意需要 import random 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) from django.core.paginator import Paginator from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated from peizhi.models import ShangjiaLianjie 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: import logging logger = logging.getLogger(__name__) logger.error(f"获取链接列表失败: {str(e)}", exc_info=True) return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500) import logging from peizhi.models import ShangjiaMoban, ShangjiaLianjie from shangpin.models import ShangpinLeixing from dingdan.models import Dingdan, DingdanShangjia # 假设利率表模型路径(根据你的项目结构调整) from dingdan.models import Lilubiao logger = logging.getLogger(__name__) # views/peizhi/sjljhq.py from rest_framework.views import APIView from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework import status from django.db import transaction from django.db.models import F from django.conf import settings from django.utils import timezone import time import random import os import logging import hashlib import secrets from decimal import Decimal logger = logging.getLogger(__name__) '''class ShangjiaGenerateLinkView(APIView): """商家生成发单链接接口""" permission_classes = [IsAuthenticated] # JWT Token认证 def post(self, request): # 1. 记录请求日志 logger.info(f"商家生成链接请求 - 用户ID: {request.user.yonghuid}, 数据: {request.data}") # 2. 获取前端参数 data = request.data shangpin_type_id = data.get('shangpinTypeId') # 商品类型ID moban_id = data.get('mobanId') # 模板ID # 3. 基本参数验证 if not all([shangpin_type_id, moban_id]): logger.warning(f"参数缺失 - shangpin_type_id: {shangpin_type_id}, moban_id: {moban_id}") return Response({ 'code': 400, 'msg': '缺少必填参数:订单类型ID和模板ID', 'data': None }, status=status.HTTP_400_BAD_REQUEST) try: shangpin_type_id = int(shangpin_type_id) moban_id = int(moban_id) except ValueError: return Response({ 'code': 400, 'msg': '参数格式错误,必须是数字', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 4. 获取商家扩展信息(使用反向查询,最高效的方式) try: shangjia = request.user.shop_profile except UserShangjia.DoesNotExist: logger.error(f"商家扩展信息不存在 - 用户ID: {request.user.yonghuid}") return Response({ 'code': 403, 'msg': '您不是商家或商家信息未完善', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 5. 验证商家状态 if shangjia.zhuangtai != 1: logger.warning(f"商家状态异常 - 用户ID: {request.user.yonghuid}, 状态: {shangjia.zhuangtai}") return Response({ 'code': 403, 'msg': '商家账号状态异常,请联系管理员', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 6. 查询商品类型信息 try: shangpin_leixing = ShangpinLeixing.objects.get(id=shangpin_type_id) except ShangpinLeixing.DoesNotExist: logger.error(f"商品类型不存在 - 类型ID: {shangpin_type_id}") return Response({ 'code': 400, 'msg': '商品类型不存在', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 提取商品类型信息 yaoqiuleixing = shangpin_leixing.yaoqiuleixing huiyuan_id = shangpin_leixing.huiyuan_id if shangpin_leixing.huiyuan_id else '' # yongjin 字段暂时不需要用到 # 7. 查询商家订单模板(使用组合查询,最高效) try: moban = ShangjiaMoban.objects.get( yonghu_id=request.user.yonghuid, leixing_id=shangpin_type_id, id=moban_id ) except ShangjiaMoban.DoesNotExist: logger.error( f"商家模板不存在 - 用户ID: {request.user.yonghuid}, 类型ID: {shangpin_type_id}, 模板ID: {moban_id}") return Response({ 'code': 400, 'msg': '订单模板不存在', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 获取模板价格和介绍 jiage = moban.jiage dingdan_jieshao = moban.moban_jieshao # 8. 验证商家余额 if jiage > shangjia.yue: logger.warning(f"商家余额不足 - 用户ID: {request.user.yonghuid}, 余额: {shangjia.yue}, 价格: {jiage}") return Response({ 'code': 400, 'msg': f'余额不足,当前余额{shangjia.yue}元', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 9. 查询利率(复用商家派单接口逻辑) try: lilu_obj = Lilubiao.objects.get(fadanpingtai='3') lilu = float(lilu_obj.lilu) if lilu <= 0: lilu = 1 # 默认值 logger.info(f"获取到利率 - 平台: 3, 利率: {lilu}") except Lilubiao.DoesNotExist: logger.warning("利率表不存在,使用默认利率1") lilu = 1 # 10. 计算打手分成 # 方法1:最安全的方式,兼容所有情况 dashou_fencheng = Decimal(str(round(float(jiage) * lilu, 2))) logger.info(f"计算打手分成 - 价格: {jiage}, 利率: {lilu}, 分成: {dashou_fencheng}") # 11. 生成订单ID(复用商家派单接口的逻辑) def generate_dingdan_id(): """生成唯一订单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}" # 🔴🆕【新增】生成安全Token的函数 def generate_secure_token(dingdan_id, shangjia_id): """ 生成安全的Token 算法:时间戳 + 订单ID + 商家ID + 随机数 → SHA256 → 截取前24位 """ timestamp = int(time.time() * 1_000_000) # 纳秒级精度 random_salt = secrets.token_hex(8) # 16位随机盐值 # 组合原始字符串 raw_string = f"{timestamp}:{dingdan_id}:{shangjia_id}:{random_salt}" # 使用SHA256哈希 hash_obj = hashlib.sha256(raw_string.encode('utf-8')) token = hash_obj.hexdigest()[:24] # 取前24位作为Token # 确保唯一性(理论上极小概率重复,但还是要检查) try: if ShangjiaLianjie.objects.filter(lianjie_token=token).exists(): # 如果重复,重新生成(极低概率) return generate_secure_token(dingdan_id, shangjia_id) except: pass return token # 🔴🆕【修改】生成链接函数 - 使用Token而不是订单ID def generate_link_url(token): """生成安全的H5页面链接 - 使用Token而不是订单ID""" base_url = getattr(settings, 'H5_DOMAIN', 'https://h5.yourdomain.com') # 确保base_url以/结尾 if not base_url.endswith('/'): base_url += '/' # 🔴🆕【关键修改】生成使用Token的链接 return f"{base_url}order/{token}" # 13. 使用事务确保数据一致性(这是最关键的步骤!) try: with transaction.atomic(): # 生成订单ID dingdan_id = generate_dingdan_id() # 🔴🆕【新增】生成安全Token secure_token = generate_secure_token(dingdan_id, request.user.yonghuid) # 创建订单主表(状态为13:灰度状态,等待老板填写) dingdan = Dingdan.objects.create( dingdan_id=dingdan_id, zhuangtai=13, # 🔴 核心:状态13表示灰度状态,等待老板填写 fadan_pingtai=2, # 商家发单 jine=jiage, dashou_fencheng=dashou_fencheng, # 以下字段留空,等待老板填写 jiedan_dashou_id='', dashou_liuyan='', zhiding_id='', shangpin_id=0, # 暂时设为0 leixing_id=shangpin_type_id, yaoqiuleixing=yaoqiuleixing if yaoqiuleixing else 1, huiyuan_id=huiyuan_id, yongjin=None, # 暂时不需要 jieshao=dingdan_jieshao[:2000], # 限制长度 beizhu='', # 备注为空,等待老板填写 nicheng='', # 🔴 游戏昵称为空,等待老板填写 tupian=request.user.avatar if request.user.avatar else '', user1_id=f"Sj{request.user.yonghuid}", # 商家标识 user2_id='', # 打手标识为空 ) logger.info(f"创建订单成功 - 订单ID: {dingdan_id}") # 创建商家订单扩展表 DingdanShangjia.objects.create( dingdan=dingdan, shangjia_id=request.user.yonghuid, sjnicheng=shangjia.nicheng if shangjia.nicheng else f"商家{request.user.yonghuid}", shangjia_pingjia='', cfliyou='', sqzhuangtai=0, bhliyou='' ) logger.info(f"创建商家订单扩展表成功 - 订单ID: {dingdan_id}") # 🔴 核心:更新商家统计信息和余额(使用F表达式,避免并发问题) 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, ) logger.info(f"更新商家统计信息成功 - 用户ID: {request.user.yonghuid}, 扣除金额: {jiage}") # 🔴🆕【修改】生成链接 - 使用Token而不是订单ID link_url = generate_link_url(secure_token) logger.info(f"生成链接 - Token: {secure_token}, 链接: {link_url}") # 🔴🆕【修改】保存链接到商家链接表 - 添加Token字段 lianjie_obj = ShangjiaLianjie.objects.create( yonghu_id=request.user.yonghuid, leixing_id=shangpin_type_id, moban_id=moban_id, lianjie=link_url, lianjie_token=secure_token, # 🔴🆕【新增】保存Token dingdan_id=dingdan_id, is_used=False, # 未使用 youxiao_shijian=timezone.now() + timezone.timedelta(days=1) # 24小时有效 ) logger.info(f"保存链接到数据库成功 - 链接ID: {lianjie_obj.id}, Token: {secure_token}") # 更新模板发布数量 ShangjiaMoban.objects.filter(id=moban_id).update( fabu_shuliang=F('fabu_shuliang') + 1, ) logger.info(f"更新模板发布数量成功 - 模板ID: {moban_id}") # 重新获取商家最新数据(因为余额已更新) shangjia.refresh_from_db() # 记录成功日志 logger.info( f"商家生成链接成功 - 用户ID: {request.user.yonghuid}, 订单ID: {dingdan_id}, Token: {secure_token}, 链接: {link_url}") # 14. 返回成功响应(字段必须与前端对应) return Response({ 'code': 200, 'msg': '链接生成成功', 'data': { 'linkUrl': link_url, # 🔴 前端需要这个字段 'newBalance': str(shangjia.yue), # 🔴 返回新的余额给前端更新 'dingdanId': dingdan_id, 'dingdanJieshao': dingdan_jieshao, 'jiage': str(jiage), 'youxiaoShijian': lianjie_obj.youxiao_shijian.strftime( '%Y-%m-%d %H:%M:%S') if lianjie_obj.youxiao_shijian else '', # 以下字段可根据前端需要添加 'jinriliushui': str(shangjia.jinriliushui), 'jinyueliushui': str(shangjia.jinyueliushui), 'jinridingdan': shangjia.jinridingdan, 'jinyuedingdan': shangjia.jinyuedingdan } }, status=status.HTTP_200_OK) except Exception as e: logger.error(f"生成链接失败 - 用户ID: {request.user.yonghuid}, 错误: {str(e)}", exc_info=True) return Response({ 'code': 500, 'msg': '系统错误,生成链接失败', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)''' # utils/ip_security.py import logging import re from django.utils import timezone from django.core.cache import cache from rest_framework.throttling import SimpleRateThrottle logger = logging.getLogger(__name__) 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}' # views/peizhi/khhqddlj.py import logging from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from django.utils import timezone from django.db.models import Q from .models import ShangjiaLianjie from dingdan.models import Dingdan, DingdanShangjia from yonghu.models import UserMain, UserShangjia from peizhi.models import ClubConfig '''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 # 默认使用我方配置 use_self_config = True target_club_id = None if is_cross == 1: # 跨平台订单 # 优先使用 partner_club_id if dingdan_obj.partner_club_id: target_club_id = dingdan_obj.partner_club_id use_self_config = False logger.info(f"跨平台订单,使用对方俱乐部ID: {target_club_id}") # 其次,如果有对方订单ID但没有俱乐部ID,使用默认俱乐部ID 000001 elif dingdan_obj.partner_order_id: target_club_id = '000001' use_self_config = False logger.info(f"跨平台订单有对方订单ID但无俱乐部ID,使用默认俱乐部ID: {target_club_id}") else: # 无任何对方信息,降级为我方配置 use_self_config = True logger.warning(f"订单标记跨平台但无对方俱乐部ID和对方订单ID,使用我方配置") else: # 非跨平台订单 use_self_config = True # 获取俱乐部配置 club_config = None if use_self_config: # 查询我方俱乐部配置 (is_self=1) try: club_config = ClubConfig.objects.filter(is_self=1).first() if club_config: logger.info(f"使用我方俱乐部配置,club_id={club_config.club_id}") else: logger.warning("未找到我方俱乐部配置(is_self=1)") except Exception as e: logger.error(f"查询我方俱乐部配置异常: {str(e)}", exc_info=True) else: # 使用确定的俱乐部ID查询对方配置 if target_club_id: try: club_config = ClubConfig.objects.filter(club_id=target_club_id).first() if club_config: logger.info(f"使用对方俱乐部配置,club_id={target_club_id}") else: logger.warning(f"未找到对方俱乐部配置,club_id={target_club_id}") except Exception as e: logger.error(f"查询对方俱乐部配置异常: {str(e)}", exc_info=True) if club_config: 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 '' # ========== 构建订单信息 ========== 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, } # 如果已使用,返回已填写的信息 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': '', }) 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': '', }) 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': '', }) # 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 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) import threading from utils.weixin_broadcast import WeixinBroadcastSender from dingdan.utils import sync_order_to_partners logger = logging.getLogger(__name__) 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 '无'}") # 🔴【添加】在订单更新成功后,异步发送微信服务号通知(原有) # 🟢【7. 新增】支付成功后的广播通知(异步,不阻塞主流程) from dingdan.tongzhi_tasks import dingdan_guangbo # 构建通知数据(你的 _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) ''' if (shangjia_user_id in settings.CROSS_PLATFORM_WHITELIST and dingdan_obj.zhuangtai == 1 and # 非指定单 dingdan_obj.yaoqiuleixing == 1 and dingdan_obj.huiyuan_id in ['VC7CTU', 'VUXP8D', 'VEX5NJ'] ): # 异步发送通知(使用独立模块) import threading # 若文件顶部已导入可省略,此处保留确保独立运行 from dingdan.cross_platform_tasks import send_partner_sync_order thread = threading.Thread( target=send_partner_sync_order, args=( dingdan_obj.dingdan_id, str(dingdan_obj.jine), str(dingdan_obj.dashou_fencheng), dingdan_obj.jieshao, dingdan_obj.beizhu, dingdan_obj.create_time.isoformat(), dingdan_obj.nicheng ) ) thread.daemon = True thread.start() logger.info(f"跨平台客户填写订单异步任务已启动: {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}") # ✅ 新方法(行为驱动,更安全) from houtai.utils import update_shangjia_daily 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: from shangpin.models import ShangpinLeixing 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)}") from datetime import timedelta logger = logging.getLogger(__name__) class AdminSelfBatchGenerateLinkView(APIView): """ 管理员批量生成商家发单链接接口 POST /dingdan/admin/batch-generate-link 仅允许白名单中的管理员调用,为指定商家(通过 phone+user_type=lianjie 确定)批量生成链接。 """ permission_classes = [IsAuthenticated] def post(self, request): # ==================== 管理员身份验证(与之前一致)==================== if not request.user.is_authenticated: return Response({ 'code': 401, 'message': '未认证', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) if request.user.user_type != 'admin': return Response({ 'code': 403, 'message': '权限不足', 'data': None }, status=status.HTTP_403_FORBIDDEN) admin_profile = getattr(request.user, 'admin_profile', None) if not admin_profile: return Response({ 'code': 403, 'message': '管理员信息不完整', 'data': None }, status=status.HTTP_403_FORBIDDEN) zhanghao = request.data.get('zhanghao') if not zhanghao or request.user.phone != zhanghao: return Response({ 'code': 401, 'message': '账号验证失败', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) allowed_phones = getattr(settings, 'ADMIN_BATCH_LINK_ALLOWED_PHONES', []) if not allowed_phones: logger.error("白名单配置为空,禁止所有管理员操作") return Response({ 'code': 500, 'message': '系统未配置可操作管理员,请联系开发', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) if request.user.phone not in allowed_phones: logger.warning(f"管理员 {request.user.phone} 不在白名单中") return Response({ 'code': 403, 'message': '您没有权限执行此操作', 'data': None }, status=status.HTTP_403_FORBIDDEN) # ==================== 管理员验证结束 ==================== # ========== 获取业务参数 ========== data = request.data shangpin_type_id = data.get('shangpinTypeId') moban_id = data.get('mobanId') count = data.get('count') if not all([shangpin_type_id, moban_id]): return Response({ 'code': 400, 'msg': '缺少必填参数:商品类型ID和模板ID', 'data': None }, status=status.HTTP_400_BAD_REQUEST) try: shangpin_type_id = int(shangpin_type_id) moban_id = int(moban_id) except ValueError: return Response({ 'code': 400, 'msg': '参数格式错误,必须是数字', 'data': None }, status=status.HTTP_400_BAD_REQUEST) default_count = getattr(settings, 'BATCH_LINK_DEFAULT_COUNT', 100) max_count = getattr(settings, 'BATCH_LINK_MAX_COUNT', 200) if count is None: gen_count = default_count else: try: gen_count = int(count) if gen_count <= 0: return Response({ 'code': 400, 'msg': '生成数量必须为正整数', 'data': None }, status=status.HTTP_400_BAD_REQUEST) if gen_count > max_count: return Response({ 'code': 400, 'msg': f'生成数量不能超过{max_count}', 'data': None }, status=status.HTTP_400_BAD_REQUEST) except ValueError: return Response({ 'code': 400, 'msg': '生成数量格式错误', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # ========== 🔴 新增:通过 phone 和 user_type 查询真正的用户ID ========== phone = request.user.phone # 当前管理员手机号 target_user_type = 'lianjie' # 目标用户类型,根据业务修改 try: target_user = UserMain.objects.get(phone=phone, user_type=target_user_type) real_yonghuid = target_user.yonghuid except UserMain.DoesNotExist: logger.error(f"未找到符合条件的用户: phone={phone}, user_type={target_user_type}") return Response({ 'code': 404, 'msg': '未找到符合条件的商家用户', 'data': None }, status=status.HTTP_404_NOT_FOUND) # ========== 新增结束 ========== # ========== 使用 real_yonghuid 获取商家扩展信息 ========== try: # 注意:这里使用 real_yonghuid 查找对应的商家扩展表 shangjia = UserShangjia.objects.get(user__yonghuid=real_yonghuid) except UserShangjia.DoesNotExist: logger.error(f"商家扩展信息不存在 - 用户ID: {real_yonghuid}") return Response({ 'code': 403, 'msg': '该用户不是商家或商家信息未完善', 'data': None }, status=status.HTTP_403_FORBIDDEN) if shangjia.zhuangtai != 1: logger.warning(f"商家状态异常 - 用户ID: {real_yonghuid}, 状态: {shangjia.zhuangtai}") return Response({ 'code': 403, 'msg': '商家账号状态异常,请联系管理员', 'data': None }, status=status.HTTP_403_FORBIDDEN) # ========== 查询商品类型与模板 ========== try: shangpin_leixing = ShangpinLeixing.objects.get(id=shangpin_type_id) except ShangpinLeixing.DoesNotExist: return Response({ 'code': 400, 'msg': '商品类型不存在', 'data': None }, status=status.HTTP_400_BAD_REQUEST) yaoqiuleixing = shangpin_leixing.yaoqiuleixing huiyuan_id = shangpin_leixing.huiyuan_id or '' # 模板必须属于查询出的真实用户 try: moban = ShangjiaMoban.objects.get( yonghu_id=real_yonghuid, leixing_id=shangpin_type_id, id=moban_id ) except ShangjiaMoban.DoesNotExist: return Response({ 'code': 400, 'msg': '订单模板不存在', 'data': None }, status=status.HTTP_400_BAD_REQUEST) jiage = moban.jiage dingdan_jieshao = moban.moban_jieshao # ========== 金额与余额 ========== try: price_decimal = Decimal(str(jiage)) total_price = price_decimal * gen_count except Exception as e: logger.error(f"金额计算错误: {e}") return Response({ 'code': 500, 'msg': '金额计算错误', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) if total_price > shangjia.yue: return Response({ 'code': 400, 'msg': f'余额不足,当前余额{shangjia.yue}元,需要{total_price}元', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # ========== 利率 ========== try: lilu_obj = Lilubiao.objects.get(fadanpingtai='3') lilu = float(lilu_obj.lilu) if lilu <= 0: lilu = 1 except Lilubiao.DoesNotExist: lilu = 1 dashou_fencheng = (price_decimal * Decimal(str(lilu))).quantize(Decimal('0.01')) # ========== 事务内批量生成 ========== try: with transaction.atomic(): # 锁定商家行 shangjia_locked = UserShangjia.objects.select_for_update().get(id=shangjia.id) if total_price > shangjia_locked.yue: return Response({ 'code': 400, 'msg': '余额不足,请刷新后重试', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 生成唯一订单ID和Token dingdan_ids = set() tokens = set() while len(dingdan_ids) < gen_count: dingdan_ids.add(self._generate_dingdan_id()) while len(tokens) < gen_count: tokens.add(self._generate_secure_token()) dingdan_id_list = list(dingdan_ids) token_list = list(tokens) # 构建订单对象列表 orders_to_create = [] for i in range(gen_count): orders_to_create.append(Dingdan( dingdan_id=dingdan_id_list[i], zhuangtai=13, fadan_pingtai=2, jine=jiage, dashou_fencheng=dashou_fencheng, leixing_id=shangpin_type_id, yaoqiuleixing=yaoqiuleixing or 1, huiyuan_id=huiyuan_id, jieshao=dingdan_jieshao[:2000], tupian=target_user.avatar or '', # 使用 target_user 的头像 user1_id=f"Sj{real_yonghuid}", user2_id='', zhiding_id='', dashou_liuyan='', beizhu='', nicheng='', shangpin_id=0, yongjin=None, )) # 批量插入订单(此时 orders 获得主键 id) created_orders = Dingdan.objects.bulk_create(orders_to_create) # 构建商家扩展对象列表(使用已保存的订单) extensions_to_create = [] # 构建商家扩展对象列表(使用循环 create,绝对安全) for order in created_orders: DingdanShangjia.objects.create( dingdan=order, # 直接使用订单对象,create 能正确处理 shangjia_id=real_yonghuid, sjnicheng=shangjia.nicheng or f"商家{real_yonghuid}", shangjia_pingjia='', cfliyou='', sqzhuangtai=0, bhliyou='' ) '''for order in created_orders: extensions_to_create.append(DingdanShangjia( dingdan=order.id, shangjia_id=real_yonghuid, sjnicheng=shangjia.nicheng or f"商家{real_yonghuid}", shangjia_pingjia='', cfliyou='', sqzhuangtai=0, bhliyou='' ))''' DingdanShangjia.objects.bulk_create(extensions_to_create) # 构建链接对象列表 base_url = getattr(settings, 'H5_DOMAIN', 'https://h5.yourdomain.com') if not base_url.endswith('/'): base_url += '/' links_to_create = [] for i, order in enumerate(created_orders): token = token_list[i] link_url = f"{base_url}order/{token}" links_to_create.append(ShangjiaLianjie( yonghu_id=real_yonghuid, leixing_id=shangpin_type_id, moban_id=moban_id, lianjie=link_url, lianjie_token=token, dingdan_id=order.dingdan_id, is_used=False, youxiao_shijian=timezone.now() + timedelta(days=1) )) ShangjiaLianjie.objects.bulk_create(links_to_create) # 更新商家统计信息 UserShangjia.objects.filter(id=shangjia.id).update( fabu=F('fabu') + gen_count, yue=F('yue') - total_price, jinridingdan=F('jinridingdan') + gen_count, jinriliushui=F('jinriliushui') + total_price, jinyuedingdan=F('jinyuedingdan') + gen_count, jinyueliushui=F('jinyueliushui') + total_price, ) # 更新模板发布数量 ShangjiaMoban.objects.filter(id=moban_id).update( fabu_shuliang=F('fabu_shuliang') + gen_count, ) # ========== 返回数据 ========== shangjia.refresh_from_db() link_list = [] for link_obj in links_to_create: link_list.append({ 'linkUrl': link_obj.lianjie, 'token': link_obj.lianjie_token, 'dingdanId': link_obj.dingdan_id, 'youxiaoShijian': link_obj.youxiao_shijian.strftime('%Y-%m-%d %H:%M:%S') if link_obj.youxiao_shijian else '', 'isUsed': link_obj.is_used, }) return Response({ 'code': 200, 'msg': '批量链接生成成功', 'data': { 'totalCount': gen_count, 'links': link_list, 'newBalance': str(shangjia.yue), 'jinriliushui': str(shangjia.jinriliushui), 'jinyueliushui': str(shangjia.jinyueliushui), 'jinridingdan': shangjia.jinridingdan, 'jinyuedingdan': shangjia.jinyuedingdan } }, status=status.HTTP_200_OK) except Exception as e: logger.error(f"批量生成链接失败 - 管理员: {request.user.yonghuid}, 错误: {str(e)}", exc_info=True) return Response({ 'code': 500, 'msg': '系统错误,生成链接失败', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # ---------- 辅助函数 ---------- def _generate_dingdan_id(self): import time import os import random 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(self): import hashlib import secrets timestamp = int(time.time() * 1_000_000) random_salt = secrets.token_hex(8) raw_string = f"{timestamp}:{secrets.token_hex(4)}:{random_salt}" hash_obj = hashlib.sha256(raw_string.encode('utf-8')) return hash_obj.hexdigest()[:24] class ShangjiaMobanListViewpl(APIView): """ 商家订单模板列表接口 POST /peizhi/sjddmblb 权限: 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') get_type = request.data.get('getType', 2) # 1=搜索,2=普通获取 page = int(request.data.get('page', 1)) page_size = int(request.data.get('pageSize', 50)) search_keyword = request.data.get('searchKeyword', '').strip() # 3. 验证必填字段 if not leixing_id: return Response({ 'code': 400, 'msg': '商品类型ID不能为空', 'data': {} }, status=400) if get_type not in [1, 2]: return Response({ 'code': 400, 'msg': '获取类型参数错误', 'data': {} }, status=400) # ========== 新增:通过手机号和用户类型查询真正的用户ID ========== phone = request.user.phone # 当前登录用户的手机号 target_user_type = 'lianjie' # 目标用户类型,请根据实际业务确认(例如:'admin'、'shangjia' 或固定值 'lianjie') try: target_user = UserMain.objects.get(phone=phone, user_type=target_user_type) yonghu_id = target_user.yonghuid except UserMain.DoesNotExist: return Response({ 'code': 404, 'msg': '未找到符合条件的用户', 'data': {} }, status=404) # ========== 新增结束 ========== # 后续代码使用 yonghu_id 进行查询,而不是 request.user.yonghuid # 4. 构建查询条件 query_conditions = Q( yonghu_id=yonghu_id, leixing_id=leixing_id, #zhuangtai=1 # 只查询正常状态的模板 ) # 5. 搜索模式处理 if get_type == 1 and search_keyword: # 搜索模式:按模板ID搜索 try: moban_id = int(search_keyword) query_conditions &= Q(id=moban_id) except ValueError: return Response({ 'code': 400, 'msg': '搜索关键词必须是有效的模板ID', 'data': {} }, status=400) # 6. 执行查询(使用values优化性能) queryset = ShangjiaMoban.objects.filter(query_conditions) # 7. 计算总数和分页 total_count = queryset.count() # 搜索模式不进行分页,直接返回所有匹配结果 if get_type == 1: moban_list = queryset.values( 'id', # 模板ID 'leixing_id', # 商品类型ID 'moban_jieshao', # 模板介绍 'jiage', # 价格 'fabu_shuliang', # 发布数量 'create_time' # 创建时间 ) # 格式化数据(与前端字段对应) formatted_list = [] for item in moban_list: formatted_list.append({ 'mobanId': item['id'], 'shangpinTypeId': item['leixing_id'], 'jieshao': item['moban_jieshao'], 'jiage': str(item['jiage']), # 转为字符串 'fabushuliang': item['fabu_shuliang'], 'createTime': item['create_time'].strftime('%Y-%m-%d %H:%M:%S') if item['create_time'] else '' }) return Response({ 'code': 200, 'msg': '搜索成功', 'data': { 'list': formatted_list, 'total': len(formatted_list), 'hasMore': False, # 搜索模式不分页,没有更多数据 'currentPage': 1, 'pageSize': len(formatted_list) } }) # 8. 普通获取模式:分页处理 else: # 使用Django分页器 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) # 获取数据并格式化 moban_list = current_page.object_list.values( 'id', # 模板ID 'leixing_id', # 商品类型ID 'moban_jieshao', # 模板介绍 'jiage', # 价格 'fabu_shuliang', # 发布数量 'create_time' # 创建时间 ) # 格式化数据(与前端字段对应) formatted_list = [] for item in moban_list: formatted_list.append({ 'mobanId': item['id'], 'shangpinTypeId': item['leixing_id'], 'jieshao': item['moban_jieshao'], 'jiage': str(item['jiage']), # 转为字符串 'fabushuliang': item['fabu_shuliang'], 'createTime': item['create_time'].strftime('%Y-%m-%d %H:%M:%S') if item['create_time'] else '' }) # 判断是否有更多数据 has_more = current_page.has_next() # 特殊逻辑:如果当前页数据量不足page_size,说明没有更多数据 if len(formatted_list) < page_size: has_more = False return Response({ 'code': 200, 'msg': '获取成功', 'data': { 'list': formatted_list, 'total': total_count, 'hasMore': has_more, # 是否允许上拉刷新 'currentPage': page, 'pageSize': page_size, 'totalPages': paginator.num_pages } }) except Exception as e: # 记录错误日志 import logging logger = logging.getLogger(__name__) logger.error(f"获取商家模板列表失败: {str(e)}", exc_info=True) return Response({ 'code': 500, 'msg': '服务器内部错误', 'data': {} }, status=500) # views.py import io import time from yonghu.models import UserGuanshi from utils.oss_utils import upload_to_oss, delete_from_oss from utils.yaoqingma_utils import chuangjianYaoqingma, yanzhengYaoqingmaWeiyixing import requests logger = logging.getLogger(__name__) import urllib.parse #和手机号绑定冲突接口 '''class GuanshiQRCodeView(APIView): """ 管事专属二维码生成接口 路径:/peizhi/guanshiewm 方法:POST 权限:JWT认证,仅限管事身份 """ permission_classes = [permissions.IsAuthenticated] def post(self, request): # 获取当前认证用户 current_user = request.user # 1. 查询管事扩展表 try: guanshi = current_user.guanshi_profile except UserGuanshi.DoesNotExist: return Response({'code': 403, 'message': '当前用户不是管事身份', 'data': None}, status=status.HTTP_403_FORBIDDEN) # 2. 验证管事状态 if guanshi.zhuangtai != 1: return Response({'code': 403, 'message': '管事账号状态异常', 'data': None}, status=status.HTTP_403_FORBIDDEN) # 3. 获取或生成邀请码 invite_code = guanshi.yaoqingma if not invite_code: invite_code = chuangjianYaoqingma(str(current_user.yonghuid)) if not yanzhengYaoqingmaWeiyixing(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']) # 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/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} } 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: # 可能是 JSON 错误信息 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/guanshi/{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 = guanshi.invite_qrcode_url if old_url: delete_from_oss(old_url) # 8. 更新数据库 with transaction.atomic(): guanshi.invite_qrcode_url = relative_path guanshi.save(update_fields=['invite_qrcode_url']) # 9. 返回相对路径 return Response({'code': 0, 'message': 'success', 'data': {'url': relative_path}}) 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 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 = chuangjianYaoqingma(str(current_user.yonghuid)) if not yanzhengYaoqingmaWeiyixing(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 = self.get_cached_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_cached_access_token(self): """获取微信全局 access_token,使用 cache.add 实现互斥锁""" cache_key = 'wechat_access_token' lock_key = 'wechat_access_token_lock' lock_timeout = 10 # 先尝试读取缓存 token = cache.get(cache_key) if token: logger.info("从缓存中读取到 access_token") return token # 尝试获取锁(原子操作) lock_acquired = cache.add(lock_key, '1', timeout=lock_timeout) if not lock_acquired: # 未获得锁,等待其他进程刷新 import time time.sleep(0.2) token = cache.get(cache_key) if token: logger.info("其他进程已刷新 token,使用缓存值") return token # 等待几秒再试 for _ in range(5): time.sleep(0.2) token = cache.get(cache_key) if token: return token logger.warning("获取锁失败且缓存中无 token,返回 None") return None # 获得锁,执行刷新 try: token = cache.get(cache_key) if token: logger.info("加锁后再次读取到缓存中的 token") return 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: cache.set(cache_key, new_token, timeout=7100) 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 finally: cache.delete(lock_key) # ==================== 修改结束 ==================== '''class GuanshiQRCodeView(APIView): """ 管事专属二维码生成接口 路径:/peizhi/guanshiewm 方法:POST 权限:JWT认证,仅限管事身份 """ permission_classes = [IsAuthenticated] def post(self, request): # 获取当前认证用户 current_user = request.user # 1. 查询管事扩展表 try: guanshi = current_user.guanshi_profile except UserGuanshi.DoesNotExist: return Response({'code': 403, 'message': '当前用户不是管事身份', 'data': None}, status=status.HTTP_403_FORBIDDEN) # 2. 验证管事状态 if guanshi.zhuangtai != 1: return Response({'code': 403, 'message': '管事账号状态异常', 'data': None}, status=status.HTTP_403_FORBIDDEN) # 3. 获取或生成邀请码 invite_code = guanshi.yaoqingma if not invite_code: invite_code = chuangjianYaoqingma(str(current_user.yonghuid)) if not yanzhengYaoqingmaWeiyixing(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']) # 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) # 5. 调用微信接口生成小程序码 page_path = 'pages/dashouduan/dashouduan' # 打手注册页 scene = invite_code wx_url = f'https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={access_token}' post_data = { 'scene': scene, 'page': 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 or 'image' not in wx_resp.headers.get('content-type', ''): error_info = wx_resp.json() if wx_resp.headers.get('content-type') == 'application/json' else {} 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) # 6. 上传到腾讯云COS timestamp = int(time.time()) filename = f"{current_user.yonghuid}_{timestamp}.png" oss_path = f"erweima/guanshi/{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) # 7. 获取相对路径 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 # 8. 删除旧二维码文件 old_url = guanshi.invite_qrcode_url if old_url: delete_from_oss(old_url) # 9. 更新数据库 with transaction.atomic(): guanshi.invite_qrcode_url = relative_path guanshi.save(update_fields=['invite_qrcode_url']) # 10. 返回相对路径 return Response({'code': 0, 'message': 'success', 'data': {'url': relative_path}}) def get_cached_access_token(self): """ 从缓存获取微信 access_token,缓存键为 'wx_mini_access_token' 如果缓存中不存在,则调用微信接口获取并存入缓存,有效期7000秒。 使用 Django 缓存框架,默认使用内存缓存,不会占用磁盘。 """ 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: # 缓存7000秒(小于7200秒,确保不会过期才使用) cache.set(cache_key, token, 7000) return token except Exception as e: logger.error(f"获取微信access_token失败: {e}") return None''' from django.db.models import Max, F 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) from yonghu.models import UserZuzhang 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. 验证组长账号状态(zhuangtai=1正常) if zuzhang.zhuangtai != 1: return Response({ 'code': 403, 'message': '组长账号状态异常,无法生成海报', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 3. 获取或生成邀请码(yaoqingma字段) invite_code = zuzhang.yaoqingma if not invite_code: # 生成唯一邀请码(使用已有工具函数) invite_code = chuangjianYaoqingma(str(current_user.yonghuid)) if not yanzhengYaoqingmaWeiyixing(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) # ========== 调用微信B接口:getUnlimited ========== # 页面路径:打手注册页(组长邀请打手) page_path = 'pages/guanshiduan/guanshiduan' # scene参数:直接放入邀请码(需确保字符符合微信要求) scene = invite_code # 假设邀请码只包含字母数字 wx_url = f'https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={access_token}' post_data = { 'scene': scene, 'page': 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: # 可能是JSON错误 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. 删除旧的二维码文件(haibao_url字段) 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 from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated from rest_framework import status from django.utils import timezone from django.db.models import Q from .models import PopupPage, PopupConfig from .serializers import PopupConfigSerializer 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 } }) from .models import WithdrawConfig 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 } }) from shangpin.models import ( Huiyuangoumai ) 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