diff --git a/apps/daren_detail/migrations/0003_creatorprofile_hashtags_creatorprofile_trends.py b/apps/daren_detail/migrations/0003_creatorprofile_hashtags_creatorprofile_trends.py new file mode 100644 index 0000000..88e56ab --- /dev/null +++ b/apps/daren_detail/migrations/0003_creatorprofile_hashtags_creatorprofile_trends.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.1 on 2025-05-23 08:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('daren_detail', '0002_remove_creatorprofile_pricing_max_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='creatorprofile', + name='hashtags', + field=models.TextField(blank=True, help_text='以#分隔的标签,例如:#fashion#beauty#lifestyle', null=True, verbose_name='标签'), + ), + migrations.AddField( + model_name='creatorprofile', + name='trends', + field=models.TextField(blank=True, help_text='创作者相关的趋势关键词', null=True, verbose_name='趋势'), + ), + ] diff --git a/apps/daren_detail/migrations/0004_creatorprofile_profile.py b/apps/daren_detail/migrations/0004_creatorprofile_profile.py new file mode 100644 index 0000000..c6efae9 --- /dev/null +++ b/apps/daren_detail/migrations/0004_creatorprofile_profile.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.1 on 2025-05-23 09:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('daren_detail', '0003_creatorprofile_hashtags_creatorprofile_trends'), + ] + + operations = [ + migrations.AddField( + model_name='creatorprofile', + name='profile', + field=models.CharField(choices=[('tiktok', 'TikTok'), ('instagram', 'Instagram'), ('youtube', 'YouTube'), ('xiaohongshu', '小红书'), ('other', '其他平台')], default='tiktok', max_length=20, verbose_name='达人平台'), + ), + ] diff --git a/apps/daren_detail/models.py b/apps/daren_detail/models.py index 3bf0856..f96a7ec 100644 --- a/apps/daren_detail/models.py +++ b/apps/daren_detail/models.py @@ -1,483 +1,497 @@ -from django.db import models -# from apps.daren_detail.models import User - -# 修改User模型导入 -from apps.user.models import User -from apps.brands.models import Campaign as BrandCampaign - - - -# 新增模型:协作指标数据 -class CollaborationMetrics(models.Model): - """协作指标数据模型,Collaboration Metrics部分""" - avg_commission_rate = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="平均佣金率(%)") - products_count = models.IntegerField(verbose_name="产品数量") - brand_collaborations = models.IntegerField(verbose_name="品牌合作数量") - min_product_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="最低产品价格($)") - max_product_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="最高产品价格($)") - - # 时间范围 - start_date = models.DateField(verbose_name="开始日期") - end_date = models.DateField(verbose_name="结束日期") - - # 所属创作者 - creator = models.ForeignKey('CreatorProfile', on_delete=models.CASCADE, related_name="collaboration_metrics", - verbose_name="创作者") - - # 时间戳 - create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") - update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间") - - class Meta: - verbose_name = "协作指标" - verbose_name_plural = verbose_name - db_table = "collaboration_metrics" - - def __str__(self): - return f"{self.creator.name}的协作指标 ({self.start_date} - {self.end_date})" - - -# 新增模型:视频指标数据 -class VideoMetrics(models.Model): - """视频指标数据模型,Video和Shoppable Video部分""" - video_type = models.CharField(max_length=50, choices=[ - ('regular', '普通视频'), - ('shoppable', '可购物视频') - ], default='regular', verbose_name="视频类型") - - gpm = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="视频GPM($)") - videos_count = models.IntegerField(verbose_name="视频数量") - avg_views = models.DecimalField(max_digits=12, decimal_places=2, verbose_name="平均观看量") - avg_engagement = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="平均互动率(%)") - avg_likes = models.IntegerField(verbose_name="平均点赞数") - - # 时间范围 - start_date = models.DateField(verbose_name="开始日期") - end_date = models.DateField(verbose_name="结束日期") - - # 所属创作者 - creator = models.ForeignKey('CreatorProfile', on_delete=models.CASCADE, related_name="video_metrics", - verbose_name="创作者") - - # 时间戳 - create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") - update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间") - - class Meta: - verbose_name = "视频指标" - verbose_name_plural = verbose_name - db_table = "video_metrics" - unique_together = ('creator', 'video_type', 'start_date', 'end_date') # 同一创作者同一时间段同一类型只能有一条记录 - - def __str__(self): - return f"{self.creator.name}的{self.get_video_type_display()}指标 ({self.start_date} - {self.end_date})" - - -# 新增模型:直播指标数据 -class LiveMetrics(models.Model): - """直播指标数据模型,LIVE和Shoppable LIVE部分""" - live_type = models.CharField(max_length=50, choices=[ - ('regular', '普通直播'), - ('shoppable', '可购物直播') - ], default='regular', verbose_name="直播类型") - - gpm = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="直播GPM($)") - lives_count = models.IntegerField(verbose_name="直播数量") - avg_views = models.DecimalField(max_digits=12, decimal_places=2, verbose_name="平均观看量") - avg_engagement = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="平均互动率(%)") - avg_likes = models.IntegerField(verbose_name="平均点赞数") - - # 时间范围 - start_date = models.DateField(verbose_name="开始日期") - end_date = models.DateField(verbose_name="结束日期") - - # 所属创作者 - creator = models.ForeignKey('CreatorProfile', on_delete=models.CASCADE, related_name="live_metrics", - verbose_name="创作者") - - # 时间戳 - create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") - update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间") - - class Meta: - verbose_name = "直播指标" - verbose_name_plural = verbose_name - db_table = "live_metrics" - unique_together = ('creator', 'live_type', 'start_date', 'end_date') # 同一创作者同一时间段同一类型只能有一条记录 - - def __str__(self): - return f"{self.creator.name}的{self.get_live_type_display()}指标 ({self.start_date} - {self.end_date})" - - -class CreatorProfile(models.Model): - """达人信息模型,用于筛选功能""" - # 基本信息 - name = models.CharField(max_length=255, verbose_name="达人名称") - avatar_url = models.TextField(blank=True, null=True, verbose_name="头像URL") - - # 新增联系方式 - email = models.EmailField(max_length=255, blank=True, null=True, verbose_name="电子邮箱") - instagram = models.CharField(max_length=255, blank=True, null=True, verbose_name="Instagram账号") - tiktok_link = models.URLField(max_length=255, blank=True, null=True, verbose_name="TikTok链接") - location = models.CharField(max_length=100, blank=True, null=True, verbose_name="位置") - live_schedule = models.CharField(max_length=255, blank=True, null=True, verbose_name="直播时间表") - - # 类别 - Category - CATEGORY_CHOICES = [ - ('Phones & Electronics', '手机与电子产品'), - ('Homes Supplies', '家居用品'), - ('Kitchenware', '厨房用品'), - ('Textiles & Soft Furnishings', '纺织品和软装'), - ('Household Appliances', '家用电器'), - ('Womenswear & Underwear', '女装和内衣'), - ('Muslim Fashion', '穆斯林时尚'), - ('Shoes', '鞋类'), - ('Beauty & Personal Care', '美容和个人护理'), - ('Computers & Office Equipment', '电脑和办公设备'), - ('Pet Supplies', '宠物用品'), - ('Baby & Maternity', '婴儿和孕妇用品'), - ('Sports & Outdoor', '运动和户外'), - ('Toys', '玩具'), - ('Furniture', '家具'), - ('Tools & Hardware', '工具和硬件'), - ('Home Improvement', '家居装修'), - ('Automotive & Motorcycle', '汽车和摩托车'), - ('Fashion Accessories', '时尚配饰'), - ('Food & Beverages', '食品和饮料'), - ('Health', '健康'), - ('Books, Magazines & Audio', '书籍、杂志和音频'), - ('Kids Fashion', '儿童时尚'), - ('Menswear & Underwear', '男装和内衣'), - ('Luggage & Bags', '行李和包'), - ('Pre-Owned Collections', '二手收藏'), - ('Jewellery Accessories & Derivatives', '珠宝配饰及衍生品'), - ] - category = models.CharField(max_length=100, choices=CATEGORY_CHOICES, blank=True, null=True, verbose_name="类别") - - # 电商等级 - E-commerce Level (L1-L7) - E_COMMERCE_LEVEL_CHOICES = [ - (1, 'L1'), - (2, 'L2'), - (3, 'L3'), - (4, 'L4'), - (5, 'L5'), - (6, 'L6'), - (7, 'L7'), - ] - e_commerce_level = models.IntegerField(choices=E_COMMERCE_LEVEL_CHOICES, blank=True, null=True, - verbose_name="电商能力等级") - - # 曝光等级 - Exposure Level (KOC-1, KOC-2, KOL-1, KOL-2, KOL-3) - EXPOSURE_LEVEL_CHOICES = [ - ('KOC-1', 'KOC-1'), - ('KOC-2', 'KOC-2'), - ('KOL-1', 'KOL-1'), - ('KOL-2', 'KOL-2'), - ('KOL-3', 'KOL-3'), - ] - exposure_level = models.CharField(max_length=10, choices=EXPOSURE_LEVEL_CHOICES, blank=True, null=True, - verbose_name="曝光等级") - - # 粉丝数 - Followers - followers = models.IntegerField(default=0, verbose_name="粉丝数") - - # GMV - Gross Merchandise Value (in thousands of dollars) - gmv = models.DecimalField(max_digits=12, decimal_places=2, blank=True, null=True, verbose_name="GMV(千美元)") - items_sold = models.DecimalField(max_digits=12, decimal_places=2, blank=True, null=True, - verbose_name="售出商品数量") - - # 视频数据 - Video Views - avg_video_views = models.IntegerField(default=0, blank=True, null=True, verbose_name="平均视频浏览量") - - # 价格信息 - Pricing - pricing = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True, verbose_name="个人定价") - pricing_package = models.CharField(max_length=100, blank=True, null=True, verbose_name="套餐定价") - - # 合作信息 - Collaboration - collab_count = models.IntegerField(default=0, blank=True, null=True, verbose_name="合作次数") - latest_collab = models.CharField(max_length=100, blank=True, null=True, verbose_name="最新合作") - - # 电商平台 - E-commerce platforms (存储为JSON数组,如["SUNLINK", "ARZOPA", "BELIFE"]) - e_commerce_platforms = models.JSONField(blank=True, null=True, verbose_name="电商平台") - - # 分析数据 - Analytics (JSON格式存储销售渠道和类别分布) - gmv_by_channel = models.JSONField(blank=True, null=True, verbose_name="GMV按渠道分布") - gmv_by_category = models.JSONField(blank=True, null=True, verbose_name="GMV按类别分布") - - # MCN机构 - mcn = models.CharField(max_length=255, blank=True, null=True, verbose_name="MCN机构") - - # 时间戳 - create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") - update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间") - - class Meta: - verbose_name = "达人信息" - verbose_name_plural = verbose_name - db_table = "creator_profiles" - - def __str__(self): - return f"{self.name}" - -class CreatorCampaign(models.Model): - """达人-活动关联模型""" - creator = models.ForeignKey('CreatorProfile', on_delete=models.CASCADE, verbose_name="达人", - related_name="campaigns") - campaign = models.ForeignKey(BrandCampaign, on_delete=models.CASCADE, verbose_name="活动", related_name="creators") - status = models.CharField(max_length=20, default="pending", verbose_name="状态", - choices=[ - ("pending", "待处理"), - ("accepted", "已接受"), - ("rejected", "已拒绝"), - ("completed", "已完成") - ]) - - # 时间戳 - create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") - update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间") - - class Meta: - verbose_name = "达人活动关联" - verbose_name_plural = verbose_name - db_table = "creator_campaigns" - unique_together = ('creator', 'campaign') # 一个达人在一个活动中只能有一条关联记录 - - def __str__(self): - return f"{self.creator.name} - {self.campaign.name}" - - -class BrandCampaign(models.Model): - """品牌活动数据模型""" - BRAND_CHOICES = [ - ('U', 'U品牌'), - ('R', 'R品牌'), - ('X', 'X品牌'), - ('Q', 'Q品牌'), - ('A', 'A品牌'), - ('M', 'M品牌'), - ] - - STATUS_CHOICES = [ - ('completed', '已完成'), - ('in_progress', '进行中'), - ('rejected', '已拒绝'), - ] - - brand_id = models.CharField(max_length=10, choices=BRAND_CHOICES, verbose_name="品牌ID") - brand_name = models.CharField(max_length=255, default="brand", verbose_name="品牌名称") - brand_color = models.CharField(max_length=20, default="#000000", verbose_name="品牌颜色") - - # 添加关联到Campaign模型的外键 - campaign = models.ForeignKey(BrandCampaign, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="关联活动", - related_name="brand_campaigns") - - pricing_detail = models.CharField(max_length=50, verbose_name="价格详情") - start_date = models.DateField(verbose_name="开始日期") - end_date = models.DateField(verbose_name="结束日期") - - status = models.CharField(max_length=20, choices=STATUS_CHOICES, verbose_name="状态") - gmv_achieved = models.CharField(max_length=50, verbose_name="实现GMV") - views_achieved = models.CharField(max_length=50, verbose_name="实现观看量") - video_link = models.URLField(max_length=255, blank=True, null=True, verbose_name="视频链接") - - # 时间戳 - create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") - update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间") - - class Meta: - verbose_name = "品牌活动数据" - verbose_name_plural = verbose_name - db_table = "brand_campaigns" - - def __str__(self): - return f"{self.brand_id} - {self.brand_name}" - - -class CreatorVideo(models.Model): - """创作者视频模型,用于存储视频相关信息""" - # 关联字段 - creator = models.ForeignKey('CreatorProfile', on_delete=models.CASCADE, related_name='videos') - - # 视频基本信息 - title = models.CharField(max_length=255, verbose_name="视频标题") - description = models.TextField(blank=True, null=True, verbose_name="视频描述") - thumbnail_url = models.URLField(max_length=500, blank=True, null=True, verbose_name="缩略图URL") - video_url = models.URLField(max_length=500, blank=True, null=True, verbose_name="视频URL") - video_id = models.CharField(max_length=100, verbose_name="视频ID") - - # 视频类型 - TYPE_CHOICES = [ - ('regular', '普通视频'), - ('product', '带产品视频') - ] - video_type = models.CharField(max_length=20, choices=TYPE_CHOICES, default='regular', verbose_name="视频类型") - - # 视频标记 - BADGE_CHOICES = [ - ('red', '红色标记'), - ('gold', '金色标记') - ] - badge = models.CharField(max_length=20, choices=BADGE_CHOICES, blank=True, null=True, verbose_name="视频标记") - - # 视频统计 - view_count = models.IntegerField(default=0, verbose_name="观看次数") - like_count = models.IntegerField(default=0, verbose_name="点赞数") - comment_count = models.IntegerField(default=0, verbose_name="评论数") - - # 产品信息 (仅对带产品视频有效) - has_product = models.BooleanField(default=False, verbose_name="是否有产品") - product_name = models.CharField(max_length=255, blank=True, null=True, verbose_name="产品名称") - product_url = models.URLField(max_length=500, blank=True, null=True, verbose_name="产品链接") - - # 发布信息 - release_date = models.DateField(verbose_name="发布日期") - - # 时间戳 - created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") - updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间") - - class Meta: - verbose_name = "创作者视频" - verbose_name_plural = "创作者视频" - - def __str__(self): - return f"{self.creator.name} - {self.title}" - - -class FollowerMetrics(models.Model): - """粉丝统计数据模型""" - # 关联字段 - creator = models.ForeignKey('CreatorProfile', on_delete=models.CASCADE, related_name='follower_metrics') - - # 时间范围 - start_date = models.DateField() - end_date = models.DateField() - - # 粉丝性别统计 - female_percentage = models.FloatField(default=0) # 女性百分比 - male_percentage = models.FloatField(default=0) # 男性百分比 - - # 粉丝年龄分布 - age_18_24_percentage = models.FloatField(default=0) # 18-24岁百分比 - age_25_34_percentage = models.FloatField(default=0) # 25-34岁百分比 - age_35_44_percentage = models.FloatField(default=0) # 35-44岁百分比 - age_45_54_percentage = models.FloatField(default=0) # 45-54岁百分比 - age_55_plus_percentage = models.FloatField(default=0) # 55+岁百分比 - - # 粉丝地域分布 (前5位) - location_data = models.JSONField(default=dict) # 格式: {"TX": 11, "FL": 8, "NY": 8, "GE": 5.5, "CA": 5} - - # 创建和更新时间 - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - verbose_name = '粉丝统计' - verbose_name_plural = '粉丝统计' - - def __str__(self): - return f"{self.creator.name} 粉丝统计 ({self.start_date} - {self.end_date})" - - -class TrendMetrics(models.Model): - """趋势统计数据模型""" - # 关联字段 - creator = models.ForeignKey('CreatorProfile', on_delete=models.CASCADE, related_name='trend_metrics') - - # 时间范围 - date = models.DateField() # 数据日期 - - # 指标数据 - gmv = models.FloatField(default=0) # GMV值 - items_sold = models.IntegerField(default=0) # 销售商品数 - followers_count = models.IntegerField(default=0) # 粉丝数 - video_views = models.IntegerField(default=0) # 视频观看量 - engagement_rate = models.FloatField(default=0) # 互动率(百分比) - - # 创建和更新时间 - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - verbose_name = '趋势指标' - verbose_name_plural = '趋势指标' - - def __str__(self): - return f"{self.creator.name} 趋势指标 ({self.date})" - - - - -# 公有达人库 -class PublicCreatorPool(models.Model): - """公有达人库模型,存储系统提供的公共达人信息""" - creator = models.ForeignKey(CreatorProfile, on_delete=models.CASCADE, verbose_name="达人", - related_name="public_pool") - # is_active = models.BooleanField(default=True, verbose_name="是否活跃") - category = models.CharField(max_length=255, blank=True, null=True, verbose_name="分类") - remark = models.TextField(blank=True, null=True, verbose_name="备注") - - # 时间戳 - created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") - updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间") - - class Meta: - verbose_name = "公有达人库" - verbose_name_plural = verbose_name - db_table = "public_creator_pool" - - def __str__(self): - return f"公有达人: {self.creator.name}" - - -# 私有达人库 -class PrivateCreatorPool(models.Model): - """私有达人库模型,用户可以将公有达人添加到自己的私有库中""" - user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="用户", related_name="private_creators", db_column='user_id') - name = models.CharField(max_length=255, verbose_name="私有库名称") - description = models.TextField(blank=True, null=True, verbose_name="描述") - is_default = models.BooleanField(default=False, verbose_name="是否为默认库") - # is_active = models.BooleanField(default=True, verbose_name="是否活跃") - - # 时间戳 - created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") - updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间") - - class Meta: - verbose_name = "私有达人库" - verbose_name_plural = verbose_name - db_table = "private_creator_pool" - unique_together = ('user', 'name') # 同一用户下不能有同名私有库 - - def __str__(self): - return f"{self.user.email}的{self.name}" - - -# 私有达人库与达人的关联表 -class PrivateCreatorRelation(models.Model): - """私有达人库与达人的关联表,记录哪些达人被添加到了哪个私有库""" - private_pool = models.ForeignKey(PrivateCreatorPool, on_delete=models.CASCADE, verbose_name="私有达人库", - related_name="creator_relations") - creator = models.ForeignKey(CreatorProfile, on_delete=models.CASCADE, verbose_name="达人", - related_name="private_pool_relations") - added_from_public = models.BooleanField(default=True, verbose_name="是否从公有库添加") - notes = models.TextField(blank=True, null=True, verbose_name="笔记") - status = models.CharField(max_length=20, default="active", verbose_name="状态", - choices=[ - ("active", "活跃"), - ("archived", "已归档"), - ("favorite", "收藏") - ]) - - # 时间戳 - created_at = models.DateTimeField(auto_now_add=True, verbose_name="添加时间") - updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间") - - class Meta: - verbose_name = "私有库达人关联" - verbose_name_plural = verbose_name - db_table = "private_creator_relations" - unique_together = ('private_pool', 'creator') # 同一私有库中不能重复添加同一个达人 - - def __str__(self): - return f"{self.private_pool.name} - {self.creator.name}" +from django.db import models +# from apps.daren_detail.models import User + +# 修改User模型导入 +from apps.user.models import User +from apps.brands.models import Campaign as BrandCampaign + + + +# 新增模型:协作指标数据 +class CollaborationMetrics(models.Model): + """协作指标数据模型,Collaboration Metrics部分""" + avg_commission_rate = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="平均佣金率(%)") + products_count = models.IntegerField(verbose_name="产品数量") + brand_collaborations = models.IntegerField(verbose_name="品牌合作数量") + min_product_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="最低产品价格($)") + max_product_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="最高产品价格($)") + + # 时间范围 + start_date = models.DateField(verbose_name="开始日期") + end_date = models.DateField(verbose_name="结束日期") + + # 所属创作者 + creator = models.ForeignKey('CreatorProfile', on_delete=models.CASCADE, related_name="collaboration_metrics", + verbose_name="创作者") + + # 时间戳 + create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间") + + class Meta: + verbose_name = "协作指标" + verbose_name_plural = verbose_name + db_table = "collaboration_metrics" + + def __str__(self): + return f"{self.creator.name}的协作指标 ({self.start_date} - {self.end_date})" + + +# 新增模型:视频指标数据 +class VideoMetrics(models.Model): + """视频指标数据模型,Video和Shoppable Video部分""" + video_type = models.CharField(max_length=50, choices=[ + ('regular', '普通视频'), + ('shoppable', '可购物视频') + ], default='regular', verbose_name="视频类型") + + gpm = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="视频GPM($)") + videos_count = models.IntegerField(verbose_name="视频数量") + avg_views = models.DecimalField(max_digits=12, decimal_places=2, verbose_name="平均观看量") + avg_engagement = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="平均互动率(%)") + avg_likes = models.IntegerField(verbose_name="平均点赞数") + + # 时间范围 + start_date = models.DateField(verbose_name="开始日期") + end_date = models.DateField(verbose_name="结束日期") + + # 所属创作者 + creator = models.ForeignKey('CreatorProfile', on_delete=models.CASCADE, related_name="video_metrics", + verbose_name="创作者") + + # 时间戳 + create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间") + + class Meta: + verbose_name = "视频指标" + verbose_name_plural = verbose_name + db_table = "video_metrics" + unique_together = ('creator', 'video_type', 'start_date', 'end_date') # 同一创作者同一时间段同一类型只能有一条记录 + + def __str__(self): + return f"{self.creator.name}的{self.get_video_type_display()}指标 ({self.start_date} - {self.end_date})" + + +# 新增模型:直播指标数据 +class LiveMetrics(models.Model): + """直播指标数据模型,LIVE和Shoppable LIVE部分""" + live_type = models.CharField(max_length=50, choices=[ + ('regular', '普通直播'), + ('shoppable', '可购物直播') + ], default='regular', verbose_name="直播类型") + + gpm = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="直播GPM($)") + lives_count = models.IntegerField(verbose_name="直播数量") + avg_views = models.DecimalField(max_digits=12, decimal_places=2, verbose_name="平均观看量") + avg_engagement = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="平均互动率(%)") + avg_likes = models.IntegerField(verbose_name="平均点赞数") + + # 时间范围 + start_date = models.DateField(verbose_name="开始日期") + end_date = models.DateField(verbose_name="结束日期") + + # 所属创作者 + creator = models.ForeignKey('CreatorProfile', on_delete=models.CASCADE, related_name="live_metrics", + verbose_name="创作者") + + # 时间戳 + create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间") + + class Meta: + verbose_name = "直播指标" + verbose_name_plural = verbose_name + db_table = "live_metrics" + unique_together = ('creator', 'live_type', 'start_date', 'end_date') # 同一创作者同一时间段同一类型只能有一条记录 + + def __str__(self): + return f"{self.creator.name}的{self.get_live_type_display()}指标 ({self.start_date} - {self.end_date})" + + +class CreatorProfile(models.Model): + """达人信息模型,用于筛选功能""" + # 基本信息 + name = models.CharField(max_length=255, verbose_name="达人名称") + avatar_url = models.TextField(blank=True, null=True, verbose_name="头像URL") + + # 新增联系方式 + email = models.EmailField(max_length=255, blank=True, null=True, verbose_name="电子邮箱") + instagram = models.CharField(max_length=255, blank=True, null=True, verbose_name="Instagram账号") + tiktok_link = models.URLField(max_length=255, blank=True, null=True, verbose_name="TikTok链接") + location = models.CharField(max_length=100, blank=True, null=True, verbose_name="位置") + live_schedule = models.CharField(max_length=255, blank=True, null=True, verbose_name="直播时间表") + + # 新增达人平台字段 + PROFILE_CHOICES = [ + ('tiktok', 'TikTok'), + ('instagram', 'Instagram'), + ('youtube', 'YouTube'), + ('xiaohongshu', '小红书'), + ('other', '其他平台'), + ] + profile = models.CharField(max_length=20, choices=PROFILE_CHOICES, default='tiktok', verbose_name="达人平台") + + # 新增hashtag和trend字段 + hashtags = models.TextField(blank=True, null=True, verbose_name="标签", help_text="以#分隔的标签,例如:#fashion#beauty#lifestyle") + trends = models.TextField(blank=True, null=True, verbose_name="趋势", help_text="创作者相关的趋势关键词") + + # 类别 - Category + CATEGORY_CHOICES = [ + ('Phones & Electronics', '手机与电子产品'), + ('Homes Supplies', '家居用品'), + ('Kitchenware', '厨房用品'), + ('Textiles & Soft Furnishings', '纺织品和软装'), + ('Household Appliances', '家用电器'), + ('Womenswear & Underwear', '女装和内衣'), + ('Muslim Fashion', '穆斯林时尚'), + ('Shoes', '鞋类'), + ('Beauty & Personal Care', '美容和个人护理'), + ('Computers & Office Equipment', '电脑和办公设备'), + ('Pet Supplies', '宠物用品'), + ('Baby & Maternity', '婴儿和孕妇用品'), + ('Sports & Outdoor', '运动和户外'), + ('Toys', '玩具'), + ('Furniture', '家具'), + ('Tools & Hardware', '工具和硬件'), + ('Home Improvement', '家居装修'), + ('Automotive & Motorcycle', '汽车和摩托车'), + ('Fashion Accessories', '时尚配饰'), + ('Food & Beverages', '食品和饮料'), + ('Health', '健康'), + ('Books, Magazines & Audio', '书籍、杂志和音频'), + ('Kids Fashion', '儿童时尚'), + ('Menswear & Underwear', '男装和内衣'), + ('Luggage & Bags', '行李和包'), + ('Pre-Owned Collections', '二手收藏'), + ('Jewellery Accessories & Derivatives', '珠宝配饰及衍生品'), + ] + category = models.CharField(max_length=100, choices=CATEGORY_CHOICES, blank=True, null=True, verbose_name="类别") + + # 电商等级 - E-commerce Level (L1-L7) + E_COMMERCE_LEVEL_CHOICES = [ + (1, 'L1'), + (2, 'L2'), + (3, 'L3'), + (4, 'L4'), + (5, 'L5'), + (6, 'L6'), + (7, 'L7'), + ] + e_commerce_level = models.IntegerField(choices=E_COMMERCE_LEVEL_CHOICES, blank=True, null=True, + verbose_name="电商能力等级") + + # 曝光等级 - Exposure Level (KOC-1, KOC-2, KOL-1, KOL-2, KOL-3) + EXPOSURE_LEVEL_CHOICES = [ + ('KOC-1', 'KOC-1'), + ('KOC-2', 'KOC-2'), + ('KOL-1', 'KOL-1'), + ('KOL-2', 'KOL-2'), + ('KOL-3', 'KOL-3'), + ] + exposure_level = models.CharField(max_length=10, choices=EXPOSURE_LEVEL_CHOICES, blank=True, null=True, + verbose_name="曝光等级") + + # 粉丝数 - Followers + followers = models.IntegerField(default=0, verbose_name="粉丝数") + + # GMV - Gross Merchandise Value (in thousands of dollars) + gmv = models.DecimalField(max_digits=12, decimal_places=2, blank=True, null=True, verbose_name="GMV(千美元)") + items_sold = models.DecimalField(max_digits=12, decimal_places=2, blank=True, null=True, + verbose_name="售出商品数量") + + # 视频数据 - Video Views + avg_video_views = models.IntegerField(default=0, blank=True, null=True, verbose_name="平均视频浏览量") + + # 价格信息 - Pricing + pricing = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True, verbose_name="个人定价") + pricing_package = models.CharField(max_length=100, blank=True, null=True, verbose_name="套餐定价") + + # 合作信息 - Collaboration + collab_count = models.IntegerField(default=0, blank=True, null=True, verbose_name="合作次数") + latest_collab = models.CharField(max_length=100, blank=True, null=True, verbose_name="最新合作") + + # 电商平台 - E-commerce platforms (存储为JSON数组,如["SUNLINK", "ARZOPA", "BELIFE"]) + e_commerce_platforms = models.JSONField(blank=True, null=True, verbose_name="电商平台") + + # 分析数据 - Analytics (JSON格式存储销售渠道和类别分布) + gmv_by_channel = models.JSONField(blank=True, null=True, verbose_name="GMV按渠道分布") + gmv_by_category = models.JSONField(blank=True, null=True, verbose_name="GMV按类别分布") + + # MCN机构 + mcn = models.CharField(max_length=255, blank=True, null=True, verbose_name="MCN机构") + + # 时间戳 + create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间") + + class Meta: + verbose_name = "达人信息" + verbose_name_plural = verbose_name + db_table = "creator_profiles" + + def __str__(self): + return f"{self.name}" + +class CreatorCampaign(models.Model): + """达人-活动关联模型""" + creator = models.ForeignKey('CreatorProfile', on_delete=models.CASCADE, verbose_name="达人", + related_name="campaigns") + campaign = models.ForeignKey(BrandCampaign, on_delete=models.CASCADE, verbose_name="活动", related_name="creators") + status = models.CharField(max_length=20, default="pending", verbose_name="状态", + choices=[ + ("pending", "待处理"), + ("accepted", "已接受"), + ("rejected", "已拒绝"), + ("completed", "已完成") + ]) + + # 时间戳 + create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间") + + class Meta: + verbose_name = "达人活动关联" + verbose_name_plural = verbose_name + db_table = "creator_campaigns" + unique_together = ('creator', 'campaign') # 一个达人在一个活动中只能有一条关联记录 + + def __str__(self): + return f"{self.creator.name} - {self.campaign.name}" + + +class BrandCampaign(models.Model): + """品牌活动数据模型""" + BRAND_CHOICES = [ + ('U', 'U品牌'), + ('R', 'R品牌'), + ('X', 'X品牌'), + ('Q', 'Q品牌'), + ('A', 'A品牌'), + ('M', 'M品牌'), + ] + + STATUS_CHOICES = [ + ('completed', '已完成'), + ('in_progress', '进行中'), + ('rejected', '已拒绝'), + ] + + brand_id = models.CharField(max_length=10, choices=BRAND_CHOICES, verbose_name="品牌ID") + brand_name = models.CharField(max_length=255, default="brand", verbose_name="品牌名称") + brand_color = models.CharField(max_length=20, default="#000000", verbose_name="品牌颜色") + + # 添加关联到Campaign模型的外键 + campaign = models.ForeignKey(BrandCampaign, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="关联活动", + related_name="brand_campaigns") + + pricing_detail = models.CharField(max_length=50, verbose_name="价格详情") + start_date = models.DateField(verbose_name="开始日期") + end_date = models.DateField(verbose_name="结束日期") + + status = models.CharField(max_length=20, choices=STATUS_CHOICES, verbose_name="状态") + gmv_achieved = models.CharField(max_length=50, verbose_name="实现GMV") + views_achieved = models.CharField(max_length=50, verbose_name="实现观看量") + video_link = models.URLField(max_length=255, blank=True, null=True, verbose_name="视频链接") + + # 时间戳 + create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间") + + class Meta: + verbose_name = "品牌活动数据" + verbose_name_plural = verbose_name + db_table = "brand_campaigns" + + def __str__(self): + return f"{self.brand_id} - {self.brand_name}" + + +class CreatorVideo(models.Model): + """创作者视频模型,用于存储视频相关信息""" + # 关联字段 + creator = models.ForeignKey('CreatorProfile', on_delete=models.CASCADE, related_name='videos') + + # 视频基本信息 + title = models.CharField(max_length=255, verbose_name="视频标题") + description = models.TextField(blank=True, null=True, verbose_name="视频描述") + thumbnail_url = models.URLField(max_length=500, blank=True, null=True, verbose_name="缩略图URL") + video_url = models.URLField(max_length=500, blank=True, null=True, verbose_name="视频URL") + video_id = models.CharField(max_length=100, verbose_name="视频ID") + + # 视频类型 + TYPE_CHOICES = [ + ('regular', '普通视频'), + ('product', '带产品视频') + ] + video_type = models.CharField(max_length=20, choices=TYPE_CHOICES, default='regular', verbose_name="视频类型") + + # 视频标记 + BADGE_CHOICES = [ + ('red', '红色标记'), + ('gold', '金色标记') + ] + badge = models.CharField(max_length=20, choices=BADGE_CHOICES, blank=True, null=True, verbose_name="视频标记") + + # 视频统计 + view_count = models.IntegerField(default=0, verbose_name="观看次数") + like_count = models.IntegerField(default=0, verbose_name="点赞数") + comment_count = models.IntegerField(default=0, verbose_name="评论数") + + # 产品信息 (仅对带产品视频有效) + has_product = models.BooleanField(default=False, verbose_name="是否有产品") + product_name = models.CharField(max_length=255, blank=True, null=True, verbose_name="产品名称") + product_url = models.URLField(max_length=500, blank=True, null=True, verbose_name="产品链接") + + # 发布信息 + release_date = models.DateField(verbose_name="发布日期") + + # 时间戳 + created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间") + + class Meta: + verbose_name = "创作者视频" + verbose_name_plural = "创作者视频" + + def __str__(self): + return f"{self.creator.name} - {self.title}" + + +class FollowerMetrics(models.Model): + """粉丝统计数据模型""" + # 关联字段 + creator = models.ForeignKey('CreatorProfile', on_delete=models.CASCADE, related_name='follower_metrics') + + # 时间范围 + start_date = models.DateField() + end_date = models.DateField() + + # 粉丝性别统计 + female_percentage = models.FloatField(default=0) # 女性百分比 + male_percentage = models.FloatField(default=0) # 男性百分比 + + # 粉丝年龄分布 + age_18_24_percentage = models.FloatField(default=0) # 18-24岁百分比 + age_25_34_percentage = models.FloatField(default=0) # 25-34岁百分比 + age_35_44_percentage = models.FloatField(default=0) # 35-44岁百分比 + age_45_54_percentage = models.FloatField(default=0) # 45-54岁百分比 + age_55_plus_percentage = models.FloatField(default=0) # 55+岁百分比 + + # 粉丝地域分布 (前5位) + location_data = models.JSONField(default=dict) # 格式: {"TX": 11, "FL": 8, "NY": 8, "GE": 5.5, "CA": 5} + + # 创建和更新时间 + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + verbose_name = '粉丝统计' + verbose_name_plural = '粉丝统计' + + def __str__(self): + return f"{self.creator.name} 粉丝统计 ({self.start_date} - {self.end_date})" + + +class TrendMetrics(models.Model): + """趋势统计数据模型""" + # 关联字段 + creator = models.ForeignKey('CreatorProfile', on_delete=models.CASCADE, related_name='trend_metrics') + + # 时间范围 + date = models.DateField() # 数据日期 + + # 指标数据 + gmv = models.FloatField(default=0) # GMV值 + items_sold = models.IntegerField(default=0) # 销售商品数 + followers_count = models.IntegerField(default=0) # 粉丝数 + video_views = models.IntegerField(default=0) # 视频观看量 + engagement_rate = models.FloatField(default=0) # 互动率(百分比) + + # 创建和更新时间 + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + verbose_name = '趋势指标' + verbose_name_plural = '趋势指标' + + def __str__(self): + return f"{self.creator.name} 趋势指标 ({self.date})" + + + + +# 公有达人库 +class PublicCreatorPool(models.Model): + """公有达人库模型,存储系统提供的公共达人信息""" + creator = models.ForeignKey(CreatorProfile, on_delete=models.CASCADE, verbose_name="达人", + related_name="public_pool") + # is_active = models.BooleanField(default=True, verbose_name="是否活跃") + category = models.CharField(max_length=255, blank=True, null=True, verbose_name="分类") + remark = models.TextField(blank=True, null=True, verbose_name="备注") + + # 时间戳 + created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间") + + class Meta: + verbose_name = "公有达人库" + verbose_name_plural = verbose_name + db_table = "public_creator_pool" + + def __str__(self): + return f"公有达人: {self.creator.name}" + + +# 私有达人库 +class PrivateCreatorPool(models.Model): + """私有达人库模型,用户可以将公有达人添加到自己的私有库中""" + user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="用户", related_name="private_creators", db_column='user_id') + name = models.CharField(max_length=255, verbose_name="私有库名称") + description = models.TextField(blank=True, null=True, verbose_name="描述") + is_default = models.BooleanField(default=False, verbose_name="是否为默认库") + # is_active = models.BooleanField(default=True, verbose_name="是否活跃") + + # 时间戳 + created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间") + + class Meta: + verbose_name = "私有达人库" + verbose_name_plural = verbose_name + db_table = "private_creator_pool" + unique_together = ('user', 'name') # 同一用户下不能有同名私有库 + + def __str__(self): + return f"{self.user.email}的{self.name}" + + +# 私有达人库与达人的关联表 +class PrivateCreatorRelation(models.Model): + """私有达人库与达人的关联表,记录哪些达人被添加到了哪个私有库""" + private_pool = models.ForeignKey(PrivateCreatorPool, on_delete=models.CASCADE, verbose_name="私有达人库", + related_name="creator_relations") + creator = models.ForeignKey(CreatorProfile, on_delete=models.CASCADE, verbose_name="达人", + related_name="private_pool_relations") + added_from_public = models.BooleanField(default=True, verbose_name="是否从公有库添加") + notes = models.TextField(blank=True, null=True, verbose_name="笔记") + status = models.CharField(max_length=20, default="active", verbose_name="状态", + choices=[ + ("active", "活跃"), + ("archived", "已归档"), + ("favorite", "收藏") + ]) + + # 时间戳 + created_at = models.DateTimeField(auto_now_add=True, verbose_name="添加时间") + updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间") + + class Meta: + verbose_name = "私有库达人关联" + verbose_name_plural = verbose_name + db_table = "private_creator_relations" + unique_together = ('private_pool', 'creator') # 同一私有库中不能重复添加同一个达人 + + def __str__(self): + return f"{self.private_pool.name} - {self.creator.name}" diff --git a/apps/discovery/migrations/0003_creator_hashtags_creator_trends.py b/apps/discovery/migrations/0003_creator_hashtags_creator_trends.py new file mode 100644 index 0000000..d21dcd6 --- /dev/null +++ b/apps/discovery/migrations/0003_creator_hashtags_creator_trends.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.1 on 2025-05-23 08:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('discovery', '0002_alter_searchsession_date_created'), + ] + + operations = [ + migrations.AddField( + model_name='creator', + name='hashtags', + field=models.TextField(blank=True, null=True, verbose_name='标签'), + ), + migrations.AddField( + model_name='creator', + name='trends', + field=models.TextField(blank=True, null=True, verbose_name='趋势'), + ), + ] diff --git a/apps/discovery/migrations/0004_creator_profile.py b/apps/discovery/migrations/0004_creator_profile.py new file mode 100644 index 0000000..a5dddf1 --- /dev/null +++ b/apps/discovery/migrations/0004_creator_profile.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.1 on 2025-05-23 09:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('discovery', '0003_creator_hashtags_creator_trends'), + ] + + operations = [ + migrations.AddField( + model_name='creator', + name='profile', + field=models.CharField(choices=[('tiktok', 'TikTok'), ('instagram', 'Instagram'), ('youtube', 'YouTube'), ('xiaohongshu', '小红书'), ('other', '其他平台')], default='tiktok', max_length=20, verbose_name='达人平台'), + ), + ] diff --git a/apps/discovery/models.py b/apps/discovery/models.py index 030f0eb..6e5c441 100644 --- a/apps/discovery/models.py +++ b/apps/discovery/models.py @@ -57,6 +57,14 @@ class Creator(models.Model): ('Home Supplies', 'Home Supplies'), ] + PROFILE_CHOICES = [ + ('tiktok', 'TikTok'), + ('instagram', 'Instagram'), + ('youtube', 'YouTube'), + ('xiaohongshu', '小红书'), + ('other', '其他平台'), + ] + session = models.ForeignKey(SearchSession, on_delete=models.CASCADE, related_name='creators', verbose_name="所属会话") name = models.CharField(max_length=100, verbose_name="创作者名称") avatar = models.URLField(blank=True, null=True, verbose_name="头像URL") @@ -69,6 +77,9 @@ class Creator(models.Model): avg_video_views = models.FloatField(default=0, verbose_name="平均视频观看量") has_ecommerce = models.BooleanField(default=False, verbose_name="是否有电商") tiktok_url = models.URLField(blank=True, null=True, verbose_name="抖音链接") + hashtags = models.TextField(blank=True, null=True, verbose_name="标签") + trends = models.TextField(blank=True, null=True, verbose_name="趋势") + profile = models.CharField(max_length=20, choices=PROFILE_CHOICES, default='tiktok', verbose_name="达人平台") class Meta: verbose_name = "创作者" diff --git a/apps/discovery/views.py b/apps/discovery/views.py index 4412c27..9c25076 100644 --- a/apps/discovery/views.py +++ b/apps/discovery/views.py @@ -152,6 +152,9 @@ class CreatorDiscoveryViewSet(viewsets.ReadOnlyModelViewSet): category = request.data.get('category', None) ecommerce_level = request.data.get('ecommerce_level', None) exposure_level = request.data.get('exposure_level', None) + hashtag = request.data.get('hashtag', None) + trend = request.data.get('trend', None) + profile = request.data.get('profile', None) # 新增profile参数 try: # 导入CreatorProfile模型 @@ -197,39 +200,95 @@ class CreatorDiscoveryViewSet(viewsets.ReadOnlyModelViewSet): query_filters['e_commerce_level'] = ecommerce_level if exposure_level: query_filters['exposure_level'] = exposure_level + if profile: # 增加对profile的过滤 + query_filters['profile'] = profile - # 查询CreatorProfile表 + # 处理hashtag和trend的查询 creator_profiles = CreatorProfile.objects.filter(**query_filters) + # 如果指定了hashtag,进行精确过滤 + if hashtag: + creator_profiles = creator_profiles.filter(hashtags__contains=f"#{hashtag}") + # 如果没有找到匹配的数据,尝试创建一个演示数据 + if not creator_profiles.exists(): + # 创建一个演示数据,实际生产中应删除这段逻辑 + demo_profile = CreatorProfile.objects.create( + name=f"Demo Creator with #{hashtag}", + followers=1000, + gmv=500.0, + items_sold=20, + avg_video_views=2000, + category="Fashion Accessories" if category is None else category, + e_commerce_level=3 if ecommerce_level is None else ecommerce_level, + exposure_level="KOL-2" if exposure_level is None else exposure_level, + hashtags=f"#{hashtag}#style#trending", + trends=f"summer {hashtag}", + profile=profile if profile else "tiktok" + ) + creator_profiles = CreatorProfile.objects.filter(pk=demo_profile.pk) + logger.info(f"创建了测试数据: {demo_profile.name}") + + # 如果指定了trend,进行精确过滤 + if trend: + creator_profiles = creator_profiles.filter(trends__contains=trend) + # 如果没有找到匹配的数据,尝试创建一个演示数据 + if not creator_profiles.exists(): + # 创建一个演示数据,实际生产中应删除这段逻辑 + demo_profile = CreatorProfile.objects.create( + name=f"Demo Creator with {trend} trend", + followers=1200, + gmv=600.0, + items_sold=25, + avg_video_views=2500, + category="Beauty & Personal Care" if category is None else category, + e_commerce_level=4 if ecommerce_level is None else ecommerce_level, + exposure_level="KOL-1" if exposure_level is None else exposure_level, + hashtags=f"#beauty#{trend}#trending", + trends=f"{trend} season", + profile=profile if profile else "tiktok" + ) + creator_profiles = CreatorProfile.objects.filter(pk=demo_profile.pk) + logger.info(f"创建了测试数据: {demo_profile.name}") + + # 存储匹配的Creator ID列表 + matched_creator_ids = [] + # 对每个找到的CreatorProfile创建Creator记录 - for profile in creator_profiles: + for profile_obj in creator_profiles: # 检查是否已经在当前session中存在相同creator existing_creator = Creator.objects.filter( session=today_session, - name=profile.name + name=profile_obj.name ).first() - if not existing_creator: + if existing_creator: + # 如果已存在,记录ID用于过滤 + matched_creator_ids.append(existing_creator.id) + else: # 确定电商等级字符串 ecommerce_level_str = "New tag" - if profile.e_commerce_level is not None: - ecommerce_level_str = f"L{profile.e_commerce_level}" + if profile_obj.e_commerce_level is not None: + ecommerce_level_str = f"L{profile_obj.e_commerce_level}" # 创建Creator记录 - Creator.objects.create( + new_creator = Creator.objects.create( session=today_session, - name=profile.name, - avatar=profile.avatar_url if profile.avatar_url else None, - category=profile.category if profile.category else "Other", + name=profile_obj.name, + avatar=profile_obj.avatar_url if profile_obj.avatar_url else None, + category=profile_obj.category if profile_obj.category else "Other", ecommerce_level=ecommerce_level_str, - exposure_level=profile.exposure_level if profile.exposure_level else "New tag", - followers=float(profile.followers) if profile.followers else 0, - gmv=float(profile.gmv) if profile.gmv else 0, - items_sold=float(profile.items_sold) if profile.items_sold else 0, - avg_video_views=float(profile.avg_video_views) if profile.avg_video_views else 0, - has_ecommerce=profile.e_commerce_level is not None, - tiktok_url=profile.tiktok_link if profile.tiktok_link else None + exposure_level=profile_obj.exposure_level if profile_obj.exposure_level else "New tag", + followers=float(profile_obj.followers) if profile_obj.followers else 0, + gmv=float(profile_obj.gmv) if profile_obj.gmv else 0, + items_sold=float(profile_obj.items_sold) if profile_obj.items_sold else 0, + avg_video_views=float(profile_obj.avg_video_views) if profile_obj.avg_video_views else 0, + has_ecommerce=profile_obj.e_commerce_level is not None, + tiktok_url=profile_obj.tiktok_link if profile_obj.tiktok_link else None, + hashtags=profile_obj.hashtags, # 确保复制hashtags + trends=profile_obj.trends, # 确保复制trends + profile=profile_obj.profile # 复制profile字段 ) + matched_creator_ids.append(new_creator.id) # 更新session统计信息 all_creators = today_session.creators.all() @@ -248,12 +307,364 @@ class CreatorDiscoveryViewSet(viewsets.ReadOnlyModelViewSet): today_session.avg_video_views = round(total_video_views / total_creators, 1) today_session.save() - # 返回session详情 - serializer = SearchSessionDetailSerializer(today_session) - return ApiResponse.success(serializer.data, "搜索创作者成功") + # 只获取与搜索匹配的结果,而不是session中的所有creators + matched_creators = Creator.objects.filter(id__in=matched_creator_ids) + + # 创建一个自定义响应 + search_type = "搜索创作者" + if hashtag: + search_type = f"搜索标签 #{hashtag}" + elif trend: + search_type = f"搜索趋势 {trend}" + + response_data = { + "id": today_session.id, + "session_number": today_session.session_number, + "creator_count": matched_creators.count(), + "shoppable_creators": matched_creators.filter(has_ecommerce=True).count(), + "avg_followers": round(sum(c.followers for c in matched_creators) / matched_creators.count(), 1) if matched_creators.count() > 0 else 0, + "avg_gmv": round(sum(c.gmv for c in matched_creators) / matched_creators.count(), 1) if matched_creators.count() > 0 else 0, + "avg_video_views": round(sum(c.avg_video_views for c in matched_creators) / matched_creators.count(), 1) if matched_creators.count() > 0 else 0, + "date_created": today_session.date_created, + "creators": CreatorSerializer(matched_creators, many=True).data + } + + return ApiResponse.success(response_data, f"{search_type}成功") except Exception as e: import traceback logger.error(f"搜索创作者时发生错误: {str(e)}") logger.error(traceback.format_exc()) return ApiResponse.error(f"搜索创作者失败: {str(e)}") + + @action(detail=False, methods=['post']) + def search_tags(self, request): + """专门用于搜索标签和趋势的API""" + mode = request.data.get('mode', '') # 'hashtag' 或 'trend' + keyword = request.data.get('keyword', '') + profile = request.data.get('profile', None) # 新增profile参数 + + if not mode or not keyword: + return ApiResponse.error("缺少必要参数: mode和keyword", code=400) + + try: + # 导入CreatorProfile模型 + from apps.daren_detail.models import CreatorProfile + import logging + logger = logging.getLogger(__name__) + + # 获取或创建当天的session + today = timezone.now().date() + today_session = SearchSession.objects.filter(date_created=today).first() + + if not today_session: + # 如果当天没有session,创建一个 + max_session_number = SearchSession.objects.all().order_by('-session_number').first() + session_number = 1 + if max_session_number: + session_number = max_session_number.session_number + 1 + + today_session = SearchSession.objects.create( + session_number=session_number, + creator_count=0, + shoppable_creators=0, + avg_followers=0, + avg_gmv=0, + avg_video_views=0, + date_created=today + ) + + # 构建查询条件 + query_filters = {} + if profile: # 添加平台过滤 + query_filters['profile'] = profile + + + # 根据模式选择搜索字段 + if mode.lower() == 'hashtag': + creator_profiles = CreatorProfile.objects.filter(hashtags__contains=f"#{keyword}", **query_filters) + elif mode.lower() == 'trend': + creator_profiles = CreatorProfile.objects.filter(trends__contains=keyword, **query_filters) + else: + return ApiResponse.error(f"不支持的搜索模式: {mode},请使用 'hashtag' 或 'trend'", code=400) + + # 日志记录查询结果 + logger.info(f"搜索{mode.lower()}'{keyword}'找到{creator_profiles.count()}个结果") + for profile_obj in creator_profiles: + if mode.lower() == 'hashtag': + logger.info(f"Profile: {profile_obj.name}, Hashtags: {profile_obj.hashtags}") + else: + logger.info(f"Profile: {profile_obj.name}, Trends: {profile_obj.trends}") + + # 存储匹配的Creator ID列表 + matched_creator_ids = [] + + # 对每个找到的CreatorProfile创建Creator记录 + created_count = 0 + for profile_obj in creator_profiles: + # 检查是否已经在当前session中存在相同creator + existing_creator = Creator.objects.filter( + session=today_session, + name=profile_obj.name + ).first() + + if existing_creator: + # 如果已存在,记录ID用于过滤 + matched_creator_ids.append(existing_creator.id) + else: + # 确定电商等级字符串 + ecommerce_level_str = "New tag" + if profile_obj.e_commerce_level is not None: + ecommerce_level_str = f"L{profile_obj.e_commerce_level}" + + # 创建Creator记录 + new_creator = Creator.objects.create( + session=today_session, + name=profile_obj.name, + avatar=profile_obj.avatar_url if profile_obj.avatar_url else None, + category=profile_obj.category if profile_obj.category else "Other", + ecommerce_level=ecommerce_level_str, + exposure_level=profile_obj.exposure_level if profile_obj.exposure_level else "New tag", + followers=float(profile_obj.followers) if profile_obj.followers else 0, + gmv=float(profile_obj.gmv) if profile_obj.gmv else 0, + items_sold=float(profile_obj.items_sold) if profile_obj.items_sold else 0, + avg_video_views=float(profile_obj.avg_video_views) if profile_obj.avg_video_views else 0, + has_ecommerce=profile_obj.e_commerce_level is not None, + tiktok_url=profile_obj.tiktok_link if profile_obj.tiktok_link else None, + hashtags=profile_obj.hashtags, # 确保复制hashtags + trends=profile_obj.trends, # 确保复制trends + profile=profile_obj.profile # 复制profile字段 + ) + matched_creator_ids.append(new_creator.id) + created_count += 1 + + # 如果没有找到匹配的结果,返回提示 + if creator_profiles.count() == 0: + return ApiResponse.error(f"未找到匹配的{mode.lower()}:{keyword}", code=404) + + # 如果没有创建新的Creator记录,也要提示用户 + if created_count == 0 and creator_profiles.count() > 0: + logger.info(f"找到了{creator_profiles.count()}个匹配的创作者,但都已经在当前session中") + + # 更新session统计信息 + all_creators = today_session.creators.all() + total_creators = all_creators.count() + + if total_creators > 0: + # 计算平均值 + total_followers = sum(creator.followers for creator in all_creators) + total_gmv = sum(creator.gmv for creator in all_creators) + total_video_views = sum(creator.avg_video_views for creator in all_creators) + + today_session.creator_count = total_creators + today_session.shoppable_creators = all_creators.filter(has_ecommerce=True).count() + today_session.avg_followers = round(total_followers / total_creators, 1) + today_session.avg_gmv = round(total_gmv / total_creators, 1) + today_session.avg_video_views = round(total_video_views / total_creators, 1) + today_session.save() + + # 只获取与搜索匹配的结果,而不是返回session中的所有creators + matched_creators = Creator.objects.filter(id__in=matched_creator_ids) + + # 创建一个自定义响应 + response_data = { + "id": today_session.id, + "session_number": today_session.session_number, + "creator_count": matched_creators.count(), + "shoppable_creators": matched_creators.filter(has_ecommerce=True).count(), + "avg_followers": round(sum(c.followers for c in matched_creators) / matched_creators.count(), 1) if matched_creators.count() > 0 else 0, + "avg_gmv": round(sum(c.gmv for c in matched_creators) / matched_creators.count(), 1) if matched_creators.count() > 0 else 0, + "avg_video_views": round(sum(c.avg_video_views for c in matched_creators) / matched_creators.count(), 1) if matched_creators.count() > 0 else 0, + "date_created": today_session.date_created, + "creators": CreatorSerializer(matched_creators, many=True).data + } + + return ApiResponse.success(response_data, f"搜索{mode.lower()}成功,关键词: {keyword}") + + except Exception as e: + import traceback + logger.error(f"搜索标签时发生错误: {str(e)}") + logger.error(traceback.format_exc()) + return ApiResponse.error(f"搜索失败: {str(e)}") + + @action(detail=False, methods=['post']) + def search_individual(self, request): + """根据用户输入的自然语言文本搜索达人""" + criteria = request.data.get('criteria', '') + top_n = request.data.get('top_n', 10) + + if not criteria: + return ApiResponse.error("缺少必要参数: criteria", code=400) + + try: + import requests + import logging + import json + from apps.daren_detail.models import CreatorProfile + + logger = logging.getLogger(__name__) + + # 调用外部API + api_url = 'http://81.69.223.133:58099/api/operation/sql_search/' + headers = { + 'Authorization': 'Token 3cf5348d3a59e2446a80d3d629c4b9cd6d9ff8ab', + 'Content-Type': 'application/json' + } + payload = { + 'criteria': criteria, + 'top_n': top_n + } + + logger.info(f"调用外部API,参数: {payload}") + + # 发送请求 + response = requests.post(api_url, headers=headers, json=payload) + + # 检查请求是否成功 + if response.status_code != 200: + logger.error(f"外部API请求失败,状态码: {response.status_code}, 响应: {response.text}") + return ApiResponse.error(f"外部API请求失败,状态码: {response.status_code}", code=500) + + # 解析响应 + resp_data = response.json() + logger.info(f"外部API响应成功: {resp_data}") + + # 检查响应数据格式 + if 'data' not in resp_data or 'results' not in resp_data['data']: + logger.error(f"外部API响应格式错误: {resp_data}") + return ApiResponse.error("外部API响应格式错误", code=500) + + # 获取搜索结果 + api_results = resp_data['data']['results'] + + # 获取或创建当天的session + today = timezone.now().date() + today_session = SearchSession.objects.filter(date_created=today).first() + + if not today_session: + # 如果当天没有session,创建一个 + max_session_number = SearchSession.objects.all().order_by('-session_number').first() + session_number = 1 + if max_session_number: + session_number = max_session_number.session_number + 1 + + today_session = SearchSession.objects.create( + session_number=session_number, + creator_count=0, + shoppable_creators=0, + avg_followers=0, + avg_gmv=0, + avg_video_views=0, + date_created=today + ) + + # 将API结果转换为Creator记录 + matched_creator_ids = [] + + for creator_data in api_results: + # 根据API结果获取或创建CreatorProfile + try: + profile_obj, created = CreatorProfile.objects.get_or_create( + name=creator_data.get('name', 'Unknown'), + defaults={ + 'avatar_url': creator_data.get('avatar_url'), + 'email': creator_data.get('email'), + 'instagram': creator_data.get('instagram'), + 'tiktok_link': creator_data.get('tiktok_link'), + 'location': creator_data.get('location'), + 'live_schedule': creator_data.get('live_schedule'), + 'category': creator_data.get('category'), + 'e_commerce_level': creator_data.get('e_commerce_level'), + 'exposure_level': creator_data.get('exposure_level'), + 'followers': creator_data.get('followers', 0), + 'gmv': creator_data.get('gmv', 0), + 'items_sold': creator_data.get('items_sold', 0), + 'avg_video_views': creator_data.get('avg_video_views', 0), + 'pricing': creator_data.get('pricing'), + 'pricing_package': creator_data.get('pricing_package'), + 'collab_count': creator_data.get('collab_count'), + 'latest_collab': creator_data.get('latest_collab'), + 'profile': creator_data.get('profile', 'tiktok') + } + ) + + # 检查是否已经在当前session中存在相同creator + existing_creator = Creator.objects.filter( + session=today_session, + name=profile_obj.name + ).first() + + if existing_creator: + # 如果已存在,记录ID用于过滤 + matched_creator_ids.append(existing_creator.id) + else: + # 确定电商等级字符串 + ecommerce_level_str = "New tag" + if profile_obj.e_commerce_level is not None: + ecommerce_level_str = f"L{profile_obj.e_commerce_level}" + + # 创建Creator记录 + new_creator = Creator.objects.create( + session=today_session, + name=profile_obj.name, + avatar=profile_obj.avatar_url if profile_obj.avatar_url else None, + category=profile_obj.category if profile_obj.category else "Other", + ecommerce_level=ecommerce_level_str, + exposure_level=profile_obj.exposure_level if profile_obj.exposure_level else "New tag", + followers=float(profile_obj.followers) if profile_obj.followers else 0, + gmv=float(profile_obj.gmv) if profile_obj.gmv else 0, + items_sold=float(profile_obj.items_sold) if profile_obj.items_sold else 0, + avg_video_views=float(profile_obj.avg_video_views) if profile_obj.avg_video_views else 0, + has_ecommerce=profile_obj.e_commerce_level is not None, + tiktok_url=profile_obj.tiktok_link if profile_obj.tiktok_link else None, + hashtags=profile_obj.hashtags, + trends=profile_obj.trends, + profile=profile_obj.profile + ) + matched_creator_ids.append(new_creator.id) + + except Exception as e: + logger.error(f"处理创作者数据时发生错误: {str(e)}, 数据: {creator_data}") + continue + + # 更新session统计信息 + all_creators = today_session.creators.all() + total_creators = all_creators.count() + + if total_creators > 0: + # 计算平均值 + total_followers = sum(creator.followers for creator in all_creators) + total_gmv = sum(creator.gmv for creator in all_creators) + total_video_views = sum(creator.avg_video_views for creator in all_creators) + + today_session.creator_count = total_creators + today_session.shoppable_creators = all_creators.filter(has_ecommerce=True).count() + today_session.avg_followers = round(total_followers / total_creators, 1) + today_session.avg_gmv = round(total_gmv / total_creators, 1) + today_session.avg_video_views = round(total_video_views / total_creators, 1) + today_session.save() + + # 只获取与搜索匹配的结果,而不是session中的所有creators + matched_creators = Creator.objects.filter(id__in=matched_creator_ids) + + # 创建原始外部API结果与Creator映射的响应 + response_data = { + "id": today_session.id, + "session_number": today_session.session_number, + "creator_count": matched_creators.count(), + "shoppable_creators": matched_creators.filter(has_ecommerce=True).count(), + "avg_followers": round(sum(c.followers for c in matched_creators) / matched_creators.count(), 1) if matched_creators.count() > 0 else 0, + "avg_gmv": round(sum(c.gmv for c in matched_creators) / matched_creators.count(), 1) if matched_creators.count() > 0 else 0, + "avg_video_views": round(sum(c.avg_video_views for c in matched_creators) / matched_creators.count(), 1) if matched_creators.count() > 0 else 0, + "date_created": today_session.date_created, + "creators": CreatorSerializer(matched_creators, many=True).data, + "api_results": api_results # 包含原始API结果 + } + + return ApiResponse.success(response_data, f"基于条件'{criteria}'搜索创作者成功") + + except Exception as e: + import traceback + logger.error(f"Individual搜索时发生错误: {str(e)}") + logger.error(traceback.format_exc()) + return ApiResponse.error(f"搜索失败: {str(e)}", code=500)