去除不要的模块

This commit is contained in:
wanjia 2025-05-22 15:26:36 +08:00
parent b728ea3d6f
commit f220c59c5a
46 changed files with 0 additions and 2773 deletions

View File

@ -1,96 +0,0 @@
from django.contrib import admin
from .models import Brand, Product, Campaign, BrandChatSession
@admin.register(Brand)
class BrandAdmin(admin.ModelAdmin):
list_display = ('name', 'category', 'source', 'collab_count', 'creators_count', 'total_gmv_achieved', 'total_views_achieved', 'shop_overall_rating', 'created_at', 'is_active')
search_fields = ('name', 'description', 'category', 'source')
list_filter = ('is_active', 'created_at', 'category', 'source')
readonly_fields = ('id', 'created_at', 'updated_at')
fieldsets = (
('基本信息', {
'fields': ('id', 'name', 'description', 'logo_url', 'is_active')
}),
('分类信息', {
'fields': ('category', 'source', 'collab_count', 'creators_count', 'campaign_id')
}),
('统计信息', {
'fields': ('total_gmv_achieved', 'total_views_achieved', 'shop_overall_rating')
}),
('知识库关联', {
'fields': ('dataset_id_list',)
}),
('时间信息', {
'fields': ('created_at', 'updated_at')
}),
)
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
list_display = ('name', 'brand', 'pid', 'commission_rate', 'stock', 'items_sold', 'product_rating', 'created_at', 'is_active')
search_fields = ('name', 'description', 'brand__name', 'pid')
list_filter = ('brand', 'is_active', 'created_at', 'tiktok_shop')
readonly_fields = ('id', 'created_at', 'updated_at')
fieldsets = (
('基本信息', {
'fields': ('id', 'name', 'brand', 'description', 'image_url', 'is_active')
}),
('产品详情', {
'fields': ('pid', 'commission_rate', 'open_collab', 'available_samples',
'sales_price_min', 'sales_price_max', 'stock', 'items_sold',
'product_rating', 'reviews_count', 'collab_creators', 'tiktok_shop')
}),
('知识库信息', {
'fields': ('dataset_id', 'external_id')
}),
('时间信息', {
'fields': ('created_at', 'updated_at')
}),
)
@admin.register(Campaign)
class CampaignAdmin(admin.ModelAdmin):
list_display = ('name', 'brand', 'service', 'creator_type', 'start_date', 'end_date', 'is_active')
search_fields = ('name', 'description', 'brand__name', 'service', 'creator_type')
list_filter = ('brand', 'is_active', 'start_date', 'end_date', 'service', 'creator_type')
readonly_fields = ('id', 'created_at', 'updated_at')
filter_horizontal = ('link_product',)
fieldsets = (
('基本信息', {
'fields': ('id', 'name', 'brand', 'description', 'image_url', 'is_active')
}),
('活动详情', {
'fields': ('service', 'creator_type', 'creator_level', 'creator_category',
'creators_count', 'gmv', 'followers', 'views', 'budget')
}),
('关联产品', {
'fields': ('link_product',)
}),
('活动时间', {
'fields': ('start_date', 'end_date')
}),
('知识库信息', {
'fields': ('dataset_id', 'external_id')
}),
('时间信息', {
'fields': ('created_at', 'updated_at')
}),
)
@admin.register(BrandChatSession)
class BrandChatSessionAdmin(admin.ModelAdmin):
list_display = ('title', 'brand', 'session_id', 'created_at', 'is_active')
search_fields = ('title', 'session_id', 'brand__name')
list_filter = ('brand', 'is_active', 'created_at')
readonly_fields = ('id', 'created_at', 'updated_at')
fieldsets = (
('基本信息', {
'fields': ('id', 'title', 'brand', 'session_id', 'is_active')
}),
('知识库信息', {
'fields': ('dataset_id_list',)
}),
('时间信息', {
'fields': ('created_at', 'updated_at')
}),
)

View File

@ -1,6 +0,0 @@
from django.apps import AppConfig
class BrandsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.brands'

View File

@ -1,99 +0,0 @@
# Generated by Django 5.2 on 2025-05-09 03:55
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Brand',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, unique=True, verbose_name='品牌名称')),
('description', models.TextField(blank=True, null=True, verbose_name='品牌描述')),
('logo_url', models.CharField(blank=True, max_length=255, null=True, verbose_name='品牌Logo')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('is_active', models.BooleanField(default=True, verbose_name='是否激活')),
('dataset_id_list', models.JSONField(blank=True, default=list, help_text='所有关联的知识库ID列表', verbose_name='知识库ID列表')),
],
options={
'verbose_name': '品牌',
'verbose_name_plural': '品牌',
'db_table': 'brands',
},
),
migrations.CreateModel(
name='Activity',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, verbose_name='活动名称')),
('description', models.TextField(blank=True, null=True, verbose_name='活动描述')),
('image_url', models.CharField(blank=True, max_length=255, null=True, verbose_name='活动图片')),
('start_date', models.DateTimeField(blank=True, null=True, verbose_name='开始日期')),
('end_date', models.DateTimeField(blank=True, null=True, verbose_name='结束日期')),
('dataset_id', models.CharField(help_text='外部知识库系统中的ID', max_length=100, verbose_name='知识库ID')),
('external_id', models.CharField(blank=True, help_text='外部系统中的唯一标识', max_length=100, null=True, verbose_name='外部ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('is_active', models.BooleanField(default=True, verbose_name='是否激活')),
('brand', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activities', to='brands.brand', verbose_name='所属品牌')),
],
options={
'verbose_name': '活动',
'verbose_name_plural': '活动',
'db_table': 'activities',
'indexes': [models.Index(fields=['brand'], name='activities_brand_i_9a57da_idx'), models.Index(fields=['dataset_id'], name='activities_dataset_c873ab_idx'), models.Index(fields=['is_active'], name='activities_is_acti_cff4bc_idx'), models.Index(fields=['start_date'], name='activities_start_d_fe1952_idx'), models.Index(fields=['end_date'], name='activities_end_dat_9cb2d8_idx')],
'unique_together': {('brand', 'name')},
},
),
migrations.CreateModel(
name='BrandChatSession',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('session_id', models.CharField(max_length=100, unique=True, verbose_name='会话ID')),
('title', models.CharField(default='新对话', max_length=200, verbose_name='会话标题')),
('dataset_id_list', models.JSONField(blank=True, default=list, verbose_name='知识库ID列表')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('is_active', models.BooleanField(default=True, verbose_name='是否激活')),
('brand', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chat_sessions', to='brands.brand', verbose_name='品牌')),
],
options={
'verbose_name': '品牌聊天会话',
'verbose_name_plural': '品牌聊天会话',
'db_table': 'brand_chat_sessions',
'indexes': [models.Index(fields=['brand'], name='brand_chat__brand_i_83752e_idx'), models.Index(fields=['session_id'], name='brand_chat__session_4bf9b0_idx'), models.Index(fields=['created_at'], name='brand_chat__created_957266_idx')],
},
),
migrations.CreateModel(
name='Product',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, verbose_name='产品名称')),
('description', models.TextField(blank=True, null=True, verbose_name='产品描述')),
('image_url', models.CharField(blank=True, max_length=255, null=True, verbose_name='产品图片')),
('dataset_id', models.CharField(help_text='外部知识库系统中的ID', max_length=100, verbose_name='知识库ID')),
('external_id', models.CharField(blank=True, help_text='外部系统中的唯一标识', max_length=100, null=True, verbose_name='外部ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('is_active', models.BooleanField(default=True, verbose_name='是否激活')),
('brand', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to='brands.brand', verbose_name='所属品牌')),
],
options={
'verbose_name': '产品',
'verbose_name_plural': '产品',
'db_table': 'products',
'indexes': [models.Index(fields=['brand'], name='products_brand_i_0d1950_idx'), models.Index(fields=['dataset_id'], name='products_dataset_faf62a_idx'), models.Index(fields=['is_active'], name='products_is_acti_cb485f_idx')],
'unique_together': {('brand', 'name')},
},
),
]

View File

