拆分了 config
This commit is contained in:
3718
config/views.py
3718
config/views.py
File diff suppressed because it is too large
Load Diff
74
config/views/__init__.py
Normal file
74
config/views/__init__.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
"""
|
||||||
|
配置应用视图聚合模块(config/views/__init__.py)
|
||||||
|
|
||||||
|
本模块原为 config/views.py(3718 行单文件),已按功能组拆分到 config/views/ 子包。
|
||||||
|
本文件作为聚合 re-export 入口,重新导出所有视图类与公共符号。
|
||||||
|
外部引用(config/urls.py、backend/urls.py、merchant_ops/views_templates.py)保持不变。
|
||||||
|
|
||||||
|
功能组拆分:
|
||||||
|
- miniapp_config 小程序动态配置(动态配置/海报/弹窗/提现方式/手机号认证)
|
||||||
|
- admin_config 管理员配置(商品公告/配置查询/图片上传/群与利率更新)
|
||||||
|
- admin_stats 管理员统计/记录(打手排行/账号查询/修改记录)
|
||||||
|
- merchant_templates 商家订单模板
|
||||||
|
- merchant_links 商家链接/客户订单(含 IPUtils、OrderLinkThrottle)
|
||||||
|
- qrcode_poster 二维码/海报(关注/管事二维码/组长海报)
|
||||||
|
- scripts 话术配置(含辅助函数)
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# 各功能组视图
|
||||||
|
from .miniapp_config import (
|
||||||
|
GetDynamicConfigView, HaibaoPeizhiView, PopupConfigView,
|
||||||
|
GetWithdrawModeView, CheckPhoneAuthView,
|
||||||
|
)
|
||||||
|
from .admin_config import (
|
||||||
|
ShangpinGonggaoView, AdminConfigQueryView, AdminUploadImageView,
|
||||||
|
AdminUpdateConfigView,
|
||||||
|
)
|
||||||
|
from .admin_stats import (
|
||||||
|
DashouPaihangView, GetAdminZhanghaoView, GetXiugaiJiluView,
|
||||||
|
)
|
||||||
|
from .merchant_templates import (
|
||||||
|
ShangjiaMobanListView, ShangjiaTianjiaMobanView,
|
||||||
|
ShangjiaShanchuMobanView, ShangjiaGengxinMobanView,
|
||||||
|
)
|
||||||
|
from .merchant_links import (
|
||||||
|
ShangjiaGenerateLinkView, ShangjiaLianjieListView,
|
||||||
|
KehuGetDingdanLianjieView, KehuTianxieDingdanView,
|
||||||
|
IPUtils, OrderLinkThrottle,
|
||||||
|
)
|
||||||
|
from .qrcode_poster import (
|
||||||
|
GuanZhuAListView, GuanshiQRCodeView, ZuzhangHaibaoView,
|
||||||
|
)
|
||||||
|
from .scripts import (
|
||||||
|
HuashuHqView, HuashuMatchView,
|
||||||
|
MiniappScriptHoutaiListView, MiniappScriptHoutaiModifyView,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 模块级符号(保持向后兼容)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
# miniapp_config
|
||||||
|
'GetDynamicConfigView', 'HaibaoPeizhiView', 'PopupConfigView',
|
||||||
|
'GetWithdrawModeView', 'CheckPhoneAuthView',
|
||||||
|
# admin_config
|
||||||
|
'ShangpinGonggaoView', 'AdminConfigQueryView', 'AdminUploadImageView',
|
||||||
|
'AdminUpdateConfigView',
|
||||||
|
# admin_stats
|
||||||
|
'DashouPaihangView', 'GetAdminZhanghaoView', 'GetXiugaiJiluView',
|
||||||
|
# merchant_templates
|
||||||
|
'ShangjiaMobanListView', 'ShangjiaTianjiaMobanView',
|
||||||
|
'ShangjiaShanchuMobanView', 'ShangjiaGengxinMobanView',
|
||||||
|
# merchant_links
|
||||||
|
'ShangjiaGenerateLinkView', 'ShangjiaLianjieListView',
|
||||||
|
'KehuGetDingdanLianjieView', 'KehuTianxieDingdanView',
|
||||||
|
'IPUtils', 'OrderLinkThrottle',
|
||||||
|
# qrcode_poster
|
||||||
|
'GuanZhuAListView', 'GuanshiQRCodeView', 'ZuzhangHaibaoView',
|
||||||
|
# scripts
|
||||||
|
'HuashuHqView', 'HuashuMatchView',
|
||||||
|
'MiniappScriptHoutaiListView', 'MiniappScriptHoutaiModifyView',
|
||||||
|
# 公共符号
|
||||||
|
'logger',
|
||||||
|
]
|
||||||
612
config/views/admin_config.py
Normal file
612
config/views/admin_config.py
Normal file
@@ -0,0 +1,612 @@
|
|||||||
|
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 utils.pdd_order_validator import validate_pdd_order_id, validate_cn_mobile
|
||||||
|
|
||||||
|
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 ShangpinGonggaoView(APIView):
|
||||||
|
"""
|
||||||
|
商品公告和轮播图接口
|
||||||
|
获取类型为1的公告和轮播图
|
||||||
|
"""
|
||||||
|
throttle_classes = [AnonRateThrottle]
|
||||||
|
permission_classes = [AllowAny]
|
||||||
|
authentication_classes = []
|
||||||
|
|
||||||
|
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 = {}
|
||||||
|
|
||||||
|
# 平台发单利率(类型1,fadanpingtai='1')
|
||||||
|
platform_rate = rates.get('platform_rate')
|
||||||
|
if platform_rate is not None:
|
||||||
|
try:
|
||||||
|
platform_rate_float = float(platform_rate)
|
||||||
|
if 0 <= platform_rate_float <= 1:
|
||||||
|
# 🔴 修复:查询现有记录,如果不存在则创建
|
||||||
|
platform_rate_obj = 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)
|
||||||
|
|
||||||
|
# 商家发单利率(类型2,fadanpingtai='2')
|
||||||
|
shop_rate = rates.get('shop_rate')
|
||||||
|
if shop_rate is not None:
|
||||||
|
try:
|
||||||
|
shop_rate_float = float(shop_rate)
|
||||||
|
if 0 <= shop_rate_float <= 1:
|
||||||
|
# 🔴 修复:查询现有记录,如果不存在则创建
|
||||||
|
shop_rate_obj = 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 🔴【新增】打手提现抽成利率(类型5,fadanpingtai='5')
|
||||||
|
dashou_withdraw_rate = rates.get('dashou_withdraw_rate')
|
||||||
|
if dashou_withdraw_rate is not None:
|
||||||
|
try:
|
||||||
|
dashou_withdraw_rate_float = float(dashou_withdraw_rate)
|
||||||
|
if 0 <= dashou_withdraw_rate_float <= 1:
|
||||||
|
# 查询现有记录,如果不存在则创建
|
||||||
|
dashou_withdraw_obj = 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)
|
||||||
|
|
||||||
|
# 🔴【新增】管事提现抽成利率(类型6,fadanpingtai='6')
|
||||||
|
guanshi_withdraw_rate = rates.get('guanshi_withdraw_rate')
|
||||||
|
if guanshi_withdraw_rate is not None:
|
||||||
|
try:
|
||||||
|
guanshi_withdraw_rate_float = float(guanshi_withdraw_rate)
|
||||||
|
if 0 <= guanshi_withdraw_rate_float <= 1:
|
||||||
|
# 查询现有记录,如果不存在则创建
|
||||||
|
guanshi_withdraw_obj = 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
718
config/views/admin_stats.py
Normal file
718
config/views/admin_stats.py
Normal file
@@ -0,0 +1,718 @@
|
|||||||
|
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 utils.pdd_order_validator import validate_pdd_order_id, validate_cn_mobile
|
||||||
|
|
||||||
|
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 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都是CharField,xiugaiid也是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转float,Integer转int)
|
||||||
|
'xiugaihou_dashoutixianjine': float(record[4]) if record[4] is not None else 0.00,
|
||||||
|
'xiugaiqian_dashoutixianjine': float(record[5]) if record[5] is not None else 0.00,
|
||||||
|
'xiugaihou_dashoujifen': int(record[6]) if record[6] is not None else 0,
|
||||||
|
'xiugaiqian_dashoujifen': int(record[7]) if record[7] is not None else 0,
|
||||||
|
'xiugaihou_dashouyajin': float(record[8]) if record[8] is not None else 0.00,
|
||||||
|
'xiugaiqian_dashouyajin': float(record[9]) if record[9] is not None else 0.00,
|
||||||
|
|
||||||
|
# 商家字段
|
||||||
|
'xiugaihou_shangjiayue': float(record[10]) if record[10] is not None else 0.00,
|
||||||
|
'xiugaiqian_shangjiayue': float(record[11]) if record[11] is not None else 0.00,
|
||||||
|
|
||||||
|
# 管事字段
|
||||||
|
'xiugaihou_guanshiyue': float(record[12]) if record[12] is not None else 0.00,
|
||||||
|
'xiugaiqian_guanshiyue': float(record[13]) if record[13] is not None else 0.00,
|
||||||
|
|
||||||
|
# 会员字段
|
||||||
|
'tianjia_huiyuantianshu': int(record[14]) if record[14] is not None else 0,
|
||||||
|
'huiyuan_id': str(record[15]) if record[15] else '', # 字符串
|
||||||
|
'huiyuan_mingzi': huiyuan_mingzi, # 字符串
|
||||||
|
|
||||||
|
# 其他字段
|
||||||
|
'qita_shuoming': str(record[16]) if record[16] else '', # 字符串
|
||||||
|
'xiugai_time': record[17].strftime('%Y-%m-%d %H:%M:%S') if record[17] else None,
|
||||||
|
'touxiang': str(record[18]) if record[18] else '' # avatar是字符串
|
||||||
|
}
|
||||||
|
|
||||||
|
jilu_list.append(jilu_data)
|
||||||
|
|
||||||
|
# 🔥 7. 分页控制
|
||||||
|
queried_records = len(jilu_list)
|
||||||
|
has_more = False
|
||||||
|
|
||||||
|
if queried_records == pagesize:
|
||||||
|
total_queried = (page - 1) * pagesize + queried_records
|
||||||
|
has_more = total_queried < total_count
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'message': '获取成功',
|
||||||
|
'data': {
|
||||||
|
'list': jilu_list,
|
||||||
|
'zongshuliang': total_count,
|
||||||
|
'dangqian_page': page,
|
||||||
|
'page_size': pagesize,
|
||||||
|
'has_more': has_more,
|
||||||
|
'shijichaxundediaoshuliang': queried_records
|
||||||
|
}
|
||||||
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"参数错误: {str(e)}")
|
||||||
|
return Response({
|
||||||
|
'code': 400,
|
||||||
|
'message': '参数错误',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"获取修改记录失败: {str(e)}")
|
||||||
|
traceback.print_exc()
|
||||||
|
return Response({
|
||||||
|
'code': 500,
|
||||||
|
'message': '系统错误',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
1032
config/views/merchant_links.py
Normal file
1032
config/views/merchant_links.py
Normal file
File diff suppressed because it is too large
Load Diff
486
config/views/merchant_templates.py
Normal file
486
config/views/merchant_templates.py
Normal file
@@ -0,0 +1,486 @@
|
|||||||
|
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 utils.pdd_order_validator import validate_pdd_order_id, validate_cn_mobile
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
344
config/views/miniapp_config.py
Normal file
344
config/views/miniapp_config.py
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
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 utils.pdd_order_validator import validate_pdd_order_id, validate_cn_mobile
|
||||||
|
|
||||||
|
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 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': '取消',
|
||||||
|
},
|
||||||
|
'merchant_dispatch_confirm': {
|
||||||
|
'title': '确认派单',
|
||||||
|
'content': '确认向该打手派发此订单?',
|
||||||
|
'confirm_text': '确认',
|
||||||
|
'cancel_text': '取消',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
553
config/views/qrcode_poster.py
Normal file
553
config/views/qrcode_poster.py
Normal file
@@ -0,0 +1,553 @@
|
|||||||
|
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 utils.pdd_order_validator import validate_pdd_order_id, validate_cn_mobile
|
||||||
|
|
||||||
|
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 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='')
|
||||||
|
# 新小程序:打手端 Tab「我的」;旧码 dashouduan 由小程序兼容页转发
|
||||||
|
page_path = f'pages/fighter/fighter?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='')
|
||||||
|
# 新小程序:打手端 Tab「我的」管事注册;旧码 guanshiduan 由小程序兼容页转发
|
||||||
|
page_path = f'pages/fighter/fighter?registerType=guanshi&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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
343
config/views/scripts.py
Normal file
343
config/views/scripts.py
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
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 utils.pdd_order_validator import validate_pdd_order_id, validate_cn_mobile
|
||||||
|
|
||||||
|
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__)
|
||||||
|
|
||||||
|
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_raw = (rule.keyword or '').strip()
|
||||||
|
if not kw_raw:
|
||||||
|
continue
|
||||||
|
keywords = [k.strip() for k in kw_raw.split(',') if k.strip()]
|
||||||
|
for kw in keywords:
|
||||||
|
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': ''}})
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- 后台:客服话术配置(/houtai/hthqhs、/houtai/htxghs) ----------
|
||||||
|
|
||||||
|
SCRIPT_CONFIG_PERMS = ('kfhs666', '8080a')
|
||||||
|
SCRIPT_SCENE_KEYS = ('chat_image_confirm', 'cs_welcome', 'merchant_dispatch_confirm')
|
||||||
|
|
||||||
|
|
||||||
|
def _has_script_config_perm(permissions):
|
||||||
|
return any(code in permissions for code in SCRIPT_CONFIG_PERMS)
|
||||||
|
|
||||||
|
|
||||||
|
def _script_scene_item_type(scene_key):
|
||||||
|
return 'text_display' if scene_key == 'cs_welcome' else 'confirm_modal'
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_script_scene(row):
|
||||||
|
return {
|
||||||
|
'id': row.id,
|
||||||
|
'scene_key': row.scene_key,
|
||||||
|
'item_type': _script_scene_item_type(row.scene_key),
|
||||||
|
'title': row.title or '',
|
||||||
|
'content': row.content or '',
|
||||||
|
'confirm_text': row.confirm_text or '',
|
||||||
|
'cancel_text': row.cancel_text or '',
|
||||||
|
'is_active': bool(row.enabled),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_script_auto_reply(row):
|
||||||
|
keywords = [k.strip() for k in (row.keyword or '').split(',') if k.strip()]
|
||||||
|
return {
|
||||||
|
'id': row.id,
|
||||||
|
'scene_key': 'cs_auto_reply',
|
||||||
|
'item_type': 'auto_reply',
|
||||||
|
'title': '',
|
||||||
|
'keywords': keywords,
|
||||||
|
'match_mode': 'contains',
|
||||||
|
'priority': row.sort_order or 0,
|
||||||
|
'content': row.reply_content or '',
|
||||||
|
'is_active': bool(row.enabled),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MiniappScriptHoutaiListView(APIView):
|
||||||
|
"""POST /houtai/hthqhs 后台获取话术与自动回复配置"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
from backend.utils import verify_kefu_permission
|
||||||
|
|
||||||
|
username = request.data.get('username')
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
|
||||||
|
if not _has_script_config_perm(permissions):
|
||||||
|
return Response({'code': 403, 'msg': '您没有权限访问客服聊天配置'})
|
||||||
|
|
||||||
|
club_id = _resolve_script_club_id(request)
|
||||||
|
scenes = MiniappScriptScene.query.filter(
|
||||||
|
club_id=club_id, scene_key__in=SCRIPT_SCENE_KEYS,
|
||||||
|
).order_by('scene_key')
|
||||||
|
auto_rules = MiniappScriptAutoReply.query.filter(club_id=club_id).order_by('-sort_order', 'id')
|
||||||
|
|
||||||
|
items = [_serialize_script_scene(r) for r in scenes]
|
||||||
|
items.extend(_serialize_script_auto_reply(r) for r in auto_rules)
|
||||||
|
return Response({'code': 0, 'data': {'items': items, 'club_id': club_id}})
|
||||||
|
|
||||||
|
|
||||||
|
class MiniappScriptHoutaiModifyView(APIView):
|
||||||
|
"""POST /houtai/htxghs 后台增删改话术与自动回复"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
from backend.utils import verify_kefu_permission
|
||||||
|
from jituan.services.display_config import forbid_display_write_in_all_scope
|
||||||
|
|
||||||
|
username = request.data.get('username')
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
|
||||||
|
if not _has_script_config_perm(permissions):
|
||||||
|
return Response({'code': 403, 'msg': '您没有权限操作客服聊天配置'})
|
||||||
|
|
||||||
|
action = (request.data.get('action') or '').strip()
|
||||||
|
if not action:
|
||||||
|
return Response({'code': 400, 'msg': '缺少 action 参数'})
|
||||||
|
|
||||||
|
club_id = _resolve_script_club_id(request)
|
||||||
|
|
||||||
|
if action == 'delete':
|
||||||
|
return self._delete_item(request, club_id)
|
||||||
|
|
||||||
|
deny = forbid_display_write_in_all_scope(request)
|
||||||
|
if deny:
|
||||||
|
return deny
|
||||||
|
|
||||||
|
if action in ('create', 'update'):
|
||||||
|
item_type = (request.data.get('item_type') or '').strip()
|
||||||
|
if item_type == 'auto_reply':
|
||||||
|
return self._save_auto_reply(request, club_id, action)
|
||||||
|
return self._save_scene(request, club_id, action)
|
||||||
|
|
||||||
|
return Response({'code': 400, 'msg': f'未知的 action: {action}'})
|
||||||
|
|
||||||
|
def _save_scene(self, request, club_id, action):
|
||||||
|
scene_key = (request.data.get('scene_key') or '').strip()
|
||||||
|
if scene_key not in SCRIPT_SCENE_KEYS:
|
||||||
|
return Response({'code': 400, 'msg': '不支持的 scene_key'})
|
||||||
|
|
||||||
|
content = (request.data.get('content') or '').strip()
|
||||||
|
if not content:
|
||||||
|
return Response({'code': 400, 'msg': '请填写正文内容'})
|
||||||
|
|
||||||
|
fields = {
|
||||||
|
'title': (request.data.get('title') or '').strip(),
|
||||||
|
'content': content,
|
||||||
|
'confirm_text': (request.data.get('confirm_text') or '').strip(),
|
||||||
|
'cancel_text': (request.data.get('cancel_text') or '').strip(),
|
||||||
|
'enabled': bool(request.data.get('is_active', True)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if action == 'update':
|
||||||
|
row_id = request.data.get('id')
|
||||||
|
if not row_id:
|
||||||
|
return Response({'code': 400, 'msg': '缺少 id'})
|
||||||
|
row = MiniappScriptScene.query.filter(id=row_id, club_id=club_id).first()
|
||||||
|
if not row:
|
||||||
|
return Response({'code': 404, 'msg': '配置不存在'})
|
||||||
|
for k, v in fields.items():
|
||||||
|
setattr(row, k, v)
|
||||||
|
row.save()
|
||||||
|
return Response({'code': 0, 'msg': '保存成功', 'data': _serialize_script_scene(row)})
|
||||||
|
|
||||||
|
existing = MiniappScriptScene.query.filter(club_id=club_id, scene_key=scene_key).first()
|
||||||
|
if existing:
|
||||||
|
for k, v in fields.items():
|
||||||
|
setattr(existing, k, v)
|
||||||
|
existing.save()
|
||||||
|
return Response({'code': 0, 'msg': '保存成功', 'data': _serialize_script_scene(existing)})
|
||||||
|
|
||||||
|
row = MiniappScriptScene.query.create(club_id=club_id, scene_key=scene_key, **fields)
|
||||||
|
return Response({'code': 0, 'msg': '创建成功', 'data': _serialize_script_scene(row)})
|
||||||
|
|
||||||
|
def _save_auto_reply(self, request, club_id, action):
|
||||||
|
keywords = request.data.get('keywords') or []
|
||||||
|
if not isinstance(keywords, list) or not keywords:
|
||||||
|
return Response({'code': 400, 'msg': '请至少添加一个触发关键词'})
|
||||||
|
|
||||||
|
kw_list = [str(k).strip() for k in keywords if str(k).strip()]
|
||||||
|
if not kw_list:
|
||||||
|
return Response({'code': 400, 'msg': '请至少添加一个触发关键词'})
|
||||||
|
|
||||||
|
content = (request.data.get('content') or '').strip()
|
||||||
|
if not content:
|
||||||
|
return Response({'code': 400, 'msg': '请填写回复内容'})
|
||||||
|
|
||||||
|
fields = {
|
||||||
|
'keyword': ','.join(kw_list),
|
||||||
|
'reply_content': content,
|
||||||
|
'sort_order': int(request.data.get('priority') or 0),
|
||||||
|
'enabled': bool(request.data.get('is_active', True)),
|
||||||
|
'reply_type': MiniappScriptAutoReply.REPLY_TEXT,
|
||||||
|
}
|
||||||
|
|
||||||
|
if action == 'update':
|
||||||
|
row_id = request.data.get('id')
|
||||||
|
if not row_id:
|
||||||
|
return Response({'code': 400, 'msg': '缺少 id'})
|
||||||
|
row = MiniappScriptAutoReply.query.filter(id=row_id, club_id=club_id).first()
|
||||||
|
if not row:
|
||||||
|
return Response({'code': 404, 'msg': '规则不存在'})
|
||||||
|
for k, v in fields.items():
|
||||||
|
setattr(row, k, v)
|
||||||
|
row.save()
|
||||||
|
return Response({'code': 0, 'msg': '保存成功', 'data': _serialize_script_auto_reply(row)})
|
||||||
|
|
||||||
|
row = MiniappScriptAutoReply.query.create(club_id=club_id, **fields)
|
||||||
|
return Response({'code': 0, 'msg': '创建成功', 'data': _serialize_script_auto_reply(row)})
|
||||||
|
|
||||||
|
def _delete_item(self, request, club_id):
|
||||||
|
from jituan.services.display_config import forbid_display_write_in_all_scope
|
||||||
|
|
||||||
|
deny = forbid_display_write_in_all_scope(request)
|
||||||
|
if deny:
|
||||||
|
return deny
|
||||||
|
|
||||||
|
row_id = request.data.get('id')
|
||||||
|
if not row_id:
|
||||||
|
return Response({'code': 400, 'msg': '缺少 id'})
|
||||||
|
|
||||||
|
row = MiniappScriptAutoReply.query.filter(id=row_id, club_id=club_id).first()
|
||||||
|
if row:
|
||||||
|
row.delete()
|
||||||
|
return Response({'code': 0, 'msg': '删除成功'})
|
||||||
|
|
||||||
|
row = MiniappScriptScene.query.filter(id=row_id, club_id=club_id).first()
|
||||||
|
if row:
|
||||||
|
return Response({'code': 400, 'msg': '场景话术不支持删除,请改为禁用'})
|
||||||
|
|
||||||
|
return Response({'code': 404, 'msg': '记录不存在'})
|
||||||
Reference in New Issue
Block a user