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