@ -1,194 +0,0 @@
# Generated by Django 5.2 on 2025-05-13 02:45
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('brands', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Campaign',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, verbose_name='活动名称')),
('description', models.TextField(blank=True, null=True, verbose_name='活动描述')),
('image_url', models.CharField(blank=True, max_length=255, null=True, verbose_name='活动图片')),
('service', models.CharField(blank=True, max_length=100, null=True, verbose_name='服务类型')),
('creator_type', models.CharField(blank=True, max_length=100, null=True, verbose_name='创作者类型')),
('creator_level', models.CharField(blank=True, max_length=100, null=True, verbose_name='创作者等级')),
('creator_category', models.CharField(blank=True, max_length=100, null=True, verbose_name='创作者分类')),
('creators_count', models.IntegerField(default=0, verbose_name='创作者数量')),
('gmv', models.CharField(blank=True, max_length=100, null=True, verbose_name='GMV范围')),
('followers', models.CharField(blank=True, max_length=100, null=True, verbose_name='粉丝数范围')),
('views', models.CharField(blank=True, max_length=100, null=True, verbose_name='浏览量范围')),
('budget', models.CharField(blank=True, max_length=100, null=True, verbose_name='预算范围')),
('start_date', models.DateTimeField(blank=True, null=True, verbose_name='开始日期')),
('end_date', models.DateTimeField(blank=True, null=True, verbose_name='结束日期')),
('dataset_id', models.CharField(help_text='外部知识库系统中的ID', max_length=100, verbose_name='知识库ID')),
('external_id', models.CharField(blank=True, help_text='外部系统中的唯一标识', max_length=100, null=True, verbose_name='外部ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('is_active', models.BooleanField(default=True, verbose_name='是否激活')),
],
options={
'verbose_name': '活动',
'verbose_name_plural': '活动',
'db_table': 'campaigns',
},
),
migrations.AlterUniqueTogether(
name='activity',
unique_together=None,
),
migrations.RemoveField(
model_name='activity',
name='brand',
),
migrations.AddField(
model_name='brand',
name='campaign_id',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='活动ID'),
),
migrations.AddField(
model_name='brand',
name='category',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='品牌分类'),
),
migrations.AddField(
model_name='brand',
name='collab_count',
field=models.IntegerField(default=0, verbose_name='合作数量'),
),
migrations.AddField(
model_name='brand',
name='creators_count',
field=models.IntegerField(default=0, verbose_name='创作者数量'),
),
migrations.AddField(
model_name='brand',
name='shop_overall_rating',
field=models.DecimalField(decimal_places=1, default=0.0, max_digits=3, verbose_name='店铺评分'),
),
migrations.AddField(
model_name='brand',
name='source',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='来源'),
),
migrations.AddField(
model_name='brand',
name='total_gmv_achieved',
field=models.DecimalField(decimal_places=2, default=0, max_digits=12, verbose_name='总GMV'),
),
migrations.AddField(
model_name='brand',
name='total_views_achieved',
field=models.DecimalField(decimal_places=2, default=0, max_digits=12, verbose_name='总浏览量'),
),
migrations.AddField(
model_name='product',
name='available_samples',
field=models.IntegerField(default=0, verbose_name='可用样品数'),
),
migrations.AddField(
model_name='product',
name='collab_creators',
field=models.IntegerField(default=0, verbose_name='合作创作者数'),
),
migrations.AddField(
model_name='product',
name='commission_rate',
field=models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='佣金率'),
),
migrations.AddField(
model_name='product',
name='items_sold',
field=models.IntegerField(default=0, verbose_name='已售数量'),
),
migrations.AddField(
model_name='product',
name='open_collab',
field=models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='开放合作率'),
),
migrations.AddField(
model_name='product',
name='pid',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='产品ID'),
),
migrations.AddField(
model_name='product',
name='product_rating',
field=models.DecimalField(decimal_places=1, default=0, max_digits=3, verbose_name='产品评分'),
),
migrations.AddField(
model_name='product',
name='reviews_count',
field=models.IntegerField(default=0, verbose_name='评价数量'),
),
migrations.AddField(
model_name='product',
name='sales_price_max',
field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='最高销售价'),
),
migrations.AddField(
model_name='product',
name='sales_price_min',
field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='最低销售价'),
),
migrations.AddField(
model_name='product',
name='stock',
field=models.IntegerField(default=0, verbose_name='库存'),
),
migrations.AddField(
model_name='product',
name='tiktok_shop',
field=models.BooleanField(default=False, verbose_name='是否TikTok商店'),
),
migrations.AddIndex(
model_name='product',
index=models.Index(fields=['pid'], name='products_pid_99aab2_idx'),
),
migrations.AddField(
model_name='campaign',
name='brand',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='campaigns', to='brands.brand', verbose_name='所属品牌'),
),
migrations.AddField(
model_name='campaign',
name='link_product',
field=models.ManyToManyField(blank=True, related_name='campaigns', to='brands.product', verbose_name='关联产品'),
),
migrations.DeleteModel(
name='Activity',
),
migrations.AddIndex(
model_name='campaign',
index=models.Index(fields=['brand'], name='campaigns_brand_i_c2d4bd_idx'),
),
migrations.AddIndex(
model_name='campaign',
index=models.Index(fields=['dataset_id'], name='campaigns_dataset_bfbb68_idx'),
),
migrations.AddIndex(
model_name='campaign',
index=models.Index(fields=['is_active'], name='campaigns_is_acti_6c57d0_idx'),
),
migrations.AddIndex(
model_name='campaign',
index=models.Index(fields=['start_date'], name='campaigns_start_d_5c2c6b_idx'),
),
migrations.AddIndex(
model_name='campaign',
index=models.Index(fields=['end_date'], name='campaigns_end_dat_6aaba4_idx'),
),
migrations.AlterUniqueTogether(
name='campaign',
unique_together={('brand', 'name')},
),
]

View File

@ -1,89 +0,0 @@
# Generated by Django 5.2 on 2025-05-21 04:30
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('brands', '0002_campaign_alter_activity_unique_together_and_more'),
]
operations = [
migrations.RenameIndex(
model_name='brandchatsession',
new_name='brands_bran_brand_i_d22614_idx',
old_name='brand_chat__brand_i_83752e_idx',
),
migrations.RenameIndex(
model_name='brandchatsession',
new_name='brands_bran_session_574261_idx',
old_name='brand_chat__session_4bf9b0_idx',
),
migrations.RenameIndex(
model_name='brandchatsession',
new_name='brands_bran_created_c31416_idx',
old_name='brand_chat__created_957266_idx',
),
migrations.RenameIndex(
model_name='campaign',
new_name='brands_camp_brand_i_51f26d_idx',
old_name='campaigns_brand_i_c2d4bd_idx',
),
migrations.RenameIndex(
model_name='campaign',
new_name='brands_camp_dataset_9b31e4_idx',
old_name='campaigns_dataset_bfbb68_idx',
),
migrations.RenameIndex(
model_name='campaign',
new_name='brands_camp_is_acti_c09fdb_idx',
old_name='campaigns_is_acti_6c57d0_idx',
),
migrations.RenameIndex(
model_name='campaign',
new_name='brands_camp_start_d_42e603_idx',
old_name='campaigns_start_d_5c2c6b_idx',
),
migrations.RenameIndex(
model_name='campaign',
new_name='brands_camp_end_dat_97d26b_idx',
old_name='campaigns_end_dat_6aaba4_idx',
),
migrations.RenameIndex(
model_name='product',
new_name='brands_prod_brand_i_e0821d_idx',
old_name='products_brand_i_0d1950_idx',
),
migrations.RenameIndex(
model_name='product',
new_name='brands_prod_dataset_c7f534_idx',
old_name='products_dataset_faf62a_idx',
),
migrations.RenameIndex(
model_name='product',
new_name='brands_prod_is_acti_fd82c6_idx',
old_name='products_is_acti_cb485f_idx',
),
migrations.RenameIndex(
model_name='product',
new_name='brands_prod_pid_452ccb_idx',
old_name='products_pid_99aab2_idx',
),
migrations.AlterModelTable(
name='brand',
table=None,
),
migrations.AlterModelTable(
name='brandchatsession',
table=None,
),
migrations.AlterModelTable(
name='campaign',
table=None,
),
migrations.AlterModelTable(
name='product',
table=None,
),
]

View File

@ -1,3 +0,0 @@

View File

@ -1,177 +0,0 @@
from django.db import models
import uuid
from django.utils import timezone
class Brand(models.Model):
"""品牌模型"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=100, unique=True, verbose_name='品牌名称')
description = models.TextField(blank=True, null=True, verbose_name='品牌描述')
logo_url = models.CharField(max_length=255, blank=True, null=True, verbose_name='品牌Logo')
category = models.CharField(max_length=100, blank=True, null=True, verbose_name='品牌分类')
source = models.CharField(max_length=100, blank=True, null=True, verbose_name='来源')
collab_count = models.IntegerField(default=0, verbose_name='合作数量')
creators_count = models.IntegerField(default=0, verbose_name='创作者数量')
campaign_id = models.CharField(max_length=100, blank=True, null=True, verbose_name='活动ID')
# 添加数据统计字段
total_gmv_achieved = models.DecimalField(max_digits=12, decimal_places=2, default=0, verbose_name='总GMV')
total_views_achieved = models.DecimalField(max_digits=12, decimal_places=2, default=0, verbose_name='总浏览量')
shop_overall_rating = models.DecimalField(max_digits=3, decimal_places=1, default=0.0, verbose_name='店铺评分')
# 存储关联到此品牌的所有产品和活动知识库ID列表
dataset_id_list = models.JSONField(default=list, blank=True, verbose_name='知识库ID列表',
help_text='所有关联的知识库ID列表')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
is_active = models.BooleanField(default=True, verbose_name='是否激活')
class Meta:
verbose_name = '品牌'
verbose_name_plural = '品牌'
def __str__(self):
return self.name
class Product(models.Model):
"""产品模型 - 作为一个知识库"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
brand = models.ForeignKey(Brand, on_delete=models.CASCADE, related_name='products', verbose_name='所属品牌')
name = models.CharField(max_length=100, verbose_name='产品名称')
description = models.TextField(blank=True, null=True, verbose_name='产品描述')
image_url = models.CharField(max_length=255, blank=True, null=True, verbose_name='产品图片')
# 添加产品详情字段
pid = models.CharField(max_length=100, blank=True, null=True, verbose_name='产品ID')
commission_rate = models.DecimalField(max_digits=5, decimal_places=2, default=0, verbose_name='佣金率')
open_collab = models.DecimalField(max_digits=5, decimal_places=2, default=0, verbose_name='开放合作率')
available_samples = models.IntegerField(default=0, verbose_name='可用样品数')
sales_price_min = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name='最低销售价')
sales_price_max = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name='最高销售价')
stock = models.IntegerField(default=0, verbose_name='库存')
items_sold = models.IntegerField(default=0, verbose_name='已售数量')
product_rating = models.DecimalField(max_digits=3, decimal_places=1, default=0, verbose_name='产品评分')
reviews_count = models.IntegerField(default=0, verbose_name='评价数量')
collab_creators = models.IntegerField(default=0, verbose_name='合作创作者数')
tiktok_shop = models.BooleanField(default=False, verbose_name='是否TikTok商店')
dataset_id = models.CharField(max_length=100, verbose_name='知识库ID',
help_text='外部知识库系统中的ID')
external_id = models.CharField(max_length=100, blank=True, null=True, verbose_name='外部ID',
help_text='外部系统中的唯一标识')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
is_active = models.BooleanField(default=True, verbose_name='是否激活')
class Meta:
verbose_name = '产品'
verbose_name_plural = '产品'
unique_together = ['brand', 'name']
indexes = [
models.Index(fields=['brand']),
models.Index(fields=['dataset_id']),
models.Index(fields=['is_active']),
models.Index(fields=['pid']),
]
def __str__(self):
return f"{self.brand.name} - {self.name}"
def save(self, *args, **kwargs):
"""重写save方法更新品牌的dataset_id_list"""
is_new = self.pk is None
super().save(*args, **kwargs)
# 刷新品牌的dataset_id_list
if is_new and self.is_active and self.dataset_id:
brand = self.brand
if self.dataset_id not in brand.dataset_id_list:
brand.dataset_id_list.append(self.dataset_id)
brand.save(update_fields=['dataset_id_list', 'updated_at'])
class Campaign(models.Model):
"""活动模型 - 作为一个知识库"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
brand = models.ForeignKey(Brand, on_delete=models.CASCADE, related_name='campaigns', verbose_name='所属品牌')
name = models.CharField(max_length=100, verbose_name='活动名称')
description = models.TextField(blank=True, null=True, verbose_name='活动描述')
image_url = models.CharField(max_length=255, blank=True, null=True, verbose_name='活动图片')
# 活动相关字段
service = models.CharField(max_length=100, blank=True, null=True, verbose_name='服务类型')
creator_type = models.CharField(max_length=100, blank=True, null=True, verbose_name='创作者类型')
creator_level = models.CharField(max_length=100, blank=True, null=True, verbose_name='创作者等级')
creator_category = models.CharField(max_length=100, blank=True, null=True, verbose_name='创作者分类')
creators_count = models.IntegerField(default=0, verbose_name='创作者数量')
gmv = models.CharField(max_length=100, blank=True, null=True, verbose_name='GMV范围')
followers = models.CharField(max_length=100, blank=True, null=True, verbose_name='粉丝数范围')
views = models.CharField(max_length=100, blank=True, null=True, verbose_name='浏览量范围')
budget = models.CharField(max_length=100, blank=True, null=True, verbose_name='预算范围')
link_product = models.ManyToManyField(Product, blank=True, related_name='campaigns', verbose_name='关联产品')
# 时间信息
start_date = models.DateTimeField(blank=True, null=True, verbose_name='开始日期')
end_date = models.DateTimeField(blank=True, null=True, verbose_name='结束日期')
# 知识库信息
dataset_id = models.CharField(max_length=100, verbose_name='知识库ID',
help_text='外部知识库系统中的ID')
external_id = models.CharField(max_length=100, blank=True, null=True, verbose_name='外部ID',
help_text='外部系统中的唯一标识')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
is_active = models.BooleanField(default=True, verbose_name='是否激活')
class Meta:
verbose_name = '活动'
verbose_name_plural = '活动'
unique_together = ['brand', 'name']
indexes = [
models.Index(fields=['brand']),
models.Index(fields=['dataset_id']),
models.Index(fields=['is_active']),
models.Index(fields=['start_date']),
models.Index(fields=['end_date']),
]
def __str__(self):
return f"{self.brand.name} - {self.name}"
def save(self, *args, **kwargs):
"""重写save方法更新品牌的dataset_id_list"""
is_new = self.pk is None
super().save(*args, **kwargs)
# 刷新品牌的dataset_id_list
if is_new and self.is_active and self.dataset_id:
brand = self.brand
if self.dataset_id not in brand.dataset_id_list:
brand.dataset_id_list.append(self.dataset_id)
brand.save(update_fields=['dataset_id_list', 'updated_at'])
class BrandChatSession(models.Model):
"""品牌聊天会话模型"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
brand = models.ForeignKey(Brand, on_delete=models.CASCADE, related_name='chat_sessions', verbose_name='品牌')
session_id = models.CharField(max_length=100, unique=True, verbose_name='会话ID')
title = models.CharField(max_length=200, default='新对话', verbose_name='会话标题')
# 存储此次会话使用的所有知识库ID
dataset_id_list = models.JSONField(default=list, blank=True, verbose_name='知识库ID列表')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
is_active = models.BooleanField(default=True, verbose_name='是否激活')
class Meta:
verbose_name = '品牌聊天会话'
verbose_name_plural = '品牌聊天会话'
indexes = [
models.Index(fields=['brand']),
models.Index(fields=['session_id']),
models.Index(fields=['created_at']),
]
def __str__(self):
return f"{self.brand.name} - {self.title}"

