拆分了 config

This commit is contained in:
2026-07-04 19:29:53 +08:00
parent 63cb8e9ee9
commit 7608faf396
9 changed files with 4162 additions and 3718 deletions

File diff suppressed because it is too large Load Diff

74
config/views/__init__.py Normal file
View File

@@ -0,0 +1,74 @@
"""
配置应用视图聚合模块config/views/__init__.py
本模块原为 config/views.py3718 行单文件),已按功能组拆分到 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',
]

View 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 = {}
# 平台发单利率类型1fadanpingtai='1'
platform_rate = rates.get('platform_rate')
if platform_rate is not None:
try:
platform_rate_float = float(platform_rate)
if 0 <= platform_rate_float <= 1:
# 🔴 修复:查询现有记录,如果不存在则创建
platform_rate_obj = CommissionRate.query.filter(Platform='1').first()
if platform_rate_obj:
# 更新现有记录
platform_rate_obj.Rate = platform_rate_float
platform_rate_obj.save()
else:
# 创建新记录
platform_rate_obj = CommissionRate.query.create(
Rate=platform_rate_float,
Platform='1'
)
updated_rates['platform_rate'] = platform_rate_float
else:
return Response({
'code': 3,
'msg': '平台发单利率必须在0-1之间',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
except ValueError:
return Response({
'code': 4,
'msg': '平台发单利率格式错误',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 商家发单利率类型2fadanpingtai='2'
shop_rate = rates.get('shop_rate')
if shop_rate is not None:
try:
shop_rate_float = float(shop_rate)
if 0 <= shop_rate_float <= 1:
# 🔴 修复:查询现有记录,如果不存在则创建
shop_rate_obj = CommissionRate.query.filter(Platform='3').first()
if shop_rate_obj:
# 更新现有记录
shop_rate_obj.Rate = shop_rate_float
shop_rate_obj.save()
else:
# 创建新记录
shop_rate_obj = CommissionRate.query.create(
Rate=shop_rate_float,
Platform='3'
)
updated_rates['shop_rate'] = shop_rate_float
else:
return Response({
'code': 5,
'msg': '商家发单利率必须在0-1之间',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
except ValueError:
return Response({
'code': 6,
'msg': '商家发单利率格式错误',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 🔴【新增】打手提现抽成利率类型5fadanpingtai='5'
dashou_withdraw_rate = rates.get('dashou_withdraw_rate')
if dashou_withdraw_rate is not None:
try:
dashou_withdraw_rate_float = float(dashou_withdraw_rate)
if 0 <= dashou_withdraw_rate_float <= 1:
# 查询现有记录,如果不存在则创建
dashou_withdraw_obj = CommissionRate.query.filter(Platform='5').first()
if dashou_withdraw_obj:
# 更新现有记录
dashou_withdraw_obj.Rate = dashou_withdraw_rate_float
dashou_withdraw_obj.save()
else:
# 创建新记录
dashou_withdraw_obj = CommissionRate.query.create(
Rate=dashou_withdraw_rate_float,
Platform='5'
)
updated_rates['dashou_withdraw_rate'] = dashou_withdraw_rate_float
else:
return Response({
'code': 7, # 注意:这里需要调整错误码,不要重复
'msg': '打手提现抽成利率必须在0-1之间',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
except ValueError:
return Response({
'code': 8,
'msg': '打手提现抽成利率格式错误',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 🔴【新增】管事提现抽成利率类型6fadanpingtai='6'
guanshi_withdraw_rate = rates.get('guanshi_withdraw_rate')
if guanshi_withdraw_rate is not None:
try:
guanshi_withdraw_rate_float = float(guanshi_withdraw_rate)
if 0 <= guanshi_withdraw_rate_float <= 1:
# 查询现有记录,如果不存在则创建
guanshi_withdraw_obj = CommissionRate.query.filter(Platform='6').first()
if guanshi_withdraw_obj:
# 更新现有记录
guanshi_withdraw_obj.Rate = guanshi_withdraw_rate_float
guanshi_withdraw_obj.save()
else:
# 创建新记录
guanshi_withdraw_obj = CommissionRate.query.create(
Rate=guanshi_withdraw_rate_float,
Platform='6'
)
updated_rates['guanshi_withdraw_rate'] = guanshi_withdraw_rate_float
else:
return Response({
'code': 9,
'msg': '管事提现抽成利率必须在0-1之间',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
except ValueError:
return Response({
'code': 10,
'msg': '管事提现抽成利率格式错误',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
if updated_rates:
response_data['rates'] = updated_rates
# 如果没有任何更新
if not response_data:
return Response({
'code': 7,
'msg': '没有需要更新的数据',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
return Response({
'code': 0,
'msg': '更新成功',
'data': response_data
})
except Exception as e:
return Response({
'code': 99,
'msg': f'系统错误: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

718
config/views/admin_stats.py Normal file
View 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都是CharFieldxiugaiid也是CharField
sql = """
SELECT
um.id,
um.phone,
um.yonghuid,
um.UserType,
COALESCE(COUNT(xjl.id), 0) as jilushuliang
FROM user_main um
LEFT JOIN xiugaijilu xjl ON um.phone = xjl.xiugaiid
WHERE um.UserType = 'admin'
GROUP BY um.id, um.phone, um.yonghuid, um.UserType
ORDER BY um.CreateTime DESC
"""
cursor.execute(sql)
results = cursor.fetchall()
# 🔥 4. 处理查询结果(全部保持字符串类型)
zhanghao_list = []
for row in results:
zhanghao_list.append({
#'id': row[0],
'id': str(row[1]) if row[1] else str(row[2]), # 🔥 保持字符串
'zhanghao':str(row[1]) if row[1] else str(row[2]), # 🔥 保持字符串
'yonghuid': str(row[2]) if row[2] else '', # 🔥 保持字符串
'user_type': str(row[3]) if row[3] else '', # 🔥 保持字符串
'jilushuliang': int(row[4]) # 只有这个是数字
})
return Response({
'code': 0,
'message': '获取成功',
'data': zhanghao_list
}, status=status.HTTP_200_OK)
except Exception as e:
print(f"获取管理员账号列表失败: {str(e)}")
traceback.print_exc()
return Response({
'code': 500,
'message': '系统错误',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class GetXiugaiJiluView(APIView):
"""
获取修改记录列表接口
路径:/peizhi/adhqxg
方法POST
权限JWT Token + 管理员验证
参数:
- zhanghao: 当前登录的管理员账号(字符串)
- xiugaizhe_zhanghaoid: 选中的管理员账号ID字符串对应phone或yonghuid
- page: 页码(整数)
- pagesize: 每页条数(整数)
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 🔥 1. 身份验证100%按表模型)
user = request.user
# 获取当前用户的手机号CharField字符串
current_phone = getattr(user, 'Phone', '')
if not current_phone:
return Response({
'code': 401,
'message': '用户手机号信息不完整',
'data': None
}, status=status.HTTP_401_UNAUTHORIZED)
# 检查前端传递的账号是否匹配(字符串比较)
frontend_zhanghao = request.data.get('zhanghao', '')
if not isinstance(frontend_zhanghao, str):
frontend_zhanghao = str(frontend_zhanghao)
frontend_zhanghao = frontend_zhanghao.strip()
if frontend_zhanghao != current_phone:
return Response({
'code': 403,
'message': '账号不匹配,无权限访问',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 🔥 2. 验证是否为管理员user_type='admin'字符串)
user_type = getattr(user, 'user_type', '')
if user_type != 'admin':
if not hasattr(user, 'AdminProfile'):
return Response({
'code': 403,
'message': '无管理员权限',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 🔥 3. 获取参数100%按实际类型)
# 🔥 xiugaizhe_zhanghaoid 是字符串对应xiugaiid字段CharField
xiugaizhe_zhanghaoid_param = request.data.get('xiugaizhe_zhanghaoid', '')
# 🔥 强制转为字符串因为数据库是CharField
if xiugaizhe_zhanghaoid_param is None:
xiugaizhe_zhanghaoid = ''
else:
# 无论如何都转为字符串
xiugaizhe_zhanghaoid = str(xiugaizhe_zhanghaoid_param).strip()
# 🔥 分页参数转为整数
try:
page = int(request.data.get('page', 1))
except:
page = 1
try:
pagesize = int(request.data.get('pagesize', 10))
except:
pagesize = 10
# 🔥 4. 参数验证
if not xiugaizhe_zhanghaoid:
return Response({
'code': 400,
'message': '修改者账号ID不能为空',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
if page < 1:
page = 1
if pagesize < 1 or pagesize > 50:
pagesize = 10
# 🔥 5. 高效查询100%按表字段类型)
with connection.cursor() as cursor:
# 🔥 5.1 查询总记录数xiugaiid是CharField用字符串查询
count_sql = """
SELECT COUNT(*)
FROM xiugaijilu
WHERE xiugaiid = %s
"""
cursor.execute(count_sql, [xiugaizhe_zhanghaoid])
total_count = cursor.fetchone()[0]
# 🔥 5.2 计算分页
offset = (page - 1) * pagesize
# 🔥 5.3 查询当前页数据使用参数化查询防止SQL注入
data_sql = """
SELECT
xjl.id,
xjl.yonghuid, -- CharField
xjl.xiugaiid, -- CharField
xjl.leixing, -- IntegerField
xjl.xiugaitijiao, -- DecimalField
xjl.xiugaitijiaoq, -- DecimalField
xjl.jifen, -- IntegerField
xjl.yjifen, -- IntegerField
xjl.yajin, -- DecimalField
xjl.yyajin, -- DecimalField
xjl.shangjiayue, -- DecimalField
xjl.shangjiayueq, -- DecimalField
xjl.guanshiyue, -- DecimalField
xjl.guanshiyueq, -- DecimalField
xjl.huiyuants, -- IntegerField
xjl.huiyuan_id, -- CharField
xjl.qitashuoming, -- TextField
xjl.CreateTime, -- DateTimeField
um.avatar -- CharField
FROM xiugaijilu xjl
LEFT JOIN user_main um ON xjl.yonghuid = um.yonghuid
WHERE xjl.xiugaiid = %s
ORDER BY xjl.CreateTime DESC
LIMIT %s OFFSET %s
"""
cursor.execute(data_sql, [xiugaizhe_zhanghaoid, pagesize, offset])
records = cursor.fetchall()
# 🔥 6. 处理查询结果(严格按照字段类型处理)
jilu_list = []
for record in records:
# 获取修改者账号信息(都是字符串)
xiugaizhe_zhanghao = ''
with connection.cursor() as cursor2:
# 🔥 phone和yonghuid都是CharField用字符串查询
cursor2.execute(
"SELECT phone, yonghuid FROM user_main WHERE phone = %s OR yonghuid = %s LIMIT 1",
[xiugaizhe_zhanghaoid, xiugaizhe_zhanghaoid]
)
xiugaizhe_info = cursor2.fetchone()
if xiugaizhe_info:
# 🔥 都是字符串
xiugaizhe_zhanghao = str(xiugaizhe_info[0]) if xiugaizhe_info[0] else str(xiugaizhe_info[1]) if \
xiugaizhe_info[1] else xiugaizhe_zhanghaoid
# 获取会员名称(如果有)
huiyuan_mingzi = ''
if record[15]: # huiyuan_id是CharField
with connection.cursor() as cursor3:
cursor3.execute(
"SELECT jieshao FROM huiyuan WHERE huiyuan_id = %s LIMIT 1",
[str(record[15])] # 🔥 转为字符串查询
)
huiyuan_info = cursor3.fetchone()
if huiyuan_info:
huiyuan_mingzi = str(huiyuan_info[0]) if huiyuan_info[0] else ''
# 🔥 严格按照前端字段名映射,保持正确类型
jilu_data = {
# 基础字段(字符串)
'id': record[0],
'beixiugai_yonghuid': str(record[1]) if record[1] else '', # yonghuid
'xiugaizhe_id': str(record[2]) if record[2] else '', # xiugaiid
'xiugaizhe_zhanghao': xiugaizhe_zhanghao, # 字符串
'beixiugai_yonghuleixing': record[3], # leixing是整数
# 打手字段Decimal转floatInteger转int
'xiugaihou_dashoutixianjine': float(record[4]) if record[4] is not None else 0.00,
'xiugaiqian_dashoutixianjine': float(record[5]) if record[5] is not None else 0.00,
'xiugaihou_dashoujifen': int(record[6]) if record[6] is not None else 0,
'xiugaiqian_dashoujifen': int(record[7]) if record[7] is not None else 0,
'xiugaihou_dashouyajin': float(record[8]) if record[8] is not None else 0.00,
'xiugaiqian_dashouyajin': float(record[9]) if record[9] is not None else 0.00,
# 商家字段
'xiugaihou_shangjiayue': float(record[10]) if record[10] is not None else 0.00,
'xiugaiqian_shangjiayue': float(record[11]) if record[11] is not None else 0.00,
# 管事字段
'xiugaihou_guanshiyue': float(record[12]) if record[12] is not None else 0.00,
'xiugaiqian_guanshiyue': float(record[13]) if record[13] is not None else 0.00,
# 会员字段
'tianjia_huiyuantianshu': int(record[14]) if record[14] is not None else 0,
'huiyuan_id': str(record[15]) if record[15] else '', # 字符串
'huiyuan_mingzi': huiyuan_mingzi, # 字符串
# 其他字段
'qita_shuoming': str(record[16]) if record[16] else '', # 字符串
'xiugai_time': record[17].strftime('%Y-%m-%d %H:%M:%S') if record[17] else None,
'touxiang': str(record[18]) if record[18] else '' # avatar是字符串
}
jilu_list.append(jilu_data)
# 🔥 7. 分页控制
queried_records = len(jilu_list)
has_more = False
if queried_records == pagesize:
total_queried = (page - 1) * pagesize + queried_records
has_more = total_queried < total_count
return Response({
'code': 0,
'message': '获取成功',
'data': {
'list': jilu_list,
'zongshuliang': total_count,
'dangqian_page': page,
'page_size': pagesize,
'has_more': has_more,
'shijichaxundediaoshuliang': queried_records
}
}, status=status.HTTP_200_OK)
except ValueError as e:
print(f"参数错误: {str(e)}")
return Response({
'code': 400,
'message': '参数错误',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
print(f"获取修改记录失败: {str(e)}")
traceback.print_exc()
return Response({
'code': 500,
'message': '系统错误',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

File diff suppressed because it is too large Load Diff

View 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)

View 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': '取消',
},
}

View 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
View 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': '记录不存在'})