from django.http import JsonResponse import logging import os from django.views.decorators.http import require_http_methods from django.views.decorators.csrf import csrf_exempt from django.shortcuts import render import json import requests import concurrent.futures import shutil import dotenv import random import uuid from django.db.models import Q from rest_framework.decorators import api_view, authentication_classes from apps.user.authentication import CustomTokenAuthentication from rest_framework.response import Response from .models import ( CreatorProfile, BrandCampaign, CreatorCampaign, CollaborationMetrics, VideoMetrics, LiveMetrics, FollowerMetrics, TrendMetrics, CreatorVideo ) from apps.brands.models import Campaign, Brand from apps.expertproducts.models import Negotiation from apps.chat.models import NegotiationChat from django.db import connection dotenv.load_dotenv() # 获取应用专属的logger logger = logging.getLogger('daren_detail') directory_monitoring = {} # 全局变量来控制检测线程 monitor_thread = None is_monitoring = False @api_view(['POST']) @authentication_classes([CustomTokenAuthentication]) @csrf_exempt @require_http_methods(["POST"]) def filter_creators(request): """根据过滤条件筛选达人信息(POST版,分页参数在URL中)""" try: import json # 从URL获取分页参数 page = int(request.GET.get('page', 1)) page_size = 10 # 固定页面大小为10条数据 # 解析POST请求体 data = json.loads(request.body) filter_data = data.get('filter', {}) # 基础查询 query = CreatorProfile.objects.all() # Category 多选过滤 category = filter_data.get('category') if category and len(category) > 0: query = query.filter(category__in=category) # 电商能力等级过滤 (L1-L7),多选 e_commerce_level = filter_data.get('e_commerce_level') if e_commerce_level and len(e_commerce_level) > 0: level_nums = [] for level_str in e_commerce_level: if level_str.startswith('L'): level_nums.append(int(level_str[1:])) if level_nums: query = query.filter(e_commerce_level__in=level_nums) # 曝光等级过滤 (KOL-1, KOL-2, KOC-1等),多选 exposure_level = filter_data.get('exposure_level') if exposure_level and len(exposure_level) > 0: query = query.filter(exposure_level__in=exposure_level) # GMV范围过滤 ($0-$5k, $5k-$25k, $25k-$50k等),多选 gmv_range = filter_data.get('gmv_range') if gmv_range and len(gmv_range) > 0: gmv_q = Q() for gmv_val in gmv_range: gmv_min, gmv_max = 0, float('inf') if gmv_val == "$0-$5k": gmv_min, gmv_max = 0, 5 elif gmv_val == "$5k-$25k": gmv_min, gmv_max = 5, 25 elif gmv_val == "$25k-$50k": gmv_min, gmv_max = 25, 50 elif gmv_val == "$50k-$150k": gmv_min, gmv_max = 50, 150 elif gmv_val == "$150k-$400k": gmv_min, gmv_max = 150, 400 elif gmv_val == "$400k-$1500k": gmv_min, gmv_max = 400, 1500 elif gmv_val == "$1500k+": gmv_min, gmv_max = 1500, float('inf') range_q = Q() if gmv_min > 0: range_q &= Q(gmv__gte=gmv_min) if gmv_max < float('inf'): range_q &= Q(gmv__lte=gmv_max) gmv_q |= range_q query = query.filter(gmv_q) # 观看量范围过滤,单选 views_range = filter_data.get('views_range') if views_range and len(views_range) > 0: views_min, views_max = 0, float('inf') views_val = views_range[0] if views_val == "0-100": views_min, views_max = 0, 100 elif views_val == "1k-10k": views_min, views_max = 1000, 10000 elif views_val == "10k-100k": views_min, views_max = 10000, 100000 elif views_val == "100k-250k": views_min, views_max = 100000, 250000 elif views_val == "250k-500k": views_min, views_max = 250000, 500000 elif views_val == "500k+": views_min = 500000 if views_min > 0: query = query.filter(avg_video_views__gte=views_min) if views_max < float('inf'): query = query.filter(avg_video_views__lte=views_max) # 价格区间过滤逻辑,单选 pricing = filter_data.get('pricing') if pricing and len(pricing) > 0: pricing_val = pricing[0] if '-' in pricing_val: min_price, max_price = pricing_val.split('-') min_price = float(min_price) max_price = float(max_price) # 修改:根据单一定价字段判断是否在区间内 query = query.filter(pricing__gte=min_price, pricing__lte=max_price) # 获取总数据量 total_count = query.count() # 计算分页 start = (page - 1) * page_size end = start + page_size # 执行查询并分页 creators = query[start:end] creator_list = [] for creator in creators: # 格式化电商等级 e_commerce_level_formatted = f"L{creator.e_commerce_level}" if creator.e_commerce_level else None # 格式化GMV gmv_formatted = f"${creator.gmv}k" if creator.gmv else "$0" # 格式化粉丝数和观看量 followers_formatted = f"{int(creator.followers / 1000)}k" if creator.followers else "0" avg_views_formatted = f"{int(creator.avg_video_views / 1000)}k" if creator.avg_video_views else "0" # 格式化价格 pricing_formatted = f"${creator.pricing}" if creator.pricing else None # 格式化结果 formatted_creator = { "Creator": { "name": creator.name, "avatar": creator.get_avatar_url() }, "Category": creator.category, "E-commerce Level": e_commerce_level_formatted, "Exposure Level": creator.exposure_level, "Followers": followers_formatted, "GMV": gmv_formatted, "Items Sold": f"{creator.items_sold}k" if creator.items_sold else None, "Avg. Video Views": avg_views_formatted, "Pricing": pricing_formatted, "Pricing Package": creator.pricing_package, "# Collab": creator.collab_count, "Latest Collab.": creator.latest_collab, "E-commerce": creator.e_commerce_platforms, } creator_list.append(formatted_creator) # 计算总页数 total_pages = (total_count + page_size - 1) // page_size # 修改响应格式为code、message和data return JsonResponse({ 'code': 200, 'message': '获取成功', 'data': { 'total_count': total_count, 'total_pages': total_pages, 'current_page': page, 'page_size': page_size, 'count': len(creator_list), 'creators': creator_list } }, json_dumps_params={'ensure_ascii': False}) except Exception as e: logger.error(f"筛选达人信息失败: {e}") import traceback logger.error(f"详细错误: {traceback.format_exc()}") return JsonResponse({ 'code': 500, 'message': f'筛选达人信息失败: {str(e)}', 'data': None }, json_dumps_params={'ensure_ascii': False}) @api_view(['POST']) @authentication_classes([CustomTokenAuthentication]) @csrf_exempt @require_http_methods(["POST"]) def add_creator(request): """添加或更新达人信息""" try: import json data = json.loads(request.body) # 必需的基本信息 name = data.get('name') if not name: return JsonResponse({ 'code': 400, 'message': '名称是必填项', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 查询是否已存在该creator creator, created = CreatorProfile.objects.get_or_create( name=name, defaults={ 'avatar_url': data.get('avatar_url'), } ) # 更新其他字段 creator.category = data.get('category', creator.category) # 处理电商等级 e_commerce_level = data.get('e_commerce_level') if e_commerce_level: if isinstance(e_commerce_level, str) and e_commerce_level.startswith('L'): creator.e_commerce_level = int(e_commerce_level[1:]) elif isinstance(e_commerce_level, int): creator.e_commerce_level = e_commerce_level creator.exposure_level = data.get('exposure_level', creator.exposure_level) creator.followers = data.get('followers', creator.followers) # 处理GMV gmv = data.get('gmv') if gmv: if isinstance(gmv, str) and gmv.startswith('$') and gmv.endswith('k'): # 将"$534.1k"转换为534.1 creator.gmv = float(gmv.strip('$k')) elif isinstance(gmv, (int, float)): creator.gmv = gmv # 处理items_sold items_sold = data.get('items_sold') if items_sold: if isinstance(items_sold, str) and items_sold.endswith('k'): creator.items_sold = float(items_sold.strip('k')) elif isinstance(items_sold, (int, float)): creator.items_sold = items_sold # 处理avg_video_views avg_video_views = data.get('avg_video_views') if avg_video_views: if isinstance(avg_video_views, str) and avg_video_views.endswith('k'): creator.avg_video_views = float(avg_video_views.strip('k')) * 1000 elif isinstance(avg_video_views, (int, float)): creator.avg_video_views = avg_video_views # 更新价格信息 pricing_data = data.get('pricing', {}) if pricing_data: # 处理individual价格或直接的pricing值 if 'individual' in pricing_data: creator.pricing = pricing_data.get('individual') elif 'pricing' in pricing_data: creator.pricing = pricing_data.get('pricing') elif isinstance(pricing_data, (int, float)): creator.pricing = pricing_data creator.pricing_package = pricing_data.get('package', creator.pricing_package) if isinstance(pricing_data, dict) else creator.pricing_package creator.collab_count = data.get('collab_count', creator.collab_count) creator.latest_collab = data.get('latest_collab', creator.latest_collab) creator.e_commerce_platforms = data.get('e_commerce_platforms', creator.e_commerce_platforms) # creator.is_active = data.get('is_active', creator.is_active) creator.mcn = data.get('mcn', creator.mcn) # 保存更新 creator.save() return JsonResponse({ 'code': 200, 'message': '达人信息已添加/更新', 'data': { 'created': created, 'creator_id': creator.id } }, json_dumps_params={'ensure_ascii': False}) except Exception as e: logger.error(f"添加/更新达人信息失败: {e}") import traceback logger.error(f"详细错误: {traceback.format_exc()}") return JsonResponse({ 'code': 500, 'message': f'添加/更新达人信息失败: {str(e)}', 'data': None }, json_dumps_params={'ensure_ascii': False}) @api_view(['GET']) @authentication_classes([CustomTokenAuthentication]) @csrf_exempt @require_http_methods(["GET"]) def get_campaigns(request): """获取所有营销活动列表""" try: from .models import Campaign # 查询所有活跃的活动 campaigns = Campaign.objects.filter(is_active=True) campaign_list = [] for campaign in campaigns: display_name = campaign.name if campaign.product: display_name = f"{campaign.brand} {campaign.product}" else: display_name = campaign.brand campaign_list.append({ "id": campaign.id, "name": display_name, "brand": campaign.brand, "product": campaign.product }) return JsonResponse({ 'code': 200, 'message': '获取成功', 'data': campaign_list }, json_dumps_params={'ensure_ascii': False}) except Exception as e: logger.error(f"获取营销活动失败: {e}") import traceback logger.error(f"详细错误: {traceback.format_exc()}") return JsonResponse({ 'code': 500, 'message': f'获取营销活动失败: {str(e)}', 'data': None }, json_dumps_params={'ensure_ascii': False}) @api_view(['POST']) @authentication_classes([CustomTokenAuthentication]) @csrf_exempt @require_http_methods(["POST"]) def add_to_campaign(request): """添加达人到营销活动 - 创建达人与产品的谈判关系""" try: from .models import CreatorProfile, CreatorCampaign from apps.brands.models import Campaign, Product, Brand from apps.expertproducts.models import Negotiation from apps.chat.models import NegotiationChat from django.db import connection import json import uuid data = json.loads(request.body) # 获取必要参数 campaign_id = data.get('campaign_id') creator_ids = data.get('creator_ids', []) brand_id = data.get('brand_id') # 可选的品牌ID参数 if not campaign_id or not creator_ids: return JsonResponse({ 'code': 400, 'message': '缺少必要参数:campaign_id 或 creator_ids', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 检查活动是否存在 try: # 使用Django ORM进行查询而不是raw SQL logger.info(f"尝试查找活动,ID: {campaign_id}") # Campaign ID可能是整数而不是UUID,直接使用原始ID进行查询 campaign = Campaign.objects.get(id=campaign_id, is_active=True) logger.info(f"找到活动: {campaign.name}, Active: {campaign.is_active}") except Campaign.DoesNotExist: logger.warning(f"找不到ID为 {campaign_id} 的活动") return JsonResponse({ 'code': 404, 'message': f'找不到ID为 {campaign_id} 的活跃营销活动', 'data': None }, json_dumps_params={'ensure_ascii': False}) except Exception as e: logger.error(f"查询活动时发生错误: {str(e)}") return JsonResponse({ 'code': 500, 'message': f'查询活动时发生错误: {str(e)}', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 查询活动关联的所有产品 # 通过ORM的ManyToMany关系查询 products = list(campaign.link_product.all()) logger.info(f"活动 {campaign_id} 关联了 {len(products)} 个产品") # 如果没有产品关联到活动,则尝试直接查询关联表 if not products: logger.info(f"尝试直接查询关联表获取活动 {campaign_id} 的产品") # 查询活动-产品关联表 with connection.cursor() as cursor: cursor.execute( """ SELECT product_id FROM brands_campaign_link_product WHERE campaign_id = %s """, [campaign_id] ) product_ids = [row[0] for row in cursor.fetchall()] if product_ids: # 获取这些产品 products = list(Product.objects.filter(id__in=product_ids)) logger.info(f"从关联表找到 {len(products)} 个产品") # 记录要添加的达人 added_creators = [] added_count = 0 skipped_count = 0 already_exists_count = 0 # 为新添加的达人创建谈判和对话 negotiations_created = [] # 处理每个达人 for creator_id in creator_ids: try: # 查询达人信息 creator = CreatorProfile.objects.get(id=creator_id) logger.info(f"找到达人: {creator.name}") # 将达人添加到返回列表 added_count += 1 added_creators.append({ 'id': creator.id, 'name': creator.name }) # 为每个产品创建谈判记录和对话 for product in products: # 检查是否已存在谈判 existing_negotiation = Negotiation.objects.filter(creator=creator, product=product).first() if not existing_negotiation: # 创建新的谈判记录 negotiation = Negotiation.objects.create( creator=creator, product=product, status='brand_review', # 初始状态为品牌回顾 current_round=1, context={} ) logger.info(f"已创建谈判记录: 谈判ID={negotiation.id}, 达人ID={creator.id}, 产品ID={product.id}") else: # 使用已存在的谈判记录 negotiation = existing_negotiation logger.info(f"使用已存在的谈判记录: 谈判ID={negotiation.id}") # 直接创建NegotiationChat记录,不检查是否已存在 try: # 创建对话ID并关联 conversation_id = str(uuid.uuid4()) logger.info(f"准备创建对话关联,参数: negotiation={negotiation.id}, conversation_id={conversation_id}, creator_id={creator.id}, product_id={product.id}") negotiation_chat = NegotiationChat.objects.create( negotiation=negotiation, conversation_id=conversation_id, creator_id=creator.id, product_id=product.id ) logger.info(f"成功创建对话关联: ID={negotiation_chat.id}, conversation_id={negotiation_chat.conversation_id}") negotiations_created.append({ 'negotiation_id': negotiation.id, 'creator_id': creator.id, 'product_id': product.id, 'conversation_id': conversation_id }) except Exception as e: logger.error(f"创建对话关联失败: {str(e)}") # 继续处理,不中断流程 logger.info(f"为达人 {creator.name}(ID:{creator.id}) 和产品 {product.name}(ID:{product.id}) {'创建' if not existing_negotiation else '更新'}谈判,对话ID: {conversation_id if 'conversation_id' in locals() else 'N/A'}") # 将达人与活动关联 - 可选操作 try: # 检查是否已存在关联 existing_relation = CreatorCampaign.objects.filter(creator=creator, campaign=campaign).exists() if not existing_relation: CreatorCampaign.objects.create( creator=creator, campaign=campaign, status='pending' ) logger.info(f"已将达人 {creator.name} 关联到活动 {campaign.name}") else: already_exists_count += 1 logger.info(f"达人 {creator.name} 已经关联到活动 {campaign.name}") except Exception as e: logger.warning(f"关联达人到活动时出错: {str(e)}") # 继续处理,不中断流程 except CreatorProfile.DoesNotExist: skipped_count += 1 logger.warning(f"找不到ID为 {creator_id} 的达人") # 如果没有找到产品,返回警告 if not products: return JsonResponse({ 'code': 200, 'message': '操作完成,但活动没有关联产品', 'data': { 'campaign': { 'id': str(campaign.id), 'name': campaign.name }, 'added_creators': added_creators, 'stats': { 'added': added_count, 'skipped': skipped_count, 'already_exists': already_exists_count, 'products_found': 0 }, 'negotiations_created': negotiations_created, 'warning': '活动没有关联产品,无法创建谈判' } }, json_dumps_params={'ensure_ascii': False}) return JsonResponse({ 'code': 200, 'message': '成功添加达人并创建谈判关系', 'data': { 'campaign': { 'id': str(campaign.id), 'name': campaign.name }, 'added_creators': added_creators, 'stats': { 'added': added_count, 'skipped': skipped_count, 'already_exists': already_exists_count, 'products_found': len(products) }, 'negotiations_created': negotiations_created } }, json_dumps_params={'ensure_ascii': False}) except Exception as e: logger.error(f"处理失败: {e}") import traceback logger.error(f"详细错误: {traceback.format_exc()}") return JsonResponse({ 'code': 500, 'message': f'处理失败: {str(e)}', 'data': None }, json_dumps_params={'ensure_ascii': False}) @api_view(['GET']) @authentication_classes([CustomTokenAuthentication]) @csrf_exempt @require_http_methods(["GET"]) def get_creator_detail(request, creator_id): """获取达人详情信息""" try: import json # 查询指定ID的达人信息 try: creator = CreatorProfile.objects.get(id=creator_id) except CreatorProfile.DoesNotExist: return JsonResponse({ 'code': 404, 'message': '未找到该达人信息', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 格式化电商等级 e_commerce_level_formatted = f"L{creator.e_commerce_level}" if creator.e_commerce_level else None # 格式化GMV gmv_formatted = f"${creator.gmv}k" if creator.gmv else "$0" # 格式化粉丝数和观看量 followers_formatted = f"{int(creator.followers / 1000)}k" if creator.followers else "0" avg_views_formatted = f"{int(creator.avg_video_views / 1000)}k" if creator.avg_video_views else "0" # 计算相关指标 gpm = 0 gmv_per_customer = 0 if creator.avg_video_views and creator.avg_video_views > 0: if creator.pricing: try: price = float(creator.pricing) gpm = (price * 1000) / creator.avg_video_views gpm = round(gpm, 2) except (ValueError, AttributeError): pass # 计算每位客户GMV if creator.gmv and creator.items_sold and creator.items_sold > 0: gmv_per_customer = float(creator.gmv) / float(creator.items_sold) gmv_per_customer = round(gmv_per_customer, 2) # 构造详细响应数据 creator_detail = { "creator": { "id": creator.id, "name": creator.name, "avatar": creator.get_avatar_url(), "email": creator.email, "social_accounts": { "instagram": creator.instagram, # 示例数据,实际应从数据库获取 "tiktok": creator.tiktok_link # 示例链接 }, "location": creator.location, "live_schedule": creator.live_schedule }, "metrics": { "e_commerce_level": e_commerce_level_formatted, "exposure_level": creator.exposure_level, "followers": followers_formatted, "actual_followers": creator.followers, "gmv": gmv_formatted, "actual_gmv": creator.gmv, "items_sold": f"{creator.items_sold}", "avg_video_views": avg_views_formatted, "actual_views": creator.avg_video_views, "gpm": f"${gpm}", "gmv_per_customer": f"${gmv_per_customer}" }, "business": { "category": creator.category, "categories": creator.category.split('|') if creator.category and '|' in creator.category else [ creator.category] if creator.category else [], "mcn": creator.mcn or "", "pricing": { "price": f"${creator.pricing}" if creator.pricing else None, "package": creator.pricing_package }, "collab_count": creator.collab_count, "latest_collab": creator.latest_collab, "e_commerce_platforms": creator.e_commerce_platforms or [] }, "analytics": { "gmv_by_channel": creator.gmv_by_channel, "gmv_by_category": creator.gmv_by_category } } # 返回响应 return JsonResponse({ 'code': 200, 'message': '获取成功', 'data': creator_detail }, json_dumps_params={'ensure_ascii': False}) except Exception as e: logger.error(f"获取达人详情失败: {e}") import traceback logger.error(f"详细错误: {traceback.format_exc()}") return JsonResponse({ 'code': 500, 'message': f'获取达人详情失败: {str(e)}', 'data': None }, json_dumps_params={'ensure_ascii': False}) @api_view(['POST']) @authentication_classes([CustomTokenAuthentication]) @csrf_exempt @require_http_methods(["POST"]) def update_creator_detail(request): """更新达人详细信息""" try: import json data = json.loads(request.body) # 必需的基本信息 creator_id = data.get('creator_id') if not creator_id: return JsonResponse({ 'code': 400, 'message': '缺少必要参数:creator_id', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 查询是否已存在该creator try: creator = CreatorProfile.objects.get(id=creator_id) except CreatorProfile.DoesNotExist: return JsonResponse({ 'code': 404, 'message': f'找不到ID为 {creator_id} 的达人信息', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 更新基础信息 creator.email = data.get('email', creator.email) creator.instagram = data.get('instagram', creator.instagram) creator.tiktok_link = data.get('tiktok_link', creator.tiktok_link) creator.location = data.get('location', creator.location) creator.live_schedule = data.get('live_schedule', creator.live_schedule) creator.mcn = data.get('mcn', creator.mcn) # 更新分析数据 if 'gmv_by_channel' in data: creator.gmv_by_channel = data.get('gmv_by_channel') if 'gmv_by_category' in data: creator.gmv_by_category = data.get('gmv_by_category') # 保存更新 creator.save() return JsonResponse({ 'code': 200, 'message': '达人详细信息已更新', 'data': { 'creator_id': creator.id, 'name': creator.name } }, json_dumps_params={'ensure_ascii': False}) except Exception as e: logger.error(f"更新达人详细信息失败: {e}") import traceback logger.error(f"详细错误: {traceback.format_exc()}") return JsonResponse({ 'code': 500, 'message': f'更新达人详细信息失败: {str(e)}', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 获取特定达人与各品牌的合作详情 @api_view(['GET']) @authentication_classes([CustomTokenAuthentication]) @csrf_exempt @require_http_methods(["GET"]) def get_creator_brand_campaigns(request, creator_id=None): """获取特定达人与各品牌的合作详情""" try: import json # 检查creator_id是否提供 if not creator_id: creator_id = request.GET.get('creator_id') if not creator_id: return JsonResponse({ 'code': 400, 'message': '缺少必要参数: creator_id', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 查询达人信息 try: creator = CreatorProfile.objects.get(id=creator_id) except CreatorProfile.DoesNotExist: return JsonResponse({ 'code': 404, 'message': f'未找到ID为 {creator_id} 的达人', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 获取分页参数 page = int(request.GET.get('page', 1)) page_size = int(request.GET.get('page_size', 10)) # 查询该达人参与的所有活动 creator_campaigns = CreatorCampaign.objects.filter( creator_id=creator_id ).select_related('campaign') # 获取所有相关活动的ID campaign_ids = [cc.campaign_id for cc in creator_campaigns] # 查询所有相关的BrandCampaign记录 # 优先查找已关联Campaign的品牌活动记录 brand_campaigns_with_campaign = BrandCampaign.objects.filter( campaign__id__in=campaign_ids ).select_related('campaign') # 如果找不到直接关联的记录,尝试使用品牌ID匹配 brands_needed = [] if not brand_campaigns_with_campaign.exists(): # 获取所有相关Campaign的品牌首字母 campaigns = Campaign.objects.filter(id__in=campaign_ids) brands_needed = [campaign.brand.name[:1].upper() if isinstance(campaign.brand, Brand) else campaign.brand[:1].upper() for campaign in campaigns if campaign.brand] # 合并品牌活动数据 campaign_list = [] # 处理有Campaign关联的BrandCampaign记录 for brand_campaign in brand_campaigns_with_campaign: # 查找对应的CreatorCampaign记录获取状态 creator_campaign = next( (cc for cc in creator_campaigns if cc.campaign_id == brand_campaign.campaign_id), None ) if creator_campaign: campaign_data = { "brand": { "id": brand_campaign.brand_id, "name": brand_campaign.brand_name, "color": brand_campaign.brand_color }, "pricing_detail": brand_campaign.pricing_detail, "start_date": brand_campaign.start_date.strftime('%m/%d/%Y'), "end_date": brand_campaign.end_date.strftime('%m/%d/%Y'), "status": creator_campaign.status, # 使用CreatorCampaign中的状态 "gmv_achieved": brand_campaign.gmv_achieved, "views_achieved": brand_campaign.views_achieved, "video_link": brand_campaign.video_link, "campaign_id": brand_campaign.campaign.id if brand_campaign.campaign else None, "campaign_name": brand_campaign.campaign.name if brand_campaign.campaign else None } campaign_list.append(campaign_data) # 如果通过Campaign关联找不到数据,尝试使用品牌ID匹配 if not campaign_list and brands_needed: brand_campaigns_by_id = BrandCampaign.objects.filter(brand_id__in=brands_needed) for brand_campaign in brand_campaigns_by_id: # 查找相关的Campaign和CreatorCampaign matching_campaign = next( (c for c in campaigns if c.brand and c.brand[:1].upper() == brand_campaign.brand_id), None ) if matching_campaign: creator_campaign = next( (cc for cc in creator_campaigns if cc.campaign_id == matching_campaign.id), None ) if creator_campaign: campaign_data = { "brand": { "id": brand_campaign.brand_id, "name": brand_campaign.brand_name, "color": brand_campaign.brand_color }, "pricing_detail": brand_campaign.pricing_detail, "start_date": brand_campaign.start_date.strftime('%m/%d/%Y'), "end_date": brand_campaign.end_date.strftime('%m/%d/%Y'), "status": creator_campaign.status, "gmv_achieved": brand_campaign.gmv_achieved, "views_achieved": brand_campaign.views_achieved, "video_link": brand_campaign.video_link, "campaign_id": matching_campaign.id, "campaign_name": matching_campaign.name } campaign_list.append(campaign_data) # 如果仍找不到数据,则直接为每个CreatorCampaign创建一个默认的活动数据 if not campaign_list: # 为每个CreatorCampaign创建一个默认的活动数据 for cc in creator_campaigns: campaign = cc.campaign # 修改获取brand_id的逻辑 brand_id = campaign.brand.name[:1].upper() if isinstance(campaign.brand, Brand) else (campaign.brand[:1].upper() if campaign.brand else 'U') # 为不同品牌设置不同颜色 colors = { 'U': '#E74694', # 粉色 'R': '#17CDCB', # 青色 'X': '#6E41BF', # 紫色 'Q': '#F9975D', # 橙色 'A': '#B4B4B4', # 灰色 'M': '#333333', # 黑色 } campaign_data = { "brand": { "id": brand_id, "name": campaign.brand.name if isinstance(campaign.brand, Brand) else (campaign.brand or "brand"), "color": colors.get(brand_id, '#000000') }, "pricing_detail": "$80", # 默认价格 "start_date": "05/31/2024", # 默认日期 "end_date": "05/29/2024", # 默认日期 "status": cc.status, "gmv_achieved": "$120", # 默认GMV "views_achieved": "650", # 默认观看量 "video_link": f"https://example.com/video/{campaign.id}", "campaign_id": campaign.id, "campaign_name": campaign.name } campaign_list.append(campaign_data) # 计算总数据量 total_count = len(campaign_list) # 计算分页 start = (page - 1) * page_size end = min(start + page_size, total_count) # 切片数据 paged_campaigns = campaign_list[start:end] # 计算总页数 total_pages = (total_count + page_size - 1) // page_size # 构造分页信息 pagination = { "current_page": page, "total_pages": total_pages, "total_count": total_count, "has_next": page < total_pages, "has_prev": page > 1 } # 构造创作者基本信息 creator_info = { "id": creator.id, "name": creator.name, "avatar": creator.get_avatar_url(), "category": creator.category, "exposure_level": creator.exposure_level, } return JsonResponse({ 'code': 200, 'message': '获取成功', 'data': paged_campaigns, 'pagination': pagination, 'creator': creator_info }, json_dumps_params={'ensure_ascii': False}) except Exception as e: logger.error(f"获取达人品牌合作详情失败: {e}") import traceback logger.error(f"详细错误: {traceback.format_exc()}") return JsonResponse({ 'code': 500, 'message': f'获取达人品牌合作详情失败: {str(e)}', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 获取创作者的协作指标、视频和直播指标数据 @api_view(['GET']) @authentication_classes([CustomTokenAuthentication]) @csrf_exempt @require_http_methods(["GET"]) def get_creator_metrics(request, creator_id): """获取创作者的协作指标、视频和直播指标数据""" try: import json from datetime import datetime # 检查creator_id是否提供 if not creator_id: return JsonResponse({ 'code': 400, 'message': '缺少必要参数: creator_id', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 获取时间范围参数,格式为:yyyy-mm-dd start_date_str = request.GET.get('start_date') end_date_str = request.GET.get('end_date') try: # 查询创作者信息 creator = CreatorProfile.objects.get(id=creator_id) except CreatorProfile.DoesNotExist: return JsonResponse({ 'code': 404, 'message': f'未找到ID为 {creator_id} 的创作者', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 构建查询条件字典 filter_kwargs = {'creator': creator} # 如果提供了时间范围,添加到过滤条件 if start_date_str and end_date_str: try: start_date = datetime.strptime(start_date_str, '%Y-%m-%d').date() end_date = datetime.strptime(end_date_str, '%Y-%m-%d').date() filter_kwargs.update({ 'start_date__lte': end_date, # 开始日期早于或等于请求的结束日期 'end_date__gte': start_date # 结束日期晚于或等于请求的开始日期 }) except ValueError: return JsonResponse({ 'code': 400, 'message': '日期格式不正确,请使用YYYY-MM-DD格式', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 查询协作指标 try: collab_metrics = CollaborationMetrics.objects.filter(**filter_kwargs).order_by('-end_date').first() except Exception as e: logger.error(f"查询协作指标出错: {e}") collab_metrics = None # 查询视频指标 - 普通视频 try: video_metrics = VideoMetrics.objects.filter(video_type='regular', **filter_kwargs).order_by( '-end_date').first() except Exception as e: logger.error(f"查询普通视频指标出错: {e}") video_metrics = None # 查询视频指标 - 可购物视频 try: shoppable_video_metrics = VideoMetrics.objects.filter(video_type='shoppable', **filter_kwargs).order_by( '-end_date').first() except Exception as e: logger.error(f"查询可购物视频指标出错: {e}") shoppable_video_metrics = None # 查询直播指标 - 普通直播 try: live_metrics = LiveMetrics.objects.filter(live_type='regular', **filter_kwargs).order_by( '-end_date').first() except Exception as e: logger.error(f"查询普通直播指标出错: {e}") live_metrics = None # 查询直播指标 - 可购物直播 try: shoppable_live_metrics = LiveMetrics.objects.filter(live_type='shoppable', **filter_kwargs).order_by( '-end_date').first() except Exception as e: logger.error(f"查询可购物直播指标出错: {e}") shoppable_live_metrics = None # 构建响应数据 metrics_data = { 'collaboration_metrics': None, 'video': None, 'shoppable_video': None, 'live': None, 'shoppable_live': None } # 填充协作指标数据 if collab_metrics: metrics_data['collaboration_metrics'] = { 'avg_commission_rate': f"{collab_metrics.avg_commission_rate}%", 'products_count': collab_metrics.products_count, 'brand_collaborations': collab_metrics.brand_collaborations, 'product_price': f"${collab_metrics.min_product_price} - ${collab_metrics.max_product_price}", 'date_range': f"{collab_metrics.start_date.strftime('%b %d, %Y')} - {collab_metrics.end_date.strftime('%b %d, %Y')}" } # 填充普通视频指标数据 if video_metrics: metrics_data['video'] = { 'gpm': f"${video_metrics.gpm}", 'videos_count': video_metrics.videos_count, 'avg_views': f"{float(video_metrics.avg_views) / 1000}k" if video_metrics.avg_views >= 1000 else f"{float(video_metrics.avg_views)}", 'avg_engagement': f"{video_metrics.avg_engagement}%", 'avg_likes': video_metrics.avg_likes, 'date_range': f"{video_metrics.start_date.strftime('%b %d, %Y')} - {video_metrics.end_date.strftime('%b %d, %Y')}" } # 填充可购物视频指标数据 if shoppable_video_metrics: metrics_data['shoppable_video'] = { 'gpm': f"${shoppable_video_metrics.gpm}", 'videos_count': shoppable_video_metrics.videos_count, 'avg_views': f"{float(shoppable_video_metrics.avg_views) / 1000}k" if shoppable_video_metrics.avg_views >= 1000 else f"{float(shoppable_video_metrics.avg_views)}", 'avg_engagement': f"{shoppable_video_metrics.avg_engagement}%", 'avg_likes': shoppable_video_metrics.avg_likes, 'date_range': f"{shoppable_video_metrics.start_date.strftime('%b %d, %Y')} - {shoppable_video_metrics.end_date.strftime('%b %d, %Y')}" } # 填充普通直播指标数据 if live_metrics: metrics_data['live'] = { 'gpm': f"${live_metrics.gpm}", 'lives_count': live_metrics.lives_count, 'avg_views': f"{float(live_metrics.avg_views) / 1000}k" if live_metrics.avg_views >= 1000 else f"{float(live_metrics.avg_views)}", 'avg_engagement': f"{live_metrics.avg_engagement}%", 'avg_likes': live_metrics.avg_likes, 'date_range': f"{live_metrics.start_date.strftime('%b %d, %Y')} - {live_metrics.end_date.strftime('%b %d, %Y')}" } # 填充可购物直播指标数据 if shoppable_live_metrics: metrics_data['shoppable_live'] = { 'gpm': f"${shoppable_live_metrics.gpm}", 'lives_count': shoppable_live_metrics.lives_count, 'avg_views': f"{float(shoppable_live_metrics.avg_views) / 1000}k" if shoppable_live_metrics.avg_views >= 1000 else f"{float(shoppable_live_metrics.avg_views)}", 'avg_engagement': f"{shoppable_live_metrics.avg_engagement}%", 'avg_likes': shoppable_live_metrics.avg_likes, 'date_range': f"{shoppable_live_metrics.start_date.strftime('%b %d, %Y')} - {shoppable_live_metrics.end_date.strftime('%b %d, %Y')}" } # 返回响应 return JsonResponse({ 'code': 200, 'message': '获取成功', 'data': metrics_data }, json_dumps_params={'ensure_ascii': False}) except Exception as e: logger.error(f"获取创作者指标数据失败: {e}") import traceback logger.error(f"详细错误: {traceback.format_exc()}") return JsonResponse({ 'code': 500, 'message': f'获取创作者指标数据失败: {str(e)}', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 更新创作者的指标数据 @api_view(['POST']) @authentication_classes([CustomTokenAuthentication]) @csrf_exempt @require_http_methods(["POST"]) def update_creator_metrics(request): """更新创作者的指标数据""" try: import json from datetime import datetime data = json.loads(request.body) # 获取必要参数 creator_id = data.get('creator_id') metrics_type = data.get('metrics_type') # collaboration, video, shoppable_video, live, shoppable_live metrics_data = data.get('metrics_data', {}) if not creator_id or not metrics_type or not metrics_data: return JsonResponse({ 'code': 400, 'message': '缺少必要参数: creator_id, metrics_type 或 metrics_data', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 查询创作者信息 try: creator = CreatorProfile.objects.get(id=creator_id) except CreatorProfile.DoesNotExist: return JsonResponse({ 'code': 404, 'message': f'未找到ID为 {creator_id} 的创作者', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 获取时间范围 start_date_str = metrics_data.get('start_date') end_date_str = metrics_data.get('end_date') if not start_date_str or not end_date_str: return JsonResponse({ 'code': 400, 'message': '缺少必要参数: start_date 或 end_date', 'data': None }, json_dumps_params={'ensure_ascii': False}) try: start_date = datetime.strptime(start_date_str, '%Y-%m-%d').date() end_date = datetime.strptime(end_date_str, '%Y-%m-%d').date() except ValueError: return JsonResponse({ 'code': 400, 'message': '日期格式不正确,请使用YYYY-MM-DD格式', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 根据metrics_type处理不同类型的指标数据 if metrics_type == 'collaboration': # 处理协作指标数据 # 从metrics_data中提取所需字段 avg_commission_rate = metrics_data.get('avg_commission_rate') if isinstance(avg_commission_rate, str) and '%' in avg_commission_rate: avg_commission_rate = float(avg_commission_rate.replace('%', '')) products_count = int(metrics_data.get('products_count', 0)) brand_collaborations = int(metrics_data.get('brand_collaborations', 0)) # 处理价格范围 product_price = metrics_data.get('product_price', '$0 - $0') if isinstance(product_price, str) and ' - ' in product_price: price_parts = product_price.split(' - ') min_price = float(price_parts[0].replace('$', '')) max_price = float(price_parts[1].replace('$', '')) else: min_price = 0 max_price = 0 # 更新或创建协作指标记录 collab_metrics, created = CollaborationMetrics.objects.update_or_create( creator=creator, start_date=start_date, end_date=end_date, defaults={ 'avg_commission_rate': avg_commission_rate, 'products_count': products_count, 'brand_collaborations': brand_collaborations, 'min_product_price': min_price, 'max_product_price': max_price } ) return JsonResponse({ 'code': 200, 'message': '协作指标数据已更新', 'data': { 'created': created, 'metrics_id': collab_metrics.id } }, json_dumps_params={'ensure_ascii': False}) elif metrics_type in ['video', 'shoppable_video']: # 处理视频指标数据 video_type = 'regular' if metrics_type == 'video' else 'shoppable' # 从metrics_data中提取所需字段 gpm = metrics_data.get('gpm', '$0') if isinstance(gpm, str) and '$' in gpm: gpm = float(gpm.replace('$', '')) videos_count = int(metrics_data.get('videos_count', 0)) avg_views = metrics_data.get('avg_views', '0') if isinstance(avg_views, str) and 'k' in avg_views: avg_views = float(avg_views.replace('k', '')) * 1000 else: avg_views = float(avg_views) avg_engagement = metrics_data.get('avg_engagement', '0%') if isinstance(avg_engagement, str) and '%' in avg_engagement: avg_engagement = float(avg_engagement.replace('%', '')) avg_likes = int(metrics_data.get('avg_likes', 0)) # 更新或创建视频指标记录 video_metrics, created = VideoMetrics.objects.update_or_create( creator=creator, video_type=video_type, start_date=start_date, end_date=end_date, defaults={ 'gpm': gpm, 'videos_count': videos_count, 'avg_views': avg_views, 'avg_engagement': avg_engagement, 'avg_likes': avg_likes } ) return JsonResponse({ 'code': 200, 'message': f'{metrics_type}指标数据已更新', 'data': { 'created': created, 'metrics_id': video_metrics.id } }, json_dumps_params={'ensure_ascii': False}) elif metrics_type in ['live', 'shoppable_live']: # 处理直播指标数据 live_type = 'regular' if metrics_type == 'live' else 'shoppable' # 从metrics_data中提取所需字段 gpm = metrics_data.get('gpm', '$0') if isinstance(gpm, str) and '$' in gpm: gpm = float(gpm.replace('$', '')) lives_count = int(metrics_data.get('lives_count', 0)) avg_views = metrics_data.get('avg_views', '0') if isinstance(avg_views, str) and 'k' in avg_views: avg_views = float(avg_views.replace('k', '')) * 1000 else: avg_views = float(avg_views) avg_engagement = metrics_data.get('avg_engagement', '0%') if isinstance(avg_engagement, str) and '%' in avg_engagement: avg_engagement = float(avg_engagement.replace('%', '')) avg_likes = int(metrics_data.get('avg_likes', 0)) # 更新或创建直播指标记录 live_metrics, created = LiveMetrics.objects.update_or_create( creator=creator, live_type=live_type, start_date=start_date, end_date=end_date, defaults={ 'gpm': gpm, 'lives_count': lives_count, 'avg_views': avg_views, 'avg_engagement': avg_engagement, 'avg_likes': avg_likes } ) return JsonResponse({ 'code': 200, 'message': f'{metrics_type}指标数据已更新', 'data': { 'created': created, 'metrics_id': live_metrics.id } }, json_dumps_params={'ensure_ascii': False}) else: return JsonResponse({ 'code': 400, 'message': f'不支持的指标类型: {metrics_type}', 'data': None }, json_dumps_params={'ensure_ascii': False}) except Exception as e: logger.error(f"更新创作者指标数据失败: {e}") import traceback logger.error(f"详细错误: {traceback.format_exc()}") return JsonResponse({ 'code': 500, 'message': f'更新创作者指标数据失败: {str(e)}', 'data': None }, json_dumps_params={'ensure_ascii': False}) @api_view(['GET']) @authentication_classes([CustomTokenAuthentication]) @csrf_exempt @require_http_methods(["GET"]) def get_creator_followers_metrics(request, creator_id=None): """获取创作者的粉丝统计指标数据""" try: import json from datetime import datetime, timedelta # 检查creator_id是否提供 if not creator_id: creator_id = request.GET.get('creator_id') if not creator_id: return JsonResponse({ 'code': 400, 'message': '缺少必要参数: creator_id', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 查询创作者信息 try: creator = CreatorProfile.objects.get(id=creator_id) except CreatorProfile.DoesNotExist: return JsonResponse({ 'code': 404, 'message': f'未找到ID为 {creator_id} 的创作者', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 获取最新的粉丝统计数据 follower_metrics = FollowerMetrics.objects.filter( creator=creator ).order_by('-end_date').first() if not follower_metrics: # 如果没有数据,返回示例数据 from datetime import datetime, timedelta end_date = datetime.now().date() start_date = end_date - timedelta(days=30) follower_data = { 'gender': { 'female': 0, 'male': 0 }, 'age': { '18-24': 0, '25-34': 0, '35-44': 0, '45-54': 0, '55+': 0 }, 'locations': { 'TX': 0, 'FL': 0, 'NY': 0, 'GE': 0, 'CA': 0 }, 'date_range': f"{start_date.strftime('%b %d, %Y')} - {end_date.strftime('%b %d, %Y')}" } else: # 构建粉丝统计数据 # 处理locations数据,对每个值保留两位小数 processed_locations = {} if follower_metrics.location_data: for location, value in follower_metrics.location_data.items(): processed_locations[location] = round(float(value), 2) follower_data = { 'gender': { 'female': round(follower_metrics.female_percentage, 2), 'male': round(follower_metrics.male_percentage, 2) }, 'age': { '18-24': round(follower_metrics.age_18_24_percentage, 2), '25-34': round(follower_metrics.age_25_34_percentage, 2), '35-44': round(follower_metrics.age_35_44_percentage, 2), '45-54': round(follower_metrics.age_45_54_percentage, 2), '55+': round(follower_metrics.age_55_plus_percentage, 2) }, 'locations': processed_locations, 'date_range': f"{follower_metrics.start_date.strftime('%b %d, %Y')} - {follower_metrics.end_date.strftime('%b %d, %Y')}" } return JsonResponse({ 'code': 200, 'message': '获取成功', 'data': follower_data }, json_dumps_params={'ensure_ascii': False}) except Exception as e: logger.error(f"获取创作者粉丝指标数据失败: {e}") import traceback logger.error(f"详细错误: {traceback.format_exc()}") return JsonResponse({ 'code': 500, 'message': f'获取创作者粉丝指标数据失败: {str(e)}', 'data': None }, json_dumps_params={'ensure_ascii': False}) @api_view(['GET']) @authentication_classes([CustomTokenAuthentication]) @csrf_exempt @require_http_methods(["GET"]) def get_creator_trends(request, creator_id=None): """获取创作者的趋势指标数据""" try: import json from datetime import datetime, timedelta # 检查creator_id是否提供 if not creator_id: creator_id = request.GET.get('creator_id') if not creator_id: return JsonResponse({ 'code': 400, 'message': '缺少必要参数: creator_id', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 获取时间范围参数,格式为:yyyy-mm-dd start_date_str = request.GET.get('start_date') end_date_str = request.GET.get('end_date') # 查询创作者信息 try: creator = CreatorProfile.objects.get(id=creator_id) except CreatorProfile.DoesNotExist: return JsonResponse({ 'code': 404, 'message': f'未找到ID为 {creator_id} 的创作者', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 如果提供了时间范围,进行过滤 if start_date_str and end_date_str: try: start_date = datetime.strptime(start_date_str, '%Y-%m-%d').date() end_date = datetime.strptime(end_date_str, '%Y-%m-%d').date() trends = TrendMetrics.objects.filter( creator=creator, date__gte=start_date, date__lte=end_date ).order_by('date') except ValueError: return JsonResponse({ 'code': 400, 'message': '日期格式不正确,请使用YYYY-MM-DD格式', 'data': None }, json_dumps_params={'ensure_ascii': False}) else: # 默认获取最近30天的数据 end_date = datetime.now().date() start_date = end_date - timedelta(days=30) trends = TrendMetrics.objects.filter( creator=creator, date__gte=start_date, date__lte=end_date ).order_by('date') # 如果没有数据,返回示例数据 if not trends.exists(): # 生成示例数据 - 模拟30天的趋势 example_date_range = [ (datetime.now().date() - timedelta(days=30 - i)).strftime('%Y-%m-%d') for i in range(31) ] # 设定随机的起始值和波动 import random base_gmv = random.uniform(500, 3000) base_items_sold = random.randint(500, 2000) base_followers = random.randint(1000, 3000) base_views = random.randint(1000, 3000) base_engagement = random.uniform(1.0, 3.0) # 生成模拟数据 trend_data = { 'gmv': [], 'items_sold': [], 'followers': [], 'video_views': [], 'engagement_rate': [], 'dates': example_date_range } for i in range(31): # 添加一些随机波动 gmv_value = base_gmv + random.uniform(-base_gmv * 0.2, base_gmv * 0.3) items_value = int(base_items_sold + random.uniform(-base_items_sold * 0.15, base_items_sold * 0.25)) followers_value = int(base_followers + random.uniform(-base_followers * 0.05, base_followers * 0.1)) views_value = int(base_views + random.uniform(-base_views * 0.2, base_views * 0.4)) engagement_value = base_engagement + random.uniform(-base_engagement * 0.1, base_engagement * 0.15) # 确保值不小于0,并对浮点数保留两位小数 trend_data['gmv'].append(round(max(0, gmv_value), 2)) trend_data['items_sold'].append(max(0, items_value)) trend_data['followers'].append(max(0, followers_value)) trend_data['video_views'].append(max(0, views_value)) trend_data['engagement_rate'].append(round(max(0, engagement_value), 2)) # 更新基准值,使数据有一定的连续性 base_gmv = gmv_value base_items_sold = items_value base_followers = followers_value base_views = views_value base_engagement = engagement_value trend_data['date_range'] = f"{datetime.strptime(example_date_range[0], '%Y-%m-%d').strftime('%b %d, %Y')} - {datetime.strptime(example_date_range[-1], '%Y-%m-%d').strftime('%b %d, %Y')}" else: # 从数据库构建趋势数据 trend_data = { 'gmv': [], 'items_sold': [], 'followers': [], 'video_views': [], 'engagement_rate': [], 'dates': [] } for trend in trends: trend_data['gmv'].append(round(trend.gmv, 2)) trend_data['items_sold'].append(trend.items_sold) trend_data['followers'].append(trend.followers_count) trend_data['video_views'].append(trend.video_views) trend_data['engagement_rate'].append(round(trend.engagement_rate, 2)) trend_data['dates'].append(trend.date.strftime('%Y-%m-%d')) trend_data['date_range'] = f"{datetime.strptime(trend_data['dates'][0], '%Y-%m-%d').strftime('%b %d, %Y')} - {datetime.strptime(trend_data['dates'][-1], '%Y-%m-%d').strftime('%b %d, %Y')}" return JsonResponse({ 'code': 200, 'message': '获取成功', 'data': trend_data }, json_dumps_params={'ensure_ascii': False}) except Exception as e: logger.error(f"获取创作者趋势指标数据失败: {e}") import traceback logger.error(f"详细错误: {traceback.format_exc()}") return JsonResponse({ 'code': 500, 'message': f'获取创作者趋势指标数据失败: {str(e)}', 'data': None }, json_dumps_params={'ensure_ascii': False}) @api_view(['POST']) @authentication_classes([CustomTokenAuthentication]) @csrf_exempt @require_http_methods(["POST"]) def update_creator_followers(request): """更新创作者的粉丝统计数据""" try: import json from datetime import datetime data = json.loads(request.body) # 获取必要参数 creator_id = data.get('creator_id') follower_data = data.get('follower_data', {}) if not creator_id or not follower_data: return JsonResponse({ 'code': 400, 'message': '缺少必要参数: creator_id 或 follower_data', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 查询创作者信息 try: creator = CreatorProfile.objects.get(id=creator_id) except CreatorProfile.DoesNotExist: return JsonResponse({ 'code': 404, 'message': f'未找到ID为 {creator_id} 的创作者', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 从请求中获取日期范围 date_range = follower_data.get('date_range', {}) start_date_str = date_range.get('start_date') end_date_str = date_range.get('end_date') if not start_date_str or not end_date_str: return JsonResponse({ 'code': 400, 'message': '缺少必要参数: date_range.start_date 或 date_range.end_date', 'data': None }, json_dumps_params={'ensure_ascii': False}) try: start_date = datetime.strptime(start_date_str, '%Y-%m-%d').date() end_date = datetime.strptime(end_date_str, '%Y-%m-%d').date() except ValueError: return JsonResponse({ 'code': 400, 'message': '日期格式不正确,请使用YYYY-MM-DD格式', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 提取粉丝性别数据 gender_data = follower_data.get('gender', {}) female_percentage = gender_data.get('female', 0) male_percentage = gender_data.get('male', 0) # 提取粉丝年龄数据 age_data = follower_data.get('age', {}) age_18_24 = age_data.get('18-24', 0) age_25_34 = age_data.get('25-34', 0) age_35_44 = age_data.get('35-44', 0) age_45_54 = age_data.get('45-54', 0) age_55_plus = age_data.get('55+', 0) # 提取粉丝地域数据 location_data = follower_data.get('locations', {}) # 更新或创建粉丝统计记录 follower_metrics, created = FollowerMetrics.objects.update_or_create( creator=creator, start_date=start_date, end_date=end_date, defaults={ 'female_percentage': female_percentage, 'male_percentage': male_percentage, 'age_18_24_percentage': age_18_24, 'age_25_34_percentage': age_25_34, 'age_35_44_percentage': age_35_44, 'age_45_54_percentage': age_45_54, 'age_55_plus_percentage': age_55_plus, 'location_data': location_data } ) return JsonResponse({ 'code': 200, 'message': '粉丝统计数据已更新', 'data': { 'created': created, 'metrics_id': follower_metrics.id } }, json_dumps_params={'ensure_ascii': False}) except Exception as e: logger.error(f"更新创作者粉丝统计数据失败: {e}") import traceback logger.error(f"详细错误: {traceback.format_exc()}") return JsonResponse({ 'code': 500, 'message': f'更新创作者粉丝统计数据失败: {str(e)}', 'data': None }, json_dumps_params={'ensure_ascii': False}) @api_view(['POST']) @authentication_classes([CustomTokenAuthentication]) @csrf_exempt @require_http_methods(["POST"]) def update_creator_trend(request): """更新创作者的趋势数据""" try: import json from datetime import datetime data = json.loads(request.body) # 获取必要参数 creator_id = data.get('creator_id') trend_date = data.get('date') metrics = data.get('metrics', {}) if not creator_id or not trend_date or not metrics: return JsonResponse({ 'code': 400, 'message': '缺少必要参数: creator_id, date 或 metrics', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 查询创作者信息 try: creator = CreatorProfile.objects.get(id=creator_id) except CreatorProfile.DoesNotExist: return JsonResponse({ 'code': 404, 'message': f'未找到ID为 {creator_id} 的创作者', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 解析日期 try: date_obj = datetime.strptime(trend_date, '%Y-%m-%d').date() except ValueError: return JsonResponse({ 'code': 400, 'message': '日期格式不正确,请使用YYYY-MM-DD格式', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 提取指标数据 gmv = metrics.get('gmv', 0) items_sold = metrics.get('items_sold', 0) followers_count = metrics.get('followers', 0) video_views = metrics.get('video_views', 0) engagement_rate = metrics.get('engagement_rate', 0) # 更新或创建趋势记录 trend_metrics, created = TrendMetrics.objects.update_or_create( creator=creator, date=date_obj, defaults={ 'gmv': gmv, 'items_sold': items_sold, 'followers_count': followers_count, 'video_views': video_views, 'engagement_rate': engagement_rate } ) return JsonResponse({ 'code': 200, 'message': '趋势数据已更新', 'data': { 'created': created, 'metrics_id': trend_metrics.id, 'date': trend_date } }, json_dumps_params={'ensure_ascii': False}) except Exception as e: logger.error(f"更新创作者趋势数据失败: {e}") import traceback logger.error(f"详细错误: {traceback.format_exc()}") return JsonResponse({ 'code': 500, 'message': f'更新创作者趋势数据失败: {str(e)}', 'data': None }, json_dumps_params={'ensure_ascii': False}) @api_view(['GET']) @authentication_classes([CustomTokenAuthentication]) @csrf_exempt @require_http_methods(["GET"]) def get_creator_videos(request, creator_id=None): """获取创作者的视频列表,分为普通视频和带产品视频""" try: import json from datetime import datetime # 检查creator_id是否提供 if not creator_id: creator_id = request.GET.get('creator_id') if not creator_id: return JsonResponse({ 'code': 400, 'message': '缺少必要参数: creator_id', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 查询创作者信息 try: creator = CreatorProfile.objects.get(id=creator_id) except CreatorProfile.DoesNotExist: return JsonResponse({ 'code': 404, 'message': f'未找到ID为 {creator_id} 的创作者', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 获取分页参数 page = int(request.GET.get('page', 1)) page_size = int(request.GET.get('page_size', 6)) # 默认每页6个视频 # 计算偏移量 offset = (page - 1) * page_size # 查询普通视频 regular_videos = CreatorVideo.objects.filter( creator=creator, video_type='regular', has_product=False ).order_by('-release_date')[offset:offset + page_size] # 查询带产品视频 product_videos = CreatorVideo.objects.filter( creator=creator, video_type='product', has_product=True ).order_by('-release_date')[offset:offset + page_size] # 如果没有找到视频,创建一些示例数据 if not regular_videos.exists() and not product_videos.exists(): # 返回示例视频数据 regular_videos_data = [ { "id": 1, "title": "Collagen + Biotin = your beauty routine's new besties. For hair, skin, nails, and join...", "thumbnail_url": "#", "badge": "red", "view_count": 2130, "like_count": 20, "release_date": "2025-03-31" }, { "id": 2, "title": "Collagen + Biotin = your beauty routine's new besties. For hair, skin, nails, and join...", "thumbnail_url": "#", "badge": "red", "view_count": 2130, "like_count": 20, "release_date": "2025-03-31" }, { "id": 3, "title": "Collagen + Biotin = your beauty routine's new besties. For hair, skin, nails, and join...", "thumbnail_url": "#", "badge": "gold", "view_count": 2130, "like_count": 20, "release_date": "2025-03-31" } ] product_videos_data = [ { "id": 4, "title": "Collagen + Biotin = your beauty routine's new besties. For hair, skin, nails, and join...", "thumbnail_url": "#", "badge": "red", "view_count": 2130, "like_count": 20, "release_date": "2025-03-31", "has_product": True, "product_name": "Collagen + Biotin", "product_url": "#" }, { "id": 5, "title": "Collagen + Biotin = your beauty routine's new besties. For hair, skin, nails, and join...", "thumbnail_url": "#", "badge": "red", "view_count": 2130, "like_count": 20, "release_date": "2025-03-31", "has_product": True, "product_name": "Collagen + Biotin", "product_url": "#" }, { "id": 6, "title": "Collagen + Biotin = your beauty routine's new besties. For hair, skin, nails, and join...", "thumbnail_url": "#", "badge": "gold", "view_count": 2130, "like_count": 20, "release_date": "2025-03-31", "has_product": True, "product_name": "Collagen + Biotin", "product_url": "#" } ] else: # 格式化查询结果 regular_videos_data = [] for video in regular_videos: regular_videos_data.append({ "id": video.id, "title": video.title, "description": video.description, "thumbnail_url": video.thumbnail_url, "video_url": video.video_url, "badge": video.badge, "view_count": video.view_count, "like_count": video.like_count, "comment_count": video.comment_count, "release_date": video.release_date.strftime("%d.%m.%Y") }) product_videos_data = [] for video in product_videos: product_videos_data.append({ "id": video.id, "title": video.title, "description": video.description, "thumbnail_url": video.thumbnail_url, "video_url": video.video_url, "badge": video.badge, "view_count": video.view_count, "like_count": video.like_count, "comment_count": video.comment_count, "release_date": video.release_date.strftime("%d.%m.%Y"), "has_product": video.has_product, "product_name": video.product_name, "product_url": video.product_url }) # 查询视频总数,用于分页 regular_videos_count = CreatorVideo.objects.filter(creator=creator, video_type='regular', has_product=False).count() product_videos_count = CreatorVideo.objects.filter(creator=creator, video_type='product', has_product=True).count() # 计算总页数 regular_total_pages = (regular_videos_count + page_size - 1) // page_size product_total_pages = (product_videos_count + page_size - 1) // page_size # 构造返回数据 response_data = { "regular_videos": { "videos": regular_videos_data, "total": regular_videos_count, "page": page, "page_size": page_size, "total_pages": regular_total_pages }, "product_videos": { "videos": product_videos_data, "total": product_videos_count, "page": page, "page_size": page_size, "total_pages": product_total_pages } } return JsonResponse({ 'code': 200, 'message': '获取成功', 'data': response_data }, json_dumps_params={'ensure_ascii': False}) except Exception as e: logger.error(f"获取创作者视频列表失败: {e}") import traceback logger.error(f"详细错误: {traceback.format_exc()}") return JsonResponse({ 'code': 500, 'message': f'获取创作者视频列表失败: {str(e)}', 'data': None }, json_dumps_params={'ensure_ascii': False}) @api_view(['POST']) @authentication_classes([CustomTokenAuthentication]) @csrf_exempt @require_http_methods(["POST"]) def add_creator_video(request): """添加创作者视频""" try: import json from datetime import datetime data = json.loads(request.body) # 获取必要参数 creator_id = data.get('creator_id') title = data.get('title') video_type = data.get('video_type', 'regular') # 默认为普通视频 release_date_str = data.get('release_date') if not creator_id or not title or not release_date_str: return JsonResponse({ 'code': 400, 'message': '缺少必要参数: creator_id, title 或 release_date', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 查询创作者信息 try: creator = CreatorProfile.objects.get(id=creator_id) except CreatorProfile.DoesNotExist: return JsonResponse({ 'code': 404, 'message': f'未找到ID为 {creator_id} 的创作者', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 解析日期 try: release_date = datetime.strptime(release_date_str, '%Y-%m-%d').date() except ValueError: return JsonResponse({ 'code': 400, 'message': '日期格式不正确,请使用YYYY-MM-DD格式', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 获取其他参数 description = data.get('description', '') thumbnail_url = data.get('thumbnail_url', '') video_url = data.get('video_url', '') video_id = data.get('video_id', f"vid_{int(datetime.now().timestamp())}") badge = data.get('badge', 'red') view_count = int(data.get('view_count', 0)) like_count = int(data.get('like_count', 0)) comment_count = int(data.get('comment_count', 0)) # 产品信息 has_product = data.get('has_product', False) product_name = data.get('product_name', '') product_url = data.get('product_url', '') # 如果视频类型是product,确保has_product为True if video_type == 'product': has_product = True # 创建或更新视频 video, created = CreatorVideo.objects.update_or_create( creator=creator, video_id=video_id, defaults={ 'title': title, 'description': description, 'thumbnail_url': thumbnail_url, 'video_url': video_url, 'video_type': video_type, 'badge': badge, 'view_count': view_count, 'like_count': like_count, 'comment_count': comment_count, 'has_product': has_product, 'product_name': product_name, 'product_url': product_url, 'release_date': release_date } ) return JsonResponse({ 'code': 200, 'message': '视频添加成功', 'data': { 'video_id': video.id, 'created': created } }, json_dumps_params={'ensure_ascii': False}) except Exception as e: logger.error(f"添加创作者视频失败: {e}") import traceback logger.error(f"详细错误: {traceback.format_exc()}") return JsonResponse({ 'code': 500, 'message': f'添加创作者视频失败: {str(e)}', 'data': None }, json_dumps_params={'ensure_ascii': False}) ######################################## 公有达人和私有达人 ######################################## @api_view(['GET']) @authentication_classes([CustomTokenAuthentication]) @csrf_exempt @require_http_methods(["GET"]) def get_public_creators(request): """获取公有达人库列表""" try: from .models import PublicCreatorPool, CreatorProfile import json # 获取分页参数 page = int(request.GET.get('page', 1)) page_size = int(request.GET.get('page_size', 10)) # 获取过滤参数 category = request.GET.get('category') keyword = request.GET.get('keyword') # 基础查询 - 从公有达人池开始 public_creators = PublicCreatorPool.objects.all() # 应用过滤条件 if category: public_creators = public_creators.filter(category=category) if keyword: public_creators = public_creators.filter( creator__name__icontains=keyword ) | public_creators.filter( creator__category__icontains=keyword ) # 观看量范围过滤,单选 views_range = request.GET.get('views_range') if views_range and len(views_range) > 0: views_val = views_range[0] # 单选,取第一个值 if ',' in views_val: # 新格式: "1000,10000" try: views_min, views_max = views_val.split(',') views_min = int(views_min.strip()) views_max = int(views_max.strip()) public_creators = public_creators.filter( creator__avg_video_views__gte=views_min, creator__avg_video_views__lte=views_max ) except (ValueError, IndexError): # 如果解析失败,忽略此过滤条件 pass else: # 兼容旧格式,保持原有逻辑 views_min, views_max = 0, float('inf') if views_val == "0-100": views_min, views_max = 0, 100 elif views_val == "1k-10k": views_min, views_max = 1000, 10000 elif views_val == "10k-100k": views_min, views_max = 10000, 100000 elif views_val == "100k-250k": views_min, views_max = 100000, 250000 elif views_val == "250k-500k": views_min, views_max = 250000, 500000 elif views_val == "500k+": views_min = 500000 if views_min > 0: public_creators = public_creators.filter(creator__avg_video_views__gte=views_min) if views_max < float('inf'): public_creators = public_creators.filter(creator__avg_video_views__lte=views_max) # 获取总数据量 total_count = public_creators.count() # 计算分页 start = (page - 1) * page_size end = start + page_size # 执行查询并分页 creators = public_creators[start:end] creator_list = [] for public_creator in creators: creator = public_creator.creator # 格式化电商等级 e_commerce_level_formatted = f"L{creator.e_commerce_level}" if creator.e_commerce_level else None # 格式化GMV gmv_formatted = f"${creator.gmv}k" if creator.gmv else "$0" # 格式化粉丝数和观看量 followers_formatted = f"{int(creator.followers / 1000)}k" if creator.followers else "0" avg_views_formatted = f"{int(creator.avg_video_views / 1000)}k" if creator.avg_video_views else "0" # 格式化价格 pricing_formatted = f"${creator.pricing}" if creator.pricing else None # 格式化结果 formatted_creator = { "public_id": public_creator.id, "creator_id": creator.id, "name": creator.name, "avatar": creator.get_avatar_url(), "category": creator.category, "e_commerce_level": e_commerce_level_formatted, "exposure_level": creator.exposure_level, "followers": followers_formatted, "gmv": gmv_formatted, "avg_video_views": avg_views_formatted, "pricing": pricing_formatted, "pricing_package": creator.pricing_package, "collab_count": creator.collab_count, "remark": public_creator.remark, "category_public": public_creator.category } creator_list.append(formatted_creator) # 计算总页数 total_pages = (total_count + page_size - 1) // page_size # 构造分页信息 pagination = { "current_page": page, "total_pages": total_pages, "total_count": total_count, "has_next": page < total_pages, "has_prev": page > 1 } return JsonResponse({ 'code': 200, 'message': '获取成功', 'data': creator_list, 'pagination': pagination }, json_dumps_params={'ensure_ascii': False}) except Exception as e: logger.error(f"获取公有达人库列表失败: {e}") import traceback logger.error(f"详细错误: {traceback.format_exc()}") return JsonResponse({ 'code': 500, 'message': f'获取公有达人库列表失败: {str(e)}', 'data': None }, json_dumps_params={'ensure_ascii': False}) @api_view(['POST']) @authentication_classes([CustomTokenAuthentication]) @csrf_exempt @require_http_methods(["POST"]) def add_to_public_pool(request): """将达人添加到公有达人库(仅管理员可操作)""" try: from .models import PublicCreatorPool, CreatorProfile import json # 检查当前用户是否有管理员权限 current_user = request.user if not current_user.is_staff and not current_user.is_superuser: return JsonResponse({ 'code': 403, 'message': '权限不足,只有管理员可以添加/更新公有库达人', 'data': None }, json_dumps_params={'ensure_ascii': False}) data = json.loads(request.body) # 获取必要参数 creator_id = data.get('creator_id') category = data.get('category') remark = data.get('remark') if not creator_id: return JsonResponse({ 'code': 400, 'message': '缺少必要参数: creator_id', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 查询达人信息 try: creator = CreatorProfile.objects.get(id=creator_id) except CreatorProfile.DoesNotExist: return JsonResponse({ 'code': 404, 'message': f'找不到ID为 {creator_id} 的达人', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 检查是否已存在于公有库 exists = PublicCreatorPool.objects.filter(creator=creator).exists() if exists: # 如果已存在,则更新信息 public_creator = PublicCreatorPool.objects.get(creator=creator) public_creator.category = category if category else public_creator.category public_creator.remark = remark if remark else public_creator.remark public_creator.save() action = "更新" else: # 创建新的公有库达人 public_creator = PublicCreatorPool.objects.create( creator=creator, category=category, remark=remark ) action = "添加" return JsonResponse({ 'code': 200, 'message': f'成功{action}达人到公有库', 'data': { 'creator': { 'id': creator.id, 'name': creator.name }, 'public_pool': { 'id': public_creator.id, 'category': public_creator.category, 'remark': public_creator.remark } } }, json_dumps_params={'ensure_ascii': False}) except Exception as e: logger.error(f"添加达人到公有库失败: {e}") import traceback logger.error(f"详细错误: {traceback.format_exc()}") return JsonResponse({ 'code': 500, 'message': f'添加达人到公有库失败: {str(e)}', 'data': None }, json_dumps_params={'ensure_ascii': False}) @api_view(['GET']) @authentication_classes([CustomTokenAuthentication]) @csrf_exempt @require_http_methods(["GET"]) def get_private_pools(request): """获取用户的私有达人库列表""" try: from .models import PrivateCreatorPool import json # 获取当前认证用户 current_user = request.user if not current_user.is_authenticated: return JsonResponse({ 'code': 401, 'message': '用户未认证', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 检查是否传入了user_id参数,如果传入且与当前用户不匹配,拒绝访问 requested_user_id = request.GET.get('user_id') if requested_user_id: try: requested_user_id = int(requested_user_id) if requested_user_id != current_user.id: return JsonResponse({ 'code': 403, 'message': '无权限访问其他用户的私有达人库', 'data': None }, json_dumps_params={'ensure_ascii': False}) except (ValueError, TypeError): return JsonResponse({ 'code': 400, 'message': 'user_id参数格式错误', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 查询当前用户的所有私有库 pools = PrivateCreatorPool.objects.filter(user_id=current_user.id) pool_list = [] for pool in pools: # 获取私有库中的达人数量 creator_count = pool.creator_relations.filter(status="active").count() pool_data = { "id": pool.id, "name": pool.name, "description": pool.description, "is_default": pool.is_default, "creator_count": creator_count, "created_at": pool.created_at.strftime('%Y-%m-%d') } pool_list.append(pool_data) return JsonResponse({ 'code': 200, 'message': '获取成功', 'data': pool_list }, json_dumps_params={'ensure_ascii': False}) except Exception as e: logger.error(f"获取私有达人库列表失败: {e}") import traceback logger.error(f"详细错误: {traceback.format_exc()}") return JsonResponse({ 'code': 500, 'message': f'获取私有达人库列表失败: {str(e)}', 'data': None }, json_dumps_params={'ensure_ascii': False}) @api_view(['POST']) @authentication_classes([CustomTokenAuthentication]) @csrf_exempt @require_http_methods(["POST"]) def create_private_pool(request): """创建私有达人库""" try: from .models import PrivateCreatorPool from apps.user.models import User import json # 获取当前认证用户 current_user = request.user if not current_user.is_authenticated: return JsonResponse({ 'code': 401, 'message': '用户未认证', 'data': None }, json_dumps_params={'ensure_ascii': False}) data = json.loads(request.body) logger.info(f"创建私有达人库请求数据: {data}") # 检查是否传入了user_id参数,如果传入且与当前用户不匹配,拒绝访问 requested_user_id = data.get('user_id') if requested_user_id: try: requested_user_id = int(requested_user_id) if requested_user_id != current_user.id: return JsonResponse({ 'code': 403, 'message': '无权限为其他用户创建私有达人库', 'data': None }, json_dumps_params={'ensure_ascii': False}) except (ValueError, TypeError): return JsonResponse({ 'code': 400, 'message': 'user_id参数格式错误', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 获取必要参数(不再需要user_id参数) name = data.get('name') description = data.get('description') is_default = data.get('is_default', False) logger.info(f"解析后的参数: user_id={current_user.id}, name={name}, description={description}, is_default={is_default}") if not name: return JsonResponse({ 'code': 400, 'message': '缺少必要参数: name', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 检查是否已存在同名私有库 try: existing_pool = PrivateCreatorPool.objects.filter(user_id=current_user.id, name=name).first() if existing_pool: return JsonResponse({ 'code': 409, 'message': f'已存在同名私有库: {name}', 'data': None }, json_dumps_params={'ensure_ascii': False}) except Exception as e: logger.error(f"检查私有库是否存在时发生错误: {str(e)}") raise # 如果设置为默认库,则将其他库设为非默认 if is_default: PrivateCreatorPool.objects.filter(user_id=current_user.id, is_default=True).update(is_default=False) # 创建私有库 try: private_pool = PrivateCreatorPool.objects.create( user_id=current_user.id, name=name, description=description, is_default=is_default ) except Exception as e: logger.error(f"创建私有库时发生错误: {str(e)}") raise return JsonResponse({ 'code': 200, 'message': '私有库创建成功', 'data': { 'id': private_pool.id, 'name': private_pool.name, 'is_default': private_pool.is_default, 'creator_count': 0, 'created_at': private_pool.created_at.strftime('%Y-%m-%d') } }, json_dumps_params={'ensure_ascii': False}) except Exception as e: logger.error(f"创建私有达人库失败: {e}") import traceback logger.error(f"详细错误: {traceback.format_exc()}") return JsonResponse({ 'code': 500, 'message': f'创建私有达人库失败: {str(e)}', 'data': None }, json_dumps_params={'ensure_ascii': False}) @api_view(['GET']) @authentication_classes([CustomTokenAuthentication]) @csrf_exempt @require_http_methods(["GET"]) def get_private_pool_creators(request, pool_id=None): """获取私有库中的达人列表""" try: from .models import PrivateCreatorPool, PrivateCreatorRelation import json # 获取当前认证用户 current_user = request.user if not current_user.is_authenticated: return JsonResponse({ 'code': 401, 'message': '用户未认证', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 检查pool_id是否提供 if not pool_id: pool_id = request.GET.get('pool_id') if not pool_id: return JsonResponse({ 'code': 400, 'message': '缺少必要参数: pool_id', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 获取分页参数 page = int(request.GET.get('page', 1)) page_size = int(request.GET.get('page_size', 10)) # 获取过滤参数 status = request.GET.get('status', 'active') # 默认只获取活跃状态的达人 keyword = request.GET.get('keyword') # 查询私有库信息并验证所有权 try: private_pool = PrivateCreatorPool.objects.get( id=pool_id, user_id=current_user.id # 确保只能访问当前用户的私有库 ) except PrivateCreatorPool.DoesNotExist: return JsonResponse({ 'code': 404, 'message': f'找不到ID为 {pool_id} 的私有库或无权限访问', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 查询私有库中的达人关联 creator_relations = PrivateCreatorRelation.objects.filter( private_pool=private_pool ).select_related('creator') # 应用过滤条件 if status: creator_relations = creator_relations.filter(status=status) if keyword: creator_relations = creator_relations.filter( creator__name__icontains=keyword ) | creator_relations.filter( creator__category__icontains=keyword ) | creator_relations.filter( notes__icontains=keyword ) # 获取总数据量 total_count = creator_relations.count() # 计算分页 start = (page - 1) * page_size end = start + page_size # 执行查询并分页 paged_relations = creator_relations[start:end] creator_list = [] for relation in paged_relations: creator = relation.creator # 格式化电商等级 e_commerce_level_formatted = f"L{creator.e_commerce_level}" if creator.e_commerce_level else None # 格式化GMV gmv_formatted = f"${creator.gmv}k" if creator.gmv else "$0" # 格式化粉丝数和观看量 followers_formatted = f"{int(creator.followers / 1000)}k" if creator.followers else "0" avg_views_formatted = f"{int(creator.avg_video_views / 1000)}k" if creator.avg_video_views else "0" # 格式化价格区间 if creator.pricing is not None: pricing_range = f"${creator.pricing}" else: pricing_range = None # 格式化结果 formatted_creator = { "relation_id": relation.id, "creator_id": creator.id, "name": creator.name, "avatar": creator.get_avatar_url(), "category": creator.category, "e_commerce_level": e_commerce_level_formatted, "exposure_level": creator.exposure_level, "followers": followers_formatted, "gmv": gmv_formatted, "avg_video_views": avg_views_formatted, "pricing": pricing_range, # 使用格式化后的价格区间 "collab_count": creator.collab_count, "notes": relation.notes, "status": relation.status, "added_from_public": relation.added_from_public, "added_at": relation.created_at.strftime('%Y-%m-%d'), "is_public_removed": relation.status == 'public_removed', # 添加公有库移除标识 "status_note": "该达人已从公有库中移除" if relation.status == 'public_removed' else None # 状态说明 } creator_list.append(formatted_creator) # 计算总页数 total_pages = (total_count + page_size - 1) // page_size # 构造分页信息 pagination = { "current_page": page, "total_pages": total_pages, "total_count": total_count, "has_next": page < total_pages, "has_prev": page > 1 } # 构造私有库信息 pool_info = { "id": private_pool.id, "name": private_pool.name, "description": private_pool.description, "is_default": private_pool.is_default, "user_id": private_pool.user_id, "created_at": private_pool.created_at.strftime('%Y-%m-%d') } return JsonResponse({ 'code': 200, 'message': '获取成功', 'data': creator_list, 'pagination': pagination, 'pool_info': pool_info }, json_dumps_params={'ensure_ascii': False}) except Exception as e: logger.error(f"获取私有库达人列表失败: {e}") import traceback logger.error(f"详细错误: {traceback.format_exc()}") return JsonResponse({ 'code': 500, 'message': f'获取私有库达人列表失败: {str(e)}', 'data': None }, json_dumps_params={'ensure_ascii': False}) @api_view(['POST']) @authentication_classes([CustomTokenAuthentication]) @csrf_exempt @require_http_methods(["POST"]) def add_creator_to_private_pool(request): """将达人添加到私有达人库""" try: from .models import PrivateCreatorPool, PrivateCreatorRelation, CreatorProfile, PublicCreatorPool import json # 获取当前认证用户 current_user = request.user if not current_user.is_authenticated: return JsonResponse({ 'code': 401, 'message': '用户未认证', 'data': None }, json_dumps_params={'ensure_ascii': False}) data = json.loads(request.body) # 获取必要参数 pool_id = data.get('pool_id') creator_id = data.get('creator_id') notes = data.get('notes') # 也接受批量添加 creator_ids = data.get('creator_ids', []) if creator_id and not creator_ids: creator_ids = [creator_id] if not creator_ids: return JsonResponse({ 'code': 400, 'message': '缺少必要参数: creator_id/creator_ids', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 查询私有库信息并验证所有权 private_pool = None is_auto_created = False if pool_id: # 如果提供了pool_id,查询指定的私有库 try: private_pool = PrivateCreatorPool.objects.get( id=pool_id, user_id=current_user.id # 确保只能操作当前用户的私有库 ) except PrivateCreatorPool.DoesNotExist: return JsonResponse({ 'code': 404, 'message': f'找不到ID为 {pool_id} 的私有库或无权限访问', 'data': None }, json_dumps_params={'ensure_ascii': False}) else: # 如果没有提供pool_id,查询用户的默认私有库 try: private_pool = PrivateCreatorPool.objects.get( user_id=current_user.id, is_default=True ) except PrivateCreatorPool.DoesNotExist: # 如果没有默认私有库,尝试获取用户的任意一个私有库 private_pool = PrivateCreatorPool.objects.filter( user_id=current_user.id ).first() if not private_pool: # 如果用户没有任何私有库,自动创建一个默认私有库 private_pool = PrivateCreatorPool.objects.create( user_id=current_user.id, name="默认私有达人库", description="系统自动创建的默认私有达人库", is_default=True ) is_auto_created = True logger.info(f"为用户 {current_user.id} 自动创建默认私有达人库,ID: {private_pool.id}") # 添加达人到私有库 added_creators = [] already_exists_count = 0 for cid in creator_ids: try: # 查询达人信息 creator = CreatorProfile.objects.get(id=cid) # 检查是否从公有库添加 from_public = PublicCreatorPool.objects.filter(creator=creator).exists() # 检查是否已存在于该私有库 exists = PrivateCreatorRelation.objects.filter( private_pool=private_pool, creator=creator ).exists() if exists: # 如果已存在,则更新信息但不显示在响应中 relation = PrivateCreatorRelation.objects.get( private_pool=private_pool, creator=creator ) # 如果状态为archived,则重新激活 if relation.status == 'archived': relation.status = 'active' if notes: relation.notes = notes relation.save() # 计数已存在的达人,但不添加到响应列表中 already_exists_count += 1 else: # 创建新的关联 relation = PrivateCreatorRelation.objects.create( private_pool=private_pool, creator=creator, notes=notes, added_from_public=from_public, status='active' ) added_creators.append({ 'id': creator.id, 'name': creator.name, 'action': '添加' }) except CreatorProfile.DoesNotExist: # 如果达人不存在,直接跳过,不在响应中显示 continue return JsonResponse({ 'code': 200, 'message': '操作成功', 'data': { 'added': added_creators, 'already_exists_count': already_exists_count, 'pool': { 'id': private_pool.id, 'name': private_pool.name, 'is_default': private_pool.is_default, 'auto_created': is_auto_created } } }, json_dumps_params={'ensure_ascii': False}) except Exception as e: logger.error(f"添加达人到私有库失败: {e}") import traceback logger.error(f"详细错误: {traceback.format_exc()}") return JsonResponse({ 'code': 500, 'message': f'添加达人到私有库失败: {str(e)}', 'data': None }, json_dumps_params={'ensure_ascii': False}) @api_view(['POST']) @authentication_classes([CustomTokenAuthentication]) @csrf_exempt @require_http_methods(["POST"]) def update_creator_in_private_pool(request): """更新私有库中达人的状态或笔记""" try: from .models import PrivateCreatorRelation import json # 获取当前认证用户 current_user = request.user if not current_user.is_authenticated: return JsonResponse({ 'code': 401, 'message': '用户未认证', 'data': None }, json_dumps_params={'ensure_ascii': False}) data = json.loads(request.body) # 获取必要参数 relation_id = data.get('relation_id') status = data.get('status') notes = data.get('notes') if not relation_id or (not status and notes is None): return JsonResponse({ 'code': 400, 'message': '缺少必要参数: relation_id 或 status/notes', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 查询关联信息 try: relation = PrivateCreatorRelation.objects.select_related('private_pool').get(id=relation_id) except PrivateCreatorRelation.DoesNotExist: return JsonResponse({ 'code': 404, 'message': f'找不到ID为 {relation_id} 的关联记录', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 验证当前用户是否有权限操作此私有库 if relation.private_pool.user_id != current_user.id: return JsonResponse({ 'code': 403, 'message': '无权限操作其他用户的私有达人库', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 更新信息 if status: relation.status = status if notes is not None: # 允许清空笔记 relation.notes = notes relation.save() return JsonResponse({ 'code': 200, 'message': '更新成功', 'data': { 'relation_id': relation.id, 'creator_id': relation.creator_id, 'pool_id': relation.private_pool_id, 'status': relation.status, 'notes': relation.notes } }, json_dumps_params={'ensure_ascii': False}) except Exception as e: logger.error(f"更新私有库达人失败: {e}") import traceback logger.error(f"详细错误: {traceback.format_exc()}") return JsonResponse({ 'code': 500, 'message': f'更新私有库达人失败: {str(e)}', 'data': None }, json_dumps_params={'ensure_ascii': False}) @api_view(['POST']) @authentication_classes([CustomTokenAuthentication]) @csrf_exempt @require_http_methods(["POST"]) def remove_creator_from_private_pool(request): """从私有库中移除达人""" try: from .models import PrivateCreatorRelation, PrivateCreatorPool import json # 获取当前认证用户 current_user = request.user if not current_user.is_authenticated: return JsonResponse({ 'code': 401, 'message': '用户未认证', 'data': None }, json_dumps_params={'ensure_ascii': False}) data = json.loads(request.body) # 方式1:通过关联ID删除 relation_id = data.get('relation_id') relation_ids = data.get('relation_ids', []) if relation_id and not relation_ids: relation_ids = [relation_id] # 方式2:通过私有库ID和达人ID删除 pool_id = data.get('pool_id') creator_id = data.get('creator_id') creator_ids = data.get('creator_ids', []) if creator_id and not creator_ids: creator_ids = [creator_id] # 检查参数有效性 if relation_ids: # 通过关联ID删除 - 需要验证每个关联的权限 relations = PrivateCreatorRelation.objects.select_related('private_pool').filter(id__in=relation_ids) # 检查是否找到要删除的记录 if not relations.exists(): return JsonResponse({ 'code': 404, 'message': '未找到指定的私有库达人关联记录', 'data': { 'relation_ids': relation_ids } }, json_dumps_params={'ensure_ascii': False}) # 验证权限 for relation in relations: if relation.private_pool.user_id != current_user.id: return JsonResponse({ 'code': 403, 'message': '无权限操作其他用户的私有达人库', 'data': None }, json_dumps_params={'ensure_ascii': False}) query = relations result_type = 'relation_ids' result_value = relation_ids elif pool_id and creator_ids: # 通过私有库ID和达人ID删除 - 先验证私有库权限 try: private_pool = PrivateCreatorPool.objects.get(id=pool_id) if private_pool.user_id != current_user.id: return JsonResponse({ 'code': 403, 'message': '无权限操作其他用户的私有达人库', 'data': None }, json_dumps_params={'ensure_ascii': False}) except PrivateCreatorPool.DoesNotExist: return JsonResponse({ 'code': 404, 'message': f'找不到ID为 {pool_id} 的私有库', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 查询要删除的记录 query = PrivateCreatorRelation.objects.filter( private_pool_id=pool_id, creator_id__in=creator_ids ) # 检查是否找到要删除的记录 if not query.exists(): return JsonResponse({ 'code': 404, 'message': '私有库中未找到指定的达人记录', 'data': { 'pool_id': pool_id, 'creator_ids': creator_ids } }, json_dumps_params={'ensure_ascii': False}) result_type = 'creator_ids' result_value = creator_ids else: return JsonResponse({ 'code': 400, 'message': '缺少必要参数: 需要提供 relation_id/relation_ids 或同时提供 pool_id 和 creator_id/creator_ids', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 执行删除操作 deleted_count = query.delete()[0] return JsonResponse({ 'code': 200, 'message': '移除成功', 'data': { 'deleted_count': deleted_count, result_type: result_value, 'pool_id': pool_id if pool_id else None } }, json_dumps_params={'ensure_ascii': False}) except Exception as e: logger.error(f"从私有库移除达人失败: {e}") import traceback logger.error(f"详细错误: {traceback.format_exc()}") return JsonResponse({ 'code': 500, 'message': f'从私有库移除达人失败: {str(e)}', 'data': None }, json_dumps_params={'ensure_ascii': False}) @api_view(['POST']) @authentication_classes([CustomTokenAuthentication]) @csrf_exempt @require_http_methods(["POST"]) def filter_public_creators(request): """根据过滤条件筛选公有达人库列表""" try: from .models import PublicCreatorPool, CreatorProfile import json from django.db.models import Q # 从URL获取分页参数 page = int(request.GET.get('page', 1)) page_size = int(request.GET.get('page_size', 10)) # 解析POST请求体 data = json.loads(request.body) filter_data = data.get('filter', {}) # 基础查询 - 从公有达人池开始 public_creators = PublicCreatorPool.objects.all() # 达人分类过滤(Category 多选过滤) category = filter_data.get('category') if category and len(category) > 0: public_creators = public_creators.filter(creator__category__in=category) # 电商能力等级过滤 (L1-L7),多选 e_commerce_level = filter_data.get('e_commerce_level') if e_commerce_level and len(e_commerce_level) > 0: level_nums = [] for level_str in e_commerce_level: if level_str.startswith('L'): level_nums.append(int(level_str[1:])) if level_nums: public_creators = public_creators.filter(creator__e_commerce_level__in=level_nums) # 曝光等级过滤 (KOL-1, KOL-2, KOC-1等),多选 exposure_level = filter_data.get('exposure_level') if exposure_level and len(exposure_level) > 0: public_creators = public_creators.filter(creator__exposure_level__in=exposure_level) # 平台过滤(tiktok, instagram, youtube等),单选 platform = filter_data.get('platform') if platform and platform: public_creators = public_creators.filter(creator__profile=platform) # GMV范围过滤 ($0-$5k, $5k-$25k, $25k-$50k等),多选 gmv_range = filter_data.get('gmv_range') if gmv_range and len(gmv_range) > 0: gmv_q = Q() for gmv_val in gmv_range: gmv_min, gmv_max = 0, float('inf') if gmv_val == "$0-$5k": gmv_min, gmv_max = 0, 5 elif gmv_val == "$5k-$25k": gmv_min, gmv_max = 5, 25 elif gmv_val == "$25k-$50k": gmv_min, gmv_max = 25, 50 elif gmv_val == "$50k-$150k": gmv_min, gmv_max = 50, 150 elif gmv_val == "$150k-$400k": gmv_min, gmv_max = 150, 400 elif gmv_val == "$400k-$1500k": gmv_min, gmv_max = 400, 1500 elif gmv_val == "$1500k+": gmv_min, gmv_max = 1500, float('inf') range_q = Q() if gmv_min > 0: range_q &= Q(creator__gmv__gte=gmv_min) if gmv_max < float('inf'): range_q &= Q(creator__gmv__lte=gmv_max) gmv_q |= range_q public_creators = public_creators.filter(gmv_q) # 观看量范围过滤,使用数值数组格式 views_range = filter_data.get('views_range') if views_range and isinstance(views_range, list) and len(views_range) == 2: views_min = views_range[0] views_max = views_range[1] if views_min is not None: public_creators = public_creators.filter(creator__avg_video_views__gte=views_min) if views_max is not None: public_creators = public_creators.filter(creator__avg_video_views__lte=views_max) # 获取总数据量 total_count = public_creators.count() # 计算分页 start = (page - 1) * page_size end = start + page_size # 执行查询并分页 creators = public_creators[start:end] creator_list = [] for public_creator in creators: creator = public_creator.creator # 格式化电商等级 e_commerce_level_formatted = f"L{creator.e_commerce_level}" if creator.e_commerce_level else None # 格式化GMV gmv_formatted = f"${creator.gmv}k" if creator.gmv else "$0" # 格式化粉丝数和观看量 followers_formatted = f"{int(creator.followers / 1000)}k" if creator.followers else "0" avg_views_formatted = f"{int(creator.avg_video_views / 1000)}k" if creator.avg_video_views else "0" # 格式化价格区间 if creator.pricing is not None: pricing_range = f"${creator.pricing}" else: pricing_range = None # 格式化结果 formatted_creator = { "public_id": public_creator.id, "creator_id": creator.id, "name": creator.name, "avatar": creator.get_avatar_url(), "category": creator.category, "e_commerce_level": e_commerce_level_formatted, "exposure_level": creator.exposure_level, "platform": creator.profile, # 添加平台信息 "followers": followers_formatted, "gmv": gmv_formatted, "avg_video_views": avg_views_formatted, "pricing": pricing_range, "pricing_package": creator.pricing_package, "collab_count": creator.collab_count, "remark": public_creator.remark, "category_public": public_creator.category } creator_list.append(formatted_creator) # 计算总页数 total_pages = (total_count + page_size - 1) // page_size # 构造分页信息 pagination = { "current_page": page, "total_pages": total_pages, "total_count": total_count, "has_next": page < total_pages, "has_prev": page > 1 } return JsonResponse({ 'code': 200, 'message': '获取成功', 'data': creator_list, 'pagination': pagination }, json_dumps_params={'ensure_ascii': False}) except Exception as e: logger.error(f"筛选公有达人库列表失败: {e}") import traceback logger.error(f"详细错误: {traceback.format_exc()}") return JsonResponse({ 'code': 500, 'message': f'筛选公有达人库列表失败: {str(e)}', 'data': None }, json_dumps_params={'ensure_ascii': False}) @api_view(['POST']) @authentication_classes([CustomTokenAuthentication]) @csrf_exempt @require_http_methods(["POST"]) def filter_private_pool_creators(request): """根据过滤条件筛选私有达人库中的达人""" try: from .models import PrivateCreatorPool, PrivateCreatorRelation import json from django.db.models import Q # 解析POST请求体 data = json.loads(request.body) # 获取私有库ID(现在是可选的) pool_id = data.get('pool_id') # 获取过滤条件 filter_data = data.get('filter', {}) # 获取分页参数 page = int(request.GET.get('page', 1)) page_size = int(request.GET.get('page_size', 10)) # 获取状态过滤参数,如果提供了才使用 status = filter_data.get('status') # 初始化查询逻辑 creator_relations = PrivateCreatorRelation.objects.filter( private_pool__user_id=request.user.id # 确保只查询当前用户的私有库 ).select_related('creator') # 私有库信息 pool_info = None # 如果提供了pool_id,则筛选特定私有库 if pool_id: try: private_pool = PrivateCreatorPool.objects.get(id=pool_id) # 检查私有库是否属于当前登录用户 if private_pool.user_id != request.user.id: return JsonResponse({ 'code': 403, 'message': '没有权限访问此私有达人库', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 进一步筛选指定私有库中的达人 creator_relations = creator_relations.filter(private_pool=private_pool) # 构造私有库信息 pool_info = { "id": private_pool.id, "name": private_pool.name, "description": private_pool.description, "is_default": private_pool.is_default, "user_id": private_pool.user_id, "created_at": private_pool.created_at.strftime('%Y-%m-%d') } except PrivateCreatorPool.DoesNotExist: return JsonResponse({ 'code': 404, 'message': f'找不到ID为 {pool_id} 的私有库', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 应用状态过滤条件(仅当提供了status参数时) if status: creator_relations = creator_relations.filter(status=status) # 应用复杂过滤条件 # --------- 从filter_creators借鉴的过滤逻辑 --------- # Category 多选过滤 category = filter_data.get('category') if category and len(category) > 0: creator_relations = creator_relations.filter(creator__category__in=category) # 电商能力等级过滤 (L1-L7),多选 e_commerce_level = filter_data.get('e_commerce_level') if e_commerce_level and len(e_commerce_level) > 0: level_nums = [] for level_str in e_commerce_level: if level_str.startswith('L'): level_nums.append(int(level_str[1:])) if level_nums: creator_relations = creator_relations.filter(creator__e_commerce_level__in=level_nums) # 曝光等级过滤 (KOL-1, KOL-2, KOC-1等),多选 exposure_level = filter_data.get('exposure_level') if exposure_level and len(exposure_level) > 0: creator_relations = creator_relations.filter(creator__exposure_level__in=exposure_level) # 平台过滤(tiktok, instagram, youtube等),单选 platform = filter_data.get('platform') if platform and platform: creator_relations = creator_relations.filter(creator__profile=platform) # GMV范围过滤 ($0-$5k, $5k-$25k, $25k-$50k等),多选 gmv_range = filter_data.get('gmv_range') if gmv_range and len(gmv_range) > 0: gmv_q = Q() for gmv_val in gmv_range: gmv_min, gmv_max = 0, float('inf') if gmv_val == "$0-$5k": gmv_min, gmv_max = 0, 5 elif gmv_val == "$5k-$25k": gmv_min, gmv_max = 5, 25 elif gmv_val == "$25k-$50k": gmv_min, gmv_max = 25, 50 elif gmv_val == "$50k-$150k": gmv_min, gmv_max = 50, 150 elif gmv_val == "$150k-$400k": gmv_min, gmv_max = 150, 400 elif gmv_val == "$400k-$1500k": gmv_min, gmv_max = 400, 1500 elif gmv_val == "$1500k+": gmv_min, gmv_max = 1500, float('inf') range_q = Q() if gmv_min > 0: range_q &= Q(creator__gmv__gte=gmv_min) if gmv_max < float('inf'): range_q &= Q(creator__gmv__lte=gmv_max) gmv_q |= range_q creator_relations = creator_relations.filter(gmv_q) # 观看量范围过滤,使用数值数组格式 views_range = filter_data.get('views_range') if views_range and isinstance(views_range, list) and len(views_range) == 2: views_min = views_range[0] views_max = views_range[1] if views_min is not None: creator_relations = creator_relations.filter(creator__avg_video_views__gte=views_min) if views_max is not None: creator_relations = creator_relations.filter(creator__avg_video_views__lte=views_max) # 价格区间过滤逻辑,使用数值数组格式 pricing = filter_data.get('pricing') if pricing and isinstance(pricing, list) and len(pricing) == 2: min_price = pricing[0] max_price = pricing[1] if min_price is not None: creator_relations = creator_relations.filter(creator__pricing__gte=min_price) if max_price is not None: creator_relations = creator_relations.filter(creator__pricing__lte=max_price) # 获取总数据量 total_count = creator_relations.count() # 计算分页 start = (page - 1) * page_size end = start + page_size # 执行查询并分页 paged_relations = creator_relations[start:end] creator_list = [] for relation in paged_relations: creator = relation.creator private_pool = relation.private_pool # 获取关联的私有库 # 格式化电商等级 e_commerce_level_formatted = f"L{creator.e_commerce_level}" if creator.e_commerce_level else None # 格式化GMV gmv_formatted = f"${creator.gmv}k" if creator.gmv else "$0" # 格式化粉丝数和观看量 followers_formatted = f"{int(creator.followers / 1000)}k" if creator.followers else "0" avg_views_formatted = f"{int(creator.avg_video_views / 1000)}k" if creator.avg_video_views else "0" # 格式化价格区间 if creator.pricing is not None: pricing_range = f"${creator.pricing}" else: pricing_range = None # 格式化结果 formatted_creator = { "relation_id": relation.id, "creator_id": creator.id, "pool_id": private_pool.id, # 添加池ID "pool_name": private_pool.name, # 添加池名称 "name": creator.name, "avatar": creator.get_avatar_url(), "category": creator.category, "e_commerce_level": e_commerce_level_formatted, "exposure_level": creator.exposure_level, "platform": creator.profile, # 添加平台信息 "followers": followers_formatted, "gmv": gmv_formatted, "avg_video_views": avg_views_formatted, "pricing": pricing_range, # 使用格式化后的价格区间 "collab_count": creator.collab_count, "notes": relation.notes, "status": relation.status, "added_from_public": relation.added_from_public, "added_at": relation.created_at.strftime('%Y-%m-%d'), "is_public_removed": relation.status == 'public_removed', # 添加公有库移除标识 "status_note": "该达人已从公有库中移除" if relation.status == 'public_removed' else None # 状态说明 } creator_list.append(formatted_creator) # 计算总页数 total_pages = (total_count + page_size - 1) // page_size # 构造分页信息 pagination = { "current_page": page, "total_pages": total_pages, "total_count": total_count, "has_next": page < total_pages, "has_prev": page > 1 } return JsonResponse({ 'code': 200, 'message': '获取成功', 'data': creator_list, 'pagination': pagination, 'pool_info': pool_info }, json_dumps_params={'ensure_ascii': False}) except Exception as e: logger.error(f"筛选私有达人库列表失败: {e}") import traceback logger.error(f"详细错误: {traceback.format_exc()}") return JsonResponse({ 'code': 500, 'message': f'筛选私有达人库列表失败: {str(e)}', 'data': None }, json_dumps_params={'ensure_ascii': False}) @api_view(['POST']) @authentication_classes([CustomTokenAuthentication]) @csrf_exempt @require_http_methods(["POST"]) def remove_from_public_pool(request): """从公有达人库中移除达人,同时删除所有用户私有库中的相关记录(仅管理员可操作)""" try: from .models import PublicCreatorPool, PrivateCreatorRelation, CreatorProfile import json # 检查当前用户是否有管理员权限 current_user = request.user if not current_user.is_staff and not current_user.is_superuser: return JsonResponse({ 'code': 403, 'message': '权限不足,只有管理员可以删除公有库达人', 'data': None }, json_dumps_params={'ensure_ascii': False}) data = json.loads(request.body) # 获取必要参数 creator_id = data.get('creator_id') public_id = data.get('public_id') # 也可以通过public_id删除 if not creator_id and not public_id: return JsonResponse({ 'code': 400, 'message': '缺少必要参数: creator_id 或 public_id', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 查询公有库记录 try: if public_id: public_creator = PublicCreatorPool.objects.get(id=public_id) creator = public_creator.creator else: creator = CreatorProfile.objects.get(id=creator_id) public_creator = PublicCreatorPool.objects.get(creator=creator) except (PublicCreatorPool.DoesNotExist, CreatorProfile.DoesNotExist): return JsonResponse({ 'code': 404, 'message': '找不到指定的公有库达人记录', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 查找所有私有库中引用此达人且标记为从公有库添加的记录 private_relations = PrivateCreatorRelation.objects.filter( creator=creator, added_from_public=True ) # 记录受影响的用户数量 affected_users = private_relations.values('private_pool__user_id').distinct().count() # 删除私有库中相关记录 deleted_private_count = 0 if private_relations.exists(): deleted_private_count = private_relations.count() private_relations.delete() # 删除公有库记录 public_creator.delete() return JsonResponse({ 'code': 200, 'message': '成功从公有库移除达人', 'data': { 'creator': { 'id': creator.id, 'name': creator.name }, 'removed_from_public': True, 'deleted_private_relations': deleted_private_count, 'affected_users': affected_users, 'note': f'已删除 {deleted_private_count} 个私有库中的相关记录,影响了 {affected_users} 个用户' } }, json_dumps_params={'ensure_ascii': False}) except Exception as e: logger.error(f"从公有库移除达人失败: {e}") import traceback logger.error(f"详细错误: {traceback.format_exc()}") return JsonResponse({ 'code': 500, 'message': f'从公有库移除达人失败: {str(e)}', 'data': None }, json_dumps_params={'ensure_ascii': False}) @api_view(['GET']) @authentication_classes([CustomTokenAuthentication]) @csrf_exempt @require_http_methods(["GET"]) def search_creators(request): """搜索创作者功能 - 基于关键词搜索创作者名称、分类等信息,支持多关键词搜索,只搜索公有达人库""" try: import json from django.db.models import Q from .models import PublicCreatorPool # 获取搜索参数 search_query = request.GET.get('q', '').strip() # 搜索关键词 search_mode = request.GET.get('mode', 'and').lower() # 搜索模式:and 或 or,默认为 and page = int(request.GET.get('page', 1)) page_size = int(request.GET.get('page_size', 10)) # 基础查询 - 只查询公有达人库中的达人 query = PublicCreatorPool.objects.select_related('creator').all() # 如果有搜索关键词,进行模糊搜索 if search_query: # 分割关键词(支持空格、逗号、分号分隔) import re keywords = re.split(r'[,;\s]+', search_query) keywords = [keyword.strip() for keyword in keywords if keyword.strip()] # 如果没有分隔符,将整个查询作为单个关键词处理(支持单个字符或汉字) if not keywords: keywords = [search_query] if keywords: if search_mode == 'or': # OR模式:任一关键词匹配即可 final_conditions = Q() for keyword in keywords: keyword_conditions = Q() # 搜索创作者名称 - 支持单个字符匹配 keyword_conditions |= Q(creator__name__icontains=keyword) # 搜索分类 - 支持单个字符匹配 keyword_conditions |= Q(creator__category__icontains=keyword) # 搜索MCN - 支持单个字符匹配 keyword_conditions |= Q(creator__mcn__icontains=keyword) # 搜索曝光等级 - 支持单个字符匹配 keyword_conditions |= Q(creator__exposure_level__icontains=keyword) # 搜索电商平台 - 支持单个字符匹配 keyword_conditions |= Q(creator__e_commerce_platforms__icontains=keyword) # 搜索公有库分类 - 支持单个字符匹配 keyword_conditions |= Q(category__icontains=keyword) # 特殊处理电商等级搜索(L+数字格式) if keyword.upper().startswith('L') and len(keyword) > 1: # 提取L后面的数字 level_match = re.match(r'L(\d+)', keyword.upper()) if level_match: level_number = int(level_match.group(1)) keyword_conditions |= Q(creator__e_commerce_level=level_number) final_conditions |= keyword_conditions else: # AND模式:所有关键词都必须匹配(默认) final_conditions = Q() for i, keyword in enumerate(keywords): keyword_conditions = Q() # 搜索创作者名称 - 支持单个字符匹配 keyword_conditions |= Q(creator__name__icontains=keyword) # 搜索分类 - 支持单个字符匹配 keyword_conditions |= Q(creator__category__icontains=keyword) # 搜索MCN - 支持单个字符匹配 keyword_conditions |= Q(creator__mcn__icontains=keyword) # 搜索曝光等级 - 支持单个字符匹配 keyword_conditions |= Q(creator__exposure_level__icontains=keyword) # 搜索电商平台 - 支持单个字符匹配 keyword_conditions |= Q(creator__e_commerce_platforms__icontains=keyword) # 搜索公有库分类 - 支持单个字符匹配 keyword_conditions |= Q(category__icontains=keyword) # 特殊处理电商等级搜索(L+数字格式) if keyword.upper().startswith('L') and len(keyword) > 1: # 提取L后面的数字 level_match = re.match(r'L(\d+)', keyword.upper()) if level_match: level_number = int(level_match.group(1)) keyword_conditions |= Q(creator__e_commerce_level=level_number) if i == 0: final_conditions = keyword_conditions else: final_conditions &= keyword_conditions # 应用搜索条件 query = query.filter(final_conditions) # 按相关性排序(名称匹配优先) if search_query: # 使用第一个关键词进行排序优化 first_keyword = keywords[0] if 'keywords' in locals() and keywords else search_query # 简化排序逻辑,避免使用tiktok_link字段 query = query.order_by('creator__name') else: # 如果没有搜索词,按名称排序 query = query.order_by('creator__name') # 获取总数据量 total_count = query.count() # 计算分页 start = (page - 1) * page_size end = start + page_size # 执行查询并分页 public_creators = query[start:end] creator_list = [] for public_creator in public_creators: creator = public_creator.creator # 格式化电商等级 e_commerce_level_formatted = f"L{creator.e_commerce_level}" if creator.e_commerce_level else None # 格式化GMV gmv_formatted = f"${creator.gmv}k" if creator.gmv else "$0" # 格式化粉丝数和观看量 followers_formatted = f"{int(creator.followers / 1000)}k" if creator.followers else "0" avg_views_formatted = f"{int(creator.avg_video_views / 1000)}k" if creator.avg_video_views else "0" # 格式化价格 pricing_formatted = f"${creator.pricing}" if creator.pricing else None # 使用与filter_public_creators相同的格式 formatted_creator = { "public_id": public_creator.id, "creator_id": creator.id, "name": creator.name, "avatar": creator.get_avatar_url(), "category": creator.category, "e_commerce_level": e_commerce_level_formatted, "exposure_level": creator.exposure_level, "platform": creator.profile if hasattr(creator, 'profile') else 'tiktok', "followers": followers_formatted, "gmv": gmv_formatted, "avg_video_views": avg_views_formatted, "pricing": pricing_formatted, "pricing_package": creator.pricing_package, "collab_count": creator.collab_count, "remark": public_creator.remark, "category_public": public_creator.category } creator_list.append(formatted_creator) # 计算总页数 total_pages = (total_count + page_size - 1) // page_size # 构造分页信息 pagination = { "current_page": page, "total_pages": total_pages, "total_count": total_count, "has_next": page < total_pages, "has_prev": page > 1 } # 构造搜索信息(可以作为额外信息保留) search_info = { "query": search_query, "keywords": keywords if 'keywords' in locals() else [], "search_mode": search_mode, "results_count": total_count, "search_applied": bool(search_query), "supports_single_char": True, # 标识支持单字符搜索 "search_scope": "public_only" # 标识只搜索公有达人库 } return JsonResponse({ 'code': 200, 'message': '搜索成功', 'data': creator_list, 'pagination': pagination, 'search_info': search_info }, json_dumps_params={'ensure_ascii': False}) except Exception as e: logger.error(f"搜索创作者失败: {e}") import traceback logger.error(f"详细错误: {traceback.format_exc()}") return JsonResponse({ 'code': 500, 'message': f'搜索创作者失败: {str(e)}', 'data': None }, json_dumps_params={'ensure_ascii': False}) @api_view(['GET']) @authentication_classes([CustomTokenAuthentication]) @csrf_exempt @require_http_methods(["GET"]) def search_private_creators(request): """搜索私有达人库功能 - 基于关键词搜索用户私有达人库中的创作者名称、分类等信息,支持多关键词搜索""" try: import json from django.db.models import Q from .models import PrivateCreatorPool, PrivateCreatorRelation # 获取当前用户 user = request.user if not user or not user.is_authenticated: return JsonResponse({ 'code': 401, 'message': '未授权访问', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 获取搜索参数 search_query = request.GET.get('q', '').strip() # 搜索关键词 search_mode = request.GET.get('mode', 'and').lower() # 搜索模式:and 或 or,默认为 and page = int(request.GET.get('page', 1)) page_size = int(request.GET.get('page_size', 10)) # 直接搜索用户所有私有库中的达人 query = PrivateCreatorRelation.objects.filter( private_pool__user=user ).select_related('creator', 'private_pool') # 如果有搜索关键词,进行模糊搜索 if search_query: # 分割关键词(支持空格、逗号、分号分隔) import re keywords = re.split(r'[,;\s]+', search_query) keywords = [keyword.strip() for keyword in keywords if keyword.strip()] # 如果没有分隔符,将整个查询作为单个关键词处理(支持单个字符或汉字) if not keywords: keywords = [search_query] if keywords: if search_mode == 'or': # OR模式:任一关键词匹配即可 final_conditions = Q() for keyword in keywords: keyword_conditions = Q() # 搜索创作者名称 - 支持单个字符匹配 keyword_conditions |= Q(creator__name__icontains=keyword) # 搜索分类 - 支持单个字符匹配 keyword_conditions |= Q(creator__category__icontains=keyword) # 搜索MCN - 支持单个字符匹配 keyword_conditions |= Q(creator__mcn__icontains=keyword) # 搜索曝光等级 - 支持单个字符匹配 keyword_conditions |= Q(creator__exposure_level__icontains=keyword) # 搜索电商平台 - 支持单个字符匹配 keyword_conditions |= Q(creator__e_commerce_platforms__icontains=keyword) # 搜索笔记 - 支持单个字符匹配 keyword_conditions |= Q(notes__icontains=keyword) # 搜索私有库名称 - 支持单个字符匹配 keyword_conditions |= Q(private_pool__name__icontains=keyword) # 特殊处理电商等级搜索(L+数字格式) if keyword.upper().startswith('L') and len(keyword) > 1: # 提取L后面的数字 level_match = re.match(r'L(\d+)', keyword.upper()) if level_match: level_number = int(level_match.group(1)) keyword_conditions |= Q(creator__e_commerce_level=level_number) final_conditions |= keyword_conditions else: # AND模式:所有关键词都必须匹配(默认) final_conditions = Q() for i, keyword in enumerate(keywords): keyword_conditions = Q() # 搜索创作者名称 - 支持单个字符匹配 keyword_conditions |= Q(creator__name__icontains=keyword) # 搜索分类 - 支持单个字符匹配 keyword_conditions |= Q(creator__category__icontains=keyword) # 搜索MCN - 支持单个字符匹配 keyword_conditions |= Q(creator__mcn__icontains=keyword) # 搜索曝光等级 - 支持单个字符匹配 keyword_conditions |= Q(creator__exposure_level__icontains=keyword) # 搜索电商平台 - 支持单个字符匹配 keyword_conditions |= Q(creator__e_commerce_platforms__icontains=keyword) # 搜索笔记 - 支持单个字符匹配 keyword_conditions |= Q(notes__icontains=keyword) # 搜索私有库名称 - 支持单个字符匹配 keyword_conditions |= Q(private_pool__name__icontains=keyword) # 特殊处理电商等级搜索(L+数字格式) if keyword.upper().startswith('L') and len(keyword) > 1: # 提取L后面的数字 level_match = re.match(r'L(\d+)', keyword.upper()) if level_match: level_number = int(level_match.group(1)) keyword_conditions |= Q(creator__e_commerce_level=level_number) if i == 0: final_conditions = keyword_conditions else: final_conditions &= keyword_conditions # 应用搜索条件 query = query.filter(final_conditions) # 按相关性排序(名称匹配优先) if search_query: # 使用第一个关键词进行排序优化 first_keyword = keywords[0] if 'keywords' in locals() and keywords else search_query # 简化排序逻辑,避免使用tiktok_link字段 query = query.order_by('creator__name') else: # 如果没有搜索词,按名称排序 query = query.order_by('creator__name') # 获取总数据量 total_count = query.count() # 计算分页 start = (page - 1) * page_size end = start + page_size # 执行查询并分页 private_creator_relations = query[start:end] creator_list = [] for relation in private_creator_relations: creator = relation.creator # 格式化电商等级 e_commerce_level_formatted = f"L{creator.e_commerce_level}" if creator.e_commerce_level else None # 格式化GMV gmv_formatted = f"${creator.gmv}k" if creator.gmv else "$0" # 格式化粉丝数和观看量 followers_formatted = f"{int(creator.followers / 1000)}k" if creator.followers else "0" avg_views_formatted = f"{int(creator.avg_video_views / 1000)}k" if creator.avg_video_views else "0" # 格式化价格区间 if creator.pricing is not None: pricing_range = f"${creator.pricing}" else: pricing_range = None # 格式化结果 - 使用与filter_private_pool_creators相同的格式 formatted_creator = { "relation_id": relation.id, "creator_id": creator.id, "name": creator.name, "avatar": creator.get_avatar_url(), "category": creator.category, "e_commerce_level": e_commerce_level_formatted, "exposure_level": creator.exposure_level, "platform": creator.profile if hasattr(creator, 'profile') else 'tiktok', "followers": followers_formatted, "gmv": gmv_formatted, "avg_video_views": avg_views_formatted, "pricing": pricing_range, "collab_count": creator.collab_count, "notes": relation.notes, "status": relation.status, "added_from_public": relation.added_from_public, "added_at": relation.created_at.strftime('%Y-%m-%d'), "is_public_removed": relation.status == 'public_removed', "status_note": "该达人已从公有库中移除" if relation.status == 'public_removed' else None, "pool_name": relation.private_pool.name # 添加所属达人库名称 } creator_list.append(formatted_creator) # 计算总页数 total_pages = (total_count + page_size - 1) // page_size # 构造分页信息 pagination = { "current_page": page, "total_pages": total_pages, "total_count": total_count, "has_next": page < total_pages, "has_prev": page > 1 } # 构造搜索信息 search_info = { "query": search_query, "keywords": keywords if 'keywords' in locals() else [], "search_mode": search_mode, "results_count": total_count, "search_applied": bool(search_query), "supports_single_char": True, # 标识支持单字符搜索 "search_scope": "private_only" # 标识只搜索私有达人库 } return JsonResponse({ 'code': 200, 'message': '搜索成功', 'data': creator_list, 'pagination': pagination, 'search_info': search_info }, json_dumps_params={'ensure_ascii': False}) except Exception as e: logger.error(f"搜索私有达人失败: {e}") import traceback logger.error(f"详细错误: {traceback.format_exc()}") return JsonResponse({ 'code': 500, 'message': f'搜索私有达人失败: {str(e)}', 'data': None }, json_dumps_params={'ensure_ascii': False}) @api_view(['GET']) @authentication_classes([CustomTokenAuthentication]) @csrf_exempt @require_http_methods(["GET"]) def get_brand_campaigns(request): """获取品牌合作活动列表,必须指定达人ID""" try: from apps.brands.models import Campaign, Brand, Product from .models import CreatorCampaign, CreatorProfile # 获取分页参数 page = int(request.GET.get('page', 1)) page_size = int(request.GET.get('page_size', 10)) # 获取过滤参数 brand_id = request.GET.get('brand_id') status = request.GET.get('status') creator_id = request.GET.get('creator_id') # 验证creator_id参数是否提供 if not creator_id: return JsonResponse({ 'code': 400, 'message': '缺少必要参数: creator_id', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 验证达人是否存在 try: creator = CreatorProfile.objects.get(id=creator_id) # 获取该达人参与的所有活动ID creator_campaigns = CreatorCampaign.objects.filter(creator=creator) campaign_ids = [cc.campaign_id for cc in creator_campaigns] # 基础查询 - 筛选该达人参与的活动 campaigns_query = Campaign.objects.filter(is_active=True, id__in=campaign_ids) except CreatorProfile.DoesNotExist: return JsonResponse({ 'code': 404, 'message': f'未找到ID为 {creator_id} 的达人', 'data': None }, json_dumps_params={'ensure_ascii': False}) # 应用品牌过滤 if brand_id: campaigns_query = campaigns_query.filter(brand_id=brand_id) # 应用状态过滤 if status: campaigns_query = campaigns_query.filter(status=status) # 获取总数据量 total_count = campaigns_query.count() # 计算分页 start = (page - 1) * page_size end = start + page_size # 执行查询并分页 campaigns = campaigns_query.order_by('-created_at')[start:end] campaign_list = [] for campaign in campaigns: # 获取品牌信息和首字母 if isinstance(campaign.brand, Brand): brand_name = campaign.brand.name brand_id_str = str(campaign.brand.id) else: brand_name = str(campaign.brand) if campaign.brand else "" brand_id_str = "" # 获取品牌首字母用于显示 first_letter = brand_name[:1].upper() if brand_name else "" # 构造品牌信息 brand_info = { "id": brand_id_str, "name": brand_name, "first_letter": first_letter } # 格式化时间 start_date = campaign.start_date.strftime('%m/%d/%Y') if campaign.start_date else "" end_date = campaign.end_date.strftime('%m/%d/%Y') if campaign.end_date else "" # 获取价格信息 price_detail = "" if campaign.budget: if isinstance(campaign.budget, str): # 如果已经是字符串且包含$符号,直接使用 if '$' in campaign.budget: price_detail = campaign.budget else: price_detail = f"${campaign.budget}" else: # 如果是数字,格式化为带$的字符串 price_detail = f"${campaign.budget}" # 格式化GMV和观看量 gmv_achieved = campaign.gmv_achieved or "" views_achieved = campaign.views_achieved or "" # 获取该达人在此活动中的状态 try: creator_campaign = CreatorCampaign.objects.get(creator_id=creator_id, campaign=campaign) status_value = creator_campaign.status except CreatorCampaign.DoesNotExist: status_value = "" # 构造活动数据 - 精确匹配图片中的表格列 campaign_data = { "brand": brand_info, "pricing_detail": price_detail, "start_date": start_date, "end_date": end_date, "status": status_value, "gmv_achieved": gmv_achieved, "views_achieved": views_achieved, "video_link": campaign.video_link or "" } campaign_list.append(campaign_data) # 计算总页数 total_pages = (total_count + page_size - 1) // page_size # 构造分页信息 pagination = { "current_page": page, "total_pages": total_pages, "total_count": total_count, "has_next": page < total_pages, "has_prev": page > 1 } return JsonResponse({ 'code': 200, 'message': '获取成功', 'data': campaign_list, 'pagination': pagination }, json_dumps_params={'ensure_ascii': False}) except Exception as e: logger.error(f"获取品牌合作活动列表失败: {e}") import traceback logger.error(f"详细错误: {traceback.format_exc()}") return JsonResponse({ 'code': 500, 'message': f'获取品牌合作活动列表失败: {str(e)}', 'data': None }, json_dumps_params={'ensure_ascii': False})