View File

@ -1,67 +0,0 @@
from rest_framework import serializers
from .models import Brand, Product, Campaign, BrandChatSession
class BrandSerializer(serializers.ModelSerializer):
"""品牌序列化器"""
class Meta:
model = Brand
fields = ['id', 'name', 'description', 'logo_url', 'category', 'source',
'collab_count', 'creators_count', 'campaign_id', 'total_gmv_achieved',
'total_views_achieved', 'shop_overall_rating', 'dataset_id_list',
'created_at', 'updated_at', 'is_active']
read_only_fields = ['id', 'created_at', 'updated_at', 'dataset_id_list']
class ProductSerializer(serializers.ModelSerializer):
"""产品序列化器"""
brand_name = serializers.CharField(source='brand.name', read_only=True)
class Meta:
model = Product
fields = ['id', 'brand', 'brand_name', 'name', 'description', 'image_url',
'pid', 'commission_rate', 'open_collab', 'available_samples',
'sales_price_min', 'sales_price_max', 'stock', 'items_sold',
'product_rating', 'reviews_count', 'collab_creators', 'tiktok_shop',
'dataset_id', 'external_id', 'created_at', 'updated_at', 'is_active']
read_only_fields = ['id', 'created_at', 'updated_at']
class CampaignSerializer(serializers.ModelSerializer):
"""活动序列化器"""
brand_name = serializers.CharField(source='brand.name', read_only=True)
link_product_details = ProductSerializer(source='link_product', many=True, read_only=True)
class Meta:
model = Campaign
fields = ['id', 'brand', 'brand_name', 'name', 'description', 'image_url',
'service', 'creator_type', 'creator_level', 'creator_category',
'creators_count', 'gmv', 'followers', 'views', 'budget',
'link_product', 'link_product_details',
'start_date', 'end_date', 'dataset_id', 'external_id',
'created_at', 'updated_at', 'is_active']
read_only_fields = ['id', 'created_at', 'updated_at']
class BrandChatSessionSerializer(serializers.ModelSerializer):
"""品牌聊天会话序列化器"""
brand_name = serializers.CharField(source='brand.name', read_only=True)
class Meta:
model = BrandChatSession
fields = ['id', 'brand', 'brand_name', 'session_id', 'title',
'dataset_id_list', 'created_at', 'updated_at', 'is_active']
read_only_fields = ['id', 'created_at', 'updated_at']
class BrandDetailSerializer(serializers.ModelSerializer):
"""品牌详情序列化器"""
products = ProductSerializer(many=True, read_only=True)
campaigns = CampaignSerializer(many=True, read_only=True)
class Meta:
model = Brand
fields = ['id', 'name', 'description', 'logo_url', 'category', 'source',
'collab_count', 'creators_count', 'campaign_id', 'total_gmv_achieved',
'total_views_achieved', 'shop_overall_rating', 'dataset_id_list',
'products', 'campaigns', 'created_at', 'updated_at', 'is_active']
read_only_fields = ['id', 'created_at', 'updated_at', 'dataset_id_list']

View File

@ -1 +0,0 @@

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -1,18 +0,0 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import (
BrandViewSet,
ProductViewSet,
CampaignViewSet,
BrandChatSessionViewSet
)
router = DefaultRouter()
router.register(r'brands', BrandViewSet)
router.register(r'products', ProductViewSet)
router.register(r'campaigns', CampaignViewSet)
router.register(r'chat-sessions', BrandChatSessionViewSet)
urlpatterns = [
path('', include(router.urls)),
]

View File

