去除不要的模块
This commit is contained in:
parent
b728ea3d6f
commit
f220c59c5a
@ -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')
|
|
||||||
}),
|
|
||||||
)
|
|
@ -1,6 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class BrandsConfig(AppConfig):
|
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
|
||||||
name = 'apps.brands'
|
|
@ -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')},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
@ -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')},
|
|
||||||
),
|
|
||||||
]
|
|
@ -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,
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,3 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
@ -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}"
|
|
@ -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']
|
|
@ -1 +0,0 @@
|
|||||||
|
|
@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
@ -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)),
|
|
||||||
]
|
|
@ -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'])
|
|
@ -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`中提供
|
|
||||||
- 接口不需要认证,可直接访问
|
|
@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
Discovery app for creator discovery and search.
|
|
||||||
"""
|
|
@ -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')
|
|
@ -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'
|
|
@ -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)
|
|
@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
Discovery app management commands.
|
|
||||||
"""
|
|
@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
Discovery app management commands.
|
|
||||||
"""
|
|
@ -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('成功创建所有模拟数据!'))
|
|
@ -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'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
@ -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='创建日期'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -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
|
|
@ -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
|
|
||||||
}
|
|
||||||
})
|
|
@ -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__'
|
|
@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
@ -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)),
|
|
||||||
]
|
|
@ -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
|
|
@ -1,7 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class TemplateConfig(AppConfig):
|
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
|
||||||
name = 'apps.template'
|
|
||||||
verbose_name = '模板管理'
|
|
@ -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)
|
|
@ -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'
|
|
||||||
]
|
|
@ -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': '模板',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
@ -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',
|
|
||||||
),
|
|
||||||
]
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
@ -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)),
|
|
||||||
]
|
|
@ -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)
|
|
@ -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="按服务类型获取模板成功"
|
|
||||||
)
|
|
@ -52,10 +52,7 @@ INSTALLED_APPS = [
|
|||||||
'apps.gmail',
|
'apps.gmail',
|
||||||
'apps.feishu',
|
'apps.feishu',
|
||||||
'apps.common',
|
'apps.common',
|
||||||
'apps.brands',
|
|
||||||
'apps.operation',
|
'apps.operation',
|
||||||
'apps.discovery', # 新添加的Discovery应用
|
|
||||||
'apps.template', # 新添加的Template应用
|
|
||||||
'django_celery_beat', # Celery定时任务
|
'django_celery_beat', # Celery定时任务
|
||||||
'django_celery_results', # Celery结果存储
|
'django_celery_results', # Celery结果存储
|
||||||
]
|
]
|
||||||
|
@ -26,8 +26,5 @@ urlpatterns = [
|
|||||||
path('api/notification/', include('apps.notification.urls')),
|
path('api/notification/', include('apps.notification.urls')),
|
||||||
path('api/gmail/', include('apps.gmail.urls')),
|
path('api/gmail/', include('apps.gmail.urls')),
|
||||||
path('api/feishu/', include('apps.feishu.urls')),
|
path('api/feishu/', include('apps.feishu.urls')),
|
||||||
path('api/', include('apps.brands.urls')),
|
|
||||||
path('api/operation/', include('apps.operation.urls')),
|
path('api/operation/', include('apps.operation.urls')),
|
||||||
path('api/discovery/', include('apps.discovery.urls')),
|
|
||||||
path('api/templates/', include('apps.template.urls')),
|
|
||||||
]
|
]
|
Loading…
Reference in New Issue
Block a user