321 lines
12 KiB
Python
321 lines
12 KiB
Python
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
|
||
|
||
|