@ -1,317 +0,0 @@
from django.shortcuts import render, get_object_or_404
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import Brand, Product, Campaign, BrandChatSession
from .serializers import (
BrandSerializer,
ProductSerializer,
CampaignSerializer,
BrandChatSessionSerializer,
BrandDetailSerializer
)
def api_response(code=200, message="成功", data=None):
"""统一API响应格式"""
return Response({
'code': code,
'message': message,
'data': data
})
class BrandViewSet(viewsets.ModelViewSet):
"""品牌API视图集"""
queryset = Brand.objects.all()
serializer_class = BrandSerializer
def get_serializer_class(self):
if self.action == 'retrieve':
return BrandDetailSerializer
return BrandSerializer
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
serializer = self.get_serializer(queryset, many=True)
return api_response(data=serializer.data)
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
self.perform_create(serializer)
return api_response(data=serializer.data)
return api_response(code=400, message="创建失败", data=serializer.errors)
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(instance)
return api_response(data=serializer.data)
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
if serializer.is_valid():
self.perform_update(serializer)
return api_response(data=serializer.data)
return api_response(code=400, message="更新失败", data=serializer.errors)
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
self.perform_destroy(instance)
return api_response(message="删除成功", data=None)
@action(detail=True, methods=['get'])
def products(self, request, pk=None):
"""获取品牌下的所有产品"""
brand = self.get_object()
products = Product.objects.filter(brand=brand, is_active=True)
serializer = ProductSerializer(products, many=True)
return api_response(data=serializer.data)
@action(detail=True, methods=['get'])
def campaigns(self, request, pk=None):
"""获取品牌下的所有活动"""
brand = self.get_object()
campaigns = Campaign.objects.filter(brand=brand, is_active=True)
serializer = CampaignSerializer(campaigns, many=True)
return api_response(data=serializer.data)
@action(detail=True, methods=['get'])
def dataset_ids(self, request, pk=None):
"""获取品牌的所有知识库ID"""
brand = self.get_object()
return api_response(data={'dataset_id_list': brand.dataset_id_list})
class ProductViewSet(viewsets.ModelViewSet):
"""产品API视图集"""
queryset = Product.objects.filter(is_active=True)
serializer_class = ProductSerializer
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
serializer = self.get_serializer(queryset, many=True)
return api_response(data=serializer.data)
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
self.perform_create(serializer)
return api_response(data=serializer.data)
return api_response(code=400, message="创建失败", data=serializer.errors)
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(instance)
return api_response(data=serializer.data)
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
if serializer.is_valid():
self.perform_update(serializer)
return api_response(data=serializer.data)
return api_response(code=400, message="更新失败", data=serializer.errors)
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
self.perform_destroy(instance)
return api_response(message="删除成功", data=None)
def perform_create(self, serializer):
# 创建产品时自动更新品牌的dataset_id_list
product = serializer.save()
brand = product.brand
# 确保dataset_id添加到品牌的dataset_id_list中
if product.dataset_id and product.dataset_id not in brand.dataset_id_list:
brand.dataset_id_list.append(product.dataset_id)
brand.save(update_fields=['dataset_id_list', 'updated_at'])
def perform_update(self, serializer):
# 获取原始产品信息
old_product = self.get_object()
old_dataset_id = old_product.dataset_id
# 保存更新后的产品
product = serializer.save()
brand = product.brand
# 从品牌的dataset_id_list中移除旧的dataset_id添加新的dataset_id
if old_dataset_id in brand.dataset_id_list:
brand.dataset_id_list.remove(old_dataset_id)
if product.dataset_id and product.dataset_id not in brand.dataset_id_list:
brand.dataset_id_list.append(product.dataset_id)
brand.save(update_fields=['dataset_id_list', 'updated_at'])
def perform_destroy(self, instance):
# 软删除产品并从品牌的dataset_id_list中移除对应的ID
instance.is_active = False
instance.save()
brand = instance.brand
if instance.dataset_id in brand.dataset_id_list:
brand.dataset_id_list.remove(instance.dataset_id)
brand.save(update_fields=['dataset_id_list', 'updated_at'])
class CampaignViewSet(viewsets.ModelViewSet):
"""活动API视图集"""
queryset = Campaign.objects.filter(is_active=True)
serializer_class = CampaignSerializer
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
serializer = self.get_serializer(queryset, many=True)
return api_response(data=serializer.data)
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
self.perform_create(serializer)
return api_response(data=serializer.data)
return api_response(code=400, message="创建失败", data=serializer.errors)
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(instance)
return api_response(data=serializer.data)
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
if serializer.is_valid():
self.perform_update(serializer)
return api_response(data=serializer.data)
return api_response(code=400, message="更新失败", data=serializer.errors)
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
self.perform_destroy(instance)
return api_response(message="删除成功", data=None)
def perform_create(self, serializer):
# 创建活动时自动更新品牌的dataset_id_list
campaign = serializer.save()
brand = campaign.brand
# 确保dataset_id添加到品牌的dataset_id_list中
if campaign.dataset_id and campaign.dataset_id not in brand.dataset_id_list:
brand.dataset_id_list.append(campaign.dataset_id)
brand.save(update_fields=['dataset_id_list', 'updated_at'])
def perform_update(self, serializer):
# 获取原始活动信息
old_campaign = self.get_object()
old_dataset_id = old_campaign.dataset_id
# 保存更新后的活动
campaign = serializer.save()
brand = campaign.brand
# 从品牌的dataset_id_list中移除旧的dataset_id添加新的dataset_id
if old_dataset_id in brand.dataset_id_list:
brand.dataset_id_list.remove(old_dataset_id)
if campaign.dataset_id and campaign.dataset_id not in brand.dataset_id_list:
brand.dataset_id_list.append(campaign.dataset_id)
brand.save(update_fields=['dataset_id_list', 'updated_at'])
def perform_destroy(self, instance):
# 软删除活动并从品牌的dataset_id_list中移除对应的ID
instance.is_active = False
instance.save()
brand = instance.brand
if instance.dataset_id in brand.dataset_id_list:
brand.dataset_id_list.remove(instance.dataset_id)
brand.save(update_fields=['dataset_id_list', 'updated_at'])
@action(detail=True, methods=['post'])
def add_product(self, request, pk=None):
"""将产品添加到活动中"""
campaign = self.get_object()
product_id = request.data.get('product_id')
if not product_id:
return api_response(code=400, message="缺少产品ID", data=None)
try:
product = Product.objects.get(id=product_id, is_active=True)
campaign.link_product.add(product)
return api_response(message="产品添加成功", data=None)
except Product.DoesNotExist:
return api_response(code=404, message="产品不存在", data=None)
except Exception as e:
return api_response(code=500, message=f"添加产品失败: {str(e)}", data=None)
@action(detail=True, methods=['post'])
def remove_product(self, request, pk=None):
"""从活动中移除产品"""
campaign = self.get_object()
product_id = request.data.get('product_id')
if not product_id:
return api_response(code=400, message="缺少产品ID", data=None)
try:
product = Product.objects.get(id=product_id)
campaign.link_product.remove(product)
return api_response(message="产品移除成功", data=None)
except Product.DoesNotExist:
return api_response(code=404, message="产品不存在", data=None)
except Exception as e:
return api_response(code=500, message=f"移除产品失败: {str(e)}", data=None)
class BrandChatSessionViewSet(viewsets.ModelViewSet):
"""品牌聊天会话API视图集"""
queryset = BrandChatSession.objects.filter(is_active=True)
serializer_class = BrandChatSessionSerializer
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
serializer = self.get_serializer(queryset, many=True)
return api_response(data=serializer.data)
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
self.perform_create(serializer)
return api_response(data=serializer.data)
return api_response(code=400, message="创建失败", data=serializer.errors)
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(instance)
return api_response(data=serializer.data)
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
if serializer.is_valid():
self.perform_update(serializer)
return api_response(data=serializer.data)
return api_response(code=400, message="更新失败", data=serializer.errors)
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
self.perform_destroy(instance)
return api_response(message="删除成功", data=None)
def perform_create(self, serializer):
# 创建聊天会话时,可以设置使用特定品牌下的所有知识库
chat_session = serializer.save()
# 如果没有提供dataset_id_list则使用品牌的dataset_id_list
if not chat_session.dataset_id_list:
brand = chat_session.brand
chat_session.dataset_id_list = brand.dataset_id_list
chat_session.save(update_fields=['dataset_id_list', 'updated_at'])

View File

@ -1,329 +0,0 @@
# Discovery API 接口文档
## 简介
Discovery API是一个用于发现和搜索创作者的接口。它提供了创作者搜索、搜索会话管理等功能。所有API响应均遵循统一格式
```json
{
"code": 200, // 状态码200表示成功
"message": "操作成功", // 操作提示消息
"data": { // 实际数据
// 具体内容
}
}
```
## 使用Apifox测试接口
1. 下载并安装Apifox: https://www.apifox.cn/
2. 创建新项目,命名为"Creator Discovery"
3. 导入API或手动创建以下接口
## API 接口列表
### 1. 搜索创作者
- **URL**: `http://localhost:8000/api/discovery/creators/search/`
- **方法**: POST
- **描述**: 根据条件搜索创作者,并创建新的搜索会话
- **请求参数**:
```json
{
"query": "创作者",
"category": "Health",
"ecommerce_level": "L5",
"exposure_level": "KOL-2"
}
```
- **响应示例**:
```json
{
"code": 200,
"message": "搜索创作者成功",
"data": {
"id": 4,
"creators": [
{
"id": 37,
"name": "Mock Creator 5",
"avatar": null,
"category": "Health",
"ecommerce_level": "L5",
"exposure_level": "KOL-2",
"followers": 162.2,
"gmv": 534.1,
"items_sold": 18.1,
"avg_video_views": 1.9,
"has_ecommerce": false,
"tiktok_url": null,
"session": 4
}
],
"session_number": 4,
"creator_count": 1,
"shoppable_creators": 0,
"avg_followers": 162.2,
"avg_gmv": 534.1,
"avg_video_views": 1.9,
"date_created": "2023-10-02"
}
}
```
### 2. 获取搜索会话列表
- **URL**: `http://localhost:8000/api/discovery/sessions/`
- **方法**: GET
- **描述**: 获取所有搜索会话的历史记录
- **查询参数**:
- `page`: 页码默认为1
- `page_size`: 每页条数默认为20最大为100
- **响应示例**:
```json
{
"code": 200,
"message": "获取数据成功",
"data": {
"count": 3,
"next": "http://localhost:8000/api/discovery/sessions/?page=2",
"previous": null,
"results": [
{
"id": 1,
"session_number": 1,
"creator_count": 42,
"shoppable_creators": 24,
"avg_followers": 162.2,
"avg_gmv": 534.1,
"avg_video_views": 1.9,
"date_created": "2024-01-06"
},
{
"id": 2,
"session_number": 2,
"creator_count": 53,
"shoppable_creators": 13,
"avg_followers": 162.2,
"avg_gmv": 534.1,
"avg_video_views": 1.9,
"date_created": "2022-01-07"
}
]
}
}
```
### 3. 获取搜索会话详情
- **URL**: `http://localhost:8000/api/discovery/sessions/{id}/`
- **方法**: GET
- **描述**: 获取特定搜索会话的详细信息,包含该会话中的所有创作者
- **请求参数**: 路径参数 `id`
- **响应示例**:
```json
{
"code": 200,
"message": "获取搜索会话详情成功",
"data": {
"id": 1,
"creators": [
{
"id": 1,
"name": "Creator 1 in Session 1",
"avatar": null,
"category": "Phones & Electronics",
"ecommerce_level": "L2",
"exposure_level": "KOC-1",
"followers": 162.2,
"gmv": 534.1,
"items_sold": 18.1,
"avg_video_views": 1.9,
"has_ecommerce": true,
"tiktok_url": null,
"session": 1
},
{
"id": 2,
"name": "Creator 9 in Session 1",
"avatar": null,
"category": "Womenswear & Underwear",
"ecommerce_level": "L3",
"exposure_level": "KOL-3",
"followers": 162.2,
"gmv": 534.1,
"items_sold": 18.1,
"avg_video_views": 1.9,
"has_ecommerce": false,
"tiktok_url": null,
"session": 1
}
],
"session_number": 1,
"creator_count": 42,
"shoppable_creators": 24,
"avg_followers": 162.2,
"avg_gmv": 534.1,
"avg_video_views": 1.9,
"date_created": "2024-01-06"
}
}
```
### 4. 获取会话中的创作者
- **URL**: `http://localhost:8000/api/discovery/sessions/{id}/results/`
- **方法**: GET
- **描述**: 获取特定会话中的所有创作者
- **请求参数**:
- 路径参数 `id`
- 查询参数 `page`、`page_size`(分页参数)
- **响应示例**:
```json
{
"code": 200,
"message": "获取数据成功",
"data": {
"count": 42,
"next": "http://localhost:8000/api/discovery/sessions/1/results/?page=2",
"previous": null,
"results": [
{
"id": 1,
"name": "Creator 1 in Session 1",
"avatar": null,
"category": "Phones & Electronics",
"ecommerce_level": "L2",
"exposure_level": "KOC-1",
"followers": 162.2,
"gmv": 534.1,
"items_sold": 18.1,
"avg_video_views": 1.9,
"has_ecommerce": true,
"tiktok_url": null,
"session": 1
},
{
"id": 2,
"name": "Creator 9 in Session 1",
"avatar": null,
"category": "Womenswear & Underwear",
"ecommerce_level": "L3",
"exposure_level": "KOL-3",
"followers": 162.2,
"gmv": 534.1,
"items_sold": 18.1,
"avg_video_views": 1.9,
"has_ecommerce": false,
"tiktok_url": null,
"session": 1
}
]
}
}
```
### 5. 获取所有创作者
- **URL**: `http://localhost:8000/api/discovery/creators/`
- **方法**: GET
- **描述**: 获取系统中的所有创作者
- **请求参数**: 查询参数 `page`、`page_size`(分页参数)
- **响应示例**:
```json
{
"code": 200,
"message": "获取数据成功",
"data": {
"count": 150,
"next": "http://localhost:8000/api/discovery/creators/?page=2",
"previous": null,
"results": [
{
"id": 1,
"name": "Creator 1 in Session 1",
"avatar": null,
"category": "Phones & Electronics",
"ecommerce_level": "L2",
"exposure_level": "KOC-1",
"followers": 162.2,
"gmv": 534.1,
"items_sold": 18.1,
"avg_video_views": 1.9,
"has_ecommerce": true,
"tiktok_url": null,
"session": 1
},
// 更多创作者...
]
}
}
```
### 6. 获取创作者详情
- **URL**: `http://localhost:8000/api/discovery/creators/{id}/`
- **方法**: GET
- **描述**: 获取特定创作者的详细信息
- **请求参数**: 路径参数 `id`
- **响应示例**:
```json
{
"code": 200,
"message": "获取创作者详情成功",
"data": {
"id": 1,
"name": "Creator 1 in Session 1",
"avatar": null,
"category": "Phones & Electronics",
"ecommerce_level": "L2",
"exposure_level": "KOC-1",
"followers": 162.2,
"gmv": 534.1,
"items_sold": 18.1,
"avg_video_views": 1.9,
"has_ecommerce": true,
"tiktok_url": null,
"session": 1
}
}
```
## 在Apifox中设置环境变量
创建一个名为"本地开发环境"的环境,设置以下变量:
- `baseUrl`: `http://localhost:8000/api`
这样可以在所有请求中使用`{{baseUrl}}/discovery/...`来替代完整URL。
## 测试流程示例
1. 启动Django服务器`python manage.py runserver`
2. 在Apifox中发送请求"获取搜索会话列表"
3. 在响应中选择一个会话ID
4. 使用该ID获取会话详情
5. 使用"搜索创作者"接口创建新的搜索会话
6. 验证新创建的会话是否出现在会话列表中
## 在Apifox中导入API
1. 在Apifox中点击"导入"按钮
2. 选择"导入API"
3. 将本文档中的API信息整理成集合导入
4. 设置每个接口的请求和响应格式
5. 设置示例响应
## 注意事项
- 所有API响应均遵循统一的格式`code`、`message`和`data`
- 分页接口返回格式为:`{ "code": 200, "message": "获取数据成功", "data": { "count": 总数, "next": 下一页URL, "previous": 上一页URL, "results": [] } }`
- 即使发生错误HTTP状态码始终为200错误信息在响应体的`code`和`message`中提供
- 接口不需要认证,可直接访问

