daren/apps/daren_detail/views.py

4259 lines
168 KiB
Python
Raw Normal View History

2025-05-19 18:23:59 +08:00
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
2025-05-20 10:36:39 +08:00
from django.db.models import Q
2025-05-20 15:58:53 +08:00
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
2025-05-19 18:23:59 +08:00
dotenv.load_dotenv()
# 获取应用专属的logger
logger = logging.getLogger('daren_detail')
directory_monitoring = {}
# 全局变量来控制检测线程
monitor_thread = None
is_monitoring = False
2025-05-20 15:58:53 +08:00
@api_view(['POST'])
@authentication_classes([CustomTokenAuthentication])
2025-05-19 18:23:59 +08:00
@csrf_exempt
@require_http_methods(["POST"])
def filter_creators(request):
2025-05-22 12:14:03 +08:00
"""根据过滤条件筛选达人信息POST版分页参数在URL中"""
2025-05-19 18:23:59 +08:00
try:
import json
2025-05-22 12:14:03 +08:00
# 从URL获取分页参数
page = int(request.GET.get('page', 1))
page_size = 10 # 固定页面大小为10条数据
2025-05-19 18:23:59 +08:00
# 解析POST请求体
data = json.loads(request.body)
filter_data = data.get('filter', {})
# 基础查询
query = CreatorProfile.objects.all()
2025-05-20 10:36:39 +08:00
# Category 多选过滤
2025-05-19 18:23:59 +08:00
category = filter_data.get('category')
if category and len(category) > 0:
2025-05-20 10:36:39 +08:00
query = query.filter(category__in=category)
2025-05-19 18:23:59 +08:00
2025-05-20 10:36:39 +08:00
# 电商能力等级过滤 (L1-L7),多选
2025-05-19 18:23:59 +08:00
e_commerce_level = filter_data.get('e_commerce_level')
if e_commerce_level and len(e_commerce_level) > 0:
2025-05-20 10:36:39 +08:00
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等),多选
2025-05-19 18:23:59 +08:00
exposure_level = filter_data.get('exposure_level')
if exposure_level and len(exposure_level) > 0:
2025-05-20 10:36:39 +08:00
query = query.filter(exposure_level__in=exposure_level)
2025-05-19 18:23:59 +08:00
2025-05-20 10:36:39 +08:00
# GMV范围过滤 ($0-$5k, $5k-$25k, $25k-$50k等),多选
2025-05-19 18:23:59 +08:00
gmv_range = filter_data.get('gmv_range')
if gmv_range and len(gmv_range) > 0:
2025-05-20 10:36:39 +08:00
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)
2025-05-19 18:23:59 +08:00
# 观看量范围过滤,单选
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)
2025-05-23 12:11:03 +08:00
# 修改:根据单一定价字段判断是否在区间内
query = query.filter(pricing__gte=min_price, pricing__lte=max_price)
2025-05-19 18:23:59 +08:00
# 获取总数据量
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"
2025-05-23 12:11:03 +08:00
# 格式化价格
pricing_formatted = f"${creator.pricing}" if creator.pricing else None
2025-05-19 18:23:59 +08:00
# 格式化结果
formatted_creator = {
"Creator": {
"name": creator.name,
2025-06-06 11:43:33 +08:00
"avatar": creator.get_avatar_url()
2025-05-19 18:23:59 +08:00
},
"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,
2025-05-23 12:11:03 +08:00
"Pricing": pricing_formatted,
"Pricing Package": creator.pricing_package,
2025-05-19 18:23:59 +08:00
"# 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})
2025-05-20 15:58:53 +08:00
@api_view(['POST'])
@authentication_classes([CustomTokenAuthentication])
2025-05-19 18:23:59 +08:00
@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
# 更新价格信息
2025-05-23 12:11:03 +08:00
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
2025-05-19 18:23:59 +08:00
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)
2025-05-20 15:58:53 +08:00
# creator.is_active = data.get('is_active', creator.is_active)
2025-05-19 18:23:59 +08:00
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})
2025-05-20 15:58:53 +08:00
@api_view(['GET'])
@authentication_classes([CustomTokenAuthentication])
2025-05-19 18:23:59 +08:00
@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})
2025-05-20 15:58:53 +08:00
@api_view(['POST'])
@authentication_classes([CustomTokenAuthentication])
2025-05-19 18:23:59 +08:00
@csrf_exempt
@require_http_methods(["POST"])
def add_to_campaign(request):
"""添加达人到营销活动 - 创建达人与产品的谈判关系"""
2025-05-19 18:23:59 +08:00
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
2025-05-19 18:23:59 +08:00
import json
import uuid
2025-05-19 18:23:59 +08:00
data = json.loads(request.body)
# 获取必要参数
campaign_id = data.get('campaign_id')
creator_ids = data.get('creator_ids', [])
brand_id = data.get('brand_id') # 可选的品牌ID参数
2025-05-19 18:23:59 +08:00
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
2025-05-19 18:23:59 +08:00
logger.info(f"尝试查找活动ID: {campaign_id}")
# Campaign ID可能是整数而不是UUID直接使用原始ID进行查询
campaign = Campaign.objects.get(id=campaign_id, is_active=True)
2025-05-19 18:23:59 +08:00
logger.info(f"找到活动: {campaign.name}, Active: {campaign.is_active}")
except Campaign.DoesNotExist:
2025-05-19 18:23:59 +08:00
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 = []
2025-05-19 18:23:59 +08:00
added_count = 0
skipped_count = 0
2025-05-22 12:14:03 +08:00
already_exists_count = 0
# 为新添加的达人创建谈判和对话
negotiations_created = []
2025-05-19 18:23:59 +08:00
# 处理每个达人
2025-05-19 18:23:59 +08:00
for creator_id in creator_ids:
try:
# 查询达人信息
2025-05-19 18:23:59 +08:00
creator = CreatorProfile.objects.get(id=creator_id)
logger.info(f"找到达人: {creator.name}")
2025-05-22 12:14:03 +08:00
# 将达人添加到返回列表
2025-05-19 18:23:59 +08:00
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)}")
# 继续处理,不中断流程
2025-05-19 18:23:59 +08:00
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})
2025-05-19 18:23:59 +08:00
return JsonResponse({
'code': 200,
'message': '成功添加达人并创建谈判关系',
2025-05-19 18:23:59 +08:00
'data': {
'campaign': {
'id': str(campaign.id),
'name': campaign.name
},
2025-05-22 12:14:03 +08:00
'added_creators': added_creators,
'stats': {
'added': added_count,
'skipped': skipped_count,
'already_exists': already_exists_count,
'products_found': len(products)
},
'negotiations_created': negotiations_created
2025-05-19 18:23:59 +08:00
}
}, json_dumps_params={'ensure_ascii': False})
except Exception as e:
logger.error(f"处理失败: {e}")
2025-05-19 18:23:59 +08:00
import traceback
logger.error(f"详细错误: {traceback.format_exc()}")
return JsonResponse({
'code': 500,
'message': f'处理失败: {str(e)}',
2025-05-19 18:23:59 +08:00
'data': None
}, json_dumps_params={'ensure_ascii': False})
2025-05-20 15:58:53 +08:00
@api_view(['GET'])
@authentication_classes([CustomTokenAuthentication])
@csrf_exempt
2025-05-19 18:23:59 +08:00
@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:
2025-05-23 12:11:03 +08:00
if creator.pricing:
2025-05-19 18:23:59 +08:00
try:
2025-05-23 12:11:03 +08:00
price = float(creator.pricing)
2025-05-19 18:23:59 +08:00
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,
2025-06-06 11:43:33 +08:00
"avatar": creator.get_avatar_url(),
2025-05-19 18:23:59 +08:00
"email": creator.email,
2025-05-29 11:07:32 +08:00
"social_accounts": {
2025-05-19 18:23:59 +08:00
"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": {
2025-05-23 12:11:03 +08:00
"price": f"${creator.pricing}" if creator.pricing else None,
2025-05-19 18:23:59 +08:00
"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})
2025-05-20 15:58:53 +08:00
@api_view(['POST'])
@authentication_classes([CustomTokenAuthentication])
2025-05-19 18:23:59 +08:00
@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})
# 获取特定达人与各品牌的合作详情
2025-05-20 15:58:53 +08:00
@api_view(['GET'])
@authentication_classes([CustomTokenAuthentication])
2025-05-19 18:23:59 +08:00
@csrf_exempt
@require_http_methods(["GET"])
def get_creator_brand_campaigns(request, creator_id=None):
"""获取特定达人与各品牌的合作详情"""
try:
2025-05-20 15:58:53 +08:00
import json
2025-05-19 18:23:59 +08:00
# 检查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)
2025-05-20 15:58:53 +08:00
brands_needed = [campaign.brand.name[:1].upper() if isinstance(campaign.brand, Brand) else campaign.brand[:1].upper()
for campaign in campaigns if campaign.brand]
2025-05-19 18:23:59 +08:00
# 合并品牌活动数据
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
2025-05-20 15:58:53 +08:00
# 修改获取brand_id的逻辑
brand_id = campaign.brand.name[:1].upper() if isinstance(campaign.brand, Brand) else (campaign.brand[:1].upper() if campaign.brand else 'U')
2025-05-19 18:23:59 +08:00
# 为不同品牌设置不同颜色
colors = {
'U': '#E74694', # 粉色
'R': '#17CDCB', # 青色
'X': '#6E41BF', # 紫色
'Q': '#F9975D', # 橙色
'A': '#B4B4B4', # 灰色
'M': '#333333', # 黑色
}
campaign_data = {
"brand": {
"id": brand_id,
2025-05-20 15:58:53 +08:00
"name": campaign.brand.name if isinstance(campaign.brand, Brand) else (campaign.brand or "brand"),
2025-05-19 18:23:59 +08:00
"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,
2025-06-06 11:43:33 +08:00
"avatar": creator.get_avatar_url(),
2025-05-19 18:23:59 +08:00
"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})
# 获取创作者的协作指标、视频和直播指标数据
2025-05-20 15:58:53 +08:00
@api_view(['GET'])
@authentication_classes([CustomTokenAuthentication])
2025-05-19 18:23:59 +08:00
@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})
# 更新创作者的指标数据
2025-05-20 15:58:53 +08:00
@api_view(['POST'])
@authentication_classes([CustomTokenAuthentication])
2025-05-19 18:23:59 +08:00
@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})
2025-05-20 15:58:53 +08:00
@api_view(['GET'])
@authentication_classes([CustomTokenAuthentication])
2025-05-19 18:23:59 +08:00
@csrf_exempt
@require_http_methods(["GET"])
def get_creator_followers_metrics(request, creator_id=None):
"""获取创作者的粉丝统计指标数据"""
try:
import json
2025-05-26 10:27:53 +08:00
from datetime import datetime, timedelta
2025-05-19 18:23:59 +08:00
# 检查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:
# 如果没有数据,返回示例数据
2025-05-26 10:27:53 +08:00
from datetime import datetime, timedelta
end_date = datetime.now().date()
start_date = end_date - timedelta(days=30)
2025-05-19 18:23:59 +08:00
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
},
2025-05-26 10:27:53 +08:00
'date_range': f"{start_date.strftime('%b %d, %Y')} - {end_date.strftime('%b %d, %Y')}"
2025-05-19 18:23:59 +08:00
}
else:
# 构建粉丝统计数据
2025-05-23 19:08:40 +08:00
# 处理locations数据对每个值保留两位小数
processed_locations = {}
if follower_metrics.location_data:
for location, value in follower_metrics.location_data.items():
processed_locations[location] = round(float(value), 2)
2025-05-19 18:23:59 +08:00
follower_data = {
'gender': {
2025-05-23 19:08:40 +08:00
'female': round(follower_metrics.female_percentage, 2),
'male': round(follower_metrics.male_percentage, 2)
2025-05-19 18:23:59 +08:00
},
'age': {
2025-05-23 19:08:40 +08:00
'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)
2025-05-19 18:23:59 +08:00
},
2025-05-23 19:08:40 +08:00
'locations': processed_locations,
2025-05-26 10:27:53 +08:00
'date_range': f"{follower_metrics.start_date.strftime('%b %d, %Y')} - {follower_metrics.end_date.strftime('%b %d, %Y')}"
2025-05-19 18:23:59 +08:00
}
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})
2025-05-20 15:58:53 +08:00
@api_view(['GET'])
@authentication_classes([CustomTokenAuthentication])
2025-05-19 18:23:59 +08:00
@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)
2025-05-23 19:08:40 +08:00
# 确保值不小于0并对浮点数保留两位小数
trend_data['gmv'].append(round(max(0, gmv_value), 2))
2025-05-19 18:23:59 +08:00
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))
2025-05-23 19:08:40 +08:00
trend_data['engagement_rate'].append(round(max(0, engagement_value), 2))
2025-05-19 18:23:59 +08:00
# 更新基准值,使数据有一定的连续性
base_gmv = gmv_value
base_items_sold = items_value
base_followers = followers_value
base_views = views_value
base_engagement = engagement_value
2025-05-26 10:27:53 +08:00
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')}"
2025-05-19 18:23:59 +08:00
else:
# 从数据库构建趋势数据
trend_data = {
'gmv': [],
'items_sold': [],
'followers': [],
'video_views': [],
'engagement_rate': [],
'dates': []
}
for trend in trends:
2025-05-23 19:08:40 +08:00
trend_data['gmv'].append(round(trend.gmv, 2))
2025-05-19 18:23:59 +08:00
trend_data['items_sold'].append(trend.items_sold)
trend_data['followers'].append(trend.followers_count)
trend_data['video_views'].append(trend.video_views)
2025-05-23 19:08:40 +08:00
trend_data['engagement_rate'].append(round(trend.engagement_rate, 2))
2025-05-19 18:23:59 +08:00
trend_data['dates'].append(trend.date.strftime('%Y-%m-%d'))
2025-05-26 10:27:53 +08:00
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')}"
2025-05-19 18:23:59 +08:00
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})
2025-05-20 15:58:53 +08:00
@api_view(['POST'])
@authentication_classes([CustomTokenAuthentication])
2025-05-19 18:23:59 +08:00
@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})
2025-05-20 15:58:53 +08:00
@api_view(['POST'])
@authentication_classes([CustomTokenAuthentication])
2025-05-19 18:23:59 +08:00
@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})
2025-05-20 15:58:53 +08:00
@api_view(['GET'])
@authentication_classes([CustomTokenAuthentication])
2025-05-19 18:23:59 +08:00
@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})
2025-05-20 15:58:53 +08:00
@api_view(['POST'])
@authentication_classes([CustomTokenAuthentication])
2025-05-19 18:23:59 +08:00
@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})
######################################## 公有达人和私有达人 ########################################
2025-05-20 15:58:53 +08:00
@api_view(['GET'])
@authentication_classes([CustomTokenAuthentication])
2025-05-19 18:23:59 +08:00
@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')
2025-05-23 12:11:03 +08:00
# 基础查询 - 从公有达人池开始
2025-05-19 18:23:59 +08:00
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
)
2025-05-26 10:27:53 +08:00
# 观看量范围过滤,单选
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)
2025-05-19 18:23:59 +08:00
# 获取总数据量
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"
2025-05-23 12:11:03 +08:00
# 格式化价格
pricing_formatted = f"${creator.pricing}" if creator.pricing else None
2025-05-19 18:23:59 +08:00
# 格式化结果
formatted_creator = {
"public_id": public_creator.id,
"creator_id": creator.id,
"name": creator.name,
2025-06-06 11:43:33 +08:00
"avatar": creator.get_avatar_url(),
2025-05-19 18:23:59 +08:00
"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,
2025-05-23 12:11:03 +08:00
"pricing": pricing_formatted,
"pricing_package": creator.pricing_package,
2025-05-19 18:23:59 +08:00
"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})
2025-05-20 15:58:53 +08:00
@api_view(['POST'])
@authentication_classes([CustomTokenAuthentication])
2025-05-19 18:23:59 +08:00
@csrf_exempt
@require_http_methods(["POST"])
def add_to_public_pool(request):
2025-06-03 15:42:45 +08:00
"""将达人添加到公有达人库(仅管理员可操作)"""
2025-05-19 18:23:59 +08:00
try:
from .models import PublicCreatorPool, CreatorProfile
import json
2025-06-03 15:42:45 +08:00
# 检查当前用户是否有管理员权限
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})
2025-05-19 18:23:59 +08:00
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})
2025-05-20 15:58:53 +08:00
@api_view(['GET'])
@authentication_classes([CustomTokenAuthentication])
2025-05-19 18:23:59 +08:00
@csrf_exempt
@require_http_methods(["GET"])
def get_private_pools(request):
"""获取用户的私有达人库列表"""
try:
from .models import PrivateCreatorPool
import json
2025-05-23 16:51:34 +08:00
# 获取当前认证用户
current_user = request.user
if not current_user.is_authenticated:
2025-05-19 18:23:59 +08:00
return JsonResponse({
2025-05-23 16:51:34 +08:00
'code': 401,
'message': '用户未认证',
2025-05-19 18:23:59 +08:00
'data': None
}, json_dumps_params={'ensure_ascii': False})
2025-05-23 16:51:34 +08:00
# 检查是否传入了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)
2025-05-19 18:23:59 +08:00
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})
2025-05-20 15:58:53 +08:00
@api_view(['POST'])
@authentication_classes([CustomTokenAuthentication])
2025-05-19 18:23:59 +08:00
@csrf_exempt
@require_http_methods(["POST"])
def create_private_pool(request):
"""创建私有达人库"""
try:
from .models import PrivateCreatorPool
from apps.user.models import User
import json
2025-05-23 16:51:34 +08:00
# 获取当前认证用户
current_user = request.user
if not current_user.is_authenticated:
return JsonResponse({
'code': 401,
'message': '用户未认证',
'data': None
}, json_dumps_params={'ensure_ascii': False})
2025-05-19 18:23:59 +08:00
data = json.loads(request.body)
logger.info(f"创建私有达人库请求数据: {data}")
2025-05-23 16:51:34 +08:00
# 检查是否传入了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参数
2025-05-19 18:23:59 +08:00
name = data.get('name')
description = data.get('description')
is_default = data.get('is_default', False)
2025-05-23 16:51:34 +08:00
logger.info(f"解析后的参数: user_id={current_user.id}, name={name}, description={description}, is_default={is_default}")
2025-05-19 18:23:59 +08:00
2025-05-23 16:51:34 +08:00
if not name:
2025-05-19 18:23:59 +08:00
return JsonResponse({
'code': 400,
2025-05-23 16:51:34 +08:00
'message': '缺少必要参数: name',
2025-05-19 18:23:59 +08:00
'data': None
}, json_dumps_params={'ensure_ascii': False})
# 检查是否已存在同名私有库
try:
2025-05-23 16:51:34 +08:00
existing_pool = PrivateCreatorPool.objects.filter(user_id=current_user.id, name=name).first()
2025-05-19 18:23:59 +08:00
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:
2025-05-23 16:51:34 +08:00
PrivateCreatorPool.objects.filter(user_id=current_user.id, is_default=True).update(is_default=False)
2025-05-19 18:23:59 +08:00
# 创建私有库
try:
private_pool = PrivateCreatorPool.objects.create(
2025-05-23 16:51:34 +08:00
user_id=current_user.id,
2025-05-19 18:23:59 +08:00
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})
2025-05-20 15:58:53 +08:00
@api_view(['GET'])
@authentication_classes([CustomTokenAuthentication])
2025-05-19 18:23:59 +08:00
@csrf_exempt
@require_http_methods(["GET"])
def get_private_pool_creators(request, pool_id=None):
"""获取私有库中的达人列表"""
try:
from .models import PrivateCreatorPool, PrivateCreatorRelation
import json
2025-05-23 16:51:34 +08:00
# 获取当前认证用户
current_user = request.user
if not current_user.is_authenticated:
return JsonResponse({
'code': 401,
'message': '用户未认证',
'data': None
}, json_dumps_params={'ensure_ascii': False})
2025-05-19 18:23:59 +08:00
# 检查pool_id是否提供
if not pool_id:
pool_id = request.GET.get('pool_id')
2025-05-23 12:11:03 +08:00
if not pool_id:
return JsonResponse({
'code': 400,
'message': '缺少必要参数: pool_id',
'data': None
}, json_dumps_params={'ensure_ascii': False})
2025-05-19 18:23:59 +08:00
# 获取分页参数
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')
2025-05-23 16:51:34 +08:00
# 查询私有库信息并验证所有权
2025-05-19 18:23:59 +08:00
try:
2025-05-23 16:51:34 +08:00
private_pool = PrivateCreatorPool.objects.get(
id=pool_id,
user_id=current_user.id # 确保只能访问当前用户的私有库
)
2025-05-19 18:23:59 +08:00
except PrivateCreatorPool.DoesNotExist:
return JsonResponse({
'code': 404,
2025-05-23 16:51:34 +08:00
'message': f'找不到ID为 {pool_id} 的私有库或无权限访问',
2025-05-19 18:23:59 +08:00
'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"
# 格式化价格区间
2025-05-23 12:11:03 +08:00
if creator.pricing is not None:
pricing_range = f"${creator.pricing}"
2025-05-19 18:23:59 +08:00
else:
pricing_range = None
# 格式化结果
formatted_creator = {
"relation_id": relation.id,
"creator_id": creator.id,
"name": creator.name,
2025-06-06 11:43:33 +08:00
"avatar": creator.get_avatar_url(),
2025-05-19 18:23:59 +08:00
"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,
2025-05-23 19:08:40 +08:00
"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 # 状态说明
2025-05-19 18:23:59 +08:00
}
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})
2025-05-20 15:58:53 +08:00
@api_view(['POST'])
@authentication_classes([CustomTokenAuthentication])
2025-05-19 18:23:59 +08:00
@csrf_exempt
@require_http_methods(["POST"])
def add_creator_to_private_pool(request):
"""将达人添加到私有达人库"""
try:
from .models import PrivateCreatorPool, PrivateCreatorRelation, CreatorProfile, PublicCreatorPool
import json
2025-05-23 16:51:34 +08:00
# 获取当前认证用户
current_user = request.user
if not current_user.is_authenticated:
return JsonResponse({
'code': 401,
'message': '用户未认证',
'data': None
}, json_dumps_params={'ensure_ascii': False})
2025-05-19 18:23:59 +08:00
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]
2025-06-06 10:46:29 +08:00
if not creator_ids:
2025-05-19 18:23:59 +08:00
return JsonResponse({
'code': 400,
2025-06-06 10:46:29 +08:00
'message': '缺少必要参数: creator_id/creator_ids',
2025-05-19 18:23:59 +08:00
'data': None
}, json_dumps_params={'ensure_ascii': False})
2025-05-23 16:51:34 +08:00
# 查询私有库信息并验证所有权
2025-06-06 10:46:29 +08:00
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}")
2025-05-19 18:23:59 +08:00
# 添加达人到私有库
added_creators = []
2025-05-23 16:51:34 +08:00
already_exists_count = 0
2025-05-19 18:23:59 +08:00
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:
2025-05-23 16:51:34 +08:00
# 如果已存在,则更新信息但不显示在响应中
2025-05-19 18:23:59 +08:00
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()
2025-05-23 16:51:34 +08:00
# 计数已存在的达人,但不添加到响应列表中
already_exists_count += 1
2025-05-19 18:23:59 +08:00
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:
2025-05-23 16:51:34 +08:00
# 如果达人不存在,直接跳过,不在响应中显示
continue
2025-05-19 18:23:59 +08:00
return JsonResponse({
'code': 200,
'message': '操作成功',
'data': {
'added': added_creators,
2025-05-23 16:51:34 +08:00
'already_exists_count': already_exists_count,
2025-05-19 18:23:59 +08:00
'pool': {
'id': private_pool.id,
2025-06-06 10:46:29 +08:00
'name': private_pool.name,
'is_default': private_pool.is_default,
'auto_created': is_auto_created
2025-05-19 18:23:59 +08:00
}
}
}, 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})
2025-05-20 15:58:53 +08:00
@api_view(['POST'])
@authentication_classes([CustomTokenAuthentication])
2025-05-19 18:23:59 +08:00
@csrf_exempt
@require_http_methods(["POST"])
def update_creator_in_private_pool(request):
"""更新私有库中达人的状态或笔记"""
try:
from .models import PrivateCreatorRelation
import json
2025-05-23 16:51:34 +08:00
# 获取当前认证用户
current_user = request.user
if not current_user.is_authenticated:
return JsonResponse({
'code': 401,
'message': '用户未认证',
'data': None
}, json_dumps_params={'ensure_ascii': False})
2025-05-19 18:23:59 +08:00
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:
2025-05-23 16:51:34 +08:00
relation = PrivateCreatorRelation.objects.select_related('private_pool').get(id=relation_id)
2025-05-19 18:23:59 +08:00
except PrivateCreatorRelation.DoesNotExist:
return JsonResponse({
'code': 404,
'message': f'找不到ID为 {relation_id} 的关联记录',
'data': None
}, json_dumps_params={'ensure_ascii': False})
2025-05-23 16:51:34 +08:00
# 验证当前用户是否有权限操作此私有库
if relation.private_pool.user_id != current_user.id:
return JsonResponse({
'code': 403,
'message': '无权限操作其他用户的私有达人库',
'data': None
}, json_dumps_params={'ensure_ascii': False})
2025-05-19 18:23:59 +08:00
# 更新信息
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})
2025-05-20 15:58:53 +08:00
@api_view(['POST'])
@authentication_classes([CustomTokenAuthentication])
2025-05-19 18:23:59 +08:00
@csrf_exempt
@require_http_methods(["POST"])
def remove_creator_from_private_pool(request):
"""从私有库中移除达人"""
try:
2025-05-23 16:51:34 +08:00
from .models import PrivateCreatorRelation, PrivateCreatorPool
2025-05-19 18:23:59 +08:00
import json
2025-05-23 16:51:34 +08:00
# 获取当前认证用户
current_user = request.user
if not current_user.is_authenticated:
return JsonResponse({
'code': 401,
'message': '用户未认证',
'data': None
}, json_dumps_params={'ensure_ascii': False})
2025-05-19 18:23:59 +08:00
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:
2025-05-23 16:51:34 +08:00
# 通过关联ID删除 - 需要验证每个关联的权限
relations = PrivateCreatorRelation.objects.select_related('private_pool').filter(id__in=relation_ids)
2025-06-03 15:34:59 +08:00
# 检查是否找到要删除的记录
if not relations.exists():
return JsonResponse({
'code': 404,
'message': '未找到指定的私有库达人关联记录',
'data': {
'relation_ids': relation_ids
}
}, json_dumps_params={'ensure_ascii': False})
2025-05-23 16:51:34 +08:00
# 验证权限
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
2025-05-19 18:23:59 +08:00
result_type = 'relation_ids'
result_value = relation_ids
elif pool_id and creator_ids:
2025-05-23 16:51:34 +08:00
# 通过私有库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})
2025-06-03 15:34:59 +08:00
# 查询要删除的记录
2025-05-19 18:23:59 +08:00
query = PrivateCreatorRelation.objects.filter(
private_pool_id=pool_id,
creator_id__in=creator_ids
)
2025-06-03 15:34:59 +08:00
# 检查是否找到要删除的记录
if not query.exists():
return JsonResponse({
'code': 404,
'message': '私有库中未找到指定的达人记录',
'data': {
'pool_id': pool_id,
'creator_ids': creator_ids
}
}, json_dumps_params={'ensure_ascii': False})
2025-05-19 18:23:59 +08:00
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})
2025-05-23 12:11:03 +08:00
@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)
2025-05-27 11:10:52 +08:00
# 平台过滤tiktok, instagram, youtube等单选
platform = filter_data.get('platform')
2025-05-29 11:07:32 +08:00
if platform and platform:
public_creators = public_creators.filter(creator__profile=platform)
2025-05-27 11:10:52 +08:00
2025-05-23 12:11:03 +08:00
# 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)
2025-05-29 12:04:43 +08:00
# 观看量范围过滤,使用数值数组格式
2025-05-23 12:11:03 +08:00
views_range = filter_data.get('views_range')
2025-05-29 12:04:43 +08:00
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)
2025-05-23 12:11:03 +08:00
# 获取总数据量
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,
2025-06-06 11:43:33 +08:00
"avatar": creator.get_avatar_url(),
2025-05-23 12:11:03 +08:00
"category": creator.category,
"e_commerce_level": e_commerce_level_formatted,
"exposure_level": creator.exposure_level,
2025-05-27 11:10:52 +08:00
"platform": creator.profile, # 添加平台信息
2025-05-23 12:11:03 +08:00
"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)
2025-06-06 10:46:29 +08:00
# 获取私有库ID现在是可选的
2025-05-23 12:11:03 +08:00
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')
2025-06-06 10:46:29 +08:00
# 初始化查询逻辑
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:
2025-05-23 12:11:03 +08:00
return JsonResponse({
2025-06-06 10:46:29 +08:00
'code': 404,
'message': f'找不到ID为 {pool_id} 的私有库',
2025-05-23 12:11:03 +08:00
'data': None
}, json_dumps_params={'ensure_ascii': False})
2025-06-06 15:15:28 +08:00
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
2025-05-27 11:10:52 +08:00
2025-05-23 12:11:03 +08:00
# 应用状态过滤条件仅当提供了status参数时
if status:
2025-06-06 15:15:28 +08:00
if pool_id:
creator_relations = creator_relations.filter(status=status)
else:
# 在内存中进行过滤
creator_relations = [relation for relation in creator_relations if relation.status == status]
2025-05-23 12:11:03 +08:00
# 应用复杂过滤条件
# --------- 从filter_creators借鉴的过滤逻辑 ---------
# Category 多选过滤
category = filter_data.get('category')
if category and len(category) > 0:
2025-06-06 15:15:28 +08:00
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]
2025-05-23 12:11:03 +08:00
# 电商能力等级过滤 (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:
2025-06-06 15:15:28 +08:00
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]
2025-05-23 12:11:03 +08:00
# 曝光等级过滤 (KOL-1, KOL-2, KOC-1等),多选
exposure_level = filter_data.get('exposure_level')
if exposure_level and len(exposure_level) > 0:
2025-06-06 15:15:28 +08:00
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]
2025-05-23 12:11:03 +08:00
2025-05-27 11:10:52 +08:00
# 平台过滤tiktok, instagram, youtube等单选
platform = filter_data.get('platform')
2025-05-29 11:07:32 +08:00
if platform and platform:
2025-06-06 15:15:28 +08:00
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]
2025-05-27 11:10:52 +08:00
2025-05-23 12:11:03 +08:00
# GMV范围过滤 ($0-$5k, $5k-$25k, $25k-$50k等),多选
gmv_range = filter_data.get('gmv_range')
if gmv_range and len(gmv_range) > 0:
2025-06-06 15:15:28 +08:00
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
2025-05-23 12:11:03 +08:00
2025-05-29 12:04:43 +08:00
# 观看量范围过滤,使用数值数组格式
2025-05-23 12:11:03 +08:00
views_range = filter_data.get('views_range')
2025-05-29 12:04:43 +08:00
if views_range and isinstance(views_range, list) and len(views_range) == 2:
views_min = views_range[0]
views_max = views_range[1]
2025-06-06 15:15:28 +08:00
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
2025-05-23 12:11:03 +08:00
2025-05-29 12:04:43 +08:00
# 价格区间过滤逻辑,使用数值数组格式
2025-05-23 12:11:03 +08:00
pricing = filter_data.get('pricing')
2025-05-29 12:04:43 +08:00
if pricing and isinstance(pricing, list) and len(pricing) == 2:
min_price = pricing[0]
max_price = pricing[1]
2025-06-06 15:15:28 +08:00
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
2025-05-23 12:11:03 +08:00
# 获取总数据量
2025-06-06 15:15:28 +08:00
total_count = len(creator_relations) if not pool_id else creator_relations.count()
2025-05-23 12:11:03 +08:00
# 计算分页
start = (page - 1) * page_size
end = start + page_size
# 执行查询并分页
2025-06-06 15:15:28 +08:00
if pool_id:
paged_relations = creator_relations[start:end]
else:
paged_relations = creator_relations[start:end]
2025-05-23 12:11:03 +08:00
creator_list = []
for relation in paged_relations:
creator = relation.creator
2025-06-06 10:46:29 +08:00
private_pool = relation.private_pool # 获取关联的私有库
2025-05-23 12:11:03 +08:00
# 格式化电商等级
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,
2025-06-06 10:46:29 +08:00
"pool_id": private_pool.id, # 添加池ID
"pool_name": private_pool.name, # 添加池名称
2025-05-23 12:11:03 +08:00
"name": creator.name,
2025-06-06 11:43:33 +08:00
"avatar": creator.get_avatar_url(),
2025-05-23 12:11:03 +08:00
"category": creator.category,
"e_commerce_level": e_commerce_level_formatted,
"exposure_level": creator.exposure_level,
2025-05-27 11:10:52 +08:00
"platform": creator.profile, # 添加平台信息
2025-05-23 12:11:03 +08:00
"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,
2025-05-23 19:08:40 +08:00
"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 # 状态说明
2025-05-23 12:11:03 +08:00
}
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})
2025-05-23 19:08:40 +08:00
@api_view(['POST'])
@authentication_classes([CustomTokenAuthentication])
@csrf_exempt
@require_http_methods(["POST"])
def remove_from_public_pool(request):
2025-06-03 15:34:59 +08:00
"""从公有达人库中移除达人,同时删除所有用户私有库中的相关记录(仅管理员可操作)"""
2025-05-23 19:08:40 +08:00
try:
from .models import PublicCreatorPool, PrivateCreatorRelation, CreatorProfile
import json
2025-06-03 15:34:59 +08:00
# 检查当前用户是否有管理员权限
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})
2025-05-23 19:08:40 +08:00
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,
2025-06-03 15:34:59 +08:00
added_from_public=True
2025-05-23 19:08:40 +08:00
)
2025-06-03 15:34:59 +08:00
# 记录受影响的用户数量
affected_users = private_relations.values('private_pool__user_id').distinct().count()
# 删除私有库中相关记录
deleted_private_count = 0
2025-05-23 19:08:40 +08:00
if private_relations.exists():
2025-06-03 15:34:59 +08:00
deleted_private_count = private_relations.count()
private_relations.delete()
2025-05-23 19:08:40 +08:00
# 删除公有库记录
public_creator.delete()
return JsonResponse({
'code': 200,
'message': '成功从公有库移除达人',
'data': {
'creator': {
'id': creator.id,
'name': creator.name
},
'removed_from_public': True,
2025-06-03 15:34:59 +08:00
'deleted_private_relations': deleted_private_count,
'affected_users': affected_users,
'note': f'已删除 {deleted_private_count} 个私有库中的相关记录,影响了 {affected_users} 个用户'
2025-05-23 19:08:40 +08:00
}
}, 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})
2025-05-28 11:37:13 +08:00
@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
2025-05-29 11:07:32 +08:00
# 简化排序逻辑避免使用tiktok_link字段
query = query.order_by('creator__name')
2025-05-28 11:37:13 +08:00
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
2025-05-29 11:07:32 +08:00
# 使用与filter_public_creators相同的格式
2025-05-28 11:37:13 +08:00
formatted_creator = {
2025-05-29 11:07:32 +08:00
"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
2025-05-28 11:37:13 +08:00
}
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,
2025-05-29 11:07:32 +08:00
"has_prev": page > 1
2025-05-28 11:37:13 +08:00
}
2025-05-29 11:07:32 +08:00
# 构造搜索信息(可以作为额外信息保留)
2025-05-28 11:37:13 +08:00
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': '搜索成功',
2025-05-29 11:07:32 +08:00
'data': creator_list,
'pagination': pagination,
'search_info': search_info
2025-05-28 11:37:13 +08:00
}, 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))
2025-06-06 15:15:28 +08:00
# 获取pool_id参数如果提供了则只搜索特定库
pool_id = request.GET.get('pool_id')
2025-05-28 11:37:13 +08:00
2025-06-06 10:46:29 +08:00
# 直接搜索用户所有私有库中的达人
query = PrivateCreatorRelation.objects.filter(
private_pool__user=user
).select_related('creator', 'private_pool')
2025-06-06 15:15:28 +08:00
# 如果提供了pool_id则只搜索特定私有库
if pool_id:
query = query.filter(private_pool_id=pool_id)
2025-05-28 11:37:13 +08:00
# 如果有搜索关键词,进行模糊搜索
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
2025-05-29 11:07:32 +08:00
# 简化排序逻辑避免使用tiktok_link字段
query = query.order_by('creator__name')
2025-05-28 11:37:13 +08:00
else:
# 如果没有搜索词,按名称排序
query = query.order_by('creator__name')
2025-06-06 15:15:28 +08:00
# 当不提供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]
2025-05-28 11:37:13 +08:00
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"
2025-05-29 11:07:32 +08:00
# 格式化价格区间
if creator.pricing is not None:
pricing_range = f"${creator.pricing}"
else:
pricing_range = None
2025-05-28 11:37:13 +08:00
2025-05-29 11:07:32 +08:00
# 格式化结果 - 使用与filter_private_pool_creators相同的格式
2025-05-28 11:37:13 +08:00
formatted_creator = {
2025-05-29 11:07:32 +08:00
"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',
2025-06-06 10:46:29 +08:00
"status_note": "该达人已从公有库中移除" if relation.status == 'public_removed' else None,
"pool_name": relation.private_pool.name # 添加所属达人库名称
2025-05-28 11:37:13 +08:00
}
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,
2025-05-29 11:07:32 +08:00
"has_prev": page > 1
2025-05-28 11:37:13 +08:00
}
# 构造搜索信息
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, # 标识支持单字符搜索
2025-06-06 15:15:28 +08:00
"search_scope": "private_only", # 标识只搜索私有达人库
"is_deduplicated": not pool_id # 标识是否进行了达人去重
2025-05-28 11:37:13 +08:00
}
return JsonResponse({
'code': 200,
'message': '搜索成功',
2025-05-29 11:07:32 +08:00
'data': creator_list,
'pagination': pagination,
2025-06-06 10:46:29 +08:00
'search_info': search_info
2025-05-28 11:37:13 +08:00
}, 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})
2025-05-19 18:23:59 +08:00
2025-06-05 15:43:42 +08:00
@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})
2025-05-19 18:23:59 +08:00