Files
along_django/peizhi/views.py

5202 lines
203 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 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
from utils.page_ziyuan_service import build_paihang_assets_for_api, build_page_assets_for_api
class GetDynamicConfigView(APIView):
permission_classes = [AllowAny]
parser_classes = [JSONParser]
def post(self, request):
try:
# 查询我方俱乐部配置is_self=1取第一条通常只有一条
club_config = ClubConfig.objects.filter(is_self=1).only(
'storage_bucket_domain',
'storage_bucket_name',
'chat_api_key',
'customer_service_api_url',
'customer_service_corp_id'
).first()
# 默认值(防止空数据导致前端报错)
cos_bucket = ''
cos_region = 'ap-shanghai'
cos_oss_url = ''
goeasy_host = 'hangzhou.goeasy.io'
goeasy_appkey = ''
kefu_link = ''
kefu_enterprise_id = ''
morentouxiang = 'beijing/morentouxiang.jpg'
dashouguize = 'a_long/dashouguize.jpg'
if club_config:
# 存储桶配置
bucket = club_config.storage_bucket_name or ''
region = 'ap-shanghai' # 若需要动态,可单独存储
cos_oss_url = club_config.storage_bucket_domain or ''
if cos_oss_url and not cos_oss_url.endswith('/'):
cos_oss_url += '/'
cos_bucket = bucket
cos_region = region
# GoEasy 配置
if club_config.chat_api_key:
goeasy_appkey = club_config.chat_api_key
# host 如果需要可动态,这里固定或从其他字段获取
# 客服配置
if club_config.customer_service_api_url:
kefu_link = club_config.customer_service_api_url
if club_config.customer_service_corp_id:
kefu_enterprise_id = club_config.customer_service_corp_id
# 其他配置可在此扩展
# 构造返回数据
paihang_assets = build_paihang_assets_for_api()
page_assets = build_page_assets_for_api()
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,
"paihangAssets": paihang_assets,
},
"pageAssets": page_assets,
"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 = {}
# 平台发单利率类型1fadanpingtai='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)
# 商家发单利率类型2fadanpingtai='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)
# 🔴【新增】打手提现抽成利率类型5fadanpingtai='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)
# 🔴【新增】管事提现抽成利率类型6fadanpingtai='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都是CharFieldxiugaiid也是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转floatInteger转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)
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. 价格格式验证
from utils.shangjia_paifa_config import get_shangjia_paifa_jiage_limits
paifa_min, paifa_max = get_shangjia_paifa_jiage_limits()
try:
jiage_decimal = Decimal(str(jiage))
if jiage_decimal <= 0:
return Response({'code': 400, 'msg': '价格必须大于0', 'data': {}}, status=400)
if jiage_decimal < paifa_min or jiage_decimal > paifa_max:
return Response({
'code': 400,
'msg': f'价格需在{paifa_min}-{paifa_max}元之间',
'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:
from utils.shangjia_paifa_config import get_shangjia_paifa_jiage_limits
paifa_min, paifa_max = get_shangjia_paifa_jiage_limits()
try:
jiage_decimal = Decimal(new_jiage)
if jiage_decimal <= 0 or jiage_decimal < paifa_min or jiage_decimal > paifa_max:
return Response({
'code': 400,
'msg': f'价格需在{paifa_min}-{paifa_max}元之间',
'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 utils.xcx_sys_config import wx_cfg
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)
# 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
logger = logging.getLogger(__name__)
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
logger = logging.getLogger(__name__)
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 ''}")
# 🔴【添加】在订单更新成功后,异步发送微信服务号通知(原有)
self.send_order_notification_async(dingdan_obj, youxi_nicheng, zhiding_dashou, zhuangtai_xin)
# 获取商家信息用于返回
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}")
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 send_order_notification_async(self, dingdan, youxi_nicheng, zhiding_dashou, zhuangtai):
"""客户填写订单后 — Celery 广播服务号模板消息"""
try:
from utils.order_broadcast import submit_dingdan_guangbo
try:
leixing_obj = ShangpinLeixing.objects.get(id=dingdan.leixing_id)
game_type = leixing_obj.jieshao
except Exception:
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 '新订单',
'order_type': '指定订单' if zhuangtai == 7 else '普通订单',
}
submit_dingdan_guangbo(order_info)
except Exception as e:
logger.error(f'【客户填写订单】提交广播失败: {e}', exc_info=True)
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=''
)
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]'''
# peizhi/views.py - AdminBatchGenerateLinkView 完整修复版
logger = logging.getLogger(__name__)
class AdminSelfBatchGenerateLinkView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
# ==================== 管理员身份验证 ====================
if not request.user.is_authenticated:
return Response({'code': 401, 'message': '未认证', 'data': None}, status=401)
if request.user.user_type != 'admin':
return Response({'code': 403, 'message': '权限不足', 'data': None}, status=403)
admin_profile = getattr(request.user, 'admin_profile', None)
if not admin_profile:
return Response({'code': 403, 'message': '管理员信息不完整', 'data': None}, status=403)
zhanghao = request.data.get('zhanghao')
if not zhanghao or request.user.phone != zhanghao:
return Response({'code': 401, 'message': '账号验证失败', 'data': None}, status=401)
allowed_phones = getattr(settings, 'ADMIN_BATCH_LINK_ALLOWED_PHONES', [])
if not allowed_phones:
logger.error("白名单配置为空")
return Response({'code': 500, 'message': '系统未配置可操作管理员', 'data': None}, status=500)
if request.user.phone not in allowed_phones:
logger.warning(f"管理员 {request.user.phone} 不在白名单中")
return Response({'code': 403, 'message': '您没有权限执行此操作', 'data': None}, status=403)
# ========== 获取业务参数 ==========
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'}, status=400)
try:
shangpin_type_id = int(shangpin_type_id)
moban_id = int(moban_id)
except ValueError:
return Response({'code': 400, 'msg': '参数格式错误'}, status=400)
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 or gen_count > max_count:
return Response({'code': 400, 'msg': f'生成数量必须在1-{max_count}之间'}, status=400)
except ValueError:
return Response({'code': 400, 'msg': '生成数量格式错误'}, status=400)
# ========== 通过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': '未找到符合条件的商家用户'}, status=404)
# ========== 获取商家扩展信息 ==========
try:
shangjia = UserShangjia.objects.get(user__yonghuid=real_yonghuid)
except UserShangjia.DoesNotExist:
logger.error(f"商家扩展信息不存在 - 用户ID: {real_yonghuid}")
return Response({'code': 403, 'msg': '该用户不是商家'}, status=403)
if shangjia.zhuangtai != 1:
return Response({'code': 403, 'msg': '商家账号状态异常'}, status=403)
# ========== 查询商品类型与模板 ==========
try:
shangpin_leixing = ShangpinLeixing.objects.get(id=shangpin_type_id)
except ShangpinLeixing.DoesNotExist:
return Response({'code': 400, 'msg': '商品类型不存在'}, status=400)
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': '订单模板不存在'}, status=400)
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': '金额计算错误'}, status=500)
if total_price > shangjia.yue:
return Response({'code': 400, 'msg': f'余额不足,当前余额{shangjia.yue}元,需要{total_price}'}, status=400)
# ========== 利率 ==========
try:
lilu_obj = Lilubiao.objects.get(fadanpingtai='3')
lilu = float(lilu_obj.lilu)
if lilu <= 0: lilu = 1
except Lilubiao.DoesNotExist:
lilu = 1
# ========== 事务内循环生成 ==========
base_url = getattr(settings, 'H5_DOMAIN', 'https://h5.yourdomain.com')
if not base_url.endswith('/'): base_url += '/'
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': '余额不足,请刷新后重试'}, status=400)
link_list = []
for i in range(gen_count):
# 调用单个生成逻辑(复用稳定代码)
link_data = self._generate_single_link(
real_yonghuid=real_yonghuid,
shangjia=shangjia,
target_user=target_user,
shangpin_type_id=shangpin_type_id,
moban=moban,
jiage=jiage,
dingdan_jieshao=dingdan_jieshao,
yaoqiuleixing=yaoqiuleixing,
huiyuan_id=huiyuan_id,
price_decimal=price_decimal,
lilu=lilu,
base_url=base_url,
)
link_list.append(link_data)
# 统一更新商家统计信息(一次性扣除余额、增加发布次数)
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()
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=200)
except Exception as e:
logger.error(f"批量生成链接失败 - 管理员: {request.user.yonghuid}, 错误: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': '系统错误,生成链接失败'}, status=500)
# ---------- 辅助函数 ----------
def _generate_dingdan_id(self):
import time, os, 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, dingdan_id, shangjia_id):
import hashlib, secrets
timestamp = int(time.time() * 1_000_000)
random_salt = secrets.token_hex(8)
raw_string = f"{timestamp}:{dingdan_id}:{shangjia_id}:{random_salt}"
hash_obj = hashlib.sha256(raw_string.encode('utf-8'))
return hash_obj.hexdigest()[:24]
def _generate_single_link(self, real_yonghuid, shangjia, target_user, shangpin_type_id,
moban, jiage, dingdan_jieshao, yaoqiuleixing, huiyuan_id,
price_decimal, lilu, base_url):
"""单个链接生成逻辑,完全复用稳定接口"""
# 生成唯一ID
dingdan_id = self._generate_dingdan_id()
secure_token = self._generate_secure_token(dingdan_id, real_yonghuid)
# 计算分成
dashou_fencheng = (price_decimal * Decimal(str(lilu))).quantize(Decimal('0.01'))
# 创建订单
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=yaoqiuleixing or 1,
huiyuan_id=huiyuan_id,
jieshao=dingdan_jieshao[:2000],
tupian=target_user.avatar or '',
user1_id=f"Sj{real_yonghuid}",
user2_id='',
zhiding_id='',
dashou_liuyan='',
beizhu='',
nicheng='',
shangpin_id=0,
yongjin=None,
)
# 创建扩展表
DingdanShangjia.objects.create(
dingdan=dingdan,
shangjia_id=real_yonghuid,
sjnicheng=shangjia.nicheng or f"商家{real_yonghuid}",
shangjia_pingjia='',
cfliyou='',
sqzhuangtai=0,
bhliyou=''
)
# 生成链接
link_url = f"{base_url}order/{secure_token}"
# 创建链接记录
lianjie = ShangjiaLianjie.objects.create(
yonghu_id=real_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() + timedelta(days=1)
)
return {
'linkUrl': link_url,
'token': secure_token,
'dingdanId': dingdan_id,
'youxiaoShijian': lianjie.youxiao_shijian.strftime('%Y-%m-%d %H:%M:%S') if lianjie.youxiao_shijian else '',
'isUsed': lianjie.is_used,
}
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):
"""处理POST请求生成管事邀请二维码"""
try:
current_user = request.user
# 1. 查询管事扩展表
try:
guanshi = current_user.guanshi_profile
except UserGuanshi.DoesNotExist:
return Response(
{'code': 403, 'msg': '当前用户不是管事身份'},
status=status.HTTP_403_FORBIDDEN
)
# 2. 验证管事状态
if guanshi.zhuangtai != 1:
return Response(
{'code': 403, 'msg': '管事账号状态异常'},
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):
return Response(
{'code': 500, 'msg': '邀请码生成失败,请重试'},
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, 'msg': '获取微信 access_token 失败,可能是网络问题或配置错误'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
# 5. 调用微信接口生成小程序码
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 requests.RequestException as e:
logger.error(f"请求微信接口异常: {e}")
return Response(
{'code': 500, 'msg': f'调用微信服务失败: {str(e)}'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
# 6. 处理微信返回
if wx_resp.status_code != 200:
error_detail = f'HTTP {wx_resp.status_code}'
if wx_resp.headers.get('content-type') == 'application/json':
try:
err_json = wx_resp.json()
error_detail += ', ' + err_json.get('errmsg', '未知错误')
except:
pass
logger.error(f"微信接口返回错误: {error_detail}")
return Response(
{'code': 500, 'msg': f'微信生成二维码失败: {error_detail}'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
content_type = wx_resp.headers.get('content-type', '')
if 'image' not in content_type:
try:
err_json = wx_resp.json()
errmsg = err_json.get('errmsg', '未知错误')
except:
errmsg = '返回非图片内容'
logger.error(f"微信返回非图片内容: {errmsg}")
return Response(
{'code': 500, 'msg': f'微信返回错误: {errmsg}'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
# 7. 上传到腾讯云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, 'msg': f'图片上传失败: {str(e)}'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
if not full_url:
return Response(
{'code': 500, 'msg': '图片上传失败返回空URL'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
# 8. 获取相对路径
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
# 9. 删除旧二维码文件
old_url = guanshi.invite_qrcode_url
if old_url:
try:
delete_from_oss(old_url)
except Exception as e:
logger.warning(f"删除旧二维码失败: {e}")
# 10. 更新数据库
with transaction.atomic():
guanshi.invite_qrcode_url = relative_path
guanshi.save(update_fields=['invite_qrcode_url'])
# 11. 返回结果
return Response({
'code': 0,
'msg': 'success',
'data': {'url': relative_path}
})
except Exception as e:
logger.error(f"生成管事二维码异常: {e}", exc_info=True)
return Response(
{'code': 500, 'msg': f'服务器错误: {str(e)}'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
def get_cached_access_token(self):
"""
获取微信全局 access_token使用兼容性更好的缓存策略。
若后端不支持 cache.lock则降级为简单 setnx。
"""
cache_key = 'wechat_access_token'
lock_key = 'wechat_access_token_lock'
appid = wx_cfg.WEIXIN_APPID
secret = wx_cfg.WEIXIN_SECRET
if not appid or not secret:
logger.error("微信小程序 AppID 或 Secret 未配置")
return None
# 尝试读取缓存
token = cache.get(cache_key)
if token:
return token
# 尝试获取锁(兼容不支持锁的后端)
lock_acquired = False
try:
# 尝试使用 cache.lock若不支持会抛 NotImplementedError
with cache.lock(lock_key, timeout=10):
lock_acquired = True
token = cache.get(cache_key)
if token:
return token
token = self.fetch_access_token_from_wechat(appid, secret)
if token:
cache.set(cache_key, token, timeout=7100)
return token
except (NotImplementedError, AttributeError):
# 不支持锁,直接刷新(概率性重复,但影响不大)
logger.warning("当前缓存后端不支持锁,降级为无锁刷新")
except Exception as e:
logger.error(f"缓存锁异常: {e}")
# 降级:若没拿到锁,再次检查缓存(其他进程可能已经更新),若仍没有则直接刷新
token = cache.get(cache_key)
if token:
return token
token = self.fetch_access_token_from_wechat(appid, secret)
if token:
cache.set(cache_key, token, timeout=7100)
return token
def fetch_access_token_from_wechat(self, appid, secret):
"""直接从微信服务器获取 access_token"""
url = 'https://api.weixin.qq.com/cgi-bin/token'
params = {
'grant_type': 'client_credential',
'appid': appid,
'secret': secret
}
try:
resp = requests.get(url, params=params, timeout=5)
data = resp.json()
new_token = data.get('access_token')
if new_token:
return new_token
else:
errmsg = data.get('errmsg', '未知错误')
logger.error(f"微信返回错误: {errmsg}")
return None
except requests.RequestException as e:
logger.error(f"请求微信 access_token 接口失败: {e}")
return None
'''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' # 打手注册页
inviteCode = invite_code
wx_url = f'https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={access_token}'
post_data = {
'scene': inviteCode,
'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 = wx_cfg.WEIXIN_APPID
secret = wx_cfg.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 = wx_cfg.WEIXIN_APPID
secret = wx_cfg.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 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 = wx_cfg.WEIXIN_APPID
secret = wx_cfg.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