View File

@ -1,3 +0,0 @@
"""
Discovery app for creator discovery and search.
"""

View File

@ -1,18 +0,0 @@
from django.contrib import admin
from .models import SearchSession, Creator
@admin.register(SearchSession)
class SearchSessionAdmin(admin.ModelAdmin):
list_display = ('session_number', 'creator_count', 'shoppable_creators',
'avg_followers', 'avg_gmv', 'avg_video_views', 'date_created')
search_fields = ('session_number',)
list_filter = ('date_created',)
@admin.register(Creator)
class CreatorAdmin(admin.ModelAdmin):
list_display = ('name', 'category', 'ecommerce_level', 'exposure_level',
'followers', 'gmv', 'avg_video_views', 'has_ecommerce')
search_fields = ('name', 'category')
list_filter = ('category', 'ecommerce_level', 'exposure_level', 'has_ecommerce')

View File

@ -1,7 +0,0 @@
from django.apps import AppConfig
class DiscoveryConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.discovery'
verbose_name = 'Creator Discovery'

View File

@ -1,31 +0,0 @@
from rest_framework.views import exception_handler
from rest_framework.response import Response
def custom_exception_handler(exc, context):
"""
自定义异常处理
"""
# 先调用REST framework默认的异常处理方法获得标准错误响应对象
response = exception_handler(exc, context)
# 如果response为None说明REST framework无法处理该异常
# 我们依然返回None让Django处理该异常
if response is None:
return None
# 定制响应格式
error_data = {
'code': response.status_code,
'message': str(exc),
'data': None
}
# 如果是验证错误,取出错误详情
if hasattr(exc, 'detail'):
error_data['message'] = str(exc.detail)
if isinstance(exc.detail, dict):
error_data['data'] = exc.detail
# 统一返回200状态码将实际错误码放入响应体
return Response(error_data, status=200)

View File

@ -1,3 +0,0 @@
"""
Discovery app management commands.
"""

View File

@ -1,3 +0,0 @@
"""
Discovery app management commands.
"""

View File

@ -1,73 +0,0 @@
import random
from django.core.management.base import BaseCommand
from django.utils import timezone
from apps.discovery.models import SearchSession, Creator
class Command(BaseCommand):
help = '创建模拟的Discovery数据'
def handle(self, *args, **kwargs):
# 创建3个搜索会话
sessions = []
for i in range(1, 4):
creator_count = random.randint(20, 100)
shoppable_creators = random.randint(3, 26)
# 创建不同的日期
if i == 1:
date = timezone.datetime(2024, 1, 6).date()
elif i == 2:
date = timezone.datetime(2022, 1, 7).date()
else:
date = timezone.datetime(2023, 4, 27).date()
session = SearchSession.objects.create(
session_number=i,
creator_count=creator_count,
shoppable_creators=shoppable_creators,
avg_followers=162.2,
avg_gmv=534.1,
avg_video_views=1.9,
date_created=date
)
sessions.append(session)
self.stdout.write(self.style.SUCCESS(f'创建会话: {session}'))
# 创建创作者数据
categories = [
'Phones & Electronics',
'Womenswear & Underwear',
'Sports & Outdoor',
'Food & Beverage',
'Health',
'Kitchenware',
'Furniture',
'Shoes',
'Home Supplies',
]
ecommerce_levels = ['L1', 'L2', 'L3', 'L4', 'L5', 'New tag']
exposure_levels = ['KOC-1', 'KOC-2', 'KOL-2', 'KOL-3', 'New tag']
for session in sessions:
# 每个会话创建随机数量的创作者
creator_count = random.randint(10, 20)
for i in range(creator_count):
has_ecommerce = random.choice([True, False])
creator = Creator.objects.create(
session=session,
name=f"Creator {i+1} in Session {session.session_number}",
category=random.choice(categories),
ecommerce_level=random.choice(ecommerce_levels),
exposure_level=random.choice(exposure_levels),
followers=162.2,
gmv=534.1,
items_sold=18.1,
avg_video_views=1.9,
has_ecommerce=has_ecommerce
)
self.stdout.write(f'创建创作者: {creator.name}')
self.stdout.write(self.style.SUCCESS('成功创建所有模拟数据!'))

View File

@ -1,57 +0,0 @@
# Generated by Django 5.2 on 2025-05-16 02:40
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='SearchSession',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('session_number', models.IntegerField(default=1, verbose_name='会话编号')),
('creator_count', models.IntegerField(default=0, verbose_name='创作者数量')),
('shoppable_creators', models.IntegerField(default=0, verbose_name='可购物创作者数量')),
('avg_followers', models.FloatField(default=0, verbose_name='平均粉丝数')),
('avg_gmv', models.FloatField(default=0, verbose_name='平均GMV')),
('avg_video_views', models.FloatField(default=0, verbose_name='平均视频观看量')),
('date_created', models.DateField(default=django.utils.timezone.now, verbose_name='创建日期')),
],
options={
'verbose_name': '搜索会话',
'verbose_name_plural': '搜索会话',
'ordering': ['-date_created'],
},
),
migrations.CreateModel(
name='Creator',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='创作者名称')),
('avatar', models.URLField(blank=True, null=True, verbose_name='头像URL')),
('category', models.CharField(choices=[('Phones & Electronics', 'Phones & Electronics'), ('Womenswear & Underwear', 'Womenswear & Underwear'), ('Sports & Outdoor', 'Sports & Outdoor'), ('Food & Beverage', 'Food & Beverage'), ('Health', 'Health'), ('Kitchenware', 'Kitchenware'), ('Furniture', 'Furniture'), ('Shoes', 'Shoes'), ('Home Supplies', 'Home Supplies')], max_length=50, verbose_name='类别')),
('ecommerce_level', models.CharField(choices=[('L1', 'Level 1'), ('L2', 'Level 2'), ('L3', 'Level 3'), ('L4', 'Level 4'), ('L5', 'Level 5'), ('New tag', 'New Tag')], max_length=10, verbose_name='电商等级')),
('exposure_level', models.CharField(choices=[('KOC-1', 'KOC-1'), ('KOC-2', 'KOC-2'), ('KOL-2', 'KOL-2'), ('KOL-3', 'KOL-3'), ('New tag', 'New Tag')], max_length=10, verbose_name='曝光等级')),
('followers', models.FloatField(default=0, verbose_name='粉丝数')),
('gmv', models.FloatField(default=0, verbose_name='GMV')),
('items_sold', models.FloatField(default=0, verbose_name='销售项目数')),
('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='抖音链接')),
('session', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='creators', to='discovery.searchsession', verbose_name='所属会话')),
],
options={
'verbose_name': '创作者',
'verbose_name_plural': '创作者',
'ordering': ['name'],
},
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 5.2 on 2025-05-16 03:12
import apps.discovery.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('discovery', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='searchsession',
name='date_created',
field=models.DateField(default=apps.discovery.models.get_current_date, verbose_name='创建日期'),
),
]

View File

