Files
Django/config/views/miniapp_config.py
XingQue 97e9093aa5 fix: 登录接口彻底跳过鉴权,消除 403 权限不足
ClubWechatLoginView / WechatMiniProgramLoginView 覆盖 initial,不做 JWT/权限/限流;动态配置接口也不再解析坏 Token。

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

336 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import io
import os
import re
import time
import random
import secrets
import hashlib
import threading
import urllib.parse
from decimal import Decimal, InvalidOperation
from django.conf import settings
from django.db import transaction, connection
from django.db.models import Q, F, Max, Prefetch
from gvsdsdk.fluent import db, func, FQ
from django.core.cache import cache
from django.core.paginator import Paginator
from django.utils import timezone
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status, permissions
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
from rest_framework.throttling import AnonRateThrottle, SimpleRateThrottle
from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image
from utils.weixin_broadcast import WeixinBroadcastSender
from utils.weixin_token import get_weixin_mini_access_token, is_weixin_token_invalid
# from utils.ip_security import *
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
from backend.utils import update_shangjia_daily
from utils.chat_utils import subscribe_merchant_link_chat
from 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):
authentication_classes = ()
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]
@staticmethod
def _phone_auth_completed(user):
"""是否已完成手机号认证(兼容 PhoneVerified 未写入的老数据)。"""
if bool(getattr(user, 'PhoneVerified', False)):
return True
phone = (getattr(user, 'Phone', None) or '').strip()
return len(phone) >= 11
def post(self, request):
uid = getattr(request.user, 'UserUID', None)
user = request.user
if uid:
try:
user = User.query.get(UserUID=uid)
except User.DoesNotExist:
user = request.user
if self._phone_auth_completed(user):
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