daren/apps/daren_detail/views.py
2025-06-06 15:15:28 +08:00

4259 lines
168 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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})
else:
# 当不提供pool_id时需要去除重复的达人
# 使用distinct('creator')获取不重复的达人关系
# 由于Django ORM的限制我们需要在内存中进行去重
seen_creator_ids = set()
unique_relations = []
# 先获取所有符合条件的关系
all_relations = list(creator_relations)
# 在内存中去重只保留每个creator_id的第一条记录
for relation in all_relations:
if relation.creator.id not in seen_creator_ids:
seen_creator_ids.add(relation.creator.id)
unique_relations.append(relation)
# 使用去重后的关系列表
creator_relations = unique_relations
# 应用状态过滤条件仅当提供了status参数时
if status:
if pool_id:
creator_relations = creator_relations.filter(status=status)
else:
# 在内存中进行过滤
creator_relations = [relation for relation in creator_relations if relation.status == status]
# 应用复杂过滤条件
# --------- 从filter_creators借鉴的过滤逻辑 ---------
# Category 多选过滤
category = filter_data.get('category')
if category and len(category) > 0:
if pool_id:
creator_relations = creator_relations.filter(creator__category__in=category)
else:
# 在内存中过滤
creator_relations = [relation for relation in creator_relations
if relation.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:
if pool_id:
creator_relations = creator_relations.filter(creator__e_commerce_level__in=level_nums)
else:
# 在内存中过滤
creator_relations = [relation for relation in creator_relations
if relation.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:
if pool_id:
creator_relations = creator_relations.filter(creator__exposure_level__in=exposure_level)
else:
# 在内存中过滤
creator_relations = [relation for relation in creator_relations
if relation.creator.exposure_level in exposure_level]
# 平台过滤tiktok, instagram, youtube等单选
platform = filter_data.get('platform')
if platform and platform:
if pool_id:
creator_relations = creator_relations.filter(creator__profile=platform)
else:
# 在内存中过滤
creator_relations = [relation for relation in creator_relations
if relation.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:
if pool_id:
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)
else:
# 在内存中过滤
filtered_relations = []
for relation in creator_relations:
creator_gmv = relation.creator.gmv or 0
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')
if gmv_min <= creator_gmv <= gmv_max:
filtered_relations.append(relation)
break
creator_relations = filtered_relations
# 观看量范围过滤,使用数值数组格式
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 pool_id:
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)
else:
# 在内存中过滤
filtered_relations = []
for relation in creator_relations:
views = relation.creator.avg_video_views or 0
if (views_min is None or views >= views_min) and (views_max is None or views <= views_max):
filtered_relations.append(relation)
creator_relations = filtered_relations
# 价格区间过滤逻辑,使用数值数组格式
pricing = filter_data.get('pricing')
if pricing and isinstance(pricing, list) and len(pricing) == 2:
min_price = pricing[0]
max_price = pricing[1]
if pool_id:
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)
else:
# 在内存中过滤
filtered_relations = []
for relation in creator_relations:
price = relation.creator.pricing or 0
if (min_price is None or price >= min_price) and (max_price is None or price <= max_price):
filtered_relations.append(relation)
creator_relations = filtered_relations
# 获取总数据量
total_count = len(creator_relations) if not pool_id else creator_relations.count()
# 计算分页
start = (page - 1) * page_size
end = start + page_size
# 执行查询并分页
if pool_id:
paged_relations = creator_relations[start:end]
else:
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))
# 获取pool_id参数如果提供了则只搜索特定库
pool_id = request.GET.get('pool_id')
# 直接搜索用户所有私有库中的达人
query = PrivateCreatorRelation.objects.filter(
private_pool__user=user
).select_related('creator', 'private_pool')
# 如果提供了pool_id则只搜索特定私有库
if pool_id:
query = query.filter(private_pool_id=pool_id)
# 如果有搜索关键词,进行模糊搜索
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')
# 当不提供pool_id时进行达人去重
if not pool_id:
# 先获取所有符合条件的关系
all_relations = list(query)
# 在内存中去重只保留每个creator_id的第一条记录
seen_creator_ids = set()
unique_relations = []
for relation in all_relations:
if relation.creator.id not in seen_creator_ids:
seen_creator_ids.add(relation.creator.id)
unique_relations.append(relation)
# 使用去重后的关系列表
relations = unique_relations
total_count = len(relations)
# 计算分页
start = (page - 1) * page_size
end = min(start + page_size, total_count)
# 执行分页
private_creator_relations = relations[start:end]
else:
# 如果提供了pool_id不需要去重直接使用数据库查询
# 获取总数据量
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", # 标识只搜索私有达人库
"is_deduplicated": not pool_id # 标识是否进行了达人去重
}
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})