@ -1,79 +0,0 @@
from django.db import models
from django.utils import timezone
def get_current_date():
"""返回当前日期(非日期时间)"""
return timezone.now().date()
class SearchSession(models.Model):
"""搜索历史记录"""
session_number = models.IntegerField(default=1, verbose_name="会话编号")
creator_count = models.IntegerField(default=0, verbose_name="创作者数量")
shoppable_creators = models.IntegerField(default=0, verbose_name="可购物创作者数量")
avg_followers = models.FloatField(default=0, verbose_name="平均粉丝数")
avg_gmv = models.FloatField(default=0, verbose_name="平均GMV")
avg_video_views = models.FloatField(default=0, verbose_name="平均视频观看量")
date_created = models.DateField(default=get_current_date, verbose_name="创建日期")
class Meta:
verbose_name = "搜索会话"
verbose_name_plural = "搜索会话"
ordering = ['-date_created']
def __str__(self):
return f"会话 {self.session_number} - {self.date_created}"
class Creator(models.Model):
"""创作者信息"""
ECOMMERCE_LEVELS = [
('L1', 'Level 1'),
('L2', 'Level 2'),
('L3', 'Level 3'),
('L4', 'Level 4'),
('L5', 'Level 5'),
('New tag', 'New Tag'),
]
EXPOSURE_LEVELS = [
('KOC-1', 'KOC-1'),
('KOC-2', 'KOC-2'),
('KOL-2', 'KOL-2'),
('KOL-3', 'KOL-3'),
('New tag', 'New Tag'),
]
CATEGORIES = [
('Phones & Electronics', 'Phones & Electronics'),
('Womenswear & Underwear', 'Womenswear & Underwear'),
('Sports & Outdoor', 'Sports & Outdoor'),
('Food & Beverage', 'Food & Beverage'),
('Health', 'Health'),
('Kitchenware', 'Kitchenware'),
('Furniture', 'Furniture'),
('Shoes', 'Shoes'),
('Home Supplies', 'Home Supplies'),
]
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")
category = models.CharField(max_length=50, choices=CATEGORIES, verbose_name="类别")
ecommerce_level = models.CharField(max_length=10, choices=ECOMMERCE_LEVELS, verbose_name="电商等级")
exposure_level = models.CharField(max_length=10, choices=EXPOSURE_LEVELS, verbose_name="曝光等级")
followers = models.FloatField(default=0, verbose_name="粉丝数")
gmv = models.FloatField(default=0, verbose_name="GMV")
items_sold = models.FloatField(default=0, verbose_name="销售项目数")
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="抖音链接")
class Meta:
verbose_name = "创作者"
verbose_name_plural = "创作者"
ordering = ['name']
def __str__(self):
return self.name

View File

@ -1,22 +0,0 @@
from rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response
class StandardResultsSetPagination(PageNumberPagination):
"""标准分页器"""
page_size = 20
page_size_query_param = 'page_size'
max_page_size = 100
def get_paginated_response(self, data):
"""自定义分页响应格式"""
return Response({
"code": 200,
"message": "获取数据成功",
"data": {
"count": self.page.paginator.count,
"next": self.get_next_link(),
"previous": self.get_previous_link(),
"results": data
}
})

View File

@ -1,35 +0,0 @@
from rest_framework import serializers
from .models import SearchSession, Creator
class CreatorSerializer(serializers.ModelSerializer):
"""创作者序列化器"""
class Meta:
model = Creator
fields = '__all__'
class CreatorDetailSerializer(serializers.ModelSerializer):
"""创作者详细信息序列化器"""
class Meta:
model = Creator
fields = '__all__'
class SearchSessionSerializer(serializers.ModelSerializer):
"""搜索会话序列化器"""
class Meta:
model = SearchSession
fields = '__all__'
class SearchSessionDetailSerializer(serializers.ModelSerializer):
"""搜索会话详细信息序列化器,包含创作者数据"""
creators = CreatorSerializer(many=True, read_only=True)
class Meta:
model = SearchSession
fields = '__all__'

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -1,12 +0,0 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import SearchSessionViewSet, CreatorDiscoveryViewSet
router = DefaultRouter()
router.register(r'sessions', SearchSessionViewSet)
router.register(r'creators', CreatorDiscoveryViewSet)
urlpatterns = [
path('', include(router.urls)),
]

View File

@ -1,276 +0,0 @@
from django.shortcuts import render
from django.db.models import Q
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import AllowAny
from django.utils import timezone
from .models import SearchSession, Creator
from .serializers import (
SearchSessionSerializer,
SearchSessionDetailSerializer,
CreatorSerializer,
CreatorDetailSerializer
)
from .pagination import StandardResultsSetPagination
class ApiResponse:
"""API统一响应格式"""
@staticmethod
def success(data=None, message="操作成功"):
return Response({
"code": 200,
"message": message,
"data": data
})
@staticmethod
def error(message="操作失败", code=400, data=None):
return Response({
"code": code,
"message": message,
"data": data
}, status=status.HTTP_200_OK) # 始终返回200状态码错误信息在内容中提供
class SearchSessionViewSet(viewsets.ModelViewSet):
"""搜索会话视图集"""
queryset = SearchSession.objects.all()
serializer_class = SearchSessionSerializer
permission_classes = [AllowAny]
pagination_class = StandardResultsSetPagination
def get_serializer_class(self):
if self.action == 'retrieve':
return SearchSessionDetailSerializer
return SearchSessionSerializer
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return ApiResponse.success(serializer.data, "获取搜索会话列表成功")
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(instance)
return ApiResponse.success(serializer.data, "获取搜索会话详情成功")
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return ApiResponse.success(serializer.data, "创建搜索会话成功")
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
return ApiResponse.success(serializer.data, "更新搜索会话成功")
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
self.perform_destroy(instance)
return ApiResponse.success(None, "删除搜索会话成功")
@action(detail=True, methods=['get'])
def results(self, request, pk=None):
"""获取指定会话的搜索结果"""
session = self.get_object()
creators = session.creators.all()
page = self.paginate_queryset(creators)
if page is not None:
serializer = CreatorSerializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = CreatorSerializer(creators, many=True)
return ApiResponse.success(serializer.data, "获取会话创作者列表成功")
class CreatorDiscoveryViewSet(viewsets.ReadOnlyModelViewSet):
"""创作者发现视图集"""
queryset = Creator.objects.all()
serializer_class = CreatorSerializer
permission_classes = [AllowAny]
pagination_class = StandardResultsSetPagination
def get_serializer_class(self):
if self.action == 'retrieve':
return CreatorDetailSerializer
return CreatorSerializer
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return ApiResponse.success(serializer.data, "获取创作者列表成功")
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(instance)
return ApiResponse.success(serializer.data, "获取创作者详情成功")
@action(detail=False, methods=['post'])
def search(self, request):
"""搜索创作者"""
query = request.data.get('query', '')
category = request.data.get('category', None)
ecommerce_level = request.data.get('ecommerce_level', None)
exposure_level = request.data.get('exposure_level', None)
# 创建模拟搜索会话
session = self._create_mock_search_session()
# 生成模拟搜索结果
creators = self._generate_mock_creators(session, query, category, ecommerce_level, exposure_level)
# 返回会话详情
serializer = SearchSessionDetailSerializer(session)
return ApiResponse.success(serializer.data, "搜索创作者成功")
def _create_mock_search_session(self):
"""创建模拟搜索会话"""
# 获取当前最大会话编号并加1
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
# 创建新会话
session = SearchSession.objects.create(
session_number=session_number,
creator_count=100,
shoppable_creators=26,
avg_followers=162.2,
avg_gmv=534.1,
avg_video_views=1.9,
date_created=timezone.now().date() # 将datetime转换为date类型
)
return session
def _generate_mock_creators(self, session, query, category=None, ecommerce_level=None, exposure_level=None):
"""生成模拟创作者数据"""
# 模拟数据 - 这里可以根据实际需求调整
mock_creators = [
{
"name": "Mock Creator 1",
"category": "Phones & Electronics",
"ecommerce_level": "L2",
"exposure_level": "KOC-1",
"followers": 162.2,
"gmv": 534.1,
"items_sold": 18.1,
"avg_video_views": 1.9,
"has_ecommerce": True
},
{
"name": "Mock Creator 2",
"category": "Womenswear & Underwear",
"ecommerce_level": "L3",
"exposure_level": "KOL-3",
"followers": 162.2,
"gmv": 534.1,
"items_sold": 18.1,
"avg_video_views": 1.9,
"has_ecommerce": False
},
{
"name": "Mock Creator 3",
"category": "Sports & Outdoor",
"ecommerce_level": "L4",
"exposure_level": "KOC-2",
"followers": 162.2,
"gmv": 534.1,
"items_sold": 18.1,
"avg_video_views": 1.9,
"has_ecommerce": True
},
{
"name": "Mock Creator 4",
"category": "Food & Beverage",
"ecommerce_level": "L1",
"exposure_level": "KOC-2",
"followers": 162.2,
"gmv": 534.1,
"items_sold": 18.1,
"avg_video_views": 1.9,
"has_ecommerce": True
},
{
"name": "Mock Creator 5",
"category": "Health",
"ecommerce_level": "L5",
"exposure_level": "KOL-2",
"followers": 162.2,
"gmv": 534.1,
"items_sold": 18.1,
"avg_video_views": 1.9,
"has_ecommerce": False
},
{
"name": "Mock Creator 6",
"category": "Kitchenware",
"ecommerce_level": "New tag",
"exposure_level": "New tag",
"followers": 162.2,
"gmv": 534.1,
"items_sold": 18.1,
"avg_video_views": 1.9,
"has_ecommerce": True
},
{
"name": "Mock Creator 7",
"category": "Furniture",
"ecommerce_level": "New tag",
"exposure_level": "New tag",
"followers": 162.2,
"gmv": 534.1,
"items_sold": 18.1,
"avg_video_views": 1.9,
"has_ecommerce": False
},
{
"name": "Mock Creator 8",
"category": "Shoes",
"ecommerce_level": "New tag",
"exposure_level": "New tag",
"followers": 162.2,
"gmv": 534.1,
"items_sold": 18.1,
"avg_video_views": 1.9,
"has_ecommerce": True
},
]
# 根据查询条件过滤模拟数据
filtered_creators = mock_creators
if category:
filtered_creators = [c for c in filtered_creators if c['category'] == category]
if ecommerce_level:
filtered_creators = [c for c in filtered_creators if c['ecommerce_level'] == ecommerce_level]
if exposure_level:
filtered_creators = [c for c in filtered_creators if c['exposure_level'] == exposure_level]
# 创建创作者记录
for creator_data in filtered_creators:
Creator.objects.create(
session=session,
**creator_data
)
# 更新会话统计信息
session.creator_count = len(filtered_creators)
session.shoppable_creators = len([c for c in filtered_creators if c['has_ecommerce']])
session.save()
return filtered_creators

