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) # 批量预取所有称号,避免循环内 N+1 _mobans_list = list(current_page.object_list) _title_ids = [m.TitleID for m in _mobans_list if m.TitleID] _ch_map = { ch.id: ch for ch in Chenghao.query.filter(id__in=_title_ids).only('id', 'mingcheng', 'texiao_miaoshu') } if _title_ids else {} formatted = [] for moban in _mobans_list: texiao_json = '' label_name = '' ch = _ch_map.get(moban.TitleID) if ch: label_name = ch.mingcheng texiao_json = ch.texiao_miaoshu or '' 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)