Files
Django/config/views.py
XingQue b2fcff303b feat: 小程序客服话术表与自动回复接口
- 新增 MiniappScriptScene / MiniappScriptAutoReply 模型与迁移
- 新增 /peizhi/huashuhq、/peizhi/huashu_match 接口及 Admin 配置

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 11:59:21 +08:00

3479 lines
140 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.
import io
import os
import re
import time
import random
import secrets
import hashlib
import threading
import urllib.parse
from decimal import Decimal, InvalidOperation
from django.conf import settings
from django.db import transaction, connection
from django.db.models import Q, F, Max, Prefetch
from gvsdsdk.fluent import db, func, FQ
from django.core.cache import cache
from django.core.paginator import Paginator
from django.utils import timezone
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status, permissions
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
from rest_framework.throttling import AnonRateThrottle, SimpleRateThrottle
from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image
from utils.weixin_broadcast import WeixinBroadcastSender
from utils.weixin_token import get_weixin_mini_access_token, is_weixin_token_invalid
# from utils.ip_security import *
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
from backend.utils import update_shangjia_daily
from utils.chat_utils import subscribe_merchant_link_chat
from .models import (
Gonggao, Lunbo, Tupianpeizhi, Qunpeizhi,
ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig,
MiniappScriptScene, MiniappScriptAutoReply,
)
from users.models import (
AdminProfile, UserDashou, UserBoss,
UserShangjia, UserGuanshi, UserZuzhang
)
from users.business_models import User
from orders.models import CommissionRate, Order, MerchantOrderExt
from orders.notice_tasks import dingdan_guangbo
from rank.models import Chenghao, DingdanBiaoqian
from products.models import ShangpinLeixing, Huiyuangoumai
from .serializers import PopupConfigSerializer
import traceback
import requests
import logging
logger = logging.getLogger(__name__)
class GetDynamicConfigView(APIView):
permission_classes = [AllowAny]
parser_classes = [JSONParser]
def post(self, request):
try:
from jituan.services.display_config import get_tupianpeizhi_url
from jituan.services.miniapp_assets import build_miniapp_assets_payload
cos_oss_url = getattr(settings, 'COS_DOMAIN', '')
if cos_oss_url and not cos_oss_url.endswith('/'):
cos_oss_url += '/'
cos_bucket = getattr(settings, 'COS_BUCKET', '')
cos_region = getattr(settings, 'COS_REGION', 'ap-shanghai')
goeasy_host = 'hangzhou.goeasy.io'
goeasy_appkey = getattr(settings, 'GOEASY_APPKEY', '')
kefu_link = getattr(settings, 'KEFU_LINK', '')
kefu_enterprise_id = getattr(settings, 'KEFU_ENTERPRISE_ID', '')
rule_img = get_tupianpeizhi_url(request, 1)
avatar_img = get_tupianpeizhi_url(request, 2)
morentouxiang = avatar_img or 'beijing/morentouxiang.jpg'
dashouguize = rule_img or 'a_long/dashouguize.jpg'
# 构造返回数据
data = {
"code": 0,
"data": {
"cos": {
"bucket": cos_bucket,
"region": cos_region,
"ossImageUrl": cos_oss_url,
"uploadPathPrefix": "order/"
},
"goEasy": {
"host": goeasy_host,
"appkey": goeasy_appkey
},
"otherConfig": {
"morentouxiang": morentouxiang,
"dashouguize": dashouguize
},
"kefu": {
"link": kefu_link,
"enterpriseId": kefu_enterprise_id
},
"miniappAssets": build_miniapp_assets_payload(request),
}
}
return Response(data)
except Exception as e:
logger.error(f"获取动态配置失败: {str(e)}", exc_info=True)
return Response({"code": 500, "msg": "服务器内部错误"}, status=500)
class HaibaoPeizhiView(APIView):
"""小程序推广海报背景配置(按俱乐部)。"""
permission_classes = [AllowAny]
parser_classes = [JSONParser]
def post(self, request):
try:
from jituan.services.display_config import _display_club_id
from jituan.services.miniapp_assets import get_poster_payload
club_id = _display_club_id(request)
poster = get_poster_payload(club_id)
return Response({
'code': 0,
'msg': 'success',
'data': {
'club_id': club_id,
'poster': poster,
},
})
except Exception as e:
logger.error('获取海报配置失败: %s', 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("收到商品公告和轮播图请求")
from jituan.services.display_config import get_gonggao_content, get_lunbo_urls, normalize_page_key
page_key = normalize_page_key(request.data.get('page_key'), image_type=1)
gonggao_content = get_gonggao_content(request, notice_type=1, page_key=page_key)
lunbo_urls = get_lunbo_urls(request, page_key=page_key, image_type=1)
response_data = {
"shangpingonggao": gonggao_content,
"shangpinlunbo": lunbo_urls,
"page_key": page_key,
}
logger.info(f"返回数据:公告长度{len(gonggao_content)},轮播图数量{len(lunbo_urls)} page_key={page_key}")
# 4. 返回响应
return Response(response_data, status=status.HTTP_200_OK)
except Exception as e:
# 记录异常
logger.error(f"获取商品公告和轮播图时发生错误:{str(e)}")
# 返回错误响应
error_data = {
"error": "服务器内部错误",
"detail": "获取数据失败,请稍后重试"
}
return Response(error_data, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class AdminConfigQueryView(APIView):
"""
管理员获取系统配置接口
URL: /peizhi/adpzhq
方法: POST
权限: JWT Token认证 + 管理员权限
参数: zhanghao (管理员账号)
"""
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
try:
# 获取前端传递的账号
zhanghao = request.data.get('zhanghao')
if not zhanghao:
return Response({
'code': 1,
'msg': '账号不能为空',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 获取当前用户User实例
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.UserType != 'admin':
return Response({
'code': 1,
'msg': '无权限',
'data': None
}, status=status.HTTP_401_UNAUTHORIZED)
# 验证管理员扩展表是否存在
try:
admin_profile = user_main.AdminProfile
except AdminProfile.DoesNotExist:
return Response({
'code': 1,
'msg': '无权限',
'data': None
}, status=status.HTTP_401_UNAUTHORIZED)
# 查询图片配置
rule_image = Tupianpeizhi.query.filter(ImageType=1).first()
default_avatar = Tupianpeizhi.query.filter(ImageType=2).first()
# 查询群配置
qun_configs = Qunpeizhi.query.all()
qq_groups = []
for config in qun_configs:
qq_groups.append({
'peizhiid': config.GroupType,
'neirong': config.GroupContent if config.GroupContent else '',
'qunid': config.GroupID if config.GroupID else '',
'jieshao': config.Description if config.Description else ''
})
# 查询利率配置
platform_rate_obj = CommissionRate.query.filter(Platform='1').first()
shop_rate_obj = CommissionRate.query.filter(Platform='3').first()
# 🔴【新增】打手提现抽成利率字符5
dashou_withdraw_obj = CommissionRate.query.filter(Platform='5').first()
# 🔴【新增】管事提现抽成利率字符6
guanshi_withdraw_obj = CommissionRate.query.filter(Platform='6').first()
# 构建返回数据
data = {
'images': {
'rule_image': rule_image.ImageURL if rule_image and rule_image.ImageURL else '',
'default_avatar': default_avatar.ImageURL if default_avatar and default_avatar.ImageURL else ''
},
'qq_groups': qq_groups,
'rates': {
'platform_rate': float(platform_rate_obj.Rate) if platform_rate_obj and platform_rate_obj.Rate else 0.0,
'shop_rate': float(shop_rate_obj.Rate) if shop_rate_obj and shop_rate_obj.Rate else 0.0,
# 🔴【新增】打手提现抽成利率
'dashou_withdraw_rate': float(
dashou_withdraw_obj.Rate) if dashou_withdraw_obj and dashou_withdraw_obj.Rate else 0.0,
# 🔴【新增】管事提现抽成利率
'guanshi_withdraw_rate': float(
guanshi_withdraw_obj.Rate) if guanshi_withdraw_obj and guanshi_withdraw_obj.Rate 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.UserType != 'admin':
return Response({
'code': 1,
'msg': '无权限',
'data': None
}, status=status.HTTP_401_UNAUTHORIZED)
try:
admin_profile = user_main.AdminProfile
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.query.get_or_create(ImageType=int(leixing))
tupian_peizhi.ImageURL = 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.UserType != 'admin':
return Response({
'code': 1,
'msg': '无权限',
'data': None
}, status=status.HTTP_401_UNAUTHORIZED)
try:
admin_profile = user_main.AdminProfile
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.query.filter(GroupType=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.GroupContent = neirong or ''
if qunid is not None:
qun_config.GroupID = qunid or ''
if jieshao is not None:
qun_config.Description = jieshao or ''
qun_config.save()
else:
# 如果不存在创建新的使用提供的peizhiid
qun_config = Qunpeizhi.query.create(
GroupType=peizhiid,
GroupContent=group_data.get('neirong', '') or '',
GroupID=group_data.get('qunid', '') or '',
Description=group_data.get('jieshao', '') or ''
)
updated_groups.append({
'peizhiid': qun_config.GroupType,
'neirong': qun_config.GroupContent,
'qunid': qun_config.GroupID,
'jieshao': qun_config.Description
})
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 = CommissionRate.query.filter(Platform='1').first()
if platform_rate_obj:
# 更新现有记录
platform_rate_obj.Rate = platform_rate_float
platform_rate_obj.save()
else:
# 创建新记录
platform_rate_obj = CommissionRate.query.create(
Rate=platform_rate_float,
Platform='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 = CommissionRate.query.filter(Platform='3').first()
if shop_rate_obj:
# 更新现有记录
shop_rate_obj.Rate = shop_rate_float
shop_rate_obj.save()
else:
# 创建新记录
shop_rate_obj = CommissionRate.query.create(
Rate=shop_rate_float,
Platform='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 = CommissionRate.query.filter(Platform='5').first()
if dashou_withdraw_obj:
# 更新现有记录
dashou_withdraw_obj.Rate = dashou_withdraw_rate_float
dashou_withdraw_obj.save()
else:
# 创建新记录
dashou_withdraw_obj = CommissionRate.query.create(
Rate=dashou_withdraw_rate_float,
Platform='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 = CommissionRate.query.filter(Platform='6').first()
if guanshi_withdraw_obj:
# 更新现有记录
guanshi_withdraw_obj.Rate = guanshi_withdraw_rate_float
guanshi_withdraw_obj.save()
else:
# 创建新记录
guanshi_withdraw_obj = CommissionRate.query.create(
Rate=guanshi_withdraw_rate_float,
Platform='6'
)
updated_rates['guanshi_withdraw_rate'] = guanshi_withdraw_rate_float
else:
return Response({
'code': 9,
'msg': '管事提现抽成利率必须在0-1之间',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
except ValueError:
return Response({
'code': 10,
'msg': '管事提现抽成利率格式错误',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
if updated_rates:
response_data['rates'] = updated_rates
# 如果没有任何更新
if not response_data:
return Response({
'code': 7,
'msg': '没有需要更新的数据',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
return Response({
'code': 0,
'msg': '更新成功',
'data': response_data
})
except Exception as e:
return Response({
'code': 99,
'msg': f'系统错误: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class DashouPaihangView(APIView):
"""
排行榜接口
路径:/peizhi/dsph
方法POST
认证JWT Token认证
请求参数:
- leixing: 1=今日收入2=今月收入
- yonghu_leixing: 1=打手2=管事3=商家
返回数据:
- code: 0=成功,其他=失败
- msg: 提示信息
- data: {
xianshi: True/False, # 是否展示真实数据
paihang_list: [ # 排行榜列表
{
uid: '用户ID',
touxiang: '头像相对URL',
nicheng: '用户昵称',
jine: 收入金额,
mingci: 排名
},
...
]
}
"""
# =============== 可配置参数 ===============
# 每次查询返回的最大记录数(可修改)
ZUI_DAO_CHA_XUN_SHU_LIANG = 50
# 最少需要多少人才能展示(可修改)
ZUI_SHAO_REN_SHU_YAO_QIU = 1
# 第一名最低金额要求(单位:元)(可修改)
DI_YI_MING_ZUI_DI_JIN_E = Decimal('10.00')
# 管事第一名最低数量要求(可修改)
DI_YI_MING_ZUI_DI_SHU_LIANG = 1
# =============== 可配置参数结束 ===============
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 获取请求参数 - 修复参数类型问题
leixing_str = request.data.get('leixing', '1')
yonghu_leixing_str = request.data.get('yonghu_leixing', '1')
# 转换为整数
try:
leixing = int(leixing_str)
except (ValueError, TypeError):
leixing = 1
try:
yonghu_leixing = int(yonghu_leixing_str)
except (ValueError, TypeError):
yonghu_leixing = 1
# 参数验证
if leixing not in [1, 2]:
return Response({
'code': 400,
'msg': '参数错误leixing必须为1(今日)或2(今月)',
'data': {'xianshi': False}
})
if yonghu_leixing not in [1, 2, 3]:
return Response({
'code': 400,
'msg': '参数错误yonghu_leixing必须为1(打手)、2(管事)或3(商家)',
'data': {'xianshi': False}
})
# 根据用户类型调用不同的查询方法
if yonghu_leixing == 1:
# 打手排行榜
paihang_list, xianshi_flag = self.chaxun_dashou_paihang(leixing)
elif yonghu_leixing == 2:
# 管事排行榜
paihang_list, xianshi_flag = self.chaxun_guanshi_paihang(leixing)
else:
# 商家排行榜
paihang_list, xianshi_flag = self.chaxun_shangjia_paihang(leixing)
# 返回结果
return Response({
'code': 0,
'msg': '获取成功',
'data': {
'xianshi': xianshi_flag,
'paihang_list': paihang_list
}
})
except Exception as e:
logger.error(f"排行榜查询失败: {str(e)}", exc_info=True)
return Response({
'code': 500,
'msg': '服务器内部错误',
'data': {'xianshi': False}
})
def chaxun_dashou_paihang(self, leixing):
"""
查询打手排行榜
:param leixing: 1=今日收入2=今月收入
:return: (排行榜列表, 是否展示标志)
"""
# 确定查询的字段
if leixing == 1:
# 今日收入
shouyi_ziduan = 'jinrishouyi'
else:
# 今月收入
shouyi_ziduan = 'jinyueshouyi'
# 查询条件收入大于0打手账号状态正常1=正常)
# 注意打手表中的zhanghaozhuangtai字段
chaxun_tiaojian = Q(**{f'{shouyi_ziduan}__gt': 0}) & Q(zhanghaozhuangtai=1)
# 查询打手扩展表,并关联用户主表
dashou_list = UserDashou.query.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.UserUID,
'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.query.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.query.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.UserUID,
'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.query.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.query.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.UserUID,
'touxiang': touxiang_url,
'nicheng': shangjia_nicheng,
'jine': float(liushui_jine),
'mingci': index + 1
})
return paihang_list, True
class GetAdminZhanghaoView(APIView):
"""
获取管理员账号列表接口
路径:/peizhi/adzhhq
方法POST
权限JWT Token + 管理员验证
参数:
- zhanghao: 当前登录的管理员账号(字符串)
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 🔥 1. 身份验证100%按表模型)
user = request.user
# 获取当前用户的手机号CharField字符串类型
current_phone = getattr(user, 'Phone', '')
if not current_phone:
return Response({
'code': 401,
'message': '用户手机号信息不完整',
'data': None
}, status=status.HTTP_401_UNAUTHORIZED)
# 检查前端传递的账号是否匹配(都是字符串)
frontend_zhanghao = request.data.get('zhanghao', '').strip()
if frontend_zhanghao != current_phone:
return Response({
'code': 403,
'message': '账号不匹配,无权限访问',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 🔥 2. 验证是否为管理员user_type是CharField值是'admin'字符串)
user_type = getattr(user, 'user_type', '')
if user_type != 'admin':
# 检查是否有管理员扩展表
if not hasattr(user, 'AdminProfile'):
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.UserType,
COALESCE(COUNT(xjl.id), 0) as jilushuliang
FROM user_main um
LEFT JOIN xiugaijilu xjl ON um.phone = xjl.xiugaiid
WHERE um.UserType = 'admin'
GROUP BY um.id, um.phone, um.yonghuid, um.UserType
ORDER BY um.CreateTime DESC
"""
cursor.execute(sql)
results = cursor.fetchall()
# 🔥 4. 处理查询结果(全部保持字符串类型)
zhanghao_list = []
for row in results:
zhanghao_list.append({
#'id': row[0],
'id': str(row[1]) if row[1] else str(row[2]), # 🔥 保持字符串
'zhanghao':str(row[1]) if row[1] else str(row[2]), # 🔥 保持字符串
'yonghuid': str(row[2]) if row[2] else '', # 🔥 保持字符串
'user_type': str(row[3]) if row[3] else '', # 🔥 保持字符串
'jilushuliang': int(row[4]) # 只有这个是数字
})
return Response({
'code': 0,
'message': '获取成功',
'data': zhanghao_list
}, status=status.HTTP_200_OK)
except Exception as e:
print(f"获取管理员账号列表失败: {str(e)}")
traceback.print_exc()
return Response({
'code': 500,
'message': '系统错误',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class GetXiugaiJiluView(APIView):
"""
获取修改记录列表接口
路径:/peizhi/adhqxg
方法POST
权限JWT Token + 管理员验证
参数:
- zhanghao: 当前登录的管理员账号(字符串)
- xiugaizhe_zhanghaoid: 选中的管理员账号ID字符串对应phone或yonghuid
- page: 页码(整数)
- pagesize: 每页条数(整数)
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 🔥 1. 身份验证100%按表模型)
user = request.user
# 获取当前用户的手机号CharField字符串
current_phone = getattr(user, 'Phone', '')
if not current_phone:
return Response({
'code': 401,
'message': '用户手机号信息不完整',
'data': None
}, status=status.HTTP_401_UNAUTHORIZED)
# 检查前端传递的账号是否匹配(字符串比较)
frontend_zhanghao = request.data.get('zhanghao', '')
if not isinstance(frontend_zhanghao, str):
frontend_zhanghao = str(frontend_zhanghao)
frontend_zhanghao = frontend_zhanghao.strip()
if frontend_zhanghao != current_phone:
return Response({
'code': 403,
'message': '账号不匹配,无权限访问',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 🔥 2. 验证是否为管理员user_type='admin'字符串)
user_type = getattr(user, 'user_type', '')
if user_type != 'admin':
if not hasattr(user, 'AdminProfile'):
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.CreateTime, -- DateTimeField
um.avatar -- CharField
FROM xiugaijilu xjl
LEFT JOIN user_main um ON xjl.yonghuid = um.yonghuid
WHERE xjl.xiugaiid = %s
ORDER BY xjl.CreateTime 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)}")
traceback.print_exc()
return Response({
'code': 500,
'message': '系统错误',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class ShangjiaMobanListView(APIView):
"""
商家订单模板列表接口
POST /peizhi/sjddmblb
权限: JWT认证
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
user = request.user
yonghu_id = user.UserUID
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(UserID=yonghu_id, ProductTypeID=leixing_id)
if get_type == 1: # 搜索模式
if keyword:
keyword = str(keyword).strip()
try:
mid = int(keyword)
query &= Q(id=mid) | Q(TemplateDesc__icontains=keyword)
except ValueError:
query &= Q(TemplateDesc__icontains=keyword)
if min_price:
try:
query &= Q(Price__gte=Decimal(str(min_price)))
except:
pass
if label_id:
query &= Q(TitleID=label_id)
mobans = ShangjiaMoban.query.filter(query).order_by('-id')
formatted = []
for moban in mobans:
texiao_json = ''
label_name = ''
if moban.TitleID:
try:
ch = Chenghao.query.get(id=moban.TitleID)
label_name = ch.mingcheng
texiao_json = ch.texiao_miaoshu or ''
except Chenghao.DoesNotExist:
pass
formatted.append({
'mobanId': moban.id,
'shangpinTypeId': moban.ProductTypeID,
'jieshao': moban.TemplateDesc,
'jiage': str(moban.Price),
'fabushuliang': moban.PublishedCount,
'commissionEnabled': moban.CommissionGrabEnabled,
'commissionValue': str(moban.CommissionPrice) if moban.CommissionPrice is not None else None,
'labelId': moban.TitleID,
'labelName': label_name,
'texiaoJson': texiao_json,
'createTime': moban.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if moban.CreateTime else ''
})
return Response({'code': 200, 'msg': '搜索成功', 'data': {'list': formatted, 'total': len(formatted), 'hasMore': False}})
# 分页模式
queryset = ShangjiaMoban.query.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.TitleID:
try:
ch = Chenghao.query.get(id=moban.TitleID)
label_name = ch.mingcheng
texiao_json = ch.texiao_miaoshu or ''
except Chenghao.DoesNotExist:
pass
formatted.append({
'mobanId': moban.id,
'shangpinTypeId': moban.ProductTypeID,
'jieshao': moban.TemplateDesc,
'jiage': str(moban.Price),
'fabushuliang': moban.PublishedCount,
'commissionEnabled': moban.CommissionGrabEnabled,
'commissionValue': str(moban.CommissionPrice) if moban.CommissionPrice is not None else None,
'labelId': moban.TitleID,
'labelName': label_name,
'texiaoJson': texiao_json,
'createTime': moban.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if moban.CreateTime else ''
})
has_more = current_page.has_next()
if len(formatted) < page_size:
has_more = False
return Response({
'code': 200,
'msg': '获取成功',
'data': {
'list': formatted,
'total': paginator.count,
'hasMore': has_more,
'currentPage': page,
'pageSize': page_size,
'totalPages': paginator.num_pages
}
})
except Exception as e:
logger.error(f"获取模板列表失败: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500)
class ShangjiaTianjiaMobanView(APIView):
"""
商家添加订单模板接口
POST /peizhi/sjtjddmb
权限: JWT认证
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 获取当前商家用户ID
user = request.user
yonghu_id = user.UserUID
# 2. 接收并清洗参数,避免类型错误
leixing_id = request.data.get('shangpinTypeId')
jieshao = request.data.get('jieshao', '').strip() if request.data.get('jieshao') else ''
jiage = request.data.get('jiage', '')
# 佣金相关
commission_enabled = request.data.get('commissionEnabled', False)
commission_value = request.data.get('commissionValue', None)
# 标签ID称号ID
label_id = request.data.get('labelId', None)
# 3. 必填验证
if not leixing_id or not jieshao or not jiage:
logger.warning(f"参数缺失 - leixing_id:{leixing_id}, jieshao:{jieshao}, jiage:{jiage}")
return Response({
'code': 400,
'msg': '缺少必填字段(订单类型、介绍、价格)',
'data': {}
}, status=400)
# 4. 价格格式验证
try:
jiage_decimal = Decimal(str(jiage))
if jiage_decimal <= 0:
return Response({'code': 400, 'msg': '价格必须大于0', 'data': {}}, status=400)
if jiage_decimal > 10000:
return Response({'code': 400, 'msg': '价格不能超过10000元', 'data': {}}, status=400)
except (InvalidOperation, ValueError, TypeError):
return Response({'code': 400, 'msg': '价格格式错误', 'data': {}}, status=400)
# 5. 介绍长度验证
if len(jieshao) > 100:
return Response({'code': 400, 'msg': '订单介绍不能超过100字', 'data': {}}, status=400)
# 6. 佣金验证(若启用)
CommissionGrabEnabled = False
CommissionPrice = None
if commission_enabled:
CommissionGrabEnabled = 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)
CommissionPrice = 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.query.get(
id=leixing_id,
shenhezhuangtai=1 # 审核通过
)
except ShangpinLeixing.DoesNotExist:
return Response({'code': 404, 'msg': '商品类型不存在或已禁用', 'data': {}}, status=404)
# 9. 检查同一用户、同一类型下是否已存在相同介绍的模板
if ShangjiaMoban.query.filter(
UserID=yonghu_id,
ProductTypeID=leixing_id,
TemplateDesc=jieshao
).exists():
return Response({'code': 400, 'msg': '已存在相同介绍的模板', 'data': {}}, status=400)
# 10. 执行数据库创建(事务)
with transaction.atomic():
moban = ShangjiaMoban.query.create(
UserID=yonghu_id,
ProductTypeID=leixing_id,
TemplateDesc=jieshao,
Price=jiage_decimal,
PublishedCount=0,
CommissionGrabEnabled=CommissionGrabEnabled,
CommissionPrice=CommissionPrice,
TitleID=cleaned_label_id
)
# 11. 返回成功数据(字段与前端对齐)
return Response({
'code': 200,
'msg': '模板创建成功',
'data': {
'mobanId': moban.id,
'shangpinTypeId': leixing_id,
'jieshao': moban.TemplateDesc,
'jiage': str(moban.Price),
'fabushuliang': moban.PublishedCount,
'createTime': moban.CreateTime.strftime('%Y-%m-%d %H:%M:%S'),
'commissionEnabled': moban.CommissionGrabEnabled,
'commissionValue': str(moban.CommissionPrice) if moban.CommissionPrice is not None else None,
'labelId': moban.TitleID
}
})
except Exception as e:
# 打印完整错误堆栈到日志
logger.error(f"添加模板失败: {str(e)}", exc_info=True)
return Response({
'code': 500,
'msg': '服务器内部错误,添加模板失败,请联系管理员',
'data': {}
}, status=500)
class ShangjiaShanchuMobanView(APIView):
"""
删除模板
POST /peizhi/sjddmbsc
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
user = request.user
yonghu_id = user.UserUID
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.query.get(id=moban_id, UserID=yonghu_id, ProductTypeID=leixing_id)
except ShangjiaMoban.DoesNotExist:
return Response({'code': 404, 'msg': '模板不存在', 'data': {}}, status=404)
# 强制硬删除
moban.delete()
return Response({'code': 200, 'msg': '删除成功', 'data': {'mobanId': moban_id, 'shangpinTypeId': leixing_id}})
except Exception as e:
logger.error(f"删除模板失败: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500)
class ShangjiaGengxinMobanView(APIView):
"""
商家更新订单模板接口
POST /peizhi/sjddmbgx
权限: JWT认证
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
user = request.user
yonghu_id = user.UserUID
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.query.get(
id=moban_id,
UserID=yonghu_id,
ProductTypeID=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.TemplateDesc:
moban.TemplateDesc = new_jieshao
update_fields.append('TemplateDesc')
# 修改价格
if new_jiage:
try:
jiage_decimal = Decimal(new_jiage)
if jiage_decimal <= 0 or jiage_decimal > 10000:
return Response({'code': 400, 'msg': '价格必须在0.1-10000元之间', 'data': {}}, status=400)
if jiage_decimal != moban.Price:
moban.Price = jiage_decimal
update_fields.append('Price')
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.TitleID:
moban.TitleID = new_label
update_fields.append('TitleID')
# 修改佣金设置
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.CommissionGrabEnabled != True or moban.CommissionPrice != comm_decimal:
moban.CommissionGrabEnabled = True
moban.CommissionPrice = comm_decimal
update_fields.extend(['CommissionGrabEnabled', 'CommissionPrice'])
except (InvalidOperation, ValueError, TypeError):
return Response({'code': 400, 'msg': '佣金格式错误', 'data': {}}, status=400)
else:
if moban.CommissionGrabEnabled != False or moban.CommissionPrice is not None:
moban.CommissionGrabEnabled = False
moban.CommissionPrice = None
update_fields.extend(['CommissionGrabEnabled', 'CommissionPrice'])
if not update_fields:
return Response({'code': 400, 'msg': '没有修改内容', 'data': {}}, status=400)
# 检查介绍唯一性
if 'TemplateDesc' in update_fields:
if ShangjiaMoban.query.filter(
UserID=yonghu_id,
ProductTypeID=leixing_id,
TemplateDesc=moban.TemplateDesc
).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.ProductTypeID,
'jieshao': moban.TemplateDesc,
'jiage': str(moban.Price),
'fabushuliang': moban.PublishedCount,
'commissionEnabled': moban.CommissionGrabEnabled,
'commissionValue': str(moban.CommissionPrice) if moban.CommissionPrice else None,
'labelId': moban.TitleID,
'updateTime': moban.UpdateTime.strftime('%Y-%m-%d %H:%M:%S')
}
})
except Exception as e:
logger = logging.getLogger(__name__)
logger.error(f"修改商家模板失败: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500)
class ShangjiaGenerateLinkView(APIView):
"""商家生成发单链接接口(支持模板佣金/标签)"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
user = request.user
# 获取参数
shangpin_type_id = request.data.get('shangpinTypeId')
moban_id = request.data.get('mobanId')
if not all([shangpin_type_id, moban_id]):
return Response({'code': 400, 'msg': '缺少参数', 'data': {}}, status=400)
try:
shangpin_type_id = int(shangpin_type_id)
moban_id = int(moban_id)
except ValueError:
return Response({'code': 400, 'msg': '参数格式错误', 'data': {}}, status=400)
# 商家信息
try:
shangjia = user.ShopProfile
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.query.get(id=shangpin_type_id, shenhezhuangtai=1)
except ShangpinLeixing.DoesNotExist:
return Response({'code': 400, 'msg': '商品类型不存在或已禁用', 'data': {}}, status=400)
# 查询模板
try:
moban = ShangjiaMoban.query.get(id=moban_id, UserID=user.UserUID, ProductTypeID=shangpin_type_id)
except ShangjiaMoban.DoesNotExist:
return Response({'code': 400, 'msg': '模板不存在', 'data': {}}, status=400)
jiage = moban.Price
if jiage > shangjia.yue:
return Response({'code': 400, 'msg': f'余额不足,当前余额{shangjia.yue}', 'data': {}}, status=400)
# 利率
try:
lilu = float(CommissionRate.query.get(Platform='3').Rate) or 1.0
except CommissionRate.DoesNotExist:
lilu = 1.0
dashou_fencheng = Decimal(str(round(float(jiage) * lilu, 2)))
# 抢单要求:根据模板是否启用佣金
chenghao_id = moban.TitleID
if moban.CommissionGrabEnabled and moban.CommissionPrice is not None:
final_yaoqiuleixing = 2 # 佣金模式
final_yongjin = moban.CommissionPrice
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.query.get(id=chenghao_id).mingcheng
except Chenghao.DoesNotExist:
pass
# 生成ID和Token
def generate_dingdan_id():
timestamp = int(time.time() * 1000)
process_id = os.getpid() % 1000
random_num = random.randint(1000, 9999)
return f"SJ{timestamp}{process_id:03d}{random_num}"
def generate_secure_token(dd_id, uid):
timestamp = int(time.time() * 1_000_000)
salt = secrets.token_hex(8)
raw = f"{timestamp}:{dd_id}:{uid}:{salt}"
token = hashlib.sha256(raw.encode()).hexdigest()[:24]
# 确保唯一性(极小概率重复再试一次)
if ShangjiaLianjie.query.filter(LinkToken=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.UserUID)
# 创建订单主表
dingdan = Order.query.create(
OrderID=dingdan_id,
Status=13,
Platform=2,
Amount=jiage,
PlayerCommission=dashou_fencheng,
ProductTypeID=shangpin_type_id,
GrabRequirement=final_yaoqiuleixing,
MembershipID=final_huiyuan_id,
CommissionReq=final_yongjin,
Description=moban.TemplateDesc[:2000],
User1ID=f"Sj{user.UserUID}",
ImageURL=user.Avatar or '',
)
# 商家扩展表
MerchantOrderExt.query.create(
Order=dingdan,
MerchantID=user.UserUID,
MerchantNickname=shangjia.nicheng or f"商家{user.UserUID}",
)
# 关联标签
if chenghao_id:
try:
ch = Chenghao.query.get(id=chenghao_id)
DingdanBiaoqian.query.create(dingdan=dingdan, chenghao=ch)
except Chenghao.DoesNotExist:
logger.warning(f"称号{chenghao_id}不存在,跳过关联")
# 更新商家余额和统计
UserShangjia.query.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.query.create(
UserID=user.UserUID,
ProductTypeID=shangpin_type_id,
TemplateID=moban_id,
LinkURL=link_url,
LinkToken=secure_token,
OrderID=dingdan_id,
is_used=False,
ExpireTime=timezone.now() + timezone.timedelta(days=1)
)
# 更新模板发布数量
ShangjiaMoban.query.filter(id=moban_id).update(PublishedCount=F('PublishedCount') + 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.TemplateDesc,
'jiage': str(jiage),
'labelId': chenghao_id,
'labelName': label_name,
'youxiaoShijian': lianjie_obj.ExpireTime.strftime('%Y-%m-%d %H:%M:%S'),
}
})
except Exception as e:
logger.error(f"生成链接事务失败: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': '系统错误,生成链接失败', 'data': {}}, status=500)
class ShangjiaLianjieListView(APIView):
"""获取已生成的链接列表(支持筛选是否使用)"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
user = request.user
yonghu_id = user.UserUID
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.query.get(id=moban_id, UserID=yonghu_id)
except ShangjiaMoban.DoesNotExist:
return Response({'code': 404, 'msg': '模板不存在或无权查看', 'data': {}}, status=404)
links = ShangjiaLianjie.query.filter(
UserID=yonghu_id,
TemplateID=moban_id,
is_used=(used == 1)
).order_by('-CreateTime')
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.LinkURL,
'LinkToken': link.LinkToken,
'is_used': link.is_used,
'dingdan_id': link.OrderID,
'ExpireTime': link.ExpireTime.strftime('%Y-%m-%d %H:%M:%S') if link.ExpireTime else '',
'CreateTime': link.CreateTime.strftime('%Y-%m-%d %H:%M:%S')
})
return Response({
'code': 200,
'msg': '获取成功',
'data': {
'list': data_list,
'total': paginator.count,
'hasMore': current_page.has_next(),
'currentPage': page,
'pageSize': page_size
}
})
except Exception as e:
logger.error(f"获取链接列表失败: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500)
class IPUtils:
"""IP工具类"""
@staticmethod
def get_client_ip(request):
"""
获取客户端真实IP处理代理情况
优先级: X-Real-IP > X-Forwarded-For > remote_addr
"""
ip = None
try:
# 1. 尝试从 X-Real-IP 获取
x_real_ip = request.META.get('HTTP_X_REAL_IP')
if x_real_ip:
ip = x_real_ip.strip()
logger.info(f"从 X-Real-IP 获取IP: {ip}")
return ip
# 2. 尝试从 X-Forwarded-For 获取(处理代理链)
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
# 可能有多个代理IP第一个是真实IP
ips = x_forwarded_for.split(',')
for possible_ip in ips:
possible_ip = possible_ip.strip()
if possible_ip and possible_ip.lower() != 'unknown':
ip = possible_ip
logger.info(f"从 X-Forwarded-For 获取IP: {ip}")
return ip
# 3. 使用 remote_addr
ip = request.META.get('REMOTE_ADDR', '')
logger.info(f"从 REMOTE_ADDR 获取IP: {ip}")
except Exception as e:
logger.error(f"获取IP失败: {str(e)}", exc_info=True)
ip = '0.0.0.0'
return ip
@staticmethod
def validate_ip(ip):
"""验证IP格式是否合法"""
if not ip:
return False
# IPv4正则
ipv4_pattern = r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
# IPv6正则简化版
ipv6_pattern = r'^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$'
return bool(re.match(ipv4_pattern, ip) or re.match(ipv6_pattern, ip))
@staticmethod
def is_private_ip(ip):
"""判断是否为内网IP"""
if not ip:
return False
# 内网IP段
private_ranges = [
('10.0.0.0', '10.255.255.255'),
('172.16.0.0', '172.31.255.255'),
('192.168.0.0', '192.168.255.255'),
('127.0.0.0', '127.255.255.255'),
]
try:
# 将IP转换为整数比较
def ip_to_int(ip):
parts = ip.split('.')
return (int(parts[0]) << 24) + (int(parts[1]) << 16) + (int(parts[2]) << 8) + int(parts[3])
ip_num = ip_to_int(ip)
for start, end in private_ranges:
if ip_to_int(start) <= ip_num <= ip_to_int(end):
return True
except:
pass
return False
class OrderLinkThrottle(SimpleRateThrottle):
"""订单链接访问频率限制"""
scope = 'order_link'
rate = '10/minute' # 每分钟最多10次
def get_cache_key(self, request, view):
# 基于IP和接口路径进行限制
ip = IPUtils.get_client_ip(request)
if not ip or IPUtils.is_private_ip(ip):
# 内网IP或不合法IP使用默认
ip = 'default'
return f'throttle_{view.__class__.__name__}_{ip}'
class KehuGetDingdanLianjieView(APIView):
"""
客户获取订单链接信息接口
路径GET /peizhi/khhqddlj?token=xxx
返回字段包括订单信息、打手标识、商家标识、俱乐部配置客服链接、存储桶、GoEasy AppKey等
"""
throttle_classes = [OrderLinkThrottle]
permission_classes = [] # 无需认证,任何人都可访问
def get(self, request):
"""
处理GET请求获取订单链接信息
"""
# 1. 记录请求开始
logger.info("=== 客户获取订单链接信息接口开始 ===")
# 2. 获取客户端真实IP
client_ip = IPUtils.get_client_ip(request)
logger.info(f"客户端IP: {client_ip}")
# 3. 验证IP合法性
if not IPUtils.validate_ip(client_ip):
logger.warning(f"IP格式不合法: {client_ip}")
return Response({
'code': 400,
'msg': '网络连接异常',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 获取Token参数
token = request.query_params.get('token', '')
if not token:
logger.warning(f"Token参数缺失 - IP: {client_ip}")
return Response({
'code': 400,
'msg': '缺少链接参数',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
logger.info(f"接收到Token: {token[:8]}...")
# 5. 查询链接记录
try:
lianjie_obj = ShangjiaLianjie.query.select_related().get(LinkToken=token)
logger.info(f"找到链接记录 - 链接ID: {lianjie_obj.id}, 商家ID: {lianjie_obj.UserID}")
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.UserIP = client_ip
lianjie_obj.save(update_fields=['UserIP'])
logger.info(f"更新访问者IP成功: {client_ip}")
except Exception as e:
logger.warning(f"更新IP失败: {str(e)}")
# 继续处理,不影响主要逻辑
# 7. 检查链接有效期
'''now = timezone.now()
if lianjie_obj.ExpireTime and lianjie_obj.ExpireTime < now:
logger.warning(f"链接已过期 - 过期时间: {lianjie_obj.ExpireTime}, 当前: {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.OrderID or '',
'youxiao_shijian': lianjie_obj.ExpireTime.strftime(
'%Y-%m-%d %H:%M:%S') if lianjie_obj.ExpireTime else '',
}
# 9. 获取订单信息
if lianjie_obj.OrderID:
try:
# 查询订单主表
dingdan_obj = Order.query.get(OrderID=lianjie_obj.OrderID)
# 获取商家昵称
shangjia_nicheng = ''
try:
user_main = User.query.get(UserUID=lianjie_obj.UserID)
shangjia_obj = user_main.ShopProfile
shangjia_nicheng = shangjia_obj.nicheng
except (User.DoesNotExist, UserShangjia.DoesNotExist):
logger.warning(f"商家信息不存在 - 用户ID: {lianjie_obj.UserID}")
# ========== 新增:确定俱乐部配置 ==========
# 默认头像(相对路径)
default_avatar = 'a_long/morentouxiang.jpg'
# 从 settings 获取配置
kefu_link = getattr(settings, 'KEFU_LINK', '')
oss_url = getattr(settings, 'COS_DOMAIN', '')
goeasy_appkey = getattr(settings, 'GOEASY_APPKEY', '')
# ========== 构建订单信息 ==========
dingdan_info = {
'jieshao': dingdan_obj.Description or '',
'jiage': str(dingdan_obj.Amount) if dingdan_obj.Amount is not None else '0.00', # 金额字段名是 Amount
'shangjia_mingcheng': shangjia_nicheng,
'zhuangtai': dingdan_obj.Status,
# 新增配置字段
'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.UserID}" if lianjie_obj.UserID else '',
}
# 如果已使用,返回已填写的信息
if lianjie_obj.is_used:
dingdan_info.update({
'youxi_nicheng': dingdan_obj.Nickname or '',
'zhiding_dashou': dingdan_obj.AssignedID or dingdan_obj.PlayerID or '',
'beizhu': dingdan_obj.Remark or '',
'waibu_dingdan_id': dingdan_obj.ExternalOrderID or '',
})
# 获取聊天相关标识(新配对群优先,旧单保留 order 群)
chat_meta, _sub_ok = subscribe_merchant_link_chat(dingdan_obj, lianjie_obj.UserID)
# 添加聊天标识
dingdan_info.update({
'dashou_biaoshi': chat_meta.get('dashou_biaoshi', ''),
'shangjia_biaoshi': chat_meta.get('shangjia_biaoshi', ''),
'you_dashou': chat_meta.get('you_dashou', False),
'group_id': chat_meta.get('group_id', ''),
'is_pair_group': chat_meta.get('is_pair_group', False),
'legacy_group_id': chat_meta.get('legacy_group_id', ''),
})
response_data.update(dingdan_info)
except Order.DoesNotExist:
logger.error(f"订单不存在 - 订单ID: {lianjie_obj.OrderID}")
response_data.update({
'jieshao': '',
'jiage': '0.00',
'shangjia_mingcheng': '',
'zhuangtai': None,
'dashou_biaoshi': '',
'shangjia_biaoshi': '',
'you_dashou': False,
'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,
'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,
'kefu_link': '',
'oss_url': '',
'default_avatar': 'a_long/morentouxiang.jpg',
'goeasy_appkey': '',
'self_user_id': '',
})
# 10. 判断订单状态是否允许修改
if not lianjie_obj.is_used and response_data.get('zhuangtai') == 13:
response_data['allow_modify'] = True
else:
response_data['allow_modify'] = False
# 11. 记录成功日志并返回
logger.info(
f"返回订单信息成功 - Token: {token[:8]}..., IP: {client_ip}, "
f"是否允许修改: {response_data['allow_modify']}, "
f"是否有打手: {response_data.get('you_dashou', False)}"
)
return Response({
'code': 200,
'msg': '成功',
'data': response_data
}, status=status.HTTP_200_OK)
class KehuTianxieDingdanView(APIView):
"""
客户填写订单信息接口
路径POST /peizhi/kehutx
"""
# 添加频率限制(比查询接口更严格)
throttle_classes = [OrderLinkThrottle]
permission_classes = [] # 无需认证
def post(self, request):
"""
处理POST请求填写订单信息
"""
# 1. 记录请求开始
logger.info("=== 客户填写订单信息接口开始 ===")
# 2. 获取客户端真实IP
client_ip = IPUtils.get_client_ip(request)
logger.info(f"客户端IP: {client_ip}")
# 3. 验证IP合法性
if not IPUtils.validate_ip(client_ip):
logger.warning(f"IP格式不合法: {client_ip}")
return Response({
'code': 400,
'msg': '网络连接异常',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 获取请求参数
data = request.data
token = data.get('token', '')
youxi_nicheng = data.get('youxi_nicheng', '').strip()
zhiding_dashou = data.get('zhiding_dashou', '').strip() # 可选
beizhu = data.get('beizhu', '').strip() # 可选
waibu_dingdan_id = str(data.get('waibu_dingdan_id') or data.get('waibuDingdanId') or '').strip()[:64]
# 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)}, 拼多多ID: {waibu_dingdan_id[:16] if waibu_dingdan_id else ''}")
# 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)
if not waibu_dingdan_id:
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(waibu_dingdan_id) > 64:
return Response({
'code': 400,
'msg': '拼多多订单号不能超过64个字符',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 9. 验证备注长度
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.query.get(LinkToken=token)
logger.info(
f"找到链接记录 - 链接ID: {lianjie_obj.id}, 商家ID: {lianjie_obj.UserID}, 是否已使用: {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.ExpireTime and lianjie_obj.ExpireTime < now:
logger.warning(f"链接已过期 - 过期时间: {lianjie_obj.ExpireTime}")
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 = User.query.get(UserUID=zhiding_dashou)
logger.info(f"找到打手用户 - 用户ID: {zhiding_dashou}")
# 通过反向关系获取打手扩展信息
try:
dashou_profile = dashou_user.DashouProfile
# 检查打手状态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 User.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.UserIP = client_ip
lianjie_obj.is_used = True
lianjie_obj.save(update_fields=['UserIP', 'is_used'])
logger.info(f"更新链接记录成功 - 链接ID: {lianjie_obj.id}, 设置为已使用")
# 查询订单
if not lianjie_obj.OrderID:
logger.error(f"链接记录中订单ID为空 - 链接ID: {lianjie_obj.id}")
raise ValueError("订单ID为空")
dingdan_obj = Order.query.get(OrderID=lianjie_obj.OrderID)
# 检查订单状态是否为13灰色状态
if dingdan_obj.Status != 13:
logger.warning(
f"订单状态不允许修改 - 订单ID: {dingdan_obj.OrderID}, 当前状态: {dingdan_obj.Status}")
# 回滚事务
transaction.set_rollback(True)
return Response({
'code': 400,
'msg': '订单状态异常,无法修改',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 更新订单信息
dingdan_obj.Nickname = youxi_nicheng # 游戏昵称
dingdan_obj.Remark = beizhu if beizhu else '' # 备注
dingdan_obj.ExternalOrderID = waibu_dingdan_id
dingdan_obj.Status = zhuangtai_xin # 新状态1或7
# 如果是指定打手设置zhiding_id
if zhiding_dashou and zhuangtai_xin == 7:
dingdan_obj.AssignedID = zhiding_dashou
dingdan_obj.save()
logger.info(
f"更新订单成功 - 订单ID: {dingdan_obj.OrderID}, 新状态: {zhuangtai_xin}, 指定打手: {zhiding_dashou or ''}")
# 构建通知数据(你的 _get_game_type_name 方法保留在视图里直接用)
order_info = {
'dingdan_id': dingdan_obj.OrderID,
'game_type': self._get_game_type_name(dingdan_obj.ProductTypeID),
'amount': str(dingdan_obj.Amount),
'order_desc': (dingdan_obj.Description or '')[:50],
}
dingdan_guangbo.delay(order_info) # 瞬间返回,不阻塞请求
# 获取商家信息用于返回
try:
user_main = User.query.get(UserUID=lianjie_obj.UserID)
shangjia_obj = user_main.ShopProfile
shangjia_mingcheng = shangjia_obj.nicheng
except:
shangjia_mingcheng = f"商家{lianjie_obj.UserID}"
logger.warning(f"获取商家昵称失败,使用默认 - 用户ID: {lianjie_obj.UserID}")
# 14. 返回成功响应
response_data = {
#'dingdan_id': dingdan_obj.dingdan_id,
#'CreateTime': dingdan_obj.CreateTime.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.OrderID}, IP: {client_ip}")
# 新方法(行为驱动,更安全)
update_shangjia_daily(
yonghuid=lianjie_obj.UserID,
amount=Decimal(str(dingdan_obj.Amount)),
action=1, # 1 = 派发
order_id=dingdan_obj.OrderID,
)
return Response({
'code': 200,
'msg': '提交成功',
'data': response_data
}, status=status.HTTP_200_OK)
except Order.DoesNotExist:
logger.error(f"订单不存在 - 订单ID: {lianjie_obj.OrderID}")
return Response({
'code': 500,
'msg': '订单数据异常',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
except Exception as e:
logger.error(f"处理订单异常: {str(e)}", exc_info=True)
return Response({
'code': 500,
'msg': '系统错误,请稍后重试',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def _get_game_type_name(self, leixing_id):
try:
gt = ShangpinLeixing.query.filter(id=leixing_id).first()
return gt.jieshao if gt else '游戏订单'
except Exception as e:
logger.error(f"获取游戏类型名称异常: {e}")
return '游戏订单'
def send_order_notification_async(self, dingdan, youxi_nicheng, zhiding_dashou, zhuangtai):
"""异步发送订单通知(客户填写订单后)"""
try:
# 避免阻塞主线程,使用线程异步执行
def notification_task():
try:
# 获取游戏类型名称
try:
leixing_obj = ShangpinLeixing.query.get(id=dingdan.ProductTypeID)
game_type = leixing_obj.jieshao
except:
game_type = '未知游戏'
# 构建订单信息
order_info = {
'dingdan_id': dingdan.OrderID,
'game_type': game_type,
'amount': str(dingdan.Amount),
'order_desc': dingdan.Description[:50] if dingdan.Description else '新订单',
'youxi_nicheng': youxi_nicheng,
'order_type': '指定订单' if zhuangtai == 7 else '普通订单',
'zhiding_dashou': zhiding_dashou or ''
}
# 调用微信广播发送器
sender = WeixinBroadcastSender()
result = sender.broadcast_order(order_info)
if result['success']:
logger.info(f"【客户填写订单】微信通知发送成功: {result['msg']}")
else:
logger.warning(f"【客户填写订单】微信通知发送失败: {result['msg']}")
except Exception as e:
logger.error(f"【客户填写订单】发送通知任务异常: {str(e)}")
# 启动异步线程
thread = threading.Thread(target=notification_task)
thread.daemon = True
thread.start()
except Exception as e:
logger.error(f"【客户填写订单】启动通知线程异常: {str(e)}")
class GuanZhuAListView(APIView):
"""
获取关注阿龙页面图片列表
POST /peizhi/gzal
需要JWT认证
返回:
{
"code": 0,
"msg": "success",
"data": {
"images": ["相对URL1", "相对URL2", ...],
"last_update": "2025-03-16 12:34:56",
"visit_count": 123
}
}
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
from jituan.services.club_context import resolve_effective_club_id
club_id = resolve_effective_club_id(request, request.user)
qs = Tupianpeizhi.query.filter(ImageType=3, club_id=club_id)
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('ImageURL', flat=True))
# 获取最新更新时间取所有记录的UpdateTime最大值
last_update = qs.aggregate(UpdateTime__max=Max('UpdateTime'))['UpdateTime__max']
last_update_str = last_update.strftime('%Y-%m-%d %H:%M:%S') if last_update else None
# 原子性增加访问次数
# 先更新所有记录,再获取更新后的最大次数
qs.update(AccessCount=F('AccessCount') + 1)
visit_count = qs.aggregate(access_count__max=Max('AccessCount'))['access_count__max'] or 0
return Response({
'code': 0,
'msg': 'success',
'data': {
'images': images,
'last_update': last_update_str,
'visit_count': visit_count
}
}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"获取关注阿龙图片列表失败: {e}", exc_info=True)
return Response({
'code': 1,
'msg': '服务器内部错误',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
#不用缓存一天2000次
class GuanshiQRCodeView(APIView):
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
logger.info("===== 开始生成管事二维码 =====")
current_user = request.user
logger.info(f"当前用户ID: {current_user.UserUID}")
try:
from jituan.services.club_context import resolve_effective_club_id
from utils.weixin_token import get_club_mini_access_token, is_weixin_token_invalid
club_id = resolve_effective_club_id(request, current_user)
try:
guanshi = current_user.GuanshiProfile
logger.info(f"管事扩展表查询成功,状态: {guanshi.zhuangtai}")
except UserGuanshi.DoesNotExist:
logger.warning(f"用户 {current_user.UserUID} 不是管事身份")
return Response(
{'code': 403, 'message': '当前用户不是管事身份', 'data': None},
status=status.HTTP_403_FORBIDDEN
)
if guanshi.zhuangtai != 1:
logger.warning(f"用户 {current_user.UserUID} 管事状态异常: {guanshi.zhuangtai}")
return Response(
{'code': 403, 'message': '管事账号状态异常', 'data': None},
status=status.HTTP_403_FORBIDDEN
)
invite_code = guanshi.yaoqingma
if not invite_code:
logger.info(f"用户 {current_user.UserUID} 无邀请码,开始生成")
invite_code = CreateInvitationCode(str(current_user.UserUID))
if not VerifyInvitationCode(invite_code):
logger.error(f"邀请码唯一性校验失败,用户: {current_user.UserUID}")
return Response(
{'code': 500, 'message': '邀请码生成失败', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
guanshi.yaoqingma = invite_code
guanshi.save(update_fields=['yaoqingma'])
logger.info(f"邀请码生成成功: {invite_code}")
else:
logger.info(f"使用已有邀请码: {invite_code}")
# 按俱乐部使用对应小程序 appid 的 access_token
access_token = get_club_mini_access_token(club_id)
if not access_token:
logger.error("获取 access_token 失败 club_id=%s", club_id)
return Response(
{'code': 500, 'message': '获取微信access_token失败', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
logger.info(f"获取到 access_token (前10位): {access_token[:10]}... club={club_id}")
encoded_invite = urllib.parse.quote(invite_code, safe='')
# 兼容页 dashouduan 跳转打手中心fighter.js 处理 inviteCode 自动注册
page_path = f'pages/dashouduan/dashouduan?inviteCode={encoded_invite}'
wx_url = f'https://api.weixin.qq.com/wxa/getwxacode?access_token={access_token}'
post_data = {
'path': page_path,
'width': 430,
'auto_color': False,
'line_color': {'r': 0, 'g': 0, 'b': 0}
}
logger.info(f"调用微信接口生成小程序码path: {page_path}")
def _request_qr(token):
try:
return requests.post(
f'https://api.weixin.qq.com/wxa/getwxacode?access_token={token}',
json=post_data, timeout=10,
)
except Exception as e:
logger.error(f"请求微信接口异常: {str(e)}", exc_info=True)
return None
wx_resp = _request_qr(access_token)
if wx_resp is None:
return Response(
{'code': 500, 'message': '调用微信服务失败', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
if wx_resp.headers.get('content-type', '').startswith('application/json'):
try:
err = wx_resp.json()
if is_weixin_token_invalid(err.get('errcode'), err.get('errmsg', '')):
access_token = get_club_mini_access_token(club_id, force_refresh=True)
if access_token:
wx_resp = _request_qr(access_token)
except Exception:
pass
logger.info(f"微信接口响应状态码: {wx_resp.status_code}")
if wx_resp.status_code != 200:
error_info = {}
if wx_resp.headers.get('content-type') == 'application/json':
try:
error_info = wx_resp.json()
except:
pass
errmsg = error_info.get('errmsg', '未知错误')
logger.error(f"微信接口返回错误 (HTTP {wx_resp.status_code}): {errmsg}")
if 'errcode' in error_info:
logger.error(f"微信错误码: {error_info.get('errcode')}")
return Response(
{'code': 500, 'message': f'生成二维码失败: {errmsg}', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
content_type = wx_resp.headers.get('content-type', '')
if 'image' not in content_type:
try:
error_info = wx_resp.json()
errmsg = error_info.get('errmsg', '未知错误')
logger.error(f"微信接口返回非图片内容: {errmsg}")
return Response(
{'code': 500, 'message': f'生成二维码失败: {errmsg}', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
except:
logger.error("微信接口返回未知内容无法解析为JSON")
return Response(
{'code': 500, 'message': '生成二维码失败', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
timestamp = int(time.time())
filename = f"{current_user.UserUID}_{timestamp}.png"
oss_path = f"erweima/guanshi/{club_id}/{filename}"
file_obj = io.BytesIO(wx_resp.content)
logger.info(f"开始上传图片到COS: {oss_path}")
try:
full_url = upload_to_oss(file_obj, oss_path)
logger.info(f"上传成功完整URL: {full_url}")
except Exception as e:
logger.error(f"上传到COS异常: {str(e)}", exc_info=True)
return Response(
{'code': 500, 'message': '图片上传失败', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
if not full_url:
logger.error("upload_to_oss 返回了空 URL")
return Response(
{'code': 500, 'message': '图片上传失败', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
cos_domain = getattr(settings, 'COS_DOMAIN', '')
if cos_domain and full_url.startswith(cos_domain):
relative_path = full_url[len(cos_domain):].lstrip('/')
else:
relative_path = oss_path
logger.info(f"相对路径: {relative_path}")
old_url = guanshi.invite_qrcode_url
if old_url:
logger.info(f"删除旧二维码: {old_url}")
try:
delete_from_oss(old_url)
except Exception as e:
logger.warning(f"删除旧二维码失败(不影响主流程): {str(e)}")
with transaction.atomic():
guanshi.invite_qrcode_url = relative_path
guanshi.save(update_fields=['invite_qrcode_url'])
logger.info("数据库更新成功")
return Response({
'code': 0,
'message': 'success',
'data': {'url': relative_path}
})
except Exception as e:
logger.error(f"生成二维码过程中发生未捕获异常: {str(e)}", exc_info=True)
return Response(
{'code': 500, 'message': '服务器内部错误', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
def get_fresh_access_token(self):
"""
直接请求微信接口获取新的 access_token不缓存保证最新。
"""
appid = getattr(settings, 'WEIXIN_APPID', '')
secret = getattr(settings, 'WEIXIN_SECRET', '')
if not appid or not secret:
logger.error("微信小程序配置缺失: WEIXIN_APPID 或 WEIXIN_SECRET 未设置")
return None
url = 'https://api.weixin.qq.com/cgi-bin/token'
params = {
'grant_type': 'client_credential',
'appid': appid,
'secret': secret
}
try:
logger.info("正在请求微信接口获取最新 access_token...")
resp = requests.get(url, params=params, timeout=5)
data = resp.json()
new_token = data.get('access_token')
if new_token:
logger.info("成功获取最新 access_token")
return new_token
else:
errcode = data.get('errcode', '未知')
errmsg = data.get('errmsg', '未知错误')
logger.error(f"获取 access_token 失败errcode={errcode}, errmsg={errmsg}")
return None
except Exception as e:
logger.error(f"请求微信 access_token 接口异常: {str(e)}", exc_info=True)
return None
class ZuzhangHaibaoView(APIView):
"""
组长专属海报二维码生成接口
路径:/peizhi/zuzhanghb
方法POST
权限JWT认证仅限组长身份
返回:
{
"code": 0,
"message": "success",
"data": {
"url": "erweima/zuzhang/1000001_1234567890.png",
"yaoqingma": "ABC123XYZ"
}
}
"""
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
current_user = request.user
from jituan.services.club_context import resolve_effective_club_id
from utils.weixin_token import get_club_mini_access_token
club_id = resolve_effective_club_id(request, current_user)
# 1. 验证组长身份
try:
zuzhang = current_user.ZuzhangProfile
except UserZuzhang.DoesNotExist:
return Response({
'code': 403,
'message': '当前用户不是组长身份',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 2. 验证组长账号状态
if zuzhang.zhuangtai != 1:
return Response({
'code': 403,
'message': '组长账号状态异常,无法生成海报',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 3. 获取或生成邀请码
invite_code = zuzhang.yaoqingma
if not invite_code:
invite_code = CreateInvitationCode(str(current_user.UserUID))
if not VerifyInvitationCode(invite_code):
logger.error(f"组长邀请码生成失败,用户: {current_user.UserUID}")
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 = get_club_mini_access_token(club_id, force_refresh=False)
if not access_token:
return Response({
'code': 500,
'message': '获取微信access_token失败',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# ===== 修改点:使用 A 接口getwxacode =====
encoded_invite = urllib.parse.quote(invite_code, safe='')
# 兼容页 guanshiduan 跳转打手中心(管事 registerType
page_path = f'pages/guanshiduan/guanshiduan?inviteCode={encoded_invite}'
wx_resp, wx_error = self._request_wx_qrcode(page_path, access_token)
if wx_error and is_weixin_token_invalid(wx_error.get('errcode'), wx_error.get('errmsg', '')):
logger.warning('组长海报二维码 token 失效,强制刷新后重试 club=%s', club_id)
access_token = get_club_mini_access_token(club_id, force_refresh=True)
if not access_token:
return Response({
'code': 500,
'message': '获取微信access_token失败',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
wx_resp, wx_error = self._request_wx_qrcode(page_path, access_token)
if wx_error:
errmsg = wx_error.get('errmsg', '未知错误')
logger.error(f"微信接口返回错误: {errmsg}")
return Response({
'code': 500,
'message': f'生成二维码失败: {errmsg}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
if wx_resp is None:
return Response({
'code': 500,
'message': '调用微信服务失败',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# 5. 上传到腾讯云 COS
timestamp = int(time.time())
filename = f"{current_user.UserUID}_{timestamp}.png"
oss_path = f"erweima/zuzhang/{club_id}/{filename}"
file_obj = io.BytesIO(wx_resp.content)
try:
full_url = upload_to_oss(file_obj, oss_path)
except Exception as e:
logger.error(f"上传到COS失败: {e}")
return Response({
'code': 500,
'message': '图片上传失败',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
if not full_url:
return Response({
'code': 500,
'message': '图片上传失败',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# 6. 获取相对路径
cos_domain = getattr(settings, 'COS_DOMAIN', '')
if cos_domain and full_url.startswith(cos_domain):
relative_path = full_url[len(cos_domain):].lstrip('/')
else:
relative_path = oss_path
# 7. 删除旧二维码文件
old_url = zuzhang.haibao_url
if old_url:
delete_from_oss(old_url)
# 8. 更新数据库中的 haibao_url 字段
with transaction.atomic():
zuzhang.haibao_url = relative_path
zuzhang.save(update_fields=['haibao_url'])
# 9. 返回邀请码和二维码相对路径
return Response({
'code': 0,
'message': 'success',
'data': {
'url': relative_path,
'yaoqingma': invite_code
}
})
def _request_wx_qrcode(self, page_path, access_token):
"""
调用 getwxacode 生成小程序码。
返回 (response, error_dict);成功时 error_dict 为 None。
"""
wx_url = f'https://api.weixin.qq.com/wxa/getwxacode?access_token={access_token}'
post_data = {
'path': page_path,
'width': 430,
'auto_color': False,
'line_color': {'r': 0, 'g': 0, 'b': 0},
}
try:
wx_resp = requests.post(wx_url, json=post_data, timeout=10)
except Exception as e:
logger.error(f'请求微信接口失败: {e}')
return None, {'errmsg': '调用微信服务失败'}
if wx_resp.status_code != 200:
error_info = {}
if wx_resp.headers.get('content-type', '').startswith('application/json'):
try:
error_info = wx_resp.json()
except Exception:
pass
return wx_resp, {
'errcode': error_info.get('errcode'),
'errmsg': error_info.get('errmsg', f'HTTP {wx_resp.status_code}'),
}
content_type = wx_resp.headers.get('content-type', '')
if 'image' not in content_type:
try:
error_info = wx_resp.json()
return wx_resp, {
'errcode': error_info.get('errcode'),
'errmsg': error_info.get('errmsg', '未知错误'),
}
except Exception:
return wx_resp, {'errmsg': '微信接口返回未知内容'}
return wx_resp, None
class PopupConfigView(APIView):
"""
获取指定页面的弹窗配置
请求格式POST { "pageKey": "dashouzhongxin" }
返回格式:{ "code": 0, "data": { "serverTime": "...", "popups": [...] } }
"""
authentication_classes = []
permission_classes = [AllowAny]
def post(self, request):
# 获取参数 pageKey
page_key = request.data.get('pageKey')
if not page_key:
return Response({
'code': 400,
'msg': '缺少参数 pageKey'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 查询页面配置(按俱乐部)
from jituan.services.display_config import get_gonggao_content
from jituan.services.club_context import resolve_club_id_from_request
club_id = resolve_club_id_from_request(request)
try:
popup_page = PopupPage.query.get(club_id=club_id, 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.query.filter(
page=popup_page,
is_active=True,
start_time__lte=now,
end_time__gte=now
).prefetch_related('images') # 预加载图片避免N+1查询
# 按 sort_order 排序model Meta 已定义)
# 5. 序列化
serializer = PopupConfigSerializer(popups, many=True)
# 6. 返回数据
return Response({
'code': 0,
'data': {
'serverTime': now.isoformat(),
'ignore_user_mute': popup_page.ignore_user_mute, # 新增
'popups': serializer.data
}
})
class GetWithdrawModeView(APIView):
"""
获取提现模式配置
前端请求 /peizhi/hqtxzsym
返回格式: { "code": 0, "data": { "mode": 1 or 2 } }
"""
permission_classes = [IsAuthenticated] # JWT 认证
def post(self, request, *args, **kwargs):
from jituan.services.club_context import resolve_club_id_from_request
from jituan.services.club_user import get_user_club_id
from jituan.services.withdraw_config import get_withdraw_mode
user = getattr(request, 'user', None)
club_id = get_user_club_id(user) if user and getattr(user, 'is_authenticated', False) else None
if not club_id:
club_id = resolve_club_id_from_request(request)
mode = get_withdraw_mode(club_id)
return Response({
'code': 0,
'data': {
'mode': mode,
'club_id': club_id,
}
})
class CheckPhoneAuthView(APIView):
"""
判断当前登录用户是否需要强制手机号认证
规则:
- 已认证过手机号PhoneVerified=True → 不需要
- 未认证时,满足以下任一条件 → 需要认证:
打手:有会员购买记录,或押金 > 0
商家:已是商家(存在商家扩展表)
管事:已是管事(存在管事扩展表)
组长:已是组长(存在组长扩展表)
老板有下单记录alldingdan > 0
子客服已是有效商家客服merchant_staff_member 活跃)
"""
permission_classes = [IsAuthenticated]
def post(self, request):
user = request.user
if getattr(user, 'PhoneVerified', False):
return Response({'code': 0, 'need_auth': False})
if self._dashou_meets_threshold(user):
return Response({'code': 0, 'need_auth': True})
if self._shangjia_meets_threshold(user):
return Response({'code': 0, 'need_auth': True})
if self._guanshi_meets_threshold(user):
return Response({'code': 0, 'need_auth': True})
if self._zuzhang_meets_threshold(user):
return Response({'code': 0, 'need_auth': True})
if self._boss_meets_threshold(user):
return Response({'code': 0, 'need_auth': True})
if self._staff_meets_threshold(user):
return Response({'code': 0, 'need_auth': True})
return Response({'code': 0, 'need_auth': False})
def _staff_meets_threshold(self, user):
"""子客服:已是有效商家客服"""
try:
from merchant_ops.services.authz import get_active_staff_member
return get_active_staff_member(user) is not None
except Exception:
return False
def _dashou_meets_threshold(self, user):
"""打手:有会员记录、押金 > 0、或有过接单/成交订单"""
try:
dashou = UserDashou.query.get(user=user)
except UserDashou.DoesNotExist:
return False
if dashou.yajin and dashou.yajin > 0:
return True
from jituan.services.club_context import resolve_effective_club_id
club_id = resolve_effective_club_id(self.request, user)
if Huiyuangoumai.query.filter(yonghu_id=user.UserUID, club_id=club_id).exists():
return True
if (dashou.jiedanzongliang or 0) > 0 or (dashou.chengjiaozongliang or 0) > 0:
return True
return Order.query.filter(PlayerID=user.UserUID).exists()
def _shangjia_meets_threshold(self, user):
"""商家:已是商家身份"""
return UserShangjia.query.filter(user=user).exists()
def _guanshi_meets_threshold(self, user):
"""管事:已是管事身份"""
return UserGuanshi.query.filter(user=user).exists()
def _zuzhang_meets_threshold(self, user):
"""组长:已是组长身份"""
return UserZuzhang.query.filter(user=user).exists()
def _boss_meets_threshold(self, user):
"""老板:有下单记录"""
try:
boss = UserBoss.query.get(user=user)
except UserBoss.DoesNotExist:
return False
return (boss.alldingdan or 0) > 0
# ---------- 小程序客服话术 / 自动回复 ----------
MINIAPP_SCRIPT_DEFAULTS = {
'cs_welcome': {
'title': '',
'content': '你好,请问有什么可以帮到您的?',
'confirm_text': '',
'cancel_text': '',
},
'chat_image_confirm': {
'title': '温馨提示',
'content': '平台禁止任何违法违规行为,聊天内容仅限于商家与接单员正常业务对接。严禁赌博、诈骗及其他非法行为,违者后果自负。',
'confirm_text': '确认',
'cancel_text': '取消',
},
}
def _resolve_script_club_id(request):
from jituan.services.club_context import resolve_club_id_from_request
from jituan.services.club_user import get_user_club_id
user = getattr(request, 'user', None)
club_id = get_user_club_id(user) if user and getattr(user, 'is_authenticated', False) else None
if not club_id:
club_id = resolve_club_id_from_request(request)
return club_id or 'xq'
def _merge_script_scene(scene_key, row):
base = dict(MINIAPP_SCRIPT_DEFAULTS.get(scene_key) or {})
if not row:
return base
if row.title:
base['title'] = row.title
if row.content:
base['content'] = row.content
if row.confirm_text:
base['confirm_text'] = row.confirm_text
if row.cancel_text:
base['cancel_text'] = row.cancel_text
return base
class HuashuHqView(APIView):
"""POST /peizhi/huashuhq 批量获取场景话术"""
permission_classes = [IsAuthenticated]
def post(self, request):
scene_keys = request.data.get('scene_keys') or []
if not isinstance(scene_keys, list) or not scene_keys:
return Response({'code': 400, 'msg': 'scene_keys 不能为空', 'data': None}, status=400)
club_id = _resolve_script_club_id(request)
rows = MiniappScriptScene.query.filter(
club_id=club_id, scene_key__in=scene_keys, enabled=True,
)
row_map = {r.scene_key: r for r in rows}
data = {}
for key in scene_keys:
if not isinstance(key, str) or not key.strip():
continue
data[key.strip()] = _merge_script_scene(key.strip(), row_map.get(key.strip()))
return Response({'code': 200, 'msg': 'success', 'data': data})
class HuashuMatchView(APIView):
"""POST /peizhi/huashu_match 客服关键词自动回复匹配"""
permission_classes = [IsAuthenticated]
def post(self, request):
scene_key = (request.data.get('scene_key') or '').strip()
user_text = (request.data.get('user_text') or '').strip()
if scene_key != 'cs_auto_reply':
return Response({'code': 400, 'msg': '不支持的 scene_key', 'data': {'matched': False, 'content': ''}})
if not user_text:
return Response({'code': 200, 'msg': 'success', 'data': {'matched': False, 'content': ''}})
club_id = _resolve_script_club_id(request)
rules = MiniappScriptAutoReply.query.filter(
club_id=club_id, enabled=True,
).order_by('-sort_order', 'id')
text_lower = user_text.lower()
for rule in rules:
kw = (rule.keyword or '').strip()
if not kw:
continue
if kw.lower() in text_lower or kw in user_text:
reply_type = rule.reply_type or MiniappScriptAutoReply.REPLY_TEXT
return Response({
'code': 200,
'msg': 'success',
'data': {
'matched': True,
'content': rule.reply_content or '',
'reply_type': reply_type,
},
})
return Response({'code': 200, 'msg': 'success', 'data': {'matched': False, 'content': ''}})