View File

View File

@ -1,7 +0,0 @@
from django.apps import AppConfig
class TemplateConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.template'
verbose_name = '模板管理'

View File

@ -1,59 +0,0 @@
from rest_framework.views import exception_handler
from rest_framework.exceptions import APIException
from rest_framework import status
from django.http import Http404
from django.core.exceptions import ValidationError
from django.db.utils import IntegrityError
from rest_framework.response import Response
def custom_exception_handler(exc, context):
"""
自定义异常处理器将所有异常转换为标准响应格式
Args:
exc: 异常对象
context: 异常上下文
Returns:
标准格式的Response对象
"""
response = exception_handler(exc, context)
if response is not None:
# 已经被DRF处理的异常转换为标准格式
return Response({
'code': response.status_code,
'message': str(exc),
'data': response.data if hasattr(response, 'data') else None
}, status=response.status_code)
# 如果是Django的404错误
if isinstance(exc, Http404):
return Response({
'code': status.HTTP_404_NOT_FOUND,
'message': '请求的资源不存在',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
# 如果是验证错误
if isinstance(exc, ValidationError):
return Response({
'code': status.HTTP_400_BAD_REQUEST,
'message': '数据验证失败',
'data': str(exc) if str(exc) else '提供的数据无效'
}, status=status.HTTP_400_BAD_REQUEST)
# 如果是数据库完整性错误(如唯一约束)
if isinstance(exc, IntegrityError):
return Response({
'code': status.HTTP_400_BAD_REQUEST,
'message': '数据库完整性错误',
'data': str(exc)
}, status=status.HTTP_400_BAD_REQUEST)
# 其他未处理的异常
return Response({
'code': status.HTTP_500_INTERNAL_SERVER_ERROR,
'message': '服务器内部错误',
'data': str(exc) if str(exc) else None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

View File

@ -1,26 +0,0 @@
import django_filters
from .models import Template
class TemplateFilter(django_filters.FilterSet):
"""模板过滤器"""
title = django_filters.CharFilter(field_name='title', lookup_expr='icontains')
content = django_filters.CharFilter(field_name='content', lookup_expr='icontains')
mission = django_filters.CharFilter(field_name='mission')
platform = django_filters.CharFilter(field_name='platform')
collaboration_type = django_filters.CharFilter(field_name='collaboration_type')
service = django_filters.CharFilter(field_name='service')
category = django_filters.NumberFilter(field_name='category__id')
category_name = django_filters.CharFilter(field_name='category__name', lookup_expr='icontains')
created_by = django_filters.NumberFilter(field_name='created_by__id')
is_public = django_filters.BooleanFilter(field_name='is_public')
created_after = django_filters.DateTimeFilter(field_name='created_at', lookup_expr='gte')
created_before = django_filters.DateTimeFilter(field_name='created_at', lookup_expr='lte')
class Meta:
model = Template
fields = [
'title', 'content', 'mission', 'platform',
'collaboration_type', 'service', 'category',
'category_name', 'created_by', 'is_public',
'created_after', 'created_before'
]

View File

@ -1,53 +0,0 @@
# Generated by Django 5.2 on 2025-05-19 04:13
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='TemplateCategory',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='分类名称')),
('description', 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='更新时间')),
],
options={
'verbose_name': '模板分类',
'verbose_name_plural': '模板分类',
},
),
migrations.CreateModel(
name='Template',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200, verbose_name='模板标题')),
('content', models.TextField(verbose_name='模板内容')),
('preview', models.TextField(blank=True, null=True, verbose_name='内容预览')),
('mission', models.CharField(choices=[('initial_contact', '初步联系'), ('follow_up', '跟进'), ('negotiation', '谈判'), ('closing', '成交'), ('other', '其他')], default='initial_contact', max_length=50, verbose_name='任务类型')),
('platform', models.CharField(choices=[('tiktok', 'TikTok'), ('instagram', 'Instagram'), ('youtube', 'YouTube'), ('facebook', 'Facebook'), ('twitter', 'Twitter'), ('other', '其他')], default='tiktok', max_length=50, verbose_name='平台')),
('collaboration_type', models.CharField(choices=[('paid_promotion', '付费推广'), ('affiliate', '联盟营销'), ('sponsored_content', '赞助内容'), ('brand_ambassador', '品牌大使'), ('other', '其他')], default='paid_promotion', max_length=50, verbose_name='合作模式')),
('service', models.CharField(choices=[('voice', '声优 - 交谈'), ('text', '文本'), ('video', '视频'), ('image', '图片'), ('other', '其他')], default='text', max_length=50, verbose_name='服务类型')),
('is_public', 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='更新时间')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='created_templates', to=settings.AUTH_USER_MODEL, verbose_name='创建者')),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='templates', to='template.templatecategory', verbose_name='模板分类')),
],
options={
'verbose_name': '模板',
'verbose_name_plural': '模板',
},
),
]

View File

@ -1,17 +0,0 @@
# Generated by Django 5.2 on 2025-05-20 09:21
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('template', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='template',
name='created_by',
),
]

View File

@ -1,77 +0,0 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
class TemplateCategory(models.Model):
"""模板分类模型"""
name = models.CharField(_('分类名称'), max_length=100)
description = models.TextField(_('分类描述'), blank=True, null=True)
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 self.name
class Template(models.Model):
"""模板模型"""
MISSION_CHOICES = [
('initial_contact', '初步联系'),
('follow_up', '跟进'),
('negotiation', '谈判'),
('closing', '成交'),
('other', '其他'),
]
PLATFORM_CHOICES = [
('tiktok', 'TikTok'),
('instagram', 'Instagram'),
('youtube', 'YouTube'),
('facebook', 'Facebook'),
('twitter', 'Twitter'),
('other', '其他'),
]
COLLABORATION_CHOICES = [
('paid_promotion', '付费推广'),
('affiliate', '联盟营销'),
('sponsored_content', '赞助内容'),
('brand_ambassador', '品牌大使'),
('other', '其他'),
]
SERVICE_CHOICES = [
('voice', '声优 - 交谈'),
('text', '文本'),
('video', '视频'),
('image', '图片'),
('other', '其他'),
]
title = models.CharField(_('模板标题'), max_length=200)
content = models.TextField(_('模板内容'))
preview = models.TextField(_('内容预览'), blank=True, null=True)
category = models.ForeignKey(TemplateCategory, on_delete=models.CASCADE, related_name='templates', verbose_name=_('模板分类'))
mission = models.CharField(_('任务类型'), max_length=50, choices=MISSION_CHOICES, default='initial_contact')
platform = models.CharField(_('平台'), max_length=50, choices=PLATFORM_CHOICES, default='tiktok')
collaboration_type = models.CharField(_('合作模式'), max_length=50, choices=COLLABORATION_CHOICES, default='paid_promotion')
service = models.CharField(_('服务类型'), max_length=50, choices=SERVICE_CHOICES, default='text')
is_public = models.BooleanField(_('是否公开'), default=True)
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 self.title
def save(self, *args, **kwargs):
# 自动生成内容预览
if self.content and not self.preview:
# 截取前100个字符作为预览
self.preview = self.content[:100] + ('...' if len(self.content) > 100 else '')
super().save(*args, **kwargs)

View File

@ -1,37 +0,0 @@
from rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response
class StandardResultsSetPagination(PageNumberPagination):
"""标准分页器,支持标准响应格式"""
page_size = 10
page_size_query_param = 'page_size'
max_page_size = 100
def get_paginated_response(self, data):
"""
返回标准格式的分页响应
Args:
data: 已经封装为标准格式的响应数据
"""
# 如果data已经是标准格式{code, message, data}则用data['data']取出实际数据
actual_data = data.get('data') if isinstance(data, dict) and 'data' in data else data
# 准备分页元数据
pagination_data = {
'count': self.page.paginator.count,
'next': self.get_next_link(),
'previous': self.get_previous_link(),
'results': actual_data,
'total_pages': self.page.paginator.num_pages,
'current_page': self.page.number,
}
# 如果data是标准格式则保持原有message和code否则使用默认值
response_data = {
'code': data.get('code', 200) if isinstance(data, dict) and 'code' in data else 200,
'message': data.get('message', '获取数据成功') if isinstance(data, dict) and 'message' in data else '获取数据成功',
'data': pagination_data
}
return Response(response_data)

View File

@ -1,104 +0,0 @@
from rest_framework import serializers
from .models import Template, TemplateCategory
class TemplateCategorySerializer(serializers.ModelSerializer):
"""模板分类序列化器"""
class Meta:
model = TemplateCategory
fields = ['id', 'name', 'description', 'created_at', 'updated_at']
read_only_fields = ['created_at', 'updated_at']
class TemplateListSerializer(serializers.ModelSerializer):
"""模板列表序列化器(简化版)"""
category_name = serializers.CharField(source='category.name', read_only=True)
mission_display = serializers.CharField(source='get_mission_display', read_only=True)
platform_display = serializers.CharField(source='get_platform_display', read_only=True)
collaboration_type_display = serializers.CharField(source='get_collaboration_type_display', read_only=True)
service_display = serializers.CharField(source='get_service_display', read_only=True)
class Meta:
model = Template
fields = [
'id', 'title', 'preview', 'category_name',
'mission', 'mission_display',
'platform', 'platform_display',
'service', 'service_display',
'collaboration_type', 'collaboration_type_display',
'is_public', 'created_at', 'updated_at'
]
read_only_fields = ['created_at', 'updated_at', 'preview']
class TemplateDetailSerializer(serializers.ModelSerializer):
"""模板详情序列化器"""
category = TemplateCategorySerializer(read_only=True)
category_id = serializers.IntegerField(write_only=True, required=False)
mission_display = serializers.CharField(source='get_mission_display', read_only=True)
platform_display = serializers.CharField(source='get_platform_display', read_only=True)
collaboration_type_display = serializers.CharField(source='get_collaboration_type_display', read_only=True)
service_display = serializers.CharField(source='get_service_display', read_only=True)
class Meta:
model = Template
fields = [
'id', 'title', 'content', 'preview',
'category', 'category_id',
'mission', 'mission_display',
'platform', 'platform_display',
'service', 'service_display',
'collaboration_type', 'collaboration_type_display',
'is_public', 'created_at', 'updated_at'
]
read_only_fields = ['created_at', 'updated_at', 'preview']
def create(self, validated_data):
"""创建模板"""
# 处理category_id字段
category_id = validated_data.pop('category_id', None)
if category_id:
try:
category = TemplateCategory.objects.get(id=category_id)
validated_data['category'] = category
except TemplateCategory.DoesNotExist:
# 如果分类不存在,创建一个默认分类
category = TemplateCategory.objects.create(name="默认分类")
validated_data['category'] = category
return super().create(validated_data)
class TemplateCreateUpdateSerializer(serializers.ModelSerializer):
"""模板创建和更新序列化器"""
class Meta:
model = Template
fields = [
'id', 'title', 'content',
'category', 'mission', 'platform',
'service', 'collaboration_type',
'is_public'
]
read_only_fields = ['preview']
def validate(self, data):
"""验证数据,处理测试期间可能缺失的字段"""
# 处理category字段确保有有效的分类
if 'category' not in data:
# 获取或创建默认分类
category, created = TemplateCategory.objects.get_or_create(name="默认分类")
data['category'] = category
# 确保其他必填字段有默认值
if 'mission' not in data:
data['mission'] = 'initial_contact'
if 'platform' not in data:
data['platform'] = 'tiktok'
if 'service' not in data:
data['service'] = 'text'
if 'collaboration_type' not in data:
data['collaboration_type'] = 'paid_promotion'
if 'is_public' not in data:
data['is_public'] = True
return data

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -1,11 +0,0 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import TemplateViewSet, TemplateCategoryViewSet
router = DefaultRouter()
router.register(r'categories', TemplateCategoryViewSet)
router.register(r'', TemplateViewSet)
urlpatterns = [
path('', include(router.urls)),
]

View File

@ -1,36 +0,0 @@
from rest_framework.response import Response
class ApiResponse:
"""API标准响应格式工具类"""
@staticmethod
def success(data=None, message="操作成功", code=200):
"""
返回成功响应
Args:
data: 响应数据
message: 成功消息
code: 状态码
"""
return Response({
"code": code,
"message": message,
"data": data
})
@staticmethod
def error(message="操作失败", code=400, data=None):
"""
返回错误响应
Args:
message: 错误消息
code: 错误状态码
data: 额外数据
"""
return Response({
"code": code,
"message": message,
"data": data
}, status=code)

View File

@ -1,297 +0,0 @@
from django.shortcuts import render
from rest_framework import viewsets, permissions, status, filters
from rest_framework.response import Response
from rest_framework.decorators import action
from django_filters.rest_framework import DjangoFilterBackend
from .models import Template, TemplateCategory
from .serializers import (
TemplateListSerializer,
TemplateDetailSerializer,
TemplateCreateUpdateSerializer,
TemplateCategorySerializer
)
from .filters import TemplateFilter
from .utils import ApiResponse
from .pagination import StandardResultsSetPagination
# Create your views here.
class TemplateCategoryViewSet(viewsets.ModelViewSet):
"""
模板分类视图集
提供模板分类的增删改查功能
"""
queryset = TemplateCategory.objects.all()
serializer_class = TemplateCategorySerializer
permission_classes = [permissions.AllowAny] # 允许所有人访问
pagination_class = StandardResultsSetPagination
def list(self, request, *args, **kwargs):
"""获取所有模板分类"""
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return ApiResponse.success(
data=serializer.data,
message="获取模板分类列表成功"
)
def retrieve(self, request, *args, **kwargs):
"""获取单个模板分类详情"""
instance = self.get_object()
serializer = self.get_serializer(instance)
return ApiResponse.success(
data=serializer.data,
message="获取模板分类详情成功"
)
def create(self, request, *args, **kwargs):
"""创建模板分类"""
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
self.perform_create(serializer)
return ApiResponse.success(
data=serializer.data,
message="模板分类创建成功",
code=status.HTTP_201_CREATED
)
return ApiResponse.error(
message="模板分类创建失败",
data=serializer.errors,
code=status.HTTP_400_BAD_REQUEST
)
def update(self, request, *args, **kwargs):
"""更新模板分类"""
partial = kwargs.pop('partial', False)
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
if serializer.is_valid():
self.perform_update(serializer)
return ApiResponse.success(
data=serializer.data,
message="模板分类更新成功"
)
return ApiResponse.error(
message="模板分类更新失败",
data=serializer.errors,
code=status.HTTP_400_BAD_REQUEST
)
def destroy(self, request, *args, **kwargs):
"""删除模板分类"""
instance = self.get_object()
self.perform_destroy(instance)
return ApiResponse.success(
data=None,
message="模板分类删除成功"
)
class TemplateViewSet(viewsets.ModelViewSet):
"""
模板视图集
提供模板的增删改查功能
"""
queryset = Template.objects.all()
permission_classes = [permissions.AllowAny] # 允许所有人访问
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_class = TemplateFilter
search_fields = ['title', 'content']
ordering_fields = ['created_at', 'updated_at', 'title']
ordering = ['-created_at']
pagination_class = StandardResultsSetPagination
def get_queryset(self):
"""
自定义查询集返回所有模板
"""
return Template.objects.all()
def get_serializer_class(self):
"""根据不同的操作返回不同的序列化器"""
if self.action == 'list':
return TemplateListSerializer
elif self.action in ['create', 'update', 'partial_update']:
return TemplateCreateUpdateSerializer
return TemplateDetailSerializer
def list(self, request, *args, **kwargs):
"""获取所有模板"""
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return ApiResponse.success(
data=serializer.data,
message="获取模板列表成功"
)
def retrieve(self, request, *args, **kwargs):
"""获取单个模板详情"""
instance = self.get_object()
serializer = self.get_serializer(instance)
return ApiResponse.success(
data=serializer.data,
message="获取模板详情成功"
)
def create(self, request, *args, **kwargs):
"""创建模板"""
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
self.perform_create(serializer)
return ApiResponse.success(
data=serializer.data,
message="模板创建成功",
code=status.HTTP_201_CREATED
)
return ApiResponse.error(
message="模板创建失败",
data=serializer.errors,
code=status.HTTP_400_BAD_REQUEST
)
def update(self, request, *args, **kwargs):
"""更新模板"""
partial = kwargs.pop('partial', False)
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
if serializer.is_valid():
self.perform_update(serializer)
return ApiResponse.success(
data=serializer.data,
message="模板更新成功"
)
return ApiResponse.error(
message="模板更新失败",
data=serializer.errors,
code=status.HTTP_400_BAD_REQUEST
)
def destroy(self, request, *args, **kwargs):
"""删除模板"""
instance = self.get_object()
self.perform_destroy(instance)
return ApiResponse.success(
data=None,
message="模板删除成功"
)
@action(detail=False, methods=['get'])
def mine(self, request):
"""获取所有模板"""
templates = Template.objects.all()
page = self.paginate_queryset(templates)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(templates, many=True)
return ApiResponse.success(
data=serializer.data,
message="获取模板成功"
)
@action(detail=False, methods=['get'])
def public(self, request):
"""获取所有公开的模板"""
templates = Template.objects.filter(is_public=True)
page = self.paginate_queryset(templates)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(templates, many=True)
return ApiResponse.success(
data=serializer.data,
message="获取公开模板成功"
)
@action(detail=False, methods=['get'])
def by_mission(self, request):
"""按任务类型获取模板"""
mission = request.query_params.get('mission', None)
if not mission:
return ApiResponse.error(
message="需要提供mission参数",
code=status.HTTP_400_BAD_REQUEST
)
queryset = self.get_queryset().filter(mission=mission)
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return ApiResponse.success(
data=serializer.data,
message="按任务类型获取模板成功"
)
@action(detail=False, methods=['get'])
def by_platform(self, request):
"""按平台获取模板"""
platform = request.query_params.get('platform', None)
if not platform:
return ApiResponse.error(
message="需要提供platform参数",
code=status.HTTP_400_BAD_REQUEST
)
queryset = self.get_queryset().filter(platform=platform)
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return ApiResponse.success(
data=serializer.data,
message="按平台获取模板成功"
)
@action(detail=False, methods=['get'])
def by_collaboration(self, request):
"""按合作模式获取模板"""
collaboration_type = request.query_params.get('collaboration_type', None)
if not collaboration_type:
return ApiResponse.error(
message="需要提供collaboration_type参数",
code=status.HTTP_400_BAD_REQUEST
)
queryset = self.get_queryset().filter(collaboration_type=collaboration_type)
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return ApiResponse.success(
data=serializer.data,
message="按合作模式获取模板成功"
)
@action(detail=False, methods=['get'])
def by_service(self, request):
"""按服务类型获取模板"""
service = request.query_params.get('service', None)
if not service:
return ApiResponse.error(
message="需要提供service参数",
code=status.HTTP_400_BAD_REQUEST
)
queryset = self.get_queryset().filter(service=service)
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return ApiResponse.success(
data=serializer.data,
message="按服务类型获取模板成功"
)

View File

@ -52,10 +52,7 @@ INSTALLED_APPS = [
'apps.gmail',
'apps.feishu',
'apps.common',
'apps.brands',
'apps.operation',
'apps.discovery', # 新添加的Discovery应用
'apps.template', # 新添加的Template应用
'django_celery_beat', # Celery定时任务
'django_celery_results', # Celery结果存储
]

View File

@ -26,8 +26,5 @@ urlpatterns = [
path('api/notification/', include('apps.notification.urls')),
path('api/gmail/', include('apps.gmail.urls')),
path('api/feishu/', include('apps.feishu.urls')),
path('api/', include('apps.brands.urls')),
path('api/operation/', include('apps.operation.urls')),
path('api/discovery/', include('apps.discovery.urls')),
path('api/templates/', include('apps.template.urls')),
]