Compare commits
2 Commits
ff99abfcd5
...
534c9f6677
Author | SHA1 | Date | |
---|---|---|---|
![]() |
534c9f6677 | ||
![]() |
93db26cb4d |
108
.gitignore
vendored
108
.gitignore
vendored
@ -1,5 +1,4 @@
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
@ -28,8 +27,6 @@ share/python-wheels/
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
@ -61,66 +58,17 @@ cover/
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
media/
|
||||
static/
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
# 虚拟环境
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
@ -129,34 +77,22 @@ ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
# 系统文件
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
|
0
apps/brands/__init__.py
Normal file
0
apps/brands/__init__.py
Normal file
96
apps/brands/admin.py
Normal file
96
apps/brands/admin.py
Normal file
@ -0,0 +1,96 @@
|
||||
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')
|
||||
}),
|
||||
)
|
6
apps/brands/apps.py
Normal file
6
apps/brands/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BrandsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.brands'
|
177
apps/brands/migrations/0001_initial.py
Normal file
177
apps/brands/migrations/0001_initial.py
Normal file
@ -0,0 +1,177 @@
|
||||
# Generated by Django 5.1.5 on 2025-05-19 08:40
|
||||
|
||||
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')),
|
||||
('category', models.CharField(blank=True, max_length=100, null=True, verbose_name='品牌分类')),
|
||||
('source', models.CharField(blank=True, max_length=100, null=True, verbose_name='来源')),
|
||||
('collab_count', models.IntegerField(default=0, verbose_name='合作数量')),
|
||||
('creators_count', models.IntegerField(default=0, verbose_name='创作者数量')),
|
||||
('campaign_id', models.CharField(blank=True, max_length=100, null=True, verbose_name='活动ID')),
|
||||
('total_gmv_achieved', models.DecimalField(decimal_places=2, default=0, max_digits=12, verbose_name='总GMV')),
|
||||
('total_views_achieved', models.DecimalField(decimal_places=2, default=0, max_digits=12, verbose_name='总浏览量')),
|
||||
('shop_overall_rating', models.DecimalField(decimal_places=1, default=0.0, max_digits=3, verbose_name='店铺评分')),
|
||||
('dataset_id_list', models.JSONField(blank=True, default=list, help_text='所有关联的知识库ID列表', 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': 'brands',
|
||||
},
|
||||
),
|
||||
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='产品图片')),
|
||||
('pid', models.CharField(blank=True, max_length=100, null=True, verbose_name='产品ID')),
|
||||
('commission_rate', models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='佣金率')),
|
||||
('open_collab', models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='开放合作率')),
|
||||
('available_samples', models.IntegerField(default=0, verbose_name='可用样品数')),
|
||||
('sales_price_min', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='最低销售价')),
|
||||
('sales_price_max', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='最高销售价')),
|
||||
('stock', models.IntegerField(default=0, verbose_name='库存')),
|
||||
('items_sold', models.IntegerField(default=0, verbose_name='已售数量')),
|
||||
('product_rating', models.DecimalField(decimal_places=1, default=0, max_digits=3, 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(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',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Campaign',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=255, 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')),
|
||||
('status', models.CharField(choices=[('pending', '待处理'), ('accepted', '已接受'), ('rejected', '已拒绝'), ('completed', '已完成'), ('in_progress', '进行中')], default='pending', max_length=20, verbose_name='状态')),
|
||||
('gmv_achieved', models.CharField(blank=True, max_length=50, null=True, verbose_name='实现GMV')),
|
||||
('views_achieved', models.CharField(blank=True, max_length=50, null=True, verbose_name='实现观看量')),
|
||||
('video_link', models.URLField(blank=True, max_length=255, null=True, verbose_name='视频链接')),
|
||||
('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='campaigns', to='brands.brand', verbose_name='所属品牌')),
|
||||
('link_product', models.ManyToManyField(blank=True, related_name='campaigns', to='brands.product', verbose_name='关联产品')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '活动',
|
||||
'verbose_name_plural': '活动',
|
||||
'db_table': 'campaigns',
|
||||
},
|
||||
),
|
||||
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.AddIndex(
|
||||
model_name='product',
|
||||
index=models.Index(fields=['brand'], name='products_brand_i_0d1950_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='product',
|
||||
index=models.Index(fields=['dataset_id'], name='products_dataset_faf62a_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='product',
|
||||
index=models.Index(fields=['is_active'], name='products_is_acti_cb485f_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='product',
|
||||
index=models.Index(fields=['pid'], name='products_pid_99aab2_idx'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='product',
|
||||
unique_together={('brand', 'name')},
|
||||
),
|
||||
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.AddIndex(
|
||||
model_name='campaign',
|
||||
index=models.Index(fields=['status'], name='campaigns_status_a03570_idx'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='campaign',
|
||||
unique_together={('brand', 'name')},
|
||||
),
|
||||
]
|
18
apps/brands/migrations/0002_alter_campaign_id.py
Normal file
18
apps/brands/migrations/0002_alter_campaign_id.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.5 on 2025-05-19 10:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('brands', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='campaign',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
]
|
3
apps/brands/migrations/__init__.py
Normal file
3
apps/brands/migrations/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
|
||||
|
||||
|
199
apps/brands/models.py
Normal file
199
apps/brands/models.py
Normal file
@ -0,0 +1,199 @@
|
||||
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:
|
||||
db_table = 'brands'
|
||||
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:
|
||||
db_table = 'products'
|
||||
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=255, 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='外部系统中的唯一标识')
|
||||
|
||||
# 状态信息
|
||||
status = models.CharField(max_length=20, default='pending', verbose_name='状态',
|
||||
choices=[
|
||||
('pending', '待处理'),
|
||||
('accepted', '已接受'),
|
||||
('rejected', '已拒绝'),
|
||||
('completed', '已完成'),
|
||||
('in_progress', '进行中')
|
||||
])
|
||||
|
||||
# 统计信息
|
||||
gmv_achieved = models.CharField(max_length=50, blank=True, null=True, verbose_name='实现GMV')
|
||||
views_achieved = models.CharField(max_length=50, blank=True, null=True, verbose_name='实现观看量')
|
||||
video_link = models.URLField(max_length=255, 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='更新时间')
|
||||
is_active = models.BooleanField(default=True, verbose_name='是否激活')
|
||||
|
||||
class Meta:
|
||||
db_table = 'campaigns'
|
||||
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']),
|
||||
models.Index(fields=['status']),
|
||||
]
|
||||
|
||||
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:
|
||||
db_table = 'brand_chat_sessions'
|
||||
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}"
|
67
apps/brands/serializers.py
Normal file
67
apps/brands/serializers.py
Normal file
@ -0,0 +1,67 @@
|
||||
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
apps/brands/services/__init__.py
Normal file
1
apps/brands/services/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
3
apps/brands/tests.py
Normal file
3
apps/brands/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
18
apps/brands/urls.py
Normal file
18
apps/brands/urls.py
Normal file
@ -0,0 +1,18 @@
|
||||
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)),
|
||||
]
|
317
apps/brands/views.py
Normal file
317
apps/brands/views.py
Normal file
@ -0,0 +1,317 @@
|
||||
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'])
|
0
apps/daren_detail/__init__.py
Normal file
0
apps/daren_detail/__init__.py
Normal file
3
apps/daren_detail/admin.py
Normal file
3
apps/daren_detail/admin.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
6
apps/daren_detail/apps.py
Normal file
6
apps/daren_detail/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class DarenDetailConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.daren_detail'
|
365
apps/daren_detail/management/commands/populate_data.py
Normal file
365
apps/daren_detail/management/commands/populate_data.py
Normal file
@ -0,0 +1,365 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils import timezone
|
||||
from apps.daren_detail.models import (
|
||||
CollaborationMetrics, VideoMetrics, LiveMetrics, CreatorProfile,
|
||||
CreatorCampaign, BrandCampaign, CreatorVideo, FollowerMetrics,
|
||||
TrendMetrics, PublicCreatorPool, PrivateCreatorPool, PrivateCreatorRelation
|
||||
)
|
||||
from apps.expertproducts.models import Product as ExpertProduct, Creator as ExpertCreator, Negotiation, Message
|
||||
from apps.discovery.models import SearchSession, Creator as DiscoveryCreator
|
||||
from apps.brands.models import Brand, Product as BrandProduct, Campaign, BrandChatSession
|
||||
from apps.template.models import TemplateCategory, Template
|
||||
from apps.user.models import User
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
import uuid
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '填充测试数据到所有相关模型'
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
self.stdout.write('开始填充数据...')
|
||||
|
||||
# 创建用户(避免重复)
|
||||
user, created = User.objects.get_or_create(
|
||||
email='test@example.com',
|
||||
defaults={
|
||||
'password': 'testpassword',
|
||||
'company': '测试公司',
|
||||
'name': '测试用户',
|
||||
'is_first_login': False,
|
||||
'last_login': timezone.now()
|
||||
}
|
||||
)
|
||||
|
||||
# 创建品牌(避免重复)
|
||||
brands = []
|
||||
brand_names = ['U品牌', 'R品牌', 'X品牌', 'Q品牌', 'A品牌', 'M品牌']
|
||||
for name in brand_names:
|
||||
brand, _ = Brand.objects.get_or_create(
|
||||
name=name,
|
||||
defaults={
|
||||
'description': f'{name}的描述信息',
|
||||
'logo_url': f'https://example.com/logos/{name}.png',
|
||||
'category': '电子产品',
|
||||
'source': '内部',
|
||||
'collab_count': random.randint(5, 20),
|
||||
'creators_count': random.randint(10, 50),
|
||||
'campaign_id': str(uuid.uuid4()),
|
||||
'total_gmv_achieved': random.randint(10000, 100000),
|
||||
'total_views_achieved': random.randint(100000, 1000000),
|
||||
'shop_overall_rating': round(random.uniform(3.5, 5.0), 1),
|
||||
'dataset_id_list': [str(uuid.uuid4()) for _ in range(3)]
|
||||
}
|
||||
)
|
||||
brands.append(brand)
|
||||
|
||||
# 创建品牌产品(避免重复)
|
||||
products = []
|
||||
for brand in brands:
|
||||
for i in range(3):
|
||||
product, _ = BrandProduct.objects.get_or_create(
|
||||
brand=brand,
|
||||
name=f'{brand.name}产品{i+1}',
|
||||
defaults={
|
||||
'description': f'{brand.name}产品{i+1}的详细描述',
|
||||
'image_url': f'https://example.com/products/{brand.name}_{i+1}.jpg',
|
||||
'pid': f'PID_{uuid.uuid4().hex[:8]}',
|
||||
'commission_rate': random.uniform(5, 20),
|
||||
'open_collab': random.uniform(10, 30),
|
||||
'available_samples': random.randint(10, 100),
|
||||
'sales_price_min': random.uniform(100, 500),
|
||||
'sales_price_max': random.uniform(500, 2000),
|
||||
'stock': random.randint(100, 1000),
|
||||
'items_sold': random.randint(50, 500),
|
||||
'product_rating': round(random.uniform(3.5, 5.0), 1),
|
||||
'reviews_count': random.randint(10, 100),
|
||||
'collab_creators': random.randint(5, 20),
|
||||
'tiktok_shop': random.choice([True, False]),
|
||||
'dataset_id': str(uuid.uuid4())
|
||||
}
|
||||
)
|
||||
products.append(product)
|
||||
|
||||
# 创建活动(避免重复)
|
||||
campaigns = []
|
||||
for brand in brands:
|
||||
campaign, _ = Campaign.objects.get_or_create(
|
||||
brand=brand,
|
||||
name=f'{brand.name}活动',
|
||||
defaults={
|
||||
'description': f'{brand.name}的活动描述',
|
||||
'image_url': f'https://example.com/campaigns/{brand.name}.jpg',
|
||||
'service': '直播带货',
|
||||
'creator_type': 'KOL',
|
||||
'creator_level': 'L3',
|
||||
'creator_category': '电子产品',
|
||||
'creators_count': random.randint(5, 20),
|
||||
'gmv': '10000-50000',
|
||||
'followers': '10000-100000',
|
||||
'views': '100000-1000000',
|
||||
'budget': '5000-20000',
|
||||
'start_date': timezone.now(),
|
||||
'end_date': timezone.now() + timedelta(days=30),
|
||||
'dataset_id': str(uuid.uuid4()),
|
||||
'status': 'in_progress',
|
||||
'gmv_achieved': '25000',
|
||||
'views_achieved': '500000',
|
||||
'video_link': 'https://example.com/videos/campaign.mp4'
|
||||
}
|
||||
)
|
||||
campaign.link_product.set(random.sample(products, 2))
|
||||
campaigns.append(campaign)
|
||||
|
||||
# 创建创作者档案
|
||||
creators = []
|
||||
for i in range(10):
|
||||
creator = CreatorProfile.objects.create(
|
||||
name=f'创作者{i+1}',
|
||||
avatar_url=f'https://example.com/avatars/creator_{i+1}.jpg',
|
||||
email=f'creator{i+1}@example.com',
|
||||
instagram=f'creator{i+1}',
|
||||
tiktok_link=f'https://tiktok.com/@{i+1}',
|
||||
location='上海',
|
||||
live_schedule='每周一、三、五 20:00-22:00',
|
||||
category=random.choice([c[0] for c in CreatorProfile.CATEGORY_CHOICES]),
|
||||
e_commerce_level=random.randint(1, 7),
|
||||
exposure_level=random.choice([e[0] for e in CreatorProfile.EXPOSURE_LEVEL_CHOICES]),
|
||||
followers=random.randint(10000, 1000000),
|
||||
gmv=random.uniform(10000, 1000000),
|
||||
items_sold=random.uniform(1000, 10000),
|
||||
avg_video_views=random.randint(10000, 100000),
|
||||
pricing_min=random.uniform(1000, 5000),
|
||||
pricing_max=random.uniform(5000, 20000),
|
||||
pricing_package='基础套餐',
|
||||
collab_count=random.randint(5, 50),
|
||||
latest_collab='最新合作项目',
|
||||
e_commerce_platforms=['TikTok', 'Instagram'],
|
||||
gmv_by_channel={'TikTok': 60, 'Instagram': 40},
|
||||
gmv_by_category={'电子产品': 70, '服装': 30},
|
||||
mcn='知名MCN机构'
|
||||
)
|
||||
creators.append(creator)
|
||||
|
||||
# 创建协作指标
|
||||
for creator in creators:
|
||||
CollaborationMetrics.objects.create(
|
||||
avg_commission_rate=random.uniform(5, 20),
|
||||
products_count=random.randint(10, 50),
|
||||
brand_collaborations=random.randint(5, 20),
|
||||
min_product_price=random.uniform(100, 500),
|
||||
max_product_price=random.uniform(500, 2000),
|
||||
start_date=timezone.now().date(),
|
||||
end_date=timezone.now().date() + timedelta(days=30),
|
||||
creator=creator
|
||||
)
|
||||
|
||||
# 创建视频指标
|
||||
for creator in creators:
|
||||
for video_type in ['regular', 'shoppable']:
|
||||
VideoMetrics.objects.create(
|
||||
video_type=video_type,
|
||||
gpm=random.uniform(100, 1000),
|
||||
videos_count=random.randint(10, 50),
|
||||
avg_views=random.uniform(10000, 100000),
|
||||
avg_engagement=random.uniform(1, 10),
|
||||
avg_likes=random.randint(1000, 10000),
|
||||
start_date=timezone.now().date(),
|
||||
end_date=timezone.now().date() + timedelta(days=30),
|
||||
creator=creator
|
||||
)
|
||||
|
||||
# 创建直播指标
|
||||
for creator in creators:
|
||||
for live_type in ['regular', 'shoppable']:
|
||||
LiveMetrics.objects.create(
|
||||
live_type=live_type,
|
||||
gpm=random.uniform(100, 1000),
|
||||
lives_count=random.randint(5, 20),
|
||||
avg_views=random.uniform(10000, 100000),
|
||||
avg_engagement=random.uniform(1, 10),
|
||||
avg_likes=random.randint(1000, 10000),
|
||||
start_date=timezone.now().date(),
|
||||
end_date=timezone.now().date() + timedelta(days=30),
|
||||
creator=creator
|
||||
)
|
||||
|
||||
# 创建创作者视频
|
||||
for creator in creators:
|
||||
for i in range(3):
|
||||
CreatorVideo.objects.create(
|
||||
creator=creator,
|
||||
title=f'视频标题{i+1}',
|
||||
description=f'视频描述{i+1}',
|
||||
thumbnail_url=f'https://example.com/thumbnails/video_{i+1}.jpg',
|
||||
video_url=f'https://example.com/videos/video_{i+1}.mp4',
|
||||
video_id=f'VID_{uuid.uuid4().hex[:8]}',
|
||||
video_type=random.choice(['regular', 'product']),
|
||||
badge=random.choice(['red', 'gold']),
|
||||
view_count=random.randint(10000, 100000),
|
||||
like_count=random.randint(1000, 10000),
|
||||
comment_count=random.randint(100, 1000),
|
||||
has_product=random.choice([True, False]),
|
||||
product_name=f'产品{i+1}' if random.choice([True, False]) else None,
|
||||
product_url=f'https://example.com/products/{i+1}' if random.choice([True, False]) else None,
|
||||
release_date=timezone.now().date()
|
||||
)
|
||||
|
||||
# 创建粉丝指标
|
||||
for creator in creators:
|
||||
FollowerMetrics.objects.create(
|
||||
creator=creator,
|
||||
start_date=timezone.now().date(),
|
||||
end_date=timezone.now().date() + timedelta(days=30),
|
||||
female_percentage=random.uniform(40, 60),
|
||||
male_percentage=random.uniform(40, 60),
|
||||
age_18_24_percentage=random.uniform(20, 40),
|
||||
age_25_34_percentage=random.uniform(30, 50),
|
||||
age_35_44_percentage=random.uniform(10, 30),
|
||||
age_45_54_percentage=random.uniform(5, 15),
|
||||
age_55_plus_percentage=random.uniform(1, 10),
|
||||
location_data={
|
||||
'上海': random.uniform(10, 30),
|
||||
'北京': random.uniform(10, 30),
|
||||
'广州': random.uniform(5, 20),
|
||||
'深圳': random.uniform(5, 20),
|
||||
'杭州': random.uniform(5, 15)
|
||||
}
|
||||
)
|
||||
|
||||
# 创建趋势指标
|
||||
for creator in creators:
|
||||
for i in range(10):
|
||||
TrendMetrics.objects.create(
|
||||
creator=creator,
|
||||
date=timezone.now().date() - timedelta(days=i),
|
||||
gmv=random.uniform(1000, 10000),
|
||||
items_sold=random.randint(100, 1000),
|
||||
followers_count=random.randint(10000, 100000),
|
||||
video_views=random.randint(10000, 100000),
|
||||
engagement_rate=random.uniform(1, 10)
|
||||
)
|
||||
|
||||
# 创建公有达人库
|
||||
for creator in creators:
|
||||
PublicCreatorPool.objects.create(
|
||||
creator=creator,
|
||||
category=creator.category,
|
||||
remark=f'备注信息:{creator.name}的详细信息'
|
||||
)
|
||||
|
||||
# 创建私有达人库(避免重复)
|
||||
private_pool, _ = PrivateCreatorPool.objects.get_or_create(
|
||||
user=user,
|
||||
name='我的收藏',
|
||||
defaults={
|
||||
'description': '收藏的达人列表',
|
||||
'is_default': True
|
||||
}
|
||||
)
|
||||
|
||||
# 创建私有达人关联
|
||||
for creator in random.sample(creators, 5):
|
||||
PrivateCreatorRelation.objects.create(
|
||||
private_pool=private_pool,
|
||||
creator=creator,
|
||||
added_from_public=True,
|
||||
notes=f'关于{creator.name}的笔记',
|
||||
status=random.choice(['active', 'archived', 'favorite'])
|
||||
)
|
||||
|
||||
# 创建专家产品
|
||||
expert_products = []
|
||||
for i in range(10):
|
||||
p = ExpertProduct.objects.create(
|
||||
name=f'专家产品{i+1}',
|
||||
category='电子产品',
|
||||
max_price=random.uniform(1000, 5000),
|
||||
min_price=random.uniform(500, 1000),
|
||||
description=f'专家产品{i+1}的详细描述'
|
||||
)
|
||||
expert_products.append(p)
|
||||
|
||||
# 创建专家创作者
|
||||
expert_creators = []
|
||||
for i in range(10):
|
||||
c = ExpertCreator.objects.create(
|
||||
name=f'专家创作者{i+1}',
|
||||
sex=random.choice(['男', '女']),
|
||||
age=random.randint(18, 45),
|
||||
category='带货类',
|
||||
followers=random.randint(10000, 1000000)
|
||||
)
|
||||
expert_creators.append(c)
|
||||
|
||||
# 创建谈判记录
|
||||
for i in range(10):
|
||||
negotiation = Negotiation.objects.create(
|
||||
creator=expert_creators[i],
|
||||
product=expert_products[i],
|
||||
status=random.choice(['brand_review', 'price_negotiation', 'contract_review']),
|
||||
current_round=random.randint(1, 5),
|
||||
context={'current_price': random.uniform(500, 2000)}
|
||||
)
|
||||
|
||||
# 为每个谈判创建消息
|
||||
for j in range(3):
|
||||
Message.objects.create(
|
||||
negotiation=negotiation,
|
||||
role=random.choice(['user', 'assistant']),
|
||||
content=f'谈判消息{j+1}',
|
||||
stage=negotiation.status
|
||||
)
|
||||
|
||||
# 创建搜索会话
|
||||
for i in range(10):
|
||||
session = SearchSession.objects.create(
|
||||
session_number=i+1,
|
||||
creator_count=random.randint(5, 20),
|
||||
shoppable_creators=random.randint(2, 10),
|
||||
avg_followers=random.uniform(10000, 100000),
|
||||
avg_gmv=random.uniform(10000, 100000),
|
||||
avg_video_views=random.uniform(10000, 100000)
|
||||
)
|
||||
|
||||
# 为每个会话创建创作者
|
||||
for j in range(5):
|
||||
DiscoveryCreator.objects.create(
|
||||
session=session,
|
||||
name=f'发现创作者{j+1}',
|
||||
avatar=f'https://example.com/avatars/discovery_{j+1}.jpg',
|
||||
category=random.choice([c[0] for c in DiscoveryCreator.CATEGORIES]),
|
||||
ecommerce_level=random.choice([l[0] for l in DiscoveryCreator.ECOMMERCE_LEVELS]),
|
||||
exposure_level=random.choice([l[0] for l in DiscoveryCreator.EXPOSURE_LEVELS]),
|
||||
followers=random.uniform(10000, 1000000),
|
||||
gmv=random.uniform(10000, 100000),
|
||||
items_sold=random.uniform(1000, 10000),
|
||||
avg_video_views=random.uniform(10000, 100000),
|
||||
has_ecommerce=random.choice([True, False]),
|
||||
tiktok_url=f'https://tiktok.com/@{j+1}'
|
||||
)
|
||||
|
||||
# 创建模板分类
|
||||
categories = []
|
||||
for i in range(5):
|
||||
category = TemplateCategory.objects.create(
|
||||
name=f'模板分类{i+1}',
|
||||
description=f'模板分类{i+1}的描述'
|
||||
)
|
||||
categories.append(category)
|
||||
|
||||
# 创建模板
|
||||
for category in categories:
|
||||
for i in range(2):
|
||||
Template.objects.create(
|
||||
title=f'模板{i+1}',
|
||||
content=f'这是模板{i+1}的详细内容,包含了很多有用的信息。',
|
||||
category=category,
|
||||
mission=random.choice([m[0] for m in Template.MISSION_CHOICES]),
|
||||
platform=random.choice([p[0] for p in Template.PLATFORM_CHOICES]),
|
||||
collaboration_type=random.choice([c[0] for c in Template.COLLABORATION_CHOICES]),
|
||||
service=random.choice([s[0] for s in Template.SERVICE_CHOICES]),
|
||||
is_public=random.choice([True, False])
|
||||
)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS('数据填充完成!'))
|
283
apps/daren_detail/migrations/0001_initial.py
Normal file
283
apps/daren_detail/migrations/0001_initial.py
Normal file
@ -0,0 +1,283 @@
|
||||
# Generated by Django 5.1.5 on 2025-05-19 08:40
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('brands', '0001_initial'),
|
||||
('user', '0002_remove_user_is_active'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CreatorProfile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, verbose_name='达人名称')),
|
||||
('avatar_url', models.TextField(blank=True, null=True, verbose_name='头像URL')),
|
||||
('email', models.EmailField(blank=True, max_length=255, null=True, verbose_name='电子邮箱')),
|
||||
('instagram', models.CharField(blank=True, max_length=255, null=True, verbose_name='Instagram账号')),
|
||||
('tiktok_link', models.URLField(blank=True, max_length=255, null=True, verbose_name='TikTok链接')),
|
||||
('location', models.CharField(blank=True, max_length=100, null=True, verbose_name='位置')),
|
||||
('live_schedule', models.CharField(blank=True, max_length=255, null=True, verbose_name='直播时间表')),
|
||||
('category', models.CharField(blank=True, choices=[('Phones & Electronics', '手机与电子产品'), ('Homes Supplies', '家居用品'), ('Kitchenware', '厨房用品'), ('Textiles & Soft Furnishings', '纺织品和软装'), ('Household Appliances', '家用电器'), ('Womenswear & Underwear', '女装和内衣'), ('Muslim Fashion', '穆斯林时尚'), ('Shoes', '鞋类'), ('Beauty & Personal Care', '美容和个人护理'), ('Computers & Office Equipment', '电脑和办公设备'), ('Pet Supplies', '宠物用品'), ('Baby & Maternity', '婴儿和孕妇用品'), ('Sports & Outdoor', '运动和户外'), ('Toys', '玩具'), ('Furniture', '家具'), ('Tools & Hardware', '工具和硬件'), ('Home Improvement', '家居装修'), ('Automotive & Motorcycle', '汽车和摩托车'), ('Fashion Accessories', '时尚配饰'), ('Food & Beverages', '食品和饮料'), ('Health', '健康'), ('Books, Magazines & Audio', '书籍、杂志和音频'), ('Kids Fashion', '儿童时尚'), ('Menswear & Underwear', '男装和内衣'), ('Luggage & Bags', '行李和包'), ('Pre-Owned Collections', '二手收藏'), ('Jewellery Accessories & Derivatives', '珠宝配饰及衍生品')], max_length=100, null=True, verbose_name='类别')),
|
||||
('e_commerce_level', models.IntegerField(blank=True, choices=[(1, 'L1'), (2, 'L2'), (3, 'L3'), (4, 'L4'), (5, 'L5'), (6, 'L6'), (7, 'L7')], null=True, verbose_name='电商能力等级')),
|
||||
('exposure_level', models.CharField(blank=True, choices=[('KOC-1', 'KOC-1'), ('KOC-2', 'KOC-2'), ('KOL-1', 'KOL-1'), ('KOL-2', 'KOL-2'), ('KOL-3', 'KOL-3')], max_length=10, null=True, verbose_name='曝光等级')),
|
||||
('followers', models.IntegerField(default=0, verbose_name='粉丝数')),
|
||||
('gmv', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True, verbose_name='GMV(千美元)')),
|
||||
('items_sold', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True, verbose_name='售出商品数量')),
|
||||
('avg_video_views', models.IntegerField(blank=True, default=0, null=True, verbose_name='平均视频浏览量')),
|
||||
('pricing_min', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='最低个人定价')),
|
||||
('pricing_max', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='最高个人定价')),
|
||||
('pricing_package', models.CharField(blank=True, max_length=100, null=True, verbose_name='套餐定价')),
|
||||
('collab_count', models.IntegerField(blank=True, default=0, null=True, verbose_name='合作次数')),
|
||||
('latest_collab', models.CharField(blank=True, max_length=100, null=True, verbose_name='最新合作')),
|
||||
('e_commerce_platforms', models.JSONField(blank=True, null=True, verbose_name='电商平台')),
|
||||
('gmv_by_channel', models.JSONField(blank=True, null=True, verbose_name='GMV按渠道分布')),
|
||||
('gmv_by_category', models.JSONField(blank=True, null=True, verbose_name='GMV按类别分布')),
|
||||
('mcn', models.CharField(blank=True, max_length=255, null=True, verbose_name='MCN机构')),
|
||||
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '达人信息',
|
||||
'verbose_name_plural': '达人信息',
|
||||
'db_table': 'creator_profiles',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BrandCampaign',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('brand_id', models.CharField(choices=[('U', 'U品牌'), ('R', 'R品牌'), ('X', 'X品牌'), ('Q', 'Q品牌'), ('A', 'A品牌'), ('M', 'M品牌')], max_length=10, verbose_name='品牌ID')),
|
||||
('brand_name', models.CharField(default='brand', max_length=255, verbose_name='品牌名称')),
|
||||
('brand_color', models.CharField(default='#000000', max_length=20, verbose_name='品牌颜色')),
|
||||
('pricing_detail', models.CharField(max_length=50, verbose_name='价格详情')),
|
||||
('start_date', models.DateField(verbose_name='开始日期')),
|
||||
('end_date', models.DateField(verbose_name='结束日期')),
|
||||
('status', models.CharField(choices=[('completed', '已完成'), ('in_progress', '进行中'), ('rejected', '已拒绝')], max_length=20, verbose_name='状态')),
|
||||
('gmv_achieved', models.CharField(max_length=50, verbose_name='实现GMV')),
|
||||
('views_achieved', models.CharField(max_length=50, verbose_name='实现观看量')),
|
||||
('video_link', models.URLField(blank=True, max_length=255, null=True, verbose_name='视频链接')),
|
||||
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('campaign', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='brand_campaigns', to='brands.campaign', verbose_name='关联活动')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '品牌活动数据',
|
||||
'verbose_name_plural': '品牌活动数据',
|
||||
'db_table': 'brand_campaigns',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CollaborationMetrics',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('avg_commission_rate', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='平均佣金率(%)')),
|
||||
('products_count', models.IntegerField(verbose_name='产品数量')),
|
||||
('brand_collaborations', models.IntegerField(verbose_name='品牌合作数量')),
|
||||
('min_product_price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='最低产品价格($)')),
|
||||
('max_product_price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='最高产品价格($)')),
|
||||
('start_date', models.DateField(verbose_name='开始日期')),
|
||||
('end_date', models.DateField(verbose_name='结束日期')),
|
||||
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='collaboration_metrics', to='daren_detail.creatorprofile', verbose_name='创作者')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '协作指标',
|
||||
'verbose_name_plural': '协作指标',
|
||||
'db_table': 'collaboration_metrics',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CreatorVideo',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=255, verbose_name='视频标题')),
|
||||
('description', models.TextField(blank=True, null=True, verbose_name='视频描述')),
|
||||
('thumbnail_url', models.URLField(blank=True, max_length=500, null=True, verbose_name='缩略图URL')),
|
||||
('video_url', models.URLField(blank=True, max_length=500, null=True, verbose_name='视频URL')),
|
||||
('video_id', models.CharField(max_length=100, verbose_name='视频ID')),
|
||||
('video_type', models.CharField(choices=[('regular', '普通视频'), ('product', '带产品视频')], default='regular', max_length=20, verbose_name='视频类型')),
|
||||
('badge', models.CharField(blank=True, choices=[('red', '红色标记'), ('gold', '金色标记')], max_length=20, null=True, verbose_name='视频标记')),
|
||||
('view_count', models.IntegerField(default=0, verbose_name='观看次数')),
|
||||
('like_count', models.IntegerField(default=0, verbose_name='点赞数')),
|
||||
('comment_count', models.IntegerField(default=0, verbose_name='评论数')),
|
||||
('has_product', models.BooleanField(default=False, verbose_name='是否有产品')),
|
||||
('product_name', models.CharField(blank=True, max_length=255, null=True, verbose_name='产品名称')),
|
||||
('product_url', models.URLField(blank=True, max_length=500, null=True, verbose_name='产品链接')),
|
||||
('release_date', models.DateField(verbose_name='发布日期')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='videos', to='daren_detail.creatorprofile')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '创作者视频',
|
||||
'verbose_name_plural': '创作者视频',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FollowerMetrics',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('start_date', models.DateField()),
|
||||
('end_date', models.DateField()),
|
||||
('female_percentage', models.FloatField(default=0)),
|
||||
('male_percentage', models.FloatField(default=0)),
|
||||
('age_18_24_percentage', models.FloatField(default=0)),
|
||||
('age_25_34_percentage', models.FloatField(default=0)),
|
||||
('age_35_44_percentage', models.FloatField(default=0)),
|
||||
('age_45_54_percentage', models.FloatField(default=0)),
|
||||
('age_55_plus_percentage', models.FloatField(default=0)),
|
||||
('location_data', models.JSONField(default=dict)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='follower_metrics', to='daren_detail.creatorprofile')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '粉丝统计',
|
||||
'verbose_name_plural': '粉丝统计',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PrivateCreatorPool',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, verbose_name='私有库名称')),
|
||||
('description', models.TextField(blank=True, null=True, verbose_name='描述')),
|
||||
('is_default', models.BooleanField(default=False, verbose_name='是否为默认库')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('user', models.ForeignKey(db_column='user_id', on_delete=django.db.models.deletion.CASCADE, related_name='private_creators', to='user.user', verbose_name='用户')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '私有达人库',
|
||||
'verbose_name_plural': '私有达人库',
|
||||
'db_table': 'private_creator_pool',
|
||||
'unique_together': {('user', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PublicCreatorPool',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('category', models.CharField(blank=True, max_length=255, null=True, verbose_name='分类')),
|
||||
('remark', models.TextField(blank=True, null=True, verbose_name='备注')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='public_pool', to='daren_detail.creatorprofile', verbose_name='达人')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '公有达人库',
|
||||
'verbose_name_plural': '公有达人库',
|
||||
'db_table': 'public_creator_pool',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TrendMetrics',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('date', models.DateField()),
|
||||
('gmv', models.FloatField(default=0)),
|
||||
('items_sold', models.IntegerField(default=0)),
|
||||
('followers_count', models.IntegerField(default=0)),
|
||||
('video_views', models.IntegerField(default=0)),
|
||||
('engagement_rate', models.FloatField(default=0)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trend_metrics', to='daren_detail.creatorprofile')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '趋势指标',
|
||||
'verbose_name_plural': '趋势指标',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CreatorCampaign',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('status', models.CharField(choices=[('pending', '待处理'), ('accepted', '已接受'), ('rejected', '已拒绝'), ('completed', '已完成')], default='pending', max_length=20, verbose_name='状态')),
|
||||
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('campaign', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='creators', to='brands.campaign', verbose_name='活动')),
|
||||
('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='campaigns', to='daren_detail.creatorprofile', verbose_name='达人')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '达人活动关联',
|
||||
'verbose_name_plural': '达人活动关联',
|
||||
'db_table': 'creator_campaigns',
|
||||
'unique_together': {('creator', 'campaign')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LiveMetrics',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('live_type', models.CharField(choices=[('regular', '普通直播'), ('shoppable', '可购物直播')], default='regular', max_length=50, verbose_name='直播类型')),
|
||||
('gpm', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='直播GPM($)')),
|
||||
('lives_count', models.IntegerField(verbose_name='直播数量')),
|
||||
('avg_views', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='平均观看量')),
|
||||
('avg_engagement', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='平均互动率(%)')),
|
||||
('avg_likes', models.IntegerField(verbose_name='平均点赞数')),
|
||||
('start_date', models.DateField(verbose_name='开始日期')),
|
||||
('end_date', models.DateField(verbose_name='结束日期')),
|
||||
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='live_metrics', to='daren_detail.creatorprofile', verbose_name='创作者')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '直播指标',
|
||||
'verbose_name_plural': '直播指标',
|
||||
'db_table': 'live_metrics',
|
||||
'unique_together': {('creator', 'live_type', 'start_date', 'end_date')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PrivateCreatorRelation',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('added_from_public', models.BooleanField(default=True, verbose_name='是否从公有库添加')),
|
||||
('notes', models.TextField(blank=True, null=True, verbose_name='笔记')),
|
||||
('status', models.CharField(choices=[('active', '活跃'), ('archived', '已归档'), ('favorite', '收藏')], default='active', max_length=20, verbose_name='状态')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='添加时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='private_pool_relations', to='daren_detail.creatorprofile', verbose_name='达人')),
|
||||
('private_pool', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='creator_relations', to='daren_detail.privatecreatorpool', verbose_name='私有达人库')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '私有库达人关联',
|
||||
'verbose_name_plural': '私有库达人关联',
|
||||
'db_table': 'private_creator_relations',
|
||||
'unique_together': {('private_pool', 'creator')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='VideoMetrics',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('video_type', models.CharField(choices=[('regular', '普通视频'), ('shoppable', '可购物视频')], default='regular', max_length=50, verbose_name='视频类型')),
|
||||
('gpm', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='视频GPM($)')),
|
||||
('videos_count', models.IntegerField(verbose_name='视频数量')),
|
||||
('avg_views', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='平均观看量')),
|
||||
('avg_engagement', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='平均互动率(%)')),
|
||||
('avg_likes', models.IntegerField(verbose_name='平均点赞数')),
|
||||
('start_date', models.DateField(verbose_name='开始日期')),
|
||||
('end_date', models.DateField(verbose_name='结束日期')),
|
||||
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='video_metrics', to='daren_detail.creatorprofile', verbose_name='创作者')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '视频指标',
|
||||
'verbose_name_plural': '视频指标',
|
||||
'db_table': 'video_metrics',
|
||||
'unique_together': {('creator', 'video_type', 'start_date', 'end_date')},
|
||||
},
|
||||
),
|
||||
]
|
0
apps/daren_detail/migrations/__init__.py
Normal file
0
apps/daren_detail/migrations/__init__.py
Normal file
484
apps/daren_detail/models.py
Normal file
484
apps/daren_detail/models.py
Normal file
@ -0,0 +1,484 @@
|
||||
from django.db import models
|
||||
# from apps.daren_detail.models import User
|
||||
|
||||
# 修改User模型导入
|
||||
from apps.user.models import User
|
||||
from apps.brands.models import Campaign as BrandCampaign
|
||||
|
||||
|
||||
|
||||
# 新增模型:协作指标数据
|
||||
class CollaborationMetrics(models.Model):
|
||||
"""协作指标数据模型,Collaboration Metrics部分"""
|
||||
avg_commission_rate = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="平均佣金率(%)")
|
||||
products_count = models.IntegerField(verbose_name="产品数量")
|
||||
brand_collaborations = models.IntegerField(verbose_name="品牌合作数量")
|
||||
min_product_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="最低产品价格($)")
|
||||
max_product_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="最高产品价格($)")
|
||||
|
||||
# 时间范围
|
||||
start_date = models.DateField(verbose_name="开始日期")
|
||||
end_date = models.DateField(verbose_name="结束日期")
|
||||
|
||||
# 所属创作者
|
||||
creator = models.ForeignKey('CreatorProfile', on_delete=models.CASCADE, related_name="collaboration_metrics",
|
||||
verbose_name="创作者")
|
||||
|
||||
# 时间戳
|
||||
create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
|
||||
update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "协作指标"
|
||||
verbose_name_plural = verbose_name
|
||||
db_table = "collaboration_metrics"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.creator.name}的协作指标 ({self.start_date} - {self.end_date})"
|
||||
|
||||
|
||||
# 新增模型:视频指标数据
|
||||
class VideoMetrics(models.Model):
|
||||
"""视频指标数据模型,Video和Shoppable Video部分"""
|
||||
video_type = models.CharField(max_length=50, choices=[
|
||||
('regular', '普通视频'),
|
||||
('shoppable', '可购物视频')
|
||||
], default='regular', verbose_name="视频类型")
|
||||
|
||||
gpm = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="视频GPM($)")
|
||||
videos_count = models.IntegerField(verbose_name="视频数量")
|
||||
avg_views = models.DecimalField(max_digits=12, decimal_places=2, verbose_name="平均观看量")
|
||||
avg_engagement = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="平均互动率(%)")
|
||||
avg_likes = models.IntegerField(verbose_name="平均点赞数")
|
||||
|
||||
# 时间范围
|
||||
start_date = models.DateField(verbose_name="开始日期")
|
||||
end_date = models.DateField(verbose_name="结束日期")
|
||||
|
||||
# 所属创作者
|
||||
creator = models.ForeignKey('CreatorProfile', on_delete=models.CASCADE, related_name="video_metrics",
|
||||
verbose_name="创作者")
|
||||
|
||||
# 时间戳
|
||||
create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
|
||||
update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "视频指标"
|
||||
verbose_name_plural = verbose_name
|
||||
db_table = "video_metrics"
|
||||
unique_together = ('creator', 'video_type', 'start_date', 'end_date') # 同一创作者同一时间段同一类型只能有一条记录
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.creator.name}的{self.get_video_type_display()}指标 ({self.start_date} - {self.end_date})"
|
||||
|
||||
|
||||
# 新增模型:直播指标数据
|
||||
class LiveMetrics(models.Model):
|
||||
"""直播指标数据模型,LIVE和Shoppable LIVE部分"""
|
||||
live_type = models.CharField(max_length=50, choices=[
|
||||
('regular', '普通直播'),
|
||||
('shoppable', '可购物直播')
|
||||
], default='regular', verbose_name="直播类型")
|
||||
|
||||
gpm = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="直播GPM($)")
|
||||
lives_count = models.IntegerField(verbose_name="直播数量")
|
||||
avg_views = models.DecimalField(max_digits=12, decimal_places=2, verbose_name="平均观看量")
|
||||
avg_engagement = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="平均互动率(%)")
|
||||
avg_likes = models.IntegerField(verbose_name="平均点赞数")
|
||||
|
||||
# 时间范围
|
||||
start_date = models.DateField(verbose_name="开始日期")
|
||||
end_date = models.DateField(verbose_name="结束日期")
|
||||
|
||||
# 所属创作者
|
||||
creator = models.ForeignKey('CreatorProfile', on_delete=models.CASCADE, related_name="live_metrics",
|
||||
verbose_name="创作者")
|
||||
|
||||
# 时间戳
|
||||
create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
|
||||
update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "直播指标"
|
||||
verbose_name_plural = verbose_name
|
||||
db_table = "live_metrics"
|
||||
unique_together = ('creator', 'live_type', 'start_date', 'end_date') # 同一创作者同一时间段同一类型只能有一条记录
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.creator.name}的{self.get_live_type_display()}指标 ({self.start_date} - {self.end_date})"
|
||||
|
||||
|
||||
class CreatorProfile(models.Model):
|
||||
"""达人信息模型,用于筛选功能"""
|
||||
# 基本信息
|
||||
name = models.CharField(max_length=255, verbose_name="达人名称")
|
||||
avatar_url = models.TextField(blank=True, null=True, verbose_name="头像URL")
|
||||
|
||||
# 新增联系方式
|
||||
email = models.EmailField(max_length=255, blank=True, null=True, verbose_name="电子邮箱")
|
||||
instagram = models.CharField(max_length=255, blank=True, null=True, verbose_name="Instagram账号")
|
||||
tiktok_link = models.URLField(max_length=255, blank=True, null=True, verbose_name="TikTok链接")
|
||||
location = models.CharField(max_length=100, blank=True, null=True, verbose_name="位置")
|
||||
live_schedule = models.CharField(max_length=255, blank=True, null=True, verbose_name="直播时间表")
|
||||
|
||||
# 类别 - Category
|
||||
CATEGORY_CHOICES = [
|
||||
('Phones & Electronics', '手机与电子产品'),
|
||||
('Homes Supplies', '家居用品'),
|
||||
('Kitchenware', '厨房用品'),
|
||||
('Textiles & Soft Furnishings', '纺织品和软装'),
|
||||
('Household Appliances', '家用电器'),
|
||||
('Womenswear & Underwear', '女装和内衣'),
|
||||
('Muslim Fashion', '穆斯林时尚'),
|
||||
('Shoes', '鞋类'),
|
||||
('Beauty & Personal Care', '美容和个人护理'),
|
||||
('Computers & Office Equipment', '电脑和办公设备'),
|
||||
('Pet Supplies', '宠物用品'),
|
||||
('Baby & Maternity', '婴儿和孕妇用品'),
|
||||
('Sports & Outdoor', '运动和户外'),
|
||||
('Toys', '玩具'),
|
||||
('Furniture', '家具'),
|
||||
('Tools & Hardware', '工具和硬件'),
|
||||
('Home Improvement', '家居装修'),
|
||||
('Automotive & Motorcycle', '汽车和摩托车'),
|
||||
('Fashion Accessories', '时尚配饰'),
|
||||
('Food & Beverages', '食品和饮料'),
|
||||
('Health', '健康'),
|
||||
('Books, Magazines & Audio', '书籍、杂志和音频'),
|
||||
('Kids Fashion', '儿童时尚'),
|
||||
('Menswear & Underwear', '男装和内衣'),
|
||||
('Luggage & Bags', '行李和包'),
|
||||
('Pre-Owned Collections', '二手收藏'),
|
||||
('Jewellery Accessories & Derivatives', '珠宝配饰及衍生品'),
|
||||
]
|
||||
category = models.CharField(max_length=100, choices=CATEGORY_CHOICES, blank=True, null=True, verbose_name="类别")
|
||||
|
||||
# 电商等级 - E-commerce Level (L1-L7)
|
||||
E_COMMERCE_LEVEL_CHOICES = [
|
||||
(1, 'L1'),
|
||||
(2, 'L2'),
|
||||
(3, 'L3'),
|
||||
(4, 'L4'),
|
||||
(5, 'L5'),
|
||||
(6, 'L6'),
|
||||
(7, 'L7'),
|
||||
]
|
||||
e_commerce_level = models.IntegerField(choices=E_COMMERCE_LEVEL_CHOICES, blank=True, null=True,
|
||||
verbose_name="电商能力等级")
|
||||
|
||||
# 曝光等级 - Exposure Level (KOC-1, KOC-2, KOL-1, KOL-2, KOL-3)
|
||||
EXPOSURE_LEVEL_CHOICES = [
|
||||
('KOC-1', 'KOC-1'),
|
||||
('KOC-2', 'KOC-2'),
|
||||
('KOL-1', 'KOL-1'),
|
||||
('KOL-2', 'KOL-2'),
|
||||
('KOL-3', 'KOL-3'),
|
||||
]
|
||||
exposure_level = models.CharField(max_length=10, choices=EXPOSURE_LEVEL_CHOICES, blank=True, null=True,
|
||||
verbose_name="曝光等级")
|
||||
|
||||
# 粉丝数 - Followers
|
||||
followers = models.IntegerField(default=0, verbose_name="粉丝数")
|
||||
|
||||
# GMV - Gross Merchandise Value (in thousands of dollars)
|
||||
gmv = models.DecimalField(max_digits=12, decimal_places=2, blank=True, null=True, verbose_name="GMV(千美元)")
|
||||
items_sold = models.DecimalField(max_digits=12, decimal_places=2, blank=True, null=True,
|
||||
verbose_name="售出商品数量")
|
||||
|
||||
# 视频数据 - Video Views
|
||||
avg_video_views = models.IntegerField(default=0, blank=True, null=True, verbose_name="平均视频浏览量")
|
||||
|
||||
# 价格信息 - Pricing
|
||||
pricing_min = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True, verbose_name="最低个人定价")
|
||||
pricing_max = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True, verbose_name="最高个人定价")
|
||||
pricing_package = models.CharField(max_length=100, blank=True, null=True, verbose_name="套餐定价")
|
||||
|
||||
# 合作信息 - Collaboration
|
||||
collab_count = models.IntegerField(default=0, blank=True, null=True, verbose_name="合作次数")
|
||||
latest_collab = models.CharField(max_length=100, blank=True, null=True, verbose_name="最新合作")
|
||||
|
||||
# 电商平台 - E-commerce platforms (存储为JSON数组,如["SUNLINK", "ARZOPA", "BELIFE"])
|
||||
e_commerce_platforms = models.JSONField(blank=True, null=True, verbose_name="电商平台")
|
||||
|
||||
# 分析数据 - Analytics (JSON格式存储销售渠道和类别分布)
|
||||
gmv_by_channel = models.JSONField(blank=True, null=True, verbose_name="GMV按渠道分布")
|
||||
gmv_by_category = models.JSONField(blank=True, null=True, verbose_name="GMV按类别分布")
|
||||
|
||||
# MCN机构
|
||||
mcn = models.CharField(max_length=255, blank=True, null=True, verbose_name="MCN机构")
|
||||
|
||||
# 时间戳
|
||||
create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
|
||||
update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "达人信息"
|
||||
verbose_name_plural = verbose_name
|
||||
db_table = "creator_profiles"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name}"
|
||||
|
||||
class CreatorCampaign(models.Model):
|
||||
"""达人-活动关联模型"""
|
||||
creator = models.ForeignKey('CreatorProfile', on_delete=models.CASCADE, verbose_name="达人",
|
||||
related_name="campaigns")
|
||||
campaign = models.ForeignKey(BrandCampaign, on_delete=models.CASCADE, verbose_name="活动", related_name="creators")
|
||||
status = models.CharField(max_length=20, default="pending", verbose_name="状态",
|
||||
choices=[
|
||||
("pending", "待处理"),
|
||||
("accepted", "已接受"),
|
||||
("rejected", "已拒绝"),
|
||||
("completed", "已完成")
|
||||
])
|
||||
|
||||
# 时间戳
|
||||
create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
|
||||
update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "达人活动关联"
|
||||
verbose_name_plural = verbose_name
|
||||
db_table = "creator_campaigns"
|
||||
unique_together = ('creator', 'campaign') # 一个达人在一个活动中只能有一条关联记录
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.creator.name} - {self.campaign.name}"
|
||||
|
||||
|
||||
class BrandCampaign(models.Model):
|
||||
"""品牌活动数据模型"""
|
||||
BRAND_CHOICES = [
|
||||
('U', 'U品牌'),
|
||||
('R', 'R品牌'),
|
||||
('X', 'X品牌'),
|
||||
('Q', 'Q品牌'),
|
||||
('A', 'A品牌'),
|
||||
('M', 'M品牌'),
|
||||
]
|
||||
|
||||
STATUS_CHOICES = [
|
||||
('completed', '已完成'),
|
||||
('in_progress', '进行中'),
|
||||
('rejected', '已拒绝'),
|
||||
]
|
||||
|
||||
brand_id = models.CharField(max_length=10, choices=BRAND_CHOICES, verbose_name="品牌ID")
|
||||
brand_name = models.CharField(max_length=255, default="brand", verbose_name="品牌名称")
|
||||
brand_color = models.CharField(max_length=20, default="#000000", verbose_name="品牌颜色")
|
||||
|
||||
# 添加关联到Campaign模型的外键
|
||||
campaign = models.ForeignKey(BrandCampaign, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="关联活动",
|
||||
related_name="brand_campaigns")
|
||||
|
||||
pricing_detail = models.CharField(max_length=50, verbose_name="价格详情")
|
||||
start_date = models.DateField(verbose_name="开始日期")
|
||||
end_date = models.DateField(verbose_name="结束日期")
|
||||
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, verbose_name="状态")
|
||||
gmv_achieved = models.CharField(max_length=50, verbose_name="实现GMV")
|
||||
views_achieved = models.CharField(max_length=50, verbose_name="实现观看量")
|
||||
video_link = models.URLField(max_length=255, blank=True, null=True, verbose_name="视频链接")
|
||||
|
||||
# 时间戳
|
||||
create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
|
||||
update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "品牌活动数据"
|
||||
verbose_name_plural = verbose_name
|
||||
db_table = "brand_campaigns"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.brand_id} - {self.brand_name}"
|
||||
|
||||
|
||||
class CreatorVideo(models.Model):
|
||||
"""创作者视频模型,用于存储视频相关信息"""
|
||||
# 关联字段
|
||||
creator = models.ForeignKey('CreatorProfile', on_delete=models.CASCADE, related_name='videos')
|
||||
|
||||
# 视频基本信息
|
||||
title = models.CharField(max_length=255, verbose_name="视频标题")
|
||||
description = models.TextField(blank=True, null=True, verbose_name="视频描述")
|
||||
thumbnail_url = models.URLField(max_length=500, blank=True, null=True, verbose_name="缩略图URL")
|
||||
video_url = models.URLField(max_length=500, blank=True, null=True, verbose_name="视频URL")
|
||||
video_id = models.CharField(max_length=100, verbose_name="视频ID")
|
||||
|
||||
# 视频类型
|
||||
TYPE_CHOICES = [
|
||||
('regular', '普通视频'),
|
||||
('product', '带产品视频')
|
||||
]
|
||||
video_type = models.CharField(max_length=20, choices=TYPE_CHOICES, default='regular', verbose_name="视频类型")
|
||||
|
||||
# 视频标记
|
||||
BADGE_CHOICES = [
|
||||
('red', '红色标记'),
|
||||
('gold', '金色标记')
|
||||
]
|
||||
badge = models.CharField(max_length=20, choices=BADGE_CHOICES, blank=True, null=True, verbose_name="视频标记")
|
||||
|
||||
# 视频统计
|
||||
view_count = models.IntegerField(default=0, verbose_name="观看次数")
|
||||
like_count = models.IntegerField(default=0, verbose_name="点赞数")
|
||||
comment_count = models.IntegerField(default=0, verbose_name="评论数")
|
||||
|
||||
# 产品信息 (仅对带产品视频有效)
|
||||
has_product = models.BooleanField(default=False, verbose_name="是否有产品")
|
||||
product_name = models.CharField(max_length=255, blank=True, null=True, verbose_name="产品名称")
|
||||
product_url = models.URLField(max_length=500, blank=True, null=True, verbose_name="产品链接")
|
||||
|
||||
# 发布信息
|
||||
release_date = models.DateField(verbose_name="发布日期")
|
||||
|
||||
# 时间戳
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "创作者视频"
|
||||
verbose_name_plural = "创作者视频"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.creator.name} - {self.title}"
|
||||
|
||||
|
||||
class FollowerMetrics(models.Model):
|
||||
"""粉丝统计数据模型"""
|
||||
# 关联字段
|
||||
creator = models.ForeignKey('CreatorProfile', on_delete=models.CASCADE, related_name='follower_metrics')
|
||||
|
||||
# 时间范围
|
||||
start_date = models.DateField()
|
||||
end_date = models.DateField()
|
||||
|
||||
# 粉丝性别统计
|
||||
female_percentage = models.FloatField(default=0) # 女性百分比
|
||||
male_percentage = models.FloatField(default=0) # 男性百分比
|
||||
|
||||
# 粉丝年龄分布
|
||||
age_18_24_percentage = models.FloatField(default=0) # 18-24岁百分比
|
||||
age_25_34_percentage = models.FloatField(default=0) # 25-34岁百分比
|
||||
age_35_44_percentage = models.FloatField(default=0) # 35-44岁百分比
|
||||
age_45_54_percentage = models.FloatField(default=0) # 45-54岁百分比
|
||||
age_55_plus_percentage = models.FloatField(default=0) # 55+岁百分比
|
||||
|
||||
# 粉丝地域分布 (前5位)
|
||||
location_data = models.JSONField(default=dict) # 格式: {"TX": 11, "FL": 8, "NY": 8, "GE": 5.5, "CA": 5}
|
||||
|
||||
# 创建和更新时间
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '粉丝统计'
|
||||
verbose_name_plural = '粉丝统计'
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.creator.name} 粉丝统计 ({self.start_date} - {self.end_date})"
|
||||
|
||||
|
||||
class TrendMetrics(models.Model):
|
||||
"""趋势统计数据模型"""
|
||||
# 关联字段
|
||||
creator = models.ForeignKey('CreatorProfile', on_delete=models.CASCADE, related_name='trend_metrics')
|
||||
|
||||
# 时间范围
|
||||
date = models.DateField() # 数据日期
|
||||
|
||||
# 指标数据
|
||||
gmv = models.FloatField(default=0) # GMV值
|
||||
items_sold = models.IntegerField(default=0) # 销售商品数
|
||||
followers_count = models.IntegerField(default=0) # 粉丝数
|
||||
video_views = models.IntegerField(default=0) # 视频观看量
|
||||
engagement_rate = models.FloatField(default=0) # 互动率(百分比)
|
||||
|
||||
# 创建和更新时间
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '趋势指标'
|
||||
verbose_name_plural = '趋势指标'
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.creator.name} 趋势指标 ({self.date})"
|
||||
|
||||
|
||||
|
||||
|
||||
# 公有达人库
|
||||
class PublicCreatorPool(models.Model):
|
||||
"""公有达人库模型,存储系统提供的公共达人信息"""
|
||||
creator = models.ForeignKey(CreatorProfile, on_delete=models.CASCADE, verbose_name="达人",
|
||||
related_name="public_pool")
|
||||
# is_active = models.BooleanField(default=True, verbose_name="是否活跃")
|
||||
category = models.CharField(max_length=255, blank=True, null=True, verbose_name="分类")
|
||||
remark = models.TextField(blank=True, null=True, verbose_name="备注")
|
||||
|
||||
# 时间戳
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "公有达人库"
|
||||
verbose_name_plural = verbose_name
|
||||
db_table = "public_creator_pool"
|
||||
|
||||
def __str__(self):
|
||||
return f"公有达人: {self.creator.name}"
|
||||
|
||||
|
||||
# 私有达人库
|
||||
class PrivateCreatorPool(models.Model):
|
||||
"""私有达人库模型,用户可以将公有达人添加到自己的私有库中"""
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="用户", related_name="private_creators", db_column='user_id')
|
||||
name = models.CharField(max_length=255, verbose_name="私有库名称")
|
||||
description = models.TextField(blank=True, null=True, verbose_name="描述")
|
||||
is_default = models.BooleanField(default=False, verbose_name="是否为默认库")
|
||||
# is_active = models.BooleanField(default=True, verbose_name="是否活跃")
|
||||
|
||||
# 时间戳
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "私有达人库"
|
||||
verbose_name_plural = verbose_name
|
||||
db_table = "private_creator_pool"
|
||||
unique_together = ('user', 'name') # 同一用户下不能有同名私有库
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.email}的{self.name}"
|
||||
|
||||
|
||||
# 私有达人库与达人的关联表
|
||||
class PrivateCreatorRelation(models.Model):
|
||||
"""私有达人库与达人的关联表,记录哪些达人被添加到了哪个私有库"""
|
||||
private_pool = models.ForeignKey(PrivateCreatorPool, on_delete=models.CASCADE, verbose_name="私有达人库",
|
||||
related_name="creator_relations")
|
||||
creator = models.ForeignKey(CreatorProfile, on_delete=models.CASCADE, verbose_name="达人",
|
||||
related_name="private_pool_relations")
|
||||
added_from_public = models.BooleanField(default=True, verbose_name="是否从公有库添加")
|
||||
notes = models.TextField(blank=True, null=True, verbose_name="笔记")
|
||||
status = models.CharField(max_length=20, default="active", verbose_name="状态",
|
||||
choices=[
|
||||
("active", "活跃"),
|
||||
("archived", "已归档"),
|
||||
("favorite", "收藏")
|
||||
])
|
||||
|
||||
# 时间戳
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="添加时间")
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "私有库达人关联"
|
||||
verbose_name_plural = verbose_name
|
||||
db_table = "private_creator_relations"
|
||||
unique_together = ('private_pool', 'creator') # 同一私有库中不能重复添加同一个达人
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.private_pool.name} - {self.creator.name}"
|
3
apps/daren_detail/tests.py
Normal file
3
apps/daren_detail/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
72
apps/daren_detail/urls.py
Normal file
72
apps/daren_detail/urls.py
Normal file
@ -0,0 +1,72 @@
|
||||
from django.urls import path, include
|
||||
from . import views
|
||||
from django.http import HttpResponse
|
||||
|
||||
|
||||
# 添加一个简单的index视图函数
|
||||
# def index(request):
|
||||
# return HttpResponse("Welcome to TikTok Videos Analysis API")
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
# path('', index, name='index'),
|
||||
# # TikTok API路由 - 旧路径保持不变以保证兼容性
|
||||
# path('tiktok/user-videos/', views.get_tiktok_user_videos, name='get-tiktok-user-videos'),
|
||||
# path('tiktok/fetch_videos/', views.fetch_tiktok_videos, name='fetch_tiktok_videos'),
|
||||
# path('tiktok/delete_user/', views.delete_tiktok_user, name='delete_tiktok_user'),
|
||||
|
||||
|
||||
|
||||
# 新的API路由结构 - 只保留TikTok API
|
||||
# path('api/tiktok/', include('app.api.tiktok.urls')), # TikTok API
|
||||
|
||||
# 添加Creator相关API
|
||||
path('creators/filter/', views.filter_creators, name='filter_creators'),
|
||||
path('creators/add/', views.add_creator, name='add_creator'),
|
||||
path('creators/<int:creator_id>/', views.get_creator_detail, name='get_creator_detail'),
|
||||
path('creators/update_detail/', views.update_creator_detail, name='update_creator_detail'),
|
||||
path('creators/<int:creator_id>/campaigns/', views.get_creator_brand_campaigns, name='get_creator_brand_campaigns'),
|
||||
|
||||
# 添加Campaign相关API
|
||||
path('campaigns/', views.get_campaigns, name='get_campaigns'),
|
||||
path('campaigns/add/', views.add_to_campaign, name='add_to_campaign'),
|
||||
|
||||
# 新增的指标相关API
|
||||
path('creators/<int:creator_id>/metrics/', views.get_creator_metrics, name='get_creator_metrics'),
|
||||
# 获取创作者的所有指标数据
|
||||
path('creators/metrics/update/', views.update_creator_metrics, name='update_creator_metrics'), # 更新创作者的指标数据
|
||||
|
||||
# 添加粉丝统计和趋势数据相关的路由
|
||||
path('creator/<int:creator_id>/followers/', views.get_creator_followers_metrics, name='get_creator_followers'),
|
||||
path('creator/followers/', views.get_creator_followers_metrics, name='get_creator_followers_query'),
|
||||
|
||||
# 获取创作者趋势数据
|
||||
path('creator/<int:creator_id>/trends/', views.get_creator_trends, name='get_creator_trends'),
|
||||
path('creator/trends/', views.get_creator_trends, name='get_creator_trends_query'),
|
||||
|
||||
# 更新创作者粉丝统计数据
|
||||
path('creator/followers/update/', views.update_creator_followers, name='update_creator_followers'),
|
||||
|
||||
# 更新创作者趋势数据
|
||||
path('creator/trend/update/', views.update_creator_trend, name='update_creator_trend'),
|
||||
|
||||
# 添加创作者视频相关的路由
|
||||
path('creator/<int:creator_id>/videos/', views.get_creator_videos, name='get_creator_videos'),
|
||||
path('creator/videos/', views.get_creator_videos, name='get_creator_videos_query'),
|
||||
|
||||
# 添加创作者视频
|
||||
path('creator/video/add/', views.add_creator_video, name='add_creator_video'),
|
||||
|
||||
# 公有达人和私有达人API
|
||||
path('public/creators/', views.get_public_creators, name='get_public_creators'),
|
||||
path('public/creators/add/', views.add_to_public_pool, name='add_to_public_pool'),
|
||||
|
||||
# 私有达人库
|
||||
path('private/pools/', views.get_private_pools, name='get_private_pools'),
|
||||
path('private/pools/create/', views.create_private_pool, name='create_private_pool'),
|
||||
path('private/pools/creators/<int:pool_id>/', views.get_private_pool_creators, name='get_private_pool_creators'),
|
||||
path('private/pools/creators/', views.get_private_pool_creators, name='get_private_pool_creators_no_id'),
|
||||
path('private/pools/creators/add/', views.add_creator_to_private_pool, name='add_creator_to_private_pool'),
|
||||
path('private/pools/creators/update/', views.update_creator_in_private_pool, name='update_creator_in_private_pool'),
|
||||
path('private/pools/creators/remove/', views.remove_creator_from_private_pool, name='remove_creator_from_private_pool'),
|
||||
]
|
3432
apps/daren_detail/views.py
Normal file
3432
apps/daren_detail/views.py
Normal file
File diff suppressed because it is too large
Load Diff
329
apps/discovery/README.md
Normal file
329
apps/discovery/README.md
Normal file
@ -0,0 +1,329 @@
|
||||
# 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`中提供
|
||||
- 接口不需要认证,可直接访问
|
3
apps/discovery/__init__.py
Normal file
3
apps/discovery/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""
|
||||
Discovery app for creator discovery and search.
|
||||
"""
|
18
apps/discovery/admin.py
Normal file
18
apps/discovery/admin.py
Normal file
@ -0,0 +1,18 @@
|
||||
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')
|
7
apps/discovery/apps.py
Normal file
7
apps/discovery/apps.py
Normal file
@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class DiscoveryConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.discovery'
|
||||
verbose_name = 'Creator Discovery'
|
31
apps/discovery/exceptions.py
Normal file
31
apps/discovery/exceptions.py
Normal file
@ -0,0 +1,31 @@
|
||||
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)
|
3
apps/discovery/management/__init__.py
Normal file
3
apps/discovery/management/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""
|
||||
Discovery app management commands.
|
||||
"""
|
3
apps/discovery/management/commands/__init__.py
Normal file
3
apps/discovery/management/commands/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""
|
||||
Discovery app management commands.
|
||||
"""
|
@ -0,0 +1,73 @@
|
||||
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('成功创建所有模拟数据!'))
|
57
apps/discovery/migrations/0001_initial.py
Normal file
57
apps/discovery/migrations/0001_initial.py
Normal file
@ -0,0 +1,57 @@
|
||||
# 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'],
|
||||
},
|
||||
),
|
||||
]
|
@ -0,0 +1,19 @@
|
||||
# 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='创建日期'),
|
||||
),
|
||||
]
|
0
apps/discovery/migrations/__init__.py
Normal file
0
apps/discovery/migrations/__init__.py
Normal file
79
apps/discovery/models.py
Normal file
79
apps/discovery/models.py
Normal file
@ -0,0 +1,79 @@
|
||||
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
|
22
apps/discovery/pagination.py
Normal file
22
apps/discovery/pagination.py
Normal file
@ -0,0 +1,22 @@
|
||||
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
|
||||
}
|
||||
})
|
35
apps/discovery/serializers.py
Normal file
35
apps/discovery/serializers.py
Normal file
@ -0,0 +1,35 @@
|
||||
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__'
|
3
apps/discovery/tests.py
Normal file
3
apps/discovery/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
12
apps/discovery/urls.py
Normal file
12
apps/discovery/urls.py
Normal file
@ -0,0 +1,12 @@
|
||||
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)),
|
||||
]
|
276
apps/discovery/views.py
Normal file
276
apps/discovery/views.py
Normal file
@ -0,0 +1,276 @@
|
||||
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
|
0
apps/expertproducts/__init__.py
Normal file
0
apps/expertproducts/__init__.py
Normal file
3
apps/expertproducts/admin.py
Normal file
3
apps/expertproducts/admin.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
6
apps/expertproducts/apps.py
Normal file
6
apps/expertproducts/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ExpertproductsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.expertproducts'
|
59
apps/expertproducts/migrations/0001_initial.py
Normal file
59
apps/expertproducts/migrations/0001_initial.py
Normal file
@ -0,0 +1,59 @@
|
||||
# Generated by Django 5.1.5 on 2025-05-19 08:40
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
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)),
|
||||
('sex', models.CharField(default='男', max_length=10)),
|
||||
('age', models.IntegerField(default=18)),
|
||||
('category', models.CharField(max_length=50)),
|
||||
('followers', models.IntegerField()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Product',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('category', models.CharField(max_length=50)),
|
||||
('max_price', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||
('min_price', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||
('description', models.TextField(blank=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Negotiation',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('status', models.CharField(choices=[('brand_review', '品牌回顾'), ('price_negotiation', '价格谈判'), ('contract_review', '合同确认'), ('draft_ready', '准备合同'), ('draft_approved', '合同提交'), ('accepted', '已接受'), ('abandoned', '已放弃')], default='price_negotiation', max_length=20)),
|
||||
('current_round', models.IntegerField(default=1)),
|
||||
('context', models.JSONField(default=dict)),
|
||||
('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='expertproducts.creator')),
|
||||
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='expertproducts.product')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Message',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('role', models.CharField(max_length=10)),
|
||||
('content', models.TextField()),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('stage', models.CharField(choices=[('brand_review', '品牌回顾'), ('price_negotiation', '价格谈判'), ('contract_review', '合同确认'), ('draft_ready', '准备合同'), ('draft_approved', '合同提交'), ('accepted', '已接受'), ('abandoned', '已放弃')], default='brand_review', max_length=32)),
|
||||
('negotiation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='expertproducts.negotiation')),
|
||||
],
|
||||
),
|
||||
]
|
0
apps/expertproducts/migrations/__init__.py
Normal file
0
apps/expertproducts/migrations/__init__.py
Normal file
55
apps/expertproducts/models.py
Normal file
55
apps/expertproducts/models.py
Normal file
@ -0,0 +1,55 @@
|
||||
from django.db import models
|
||||
from django.db.models import JSONField # 用于存储动态谈判条款
|
||||
from django.utils import timezone # Import timezone for timestamping
|
||||
|
||||
|
||||
# Create your models here.
|
||||
|
||||
class Product(models.Model):
|
||||
name = models.CharField(max_length=100) # 商品名称
|
||||
category = models.CharField(max_length=50) # 商品类目
|
||||
max_price = models.DecimalField(max_digits=10, decimal_places=2) # 最高价格(公开报价)
|
||||
min_price = models.DecimalField(max_digits=10, decimal_places=2) # 最低价格(底线)
|
||||
description = models.TextField(blank=True) # 商品描述(可选)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.max_price}元)"
|
||||
|
||||
class Creator(models.Model):
|
||||
name = models.CharField(max_length=100) # 达人名称
|
||||
sex = models.CharField(max_length=10, default='男') # 达人性别,设置默认值为未知
|
||||
age = models.IntegerField(default=18) # 达人年龄,设置默认值为18
|
||||
category = models.CharField(max_length=50) # 达人类别(如带货类)
|
||||
followers = models.IntegerField() # 粉丝数
|
||||
|
||||
|
||||
class Negotiation(models.Model):
|
||||
STATUS_CHOICES = [
|
||||
('brand_review', '品牌回顾'),
|
||||
('price_negotiation', '价格谈判'),
|
||||
('contract_review', '合同确认'),
|
||||
('draft_ready', '准备合同'),
|
||||
('draft_approved', '合同提交'),
|
||||
('accepted', '已接受'),
|
||||
('abandoned', '已放弃'),
|
||||
]
|
||||
|
||||
creator = models.ForeignKey('Creator', on_delete=models.CASCADE)
|
||||
product = models.ForeignKey('Product', on_delete=models.CASCADE)
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='price_negotiation')
|
||||
current_round = models.IntegerField(default=1) # 当前谈判轮次
|
||||
context = models.JSONField(default=dict) # 存储谈判上下文(如当前报价)
|
||||
|
||||
|
||||
class Message(models.Model):
|
||||
negotiation = models.ForeignKey(Negotiation, on_delete=models.CASCADE)
|
||||
role = models.CharField(max_length=10) # user/assistant
|
||||
content = models.TextField()
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
stage = models.CharField(
|
||||
max_length=32,
|
||||
choices=Negotiation.STATUS_CHOICES,
|
||||
default='brand_review' # 或你想要的其他默认阶段
|
||||
)
|
||||
|
||||
# 我们可以在这里添加额外的模型或关系,但现在使用user_management中的现有模型
|
174
apps/expertproducts/serializers.py
Normal file
174
apps/expertproducts/serializers.py
Normal file
@ -0,0 +1,174 @@
|
||||
from rest_framework import serializers
|
||||
# from user_management.models import OperatorAccount, PlatformAccount, Video, KnowledgeBase, KnowledgeBaseDocument
|
||||
import uuid
|
||||
from django.db.models import Q
|
||||
|
||||
from .models import Product, Creator, Negotiation, Message
|
||||
|
||||
#
|
||||
# class OperatorAccountSerializer(serializers.ModelSerializer):
|
||||
# id = serializers.UUIDField(read_only=False, required=False) # 允许前端不提供ID,但如果提供则必须是有效的UUID
|
||||
#
|
||||
# class Meta:
|
||||
# model = OperatorAccount
|
||||
# fields = ['id', 'username', 'password', 'real_name', 'email', 'phone', 'position', 'department', 'is_active',
|
||||
# 'created_at', 'updated_at']
|
||||
# read_only_fields = ['created_at', 'updated_at']
|
||||
# extra_kwargs = {
|
||||
# 'password': {'write_only': True}
|
||||
# }
|
||||
#
|
||||
# def create(self, validated_data):
|
||||
# # 如果没有提供ID,则生成一个UUID
|
||||
# if 'id' not in validated_data:
|
||||
# validated_data['id'] = uuid.uuid4()
|
||||
#
|
||||
# password = validated_data.pop('password', None)
|
||||
# instance = self.Meta.model(**validated_data)
|
||||
# if password:
|
||||
# instance.password = password # 在实际应用中应该加密存储密码
|
||||
# instance.save()
|
||||
# return instance
|
||||
#
|
||||
#
|
||||
# class PlatformAccountSerializer(serializers.ModelSerializer):
|
||||
# operator_name = serializers.CharField(source='operator.real_name', read_only=True)
|
||||
#
|
||||
# class Meta:
|
||||
# model = PlatformAccount
|
||||
# fields = ['id', 'operator', 'operator_name', 'platform_name', 'account_name', 'account_id',
|
||||
# 'status', 'followers_count', 'account_url', 'description',
|
||||
# 'tags', 'profile_image', 'last_posting',
|
||||
# 'created_at', 'updated_at', 'last_login']
|
||||
# read_only_fields = ['id', 'created_at', 'updated_at']
|
||||
#
|
||||
# def to_internal_value(self, data):
|
||||
# # 处理operator字段,可能是字符串格式的UUID
|
||||
# if 'operator' in data and isinstance(data['operator'], str):
|
||||
# try:
|
||||
# # 尝试获取对应的运营账号对象
|
||||
# operator = OperatorAccount.objects.get(id=data['operator'])
|
||||
# data['operator'] = operator.id # 确保使用正确的ID格式
|
||||
# except OperatorAccount.DoesNotExist:
|
||||
# # 如果找不到对应的运营账号,保持原值,让验证器捕获此错误
|
||||
# pass
|
||||
# except Exception as e:
|
||||
# # 其他类型的错误,如ID格式不正确等
|
||||
# pass
|
||||
#
|
||||
# return super().to_internal_value(data)
|
||||
#
|
||||
#
|
||||
# class PlatformDetailSerializer(serializers.Serializer):
|
||||
# """平台详情序列化器,用于多平台账号创建"""
|
||||
# platform_name = serializers.ChoiceField(choices=PlatformAccount.PLATFORM_CHOICES)
|
||||
# platform_url = serializers.URLField()
|
||||
#
|
||||
#
|
||||
# class MultiPlatformAccountSerializer(serializers.Serializer):
|
||||
# """多平台账号创建序列化器"""
|
||||
# operator = serializers.PrimaryKeyRelatedField(queryset=OperatorAccount.objects.all())
|
||||
# account_name = serializers.CharField(max_length=100)
|
||||
# account_id = serializers.CharField(max_length=100)
|
||||
# status = serializers.ChoiceField(choices=PlatformAccount.STATUS_CHOICES, default='active')
|
||||
# followers_count = serializers.IntegerField(default=0)
|
||||
# description = serializers.CharField(required=False, allow_blank=True, allow_null=True)
|
||||
# tags = serializers.CharField(required=False, allow_blank=True, allow_null=True, max_length=255)
|
||||
# profile_image = serializers.URLField(required=False, allow_blank=True, allow_null=True)
|
||||
# last_posting = serializers.DateTimeField(required=False, allow_null=True)
|
||||
# platforms = PlatformDetailSerializer(many=True)
|
||||
#
|
||||
# def to_internal_value(self, data):
|
||||
# # 处理operator字段,可能是字符串类型的ID
|
||||
# if 'operator' in data and isinstance(data['operator'], str):
|
||||
# try:
|
||||
# # 尝试通过ID查找运营账号
|
||||
# operator_id = data['operator']
|
||||
# try:
|
||||
# # 先尝试通过整数ID查找
|
||||
# operator_id_int = int(operator_id)
|
||||
# operator = OperatorAccount.objects.get(id=operator_id_int)
|
||||
# data['operator'] = operator.id
|
||||
# except (ValueError, OperatorAccount.DoesNotExist):
|
||||
# # 如果无法转换为整数或找不到对应账号,尝试通过用户名或真实姓名查找
|
||||
# operator = OperatorAccount.objects.filter(
|
||||
# Q(username=operator_id) | Q(real_name=operator_id)
|
||||
# ).first()
|
||||
#
|
||||
# if operator:
|
||||
# data['operator'] = operator.id
|
||||
# except Exception as e:
|
||||
# pass
|
||||
#
|
||||
# return super().to_internal_value(data)
|
||||
#
|
||||
#
|
||||
# class VideoSerializer(serializers.ModelSerializer):
|
||||
# platform_account_name = serializers.CharField(source='platform_account.account_name', read_only=True)
|
||||
# platform_name = serializers.CharField(source='platform_account.platform_name', read_only=True)
|
||||
#
|
||||
# class Meta:
|
||||
# model = Video
|
||||
# fields = ['id', 'platform_account', 'platform_account_name', 'platform_name', 'title',
|
||||
# 'description', 'video_url', 'local_path', 'thumbnail_url', 'status',
|
||||
# 'views_count', 'likes_count', 'comments_count', 'shares_count', 'tags',
|
||||
# 'publish_time', 'scheduled_time', 'created_at', 'updated_at']
|
||||
# read_only_fields = ['id', 'created_at', 'updated_at', 'views_count', 'likes_count',
|
||||
# 'comments_count', 'shares_count']
|
||||
#
|
||||
# def to_internal_value(self, data):
|
||||
# # 处理platform_account字段,可能是字符串格式的UUID
|
||||
# if 'platform_account' in data and isinstance(data['platform_account'], str):
|
||||
# try:
|
||||
# # 尝试获取对应的平台账号对象
|
||||
# platform_account = PlatformAccount.objects.get(id=data['platform_account'])
|
||||
# data['platform_account'] = platform_account.id # 确保使用正确的ID格式
|
||||
# except PlatformAccount.DoesNotExist:
|
||||
# # 如果找不到对应的平台账号,保持原值,让验证器捕获此错误
|
||||
# pass
|
||||
# except Exception as e:
|
||||
# # 其他类型的错误,如ID格式不正确等
|
||||
# pass
|
||||
#
|
||||
# return super().to_internal_value(data)
|
||||
#
|
||||
#
|
||||
# class KnowledgeBaseSerializer(serializers.ModelSerializer):
|
||||
# class Meta:
|
||||
# model = KnowledgeBase
|
||||
# fields = ['id', 'user_id', 'name', 'desc', 'type', 'department', 'group',
|
||||
# 'external_id', 'create_time', 'update_time']
|
||||
# read_only_fields = ['id', 'create_time', 'update_time']
|
||||
#
|
||||
#
|
||||
# class KnowledgeBaseDocumentSerializer(serializers.ModelSerializer):
|
||||
# class Meta:
|
||||
# model = KnowledgeBaseDocument
|
||||
# fields = ['id', 'knowledge_base', 'document_id', 'document_name',
|
||||
# 'external_id', 'uploader_name', 'status', 'create_time', 'update_time']
|
||||
# read_only_fields = ['id', 'create_time', 'update_time']
|
||||
#
|
||||
|
||||
class ProductSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class CreatorSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Creator
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class NegotiationSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Negotiation
|
||||
fields = '__all__'
|
||||
read_only_fields = ('status', 'current_round')
|
||||
|
||||
|
||||
class MessageSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Message
|
||||
fields = '__all__'
|
3
apps/expertproducts/tests.py
Normal file
3
apps/expertproducts/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
18
apps/expertproducts/urls.py
Normal file
18
apps/expertproducts/urls.py
Normal file
@ -0,0 +1,18 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import (
|
||||
ContentAnalysisAPI,
|
||||
TopCreatorsAPI,
|
||||
NegotiationViewSet,
|
||||
CreatorSQLSearchAPI
|
||||
)
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'negotiations', NegotiationViewSet, basename='negotiation')
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
path('analyze/', ContentAnalysisAPI.as_view(), name='content-analysis'),
|
||||
path('top-creators/', TopCreatorsAPI.as_view(), name='top-creators'),
|
||||
path('sql_search/', CreatorSQLSearchAPI.as_view(), name='sql-search'),
|
||||
]
|
795
apps/expertproducts/views.py
Normal file
795
apps/expertproducts/views.py
Normal file
@ -0,0 +1,795 @@
|
||||
## 根据商品信息生成视频文案
|
||||
from django.shortcuts import render
|
||||
import json
|
||||
import uuid
|
||||
import logging
|
||||
from django.db import transaction
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from django.db.models import Q
|
||||
import os
|
||||
import subprocess
|
||||
import re
|
||||
from django.db import connection
|
||||
from django.core.mail import EmailMessage
|
||||
|
||||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.parsers import MultiPartParser, FormParser
|
||||
|
||||
from .models import Product, Creator, Negotiation, Message
|
||||
from .serializers import ProductSerializer, CreatorSerializer, NegotiationSerializer
|
||||
import requests
|
||||
from ollama import Client
|
||||
client = Client(host="http://localhost:11434")
|
||||
class ContentAnalysisAPI(APIView):
|
||||
parser_classes = (MultiPartParser, FormParser)
|
||||
|
||||
def post(self, request):
|
||||
# 1. 接收所有字段
|
||||
data = request.data
|
||||
files = request.FILES.getlist('files') # 获取上传的文件列表
|
||||
|
||||
# 2. 验证必填字段
|
||||
required_fields = [
|
||||
"product_info.name", # 商品信息(字典)
|
||||
"product_info.price",
|
||||
"product_info.stock",
|
||||
"posting_suggestion", # 视频要求(字典)
|
||||
"acceptance_standard",
|
||||
"selling_points" # 商品卖点(列表)
|
||||
]
|
||||
|
||||
for field in required_fields:
|
||||
if field not in data:
|
||||
return Response(
|
||||
{"error": f"Field '{field}' is required."},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# 3. 解析 JSON 字段
|
||||
try:
|
||||
product_info = {
|
||||
"name": data.get('product_info.name'),
|
||||
"price": data.get('product_info.price'),
|
||||
"stock": data.get('product_info.stock')
|
||||
}
|
||||
video_requirements = {
|
||||
"posting_suggestion": data.get('posting_suggestion'),
|
||||
"acceptance_standard": data.get('acceptance_standard')
|
||||
}
|
||||
selling_points = data.get('selling_points')
|
||||
except json.JSONDecodeError as e:
|
||||
return Response(
|
||||
{"error": f"Invalid JSON format: {str(e)}"},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# 4. 处理文件
|
||||
file_contents = []
|
||||
for file in files:
|
||||
file_contents.append(file.read().decode('utf-8')) # 假设文件是文本文件
|
||||
|
||||
# 5. 用 messages 形式调用大模型
|
||||
try:
|
||||
system_prompt = self._build_video_prompt(product_info, video_requirements, selling_points)
|
||||
messages = [{'role': 'system', 'content': system_prompt}]
|
||||
|
||||
###### 调用steven的api
|
||||
url = "http://localhost:8002/api/text"
|
||||
data = {
|
||||
"question": "{}".format(system_prompt)
|
||||
}
|
||||
|
||||
response = requests.post(url, data=data)
|
||||
response_json = response.json() # 解析为字典
|
||||
full_response = response_json.get('response')
|
||||
|
||||
#### 调用本地deepseek
|
||||
# response = client.chat(
|
||||
# model="deepseek-r1:70b",
|
||||
# messages=messages,
|
||||
# )
|
||||
# full_response = response['message']['content']
|
||||
version1, version2 = self.filter_ai_response(full_response)
|
||||
print("full_respons:", full_response)
|
||||
# 6. 返回分析结果
|
||||
response_data = {
|
||||
"code": 200,
|
||||
"message": "成功生成视频文案",
|
||||
"data": {
|
||||
"version1": version1,
|
||||
"version2": version2
|
||||
}
|
||||
}
|
||||
|
||||
return Response(response_data)
|
||||
|
||||
except Exception as e:
|
||||
response_data = {
|
||||
"code": 500,
|
||||
"message": "AI分析失败",
|
||||
"data": {
|
||||
"version1": "",
|
||||
"version2": ""
|
||||
}
|
||||
}
|
||||
return Response(response_data)
|
||||
|
||||
def _build_video_prompt(self, product_info, video_requirements, selling_points):
|
||||
"""Build the prompt for the price negotiation phase"""
|
||||
content = f"""
|
||||
你是一名专业的新媒体短视频文案策划师。请根据以下商品信息、视频要求和商品卖点以及上传文件的内容,生成一段适合达人在短视频中介绍产品的中文口播文案,要求内容流畅、自然、有吸引力,能激发观众的兴趣和购买欲望。文案要突出商品的核心卖点,结合视频要求,适当加入情感化和场景化描述,结尾可以有引导关注或购买的号召。
|
||||
|
||||
【商品信息】
|
||||
{product_info}
|
||||
|
||||
【视频要求】
|
||||
{video_requirements}
|
||||
|
||||
【商品卖点】
|
||||
{selling_points}
|
||||
|
||||
请直接输出完整的中文视频口播文案,不要加任何解释说明。
|
||||
|
||||
请生成两个版本, 分别是Version1和Version2。
|
||||
|
||||
"""
|
||||
return content
|
||||
|
||||
def filter_ai_response(self, ai_response):
|
||||
"""过滤掉 <think>...</think> 及其内容, 只保留模型回复内容, 并解析出 Version1 和 Version2"""
|
||||
import re
|
||||
text = re.sub(r"<think>.*?</think>\s*", "", ai_response, flags=re.DOTALL)
|
||||
# 匹配 Version1 和 Version2,无需 ###
|
||||
match = re.search(r'Version1:?\s*([\s\S]*?)Version2:?\s*([\s\S]*)', text, re.IGNORECASE)
|
||||
if match:
|
||||
version1 = match.group(1).strip().strip('"')
|
||||
version2 = match.group(2).strip().strip('"')
|
||||
return version1, version2
|
||||
else:
|
||||
# 兜底:如果没匹配到,返回原文
|
||||
return text, ""
|
||||
|
||||
|
||||
## 轮训问答
|
||||
class NegotiationViewSet(viewsets.ModelViewSet):
|
||||
queryset = Negotiation.objects.all()
|
||||
serializer_class = NegotiationSerializer
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""创建谈判并返回包含初始消息的响应"""
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
# Extract creator and product from validated data
|
||||
creator = serializer.validated_data['creator']
|
||||
product = serializer.validated_data['product']
|
||||
|
||||
# 检查该用户是否存在
|
||||
if not Creator.objects.filter(id=creator.id).exists():
|
||||
return Response({
|
||||
"code": 404,
|
||||
"message": "未找到指定的达人",
|
||||
"data": None
|
||||
})
|
||||
|
||||
# Check if the product exists
|
||||
if not Product.objects.filter(id=product.id).exists():
|
||||
return Response({
|
||||
"code": 404,
|
||||
"message": "未找到指定的商品",
|
||||
"data": None
|
||||
})
|
||||
|
||||
# Check if a negotiation already exists for the same creator and product
|
||||
existing_negotiation = Negotiation.objects.filter(creator=creator, product=product).first()
|
||||
if existing_negotiation:
|
||||
return Response({
|
||||
"code": 400,
|
||||
"message": "谈判已存在",
|
||||
"data": {
|
||||
"negotiation_id": existing_negotiation.id
|
||||
}
|
||||
})
|
||||
|
||||
# 1. 创建谈判记录
|
||||
negotiation = serializer.save()
|
||||
|
||||
# 2. 生成并保存初始消息
|
||||
initial_message = self._generate_welcome_message(negotiation)
|
||||
message = Message.objects.create(
|
||||
negotiation=negotiation,
|
||||
role='assistant',
|
||||
content=initial_message,
|
||||
stage=negotiation.status
|
||||
)
|
||||
|
||||
# 4. 构建响应数据
|
||||
response_data = {
|
||||
"code": 200,
|
||||
"message": "谈判已创建",
|
||||
"data": {
|
||||
"id": message.id,
|
||||
**serializer.data,
|
||||
"content": initial_message,
|
||||
"created_at": message.created_at
|
||||
}
|
||||
}
|
||||
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(response_data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def chat(self, request, pk=None):
|
||||
"""统一对话接口"""
|
||||
negotiation = self.get_object()
|
||||
print("negotiation: ", negotiation.id)
|
||||
user_message = request.data.get("message", "")
|
||||
|
||||
# 1. 保存用户消息
|
||||
Message.objects.create(
|
||||
negotiation=negotiation,
|
||||
role='user',
|
||||
content=user_message,
|
||||
stage=negotiation.status
|
||||
)
|
||||
|
||||
# 2. 获取本阶段历史消息
|
||||
history = self._get_stage_messages(negotiation)
|
||||
# 如果没有历史(首次对话),手动加上本次用户输入
|
||||
if not history:
|
||||
history = [{'role': 'user', 'content': user_message}]
|
||||
|
||||
# 4. 构建大模型messages
|
||||
if negotiation.status == 'brand_review':
|
||||
system_prompt = self._build_brand_review_prompt(negotiation)
|
||||
elif negotiation.status == 'price_negotiation':
|
||||
system_prompt = self._build_price_prompt(negotiation)
|
||||
elif negotiation.status == 'contract_review':
|
||||
system_prompt = self._build_contract_review_prompt(negotiation)
|
||||
elif negotiation.status == 'draft_ready':
|
||||
system_prompt = self._build_draft_ready_prompt(negotiation)
|
||||
else:
|
||||
return Response({"error": "谈判已结束"}, status=400)
|
||||
|
||||
# 5. 组装最终messages
|
||||
messages = [{'role': 'system', 'content': system_prompt}] + history
|
||||
|
||||
# print("final_messages: ", messages)
|
||||
|
||||
# 6. 调用大模型
|
||||
try:
|
||||
response = client.chat(
|
||||
model="deepseek-r1:70b",
|
||||
messages=messages,
|
||||
)
|
||||
ai_response, current_status_json = self.filter_ai_response(response['message']['content'])
|
||||
|
||||
if negotiation.status == 'draft_ready':
|
||||
if current_status_json['is_resend_email']:
|
||||
self._send_contract_to_email(negotiation)
|
||||
|
||||
# print("response: ", response['message']['content'])
|
||||
except Exception as e:
|
||||
return Response({"error": f"AI服务错误: {str(e)}"}, status=500)
|
||||
|
||||
# 7. 自动状态转换
|
||||
old_status = negotiation.status # 记录更新前的状态
|
||||
self._update_negotiation_status(negotiation, current_status_json['status'])
|
||||
if old_status != 'draft_ready' and negotiation.status == 'draft_ready':
|
||||
self._send_contract_to_email(negotiation)
|
||||
|
||||
# 8. 保存AI回复
|
||||
Message.objects.create(
|
||||
negotiation=negotiation,
|
||||
role='assistant',
|
||||
content=ai_response,
|
||||
stage=negotiation.status
|
||||
)
|
||||
|
||||
return Response({
|
||||
"response": ai_response,
|
||||
"status": negotiation.status,
|
||||
})
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def submit(self, request, pk=None):
|
||||
"""接收一个文件并返回文件名,后续处理由你来实现"""
|
||||
negotiation = self.get_object()
|
||||
upload_file = request.FILES.get('file')
|
||||
if not upload_file:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '未收到文件',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
####### 这里实现对合同文件处理的后续业务 ########
|
||||
|
||||
####### 这里实现对合同文件处理的后续业务 ########
|
||||
|
||||
# 处理完成后给会话更新状态
|
||||
|
||||
self._update_negotiation_status(negotiation, "draft_approved")
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': '文件上传成功',
|
||||
'data': {
|
||||
'filename': upload_file.name,
|
||||
'size': upload_file.size
|
||||
}
|
||||
})
|
||||
|
||||
@action(detail=False, methods=['post'], url_path='search_creator/status')
|
||||
def search_creator_by_status(self, request):
|
||||
"""根据状态搜索达人"""
|
||||
status = request.data.get('status')
|
||||
if not status:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '未收到状态',
|
||||
'data': None
|
||||
})
|
||||
|
||||
# 查找符合状态的谈判
|
||||
negotiations = Negotiation.objects.filter(status=status)
|
||||
if not negotiations.exists():
|
||||
return Response({
|
||||
'code': 404,
|
||||
'message': '未找到符合条件的谈判',
|
||||
'data': None
|
||||
})
|
||||
|
||||
# 获取所有相关的达人
|
||||
creators = Creator.objects.filter(negotiation__in=negotiations).distinct()
|
||||
if creators.exists():
|
||||
# 序列化达人数据
|
||||
creator_data = [{'name': creator.name, 'category': creator.category, 'followers': creator.followers} for
|
||||
creator in creators]
|
||||
else:
|
||||
creator_data = []
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': '成功找到符合条件的达人',
|
||||
'data': creator_data
|
||||
})
|
||||
|
||||
@action(detail=False, methods=['post'])
|
||||
def offer_status(self, request):
|
||||
"""获取谈判状态"""
|
||||
creator_id = request.data.get('creator_id')
|
||||
product_id = request.data.get('product_id')
|
||||
|
||||
if not creator_id or not product_id:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '未收到达人名称或产品ID',
|
||||
'data': None
|
||||
})
|
||||
|
||||
# 查找符合条件的谈判
|
||||
try:
|
||||
creator = Creator.objects.get(id=creator_id)
|
||||
negotiation = Negotiation.objects.get(creator=creator, product_id=product_id)
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': '成功获取谈判状态',
|
||||
'data': {
|
||||
'status': negotiation.status
|
||||
}
|
||||
})
|
||||
except Negotiation.DoesNotExist:
|
||||
return Response({
|
||||
'code': 404,
|
||||
'message': f'不存在与用户id为{creator_id}和商品id为{product_id}的谈判',
|
||||
'data': None
|
||||
})
|
||||
|
||||
def filter_ai_response(self, ai_response):
|
||||
"""过滤掉 <think>...</think> 及其内容, 只保留模型回复内容, 并解析末尾的json状态"""
|
||||
import re
|
||||
import json
|
||||
# 1. 过滤 <think>...</think>
|
||||
text = re.sub(r"<think>.*?</think>\s*", "", ai_response, flags=re.DOTALL)
|
||||
# 2. 匹配最后一个 {...},允许后面有空白
|
||||
matches = list(re.finditer(r'\{[\s\S]*\}', text))
|
||||
status_json = None
|
||||
if matches:
|
||||
match = matches[-1]
|
||||
try:
|
||||
status_json = json.loads(match.group())
|
||||
# 去掉json部分
|
||||
text = text[:match.start()].rstrip()
|
||||
except Exception:
|
||||
status_json = None
|
||||
return text, status_json
|
||||
|
||||
def _generate_welcome_message(self, negotiation):
|
||||
"""生成欢迎消息"""
|
||||
product = negotiation.product
|
||||
creator = negotiation.creator
|
||||
return f"""
|
||||
尊敬的{creator.name},感谢您对{product.name}的关注!
|
||||
|
||||
🔍 商品详情:
|
||||
- 名称:{product.name}
|
||||
- 类目:{product.category}
|
||||
- 核心卖点:{product.description}
|
||||
|
||||
🤝 合作优势:
|
||||
- 我们的产品在市场上具有很高的竞争力和良好的口碑。
|
||||
- 我们提供灵活的合作方案和丰厚的回报。
|
||||
- 您的粉丝群体与我们的目标市场高度契合,能够有效提升产品的曝光率和销售量。
|
||||
|
||||
🎯 契合度:
|
||||
- 您在{creator.category}领域的影响力和专业性与我们的产品完美契合。
|
||||
- 您的创意和内容风格能够为我们的产品增添独特的价值。
|
||||
|
||||
我们诚挚地希望能与您合作, 这边想问一下您的报价是多少?
|
||||
""".strip()
|
||||
|
||||
def _build_brand_review_prompt(self, negotiation):
|
||||
"""Build the prompt for the brand review phase"""
|
||||
history = self._get_stage_messages(negotiation)
|
||||
content = f"""
|
||||
你正在与达人 {negotiation.creator.name} 沟通商品合作。
|
||||
已发送的商品信息:
|
||||
{self._get_last_assistant_message(negotiation)}
|
||||
|
||||
达人回复:
|
||||
{history[-1]['content']}
|
||||
请根据以下要求回应:
|
||||
1. 回答达人关于商品的问题
|
||||
2. 自然引导进入价格谈判阶段
|
||||
3. 如果达人表现出兴趣,直接询问"您希望以什么价格合作?"
|
||||
"""
|
||||
return content
|
||||
|
||||
def _build_price_prompt(self, negotiation):
|
||||
"""Build the prompt for the price negotiation phase"""
|
||||
history = self._get_stage_messages(negotiation)
|
||||
content = f"""
|
||||
你是作为一个专业的商务谈判专家, 现在正在与达人 {negotiation.creator.name} 进行价格谈判。
|
||||
|
||||
当前谈判轮次:{negotiation.current_round}/4
|
||||
商品参考价格:{negotiation.product.max_price}元
|
||||
商品最低价格:{negotiation.product.min_price}元
|
||||
达人最新回复:
|
||||
{history[-1]['content']}
|
||||
|
||||
如果说用户的报价合理: 就推进到合同审查阶段。
|
||||
如果说用户的报价不合理: 请根据我们的最低价格和最高价格的区间来跟用户去讲价, 总之就是尽量压低价格, 但是不要压的太低, 不要超过最高价格。如果用户同意我们的价格, 就推进到合同审查阶段。如果用户不同意我们的价格, 就继续谈判, 但是不要超过4轮谈判。
|
||||
在回复消息的最后返回一个json格式status, 根据用户回复的情况来选择返回的状态, 示例如下:
|
||||
{{
|
||||
"status": "contract_review" # 如果用户同意我们的价格, 就推进到合同审查阶段。
|
||||
"status": "price_negotiation" # 如果用户不同意我们的价格, 就继续谈判, 但是不要超过4轮谈判。
|
||||
}}
|
||||
|
||||
如果用户同意我们的价格的话或者用户的报价在最低价格和最高价格之间, 我们就顺带将初步合同模版发给用户, 合同模版如下:
|
||||
{self._generate_contract_template(negotiation)}
|
||||
|
||||
"""
|
||||
return content
|
||||
|
||||
def _build_contract_review_prompt(self, negotiation):
|
||||
"""Build the prompt for the contract review phase"""
|
||||
history = self._get_stage_messages(negotiation)
|
||||
content = f"""
|
||||
当前谈判已进入初步合同审查阶段。
|
||||
商品名称:{negotiation.product.name}
|
||||
达人:{negotiation.creator.name}
|
||||
达人最新回复:
|
||||
{history[-1]['content']}
|
||||
|
||||
请根据以下要求回应:
|
||||
1. 回答达人关于合同条款的问题
|
||||
2. 确保合同条款清晰明了
|
||||
3. 如果达人同意合同条款,确认并推进到签署阶段
|
||||
4. 如果有异议,记录并准备进一步协商
|
||||
|
||||
如果用户对合同条款有异议, 就继续回答相关问题, 但是不要超过4轮问询。
|
||||
如果用户觉得合同内容没有问题, 我们会将正式合同发送到用户的邮箱地址, 并推进到签署阶段。
|
||||
|
||||
如果用户想再次要一下合同模版, 我们就将合同模版发给用户, 合同模版如下:
|
||||
{self._generate_contract_template(negotiation)}
|
||||
|
||||
在回复消息的最后返回一个json格式status, 根据用户回复的情况来选择返回的状态, 示例如下:
|
||||
{{
|
||||
"status": "contract_review" # 如果用户对合同仍有疑问或者异议
|
||||
"status": "draft_ready" # 如果用户觉得合同内容没有问题
|
||||
}}
|
||||
|
||||
"""
|
||||
return content
|
||||
|
||||
def _build_draft_ready_prompt(self, negotiation):
|
||||
"""Build the prompt for the draft ready phase"""
|
||||
# history = self._get_stage_messages(negotiation)
|
||||
content = f"""
|
||||
当前谈判已进入正式合同的准备阶段。
|
||||
|
||||
如果用户表示没有收到合同邮件的话,我们就再次向用户发送合同邮件。
|
||||
如果用户表示接受的话,我们引导用户去查看邮件并签署合同。
|
||||
如果用户表示拒绝的话,我们引导用户去查看合同邮件然后在邮件中拒绝签署此合同。
|
||||
如果用户还有其他疑问的话,我们不做回答,引导用户去看合同邮件。
|
||||
|
||||
在回复消息的最后返回一个json格式的status, 内容如下:
|
||||
{{
|
||||
"status": "draft_ready",
|
||||
"is_resend_email": true/false # 默认为false。如果用户表示没有收到合同邮件的话就为true, 否则为false
|
||||
}}
|
||||
"""
|
||||
return content
|
||||
|
||||
def _update_negotiation_status(self, negotiation, current_status):
|
||||
"""状态自动转换"""
|
||||
# ai_response_lower = ai_response.lower()
|
||||
|
||||
if current_status == 'price_negotiation':
|
||||
negotiation.status = 'price_negotiation'
|
||||
elif current_status == 'contract_review':
|
||||
negotiation.status = 'contract_review'
|
||||
elif current_status == 'draft_ready':
|
||||
negotiation.status = 'draft_ready'
|
||||
elif current_status == 'draft_approved':
|
||||
negotiation.status = 'draft_approved'
|
||||
elif current_status == 'published':
|
||||
negotiation.status = 'published'
|
||||
elif current_status == 'abandoned':
|
||||
negotiation.status = 'abandoned'
|
||||
negotiation.save()
|
||||
|
||||
def _get_last_assistant_message(self, negotiation):
|
||||
"""获取上一条系统消息"""
|
||||
last_msg = negotiation.message_set.filter(role='assistant').last()
|
||||
return last_msg.content if last_msg else ""
|
||||
|
||||
def _get_stage_messages(self, negotiation):
|
||||
"""获取当前阶段的所有历史消息,按时间排序"""
|
||||
# 只查当前阶段的消息
|
||||
messages = negotiation.message_set.filter(stage=negotiation.status).order_by('created_at')
|
||||
return [{'role': m.role, 'content': m.content} for m in messages]
|
||||
|
||||
def _generate_contract_template(self, negotiation):
|
||||
"""生成合同模版"""
|
||||
return f"""
|
||||
以下为本次合作的合同模板内容,请展示给用户:
|
||||
================ 合同模板 ================
|
||||
合同编号:HT-{negotiation.id:06d}
|
||||
甲方(品牌方):XXX公司
|
||||
乙方(达人):{negotiation.creator.name}
|
||||
商品名称:{negotiation.product.name}
|
||||
合作价格:请以最终谈判价格为准
|
||||
合作内容:乙方需在其社交平台发布与商品相关的推广内容,具体要求以双方沟通为准。
|
||||
付款方式:甲方在乙方完成推广后7个工作日内支付合作费用。
|
||||
合同生效日期:以双方签署日期为准
|
||||
其他条款:如有未尽事宜,双方友好协商解决。
|
||||
========================================
|
||||
"""
|
||||
|
||||
def _send_contract_to_email(self, negotiation,
|
||||
contract_path='/Users/liuzizhen/OOIN/daren_project/operation/商品合同.docx'):
|
||||
"""发送合同到用户邮箱,支持附件"""
|
||||
contract_content = self._generate_contract_template(negotiation)
|
||||
subject = f"合同文件 - {negotiation.product.name}"
|
||||
recipient = '3299361176@qq.com'
|
||||
if not recipient:
|
||||
logger.error(f"未找到达人 {negotiation.creator.name} 的邮箱,无法发送合同。")
|
||||
return False
|
||||
try:
|
||||
email = EmailMessage(
|
||||
subject=subject,
|
||||
body=contract_content,
|
||||
from_email=getattr(settings, 'DEFAULT_FROM_EMAIL', None),
|
||||
to=[recipient],
|
||||
)
|
||||
# 如果有附件路径,添加附件
|
||||
if contract_path:
|
||||
with open(contract_path, 'rb') as f:
|
||||
email.attach('商品合同.docx', f.read(),
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document')
|
||||
email.send(fail_silently=False)
|
||||
logger.info(f"合同已发送到 {recipient}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"发送合同邮件失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
## 根据接收到的达人列表来查找达人(已弃用)
|
||||
class TopCreatorsAPI(APIView):
|
||||
def post(self, request):
|
||||
# Extract filtering criteria and creator list from the request
|
||||
criteria = request.data.get('criteria', "")
|
||||
creators = request.data.get('creators', [])
|
||||
top_n = request.data.get('top_n', 5) # Default to top 5 if not specified
|
||||
|
||||
if not creators:
|
||||
return Response({"error": "没有合适的达人"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Prepare the message for the Ollama model
|
||||
messages = self._build_messages(criteria, creators)
|
||||
|
||||
try:
|
||||
# Call the Ollama model
|
||||
response = client.chat(
|
||||
model="deepseek-r1:70b",
|
||||
messages=messages,
|
||||
)
|
||||
|
||||
ranked_creators = self._parse_response(response['message']['content'], top_n)
|
||||
|
||||
return Response({
|
||||
"top_creators": ranked_creators
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
except Exception as e:
|
||||
return Response({"error": f"AI service error: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
def _build_messages(self, criteria, creators):
|
||||
"""Build the messages for the Ollama model"""
|
||||
creators_str = "\n".join([
|
||||
f"name: {creator['name']}, sex: {creator['sex']}, age: {creator['age']}, category: {creator['category']}, followers: {creator['followers']}"
|
||||
for creator in creators])
|
||||
|
||||
content = f"""
|
||||
以下是给定的筛选标准, 请按照此标准来筛选达人:
|
||||
{criteria}
|
||||
|
||||
评估以下所有达人,从中筛选出符合条件的达人:
|
||||
{creators_str}
|
||||
|
||||
如果没有找到合适的达人, 就按照当前达人最匹配的达人返回。
|
||||
记住, 我只要json结果, 不要任何别的回答包括你的think部分!!!
|
||||
请按照以下格式返回结果:
|
||||
[{{
|
||||
"name": "达人名称",
|
||||
"sex": "达人性别",
|
||||
"age": "达人年龄",
|
||||
"category": "达人分类",
|
||||
"followers": "达人粉丝数"
|
||||
}},
|
||||
{{
|
||||
"name": "达人名称",
|
||||
"sex": "达人性别",
|
||||
"age": "达人年龄",
|
||||
"category": "达人分类",
|
||||
"followers": "达人粉丝数"
|
||||
}},
|
||||
...
|
||||
]]
|
||||
"""
|
||||
|
||||
return [{'role': 'user', 'content': content}]
|
||||
|
||||
def _parse_response(self, response, top_n):
|
||||
"""Parse the response from the Ollama model to extract the top N creators"""
|
||||
# Use a regular expression to find the JSON block
|
||||
json_match = re.search(r'```json\n(.*?)\n```', response, re.DOTALL)
|
||||
|
||||
if json_match:
|
||||
json_str = json_match.group(1)
|
||||
try:
|
||||
# Parse the JSON string
|
||||
creators = json.loads(json_str)
|
||||
# Return the top N creators
|
||||
return creators[:top_n]
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error decoding JSON: {e}")
|
||||
return []
|
||||
else:
|
||||
print("No JSON found in response")
|
||||
return []
|
||||
|
||||
|
||||
## 生成sql直接查询达人库
|
||||
class CreatorSQLSearchAPI(APIView):
|
||||
def post(self, request):
|
||||
criteria = request.data.get('criteria', '')
|
||||
top_n = int(request.data.get('top_n', 10)) # 默认为10
|
||||
if not criteria:
|
||||
return Response({"error": "缺少筛选条件"}, status=400)
|
||||
|
||||
table_schema = '''
|
||||
feishu_creators(
|
||||
id char(32) 主键,
|
||||
record_id varchar(100) 记录ID,
|
||||
contact_person varchar(50) 联系人姓名,
|
||||
handle longtext 账号/达人昵称,
|
||||
tiktok_url longtext 抖音主页链接,
|
||||
fans_count varchar(50) 粉丝数,
|
||||
gmv varchar(100) GMV,
|
||||
email varchar(254) 邮箱,
|
||||
phone varchar(50) 电话,
|
||||
account_type varchar(50) 账号类型,
|
||||
price_quote longtext 报价,
|
||||
response_speed varchar(50) 响应速度,
|
||||
cooperation_intention varchar(50) 合作意向,
|
||||
payment_method varchar(50) 支付方式,
|
||||
payment_account varchar(100) 支付账号,
|
||||
address longtext 地址,
|
||||
has_ooin varchar(10) 是否有OOIN,
|
||||
source varchar(100) 数据来源,
|
||||
contact_status varchar(50) 联系状态,
|
||||
cooperation_brands json 合作品牌,
|
||||
system_categories varchar(100) 系统分类,
|
||||
actual_categories varchar(100) 实际分类,
|
||||
human_categories varchar(100) 人工分类,
|
||||
creator_base varchar(100) 达人基地,
|
||||
notes longtext 备注,
|
||||
created_at datetime 创建时间,
|
||||
updated_at datetime 更新时间
|
||||
)
|
||||
'''
|
||||
|
||||
prompt = f"""
|
||||
你是一个SQL专家。下面是MySQL表feishu_creators的结构:
|
||||
{table_schema}
|
||||
|
||||
以下是我对表中每个字段的解释: 方便你写出正确的sql查询语句
|
||||
注意: fans_count 字段为字符串,可能为纯数字(如 '1234'),也可能带有 K(千)或 M(百万)后缀(如 '9K', '56.5K', '13.2M')。请在SQL中将其统一转换为数字后再进行比较, K=1000, M=1000000。例如查找粉丝数量大于10000的博主: SELECT * FROM your_table WHERE (CASE WHEN fans_count LIKE '%K' THEN CAST(REPLACE(fans_count, 'K', '') AS DECIMAL(10,2)) * 1000 WHEN fans_count LIKE '%M' THEN CAST(REPLACE(fans_count, 'M', '') AS DECIMAL(10,2)) * 1000000 ELSE CAST(fans_count AS DECIMAL(10,2)) END) > 100000;
|
||||
注意: response_speed 字段有以下几个取值: 1、无回复 2、一般 3、正常 4、积极。请在sql中直接使用上述的某个值就好, 不要进行任何转换。例如: SELECT * FROM daren.feishu_creators WHERE response_speed = '积极'
|
||||
注意: source 字段有以下几个取值: 1. 线下活动 2、TAP后台。请在sql中直接使用上述的某个值就好, 不要进行任何转换。例如: SELECT * FROM daren.feishu_creators WHERE source = 'TAP后台'
|
||||
注意: system_categories 字段是一个varchar类型的值, 是一个列表形式表示的,例如 ['日用百货']、['美妆个护,保健'] 这种。例如查找'美妆个护'的博主: SELECT * FROM daren.feishu_creators WHERE system_categories LIKE '%美妆个护%';
|
||||
注意: gmv 字段是一个varchar类型的值, 例如 $86.89、$8761.98、$15.5K、$12.5M 这种。例如我要查询gmv大于10000美金的达人: SELECT * FROM daren.feishu_creators WHERE gmv NOT LIKE '$0-%' AND (CASE WHEN gmv LIKE '%K' THEN CAST(SUBSTRING(gmv, 2, LENGTH(gmv) - 2) AS DECIMAL(10,2)) * 1000 WHEN gmv LIKE '%M' THEN CAST(SUBSTRING(gmv, 2, LENGTH(gmv) - 2) AS DECIMAL(10,2)) * 1000000 ELSE CAST(SUBSTRING(gmv, 2) AS DECIMAL(10,2)) END) > 10000;
|
||||
请根据以下自然语言筛选条件, 生成一条MySQL的SELECT语句, 查询daren.feishu_creators表(注意一定要写数据库名称.表名称), 返回所有字段。不要加任何解释说明, 只输出SQL语句本身。
|
||||
筛选条件:{criteria}
|
||||
"""
|
||||
|
||||
# 2. 让大模型生成SQL
|
||||
response = client.chat(
|
||||
model="deepseek-r1:70b",
|
||||
messages=[{'role': 'user', 'content': prompt}],
|
||||
)
|
||||
sql = self._extract_sql(response['message']['content'])
|
||||
|
||||
# 3. 校验SQL(只允许select,防止注入)
|
||||
if not sql.strip().lower().startswith('select'):
|
||||
response_data = {
|
||||
"code": 500,
|
||||
"message": "生成的SQL不合法",
|
||||
"data": {
|
||||
"results": []
|
||||
}
|
||||
}
|
||||
return Response(response_data)
|
||||
|
||||
# 4. 执行SQL
|
||||
with connection.cursor() as cursor:
|
||||
try:
|
||||
cursor.execute(sql)
|
||||
columns = [col[0] for col in cursor.description]
|
||||
results = [dict(zip(columns, row)) for row in cursor.fetchall()]
|
||||
response_data = {
|
||||
"code": 200,
|
||||
"message": "成功生成SQL",
|
||||
"data": {
|
||||
"results": results[:top_n]
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
response_data = {
|
||||
"code": 500,
|
||||
"message": "SQL执行异常",
|
||||
"data": {
|
||||
"results": []
|
||||
}
|
||||
}
|
||||
raise
|
||||
|
||||
return Response(response_data)
|
||||
|
||||
def _extract_sql(self, text):
|
||||
# 先去除 <think>...</think> 内容
|
||||
import re
|
||||
text = re.sub(r"<think>.*?</think>\s*", "", text, flags=re.DOTALL)
|
||||
# 再提取SQL语句
|
||||
match = re.search(r"select[\s\S]+?;", text, re.IGNORECASE)
|
||||
if match:
|
||||
return match.group()
|
||||
return text.strip()
|
1
apps/template/admin.py
Normal file
1
apps/template/admin.py
Normal file
@ -0,0 +1 @@
|
||||
|
7
apps/template/apps.py
Normal file
7
apps/template/apps.py
Normal file
@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class TemplateConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.template'
|
||||
verbose_name = '模板管理'
|
59
apps/template/exceptions.py
Normal file
59
apps/template/exceptions.py
Normal file
@ -0,0 +1,59 @@
|
||||
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)
|
26
apps/template/filters.py
Normal file
26
apps/template/filters.py
Normal file
@ -0,0 +1,26 @@
|
||||
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'
|
||||
]
|
50
apps/template/migrations/0001_initial.py
Normal file
50
apps/template/migrations/0001_initial.py
Normal file
@ -0,0 +1,50 @@
|
||||
# Generated by Django 5.1.5 on 2025-05-19 08:40
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
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='更新时间')),
|
||||
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='templates', to='template.templatecategory', verbose_name='模板分类')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '模板',
|
||||
'verbose_name_plural': '模板',
|
||||
},
|
||||
),
|
||||
]
|
0
apps/template/migrations/__init__.py
Normal file
0
apps/template/migrations/__init__.py
Normal file
77
apps/template/models.py
Normal file
77
apps/template/models.py
Normal file
@ -0,0 +1,77 @@
|
||||
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)
|
37
apps/template/pagination.py
Normal file
37
apps/template/pagination.py
Normal file
@ -0,0 +1,37 @@
|
||||
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)
|
104
apps/template/serializers.py
Normal file
104
apps/template/serializers.py
Normal file
@ -0,0 +1,104 @@
|
||||
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
|
3
apps/template/tests.py
Normal file
3
apps/template/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
11
apps/template/urls.py
Normal file
11
apps/template/urls.py
Normal file
@ -0,0 +1,11 @@
|
||||
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)),
|
||||
]
|
36
apps/template/utils.py
Normal file
36
apps/template/utils.py
Normal file
@ -0,0 +1,36 @@
|
||||
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)
|
297
apps/template/views.py
Normal file
297
apps/template/views.py
Normal file
@ -0,0 +1,297 @@
|
||||
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="按服务类型获取模板成功"
|
||||
)
|
0
apps/user/__init__.py
Normal file
0
apps/user/__init__.py
Normal file
3
apps/user/admin.py
Normal file
3
apps/user/admin.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
6
apps/user/apps.py
Normal file
6
apps/user/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class UserConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.user'
|
34
apps/user/migrations/0001_initial.py
Normal file
34
apps/user/migrations/0001_initial.py
Normal file
@ -0,0 +1,34 @@
|
||||
# Generated by Django 5.1.5 on 2025-05-16 09:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='User',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('email', models.EmailField(max_length=255, unique=True, verbose_name='电子邮箱')),
|
||||
('password', models.CharField(max_length=255, verbose_name='密码')),
|
||||
('company', models.CharField(blank=True, max_length=255, null=True, verbose_name='公司名称')),
|
||||
('name', models.CharField(blank=True, max_length=255, null=True, verbose_name='用户姓名')),
|
||||
('is_first_login', models.BooleanField(default=True, verbose_name='是否首次登录')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='最近登录时间')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='是否活跃')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '用户',
|
||||
'verbose_name_plural': '用户',
|
||||
'db_table': 'users',
|
||||
},
|
||||
),
|
||||
]
|
17
apps/user/migrations/0002_remove_user_is_active.py
Normal file
17
apps/user/migrations/0002_remove_user_is_active.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.1.5 on 2025-05-19 04:33
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('user', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='is_active',
|
||||
),
|
||||
]
|
0
apps/user/migrations/__init__.py
Normal file
0
apps/user/migrations/__init__.py
Normal file
23
apps/user/models.py
Normal file
23
apps/user/models.py
Normal file
@ -0,0 +1,23 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
class User(models.Model):
|
||||
"""用户模型,用于登录和账户管理"""
|
||||
email = models.EmailField(max_length=255, unique=True, verbose_name="电子邮箱")
|
||||
password = models.CharField(max_length=255, verbose_name="密码")
|
||||
company = models.CharField(max_length=255, blank=True, null=True, verbose_name="公司名称")
|
||||
name = models.CharField(max_length=255, blank=True, null=True, verbose_name="用户姓名")
|
||||
is_first_login = models.BooleanField(default=True, verbose_name="是否首次登录")
|
||||
last_login = models.DateTimeField(blank=True, null=True, verbose_name="最近登录时间")
|
||||
|
||||
# 时间戳
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "用户"
|
||||
verbose_name_plural = verbose_name
|
||||
db_table = "users"
|
||||
|
||||
def __str__(self):
|
||||
return self.email
|
3
apps/user/tests.py
Normal file
3
apps/user/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
10
apps/user/urls.py
Normal file
10
apps/user/urls.py
Normal file
@ -0,0 +1,10 @@
|
||||
from django.urls import path, include
|
||||
from . import views
|
||||
from django.http import HttpResponse
|
||||
|
||||
urlpatterns = [
|
||||
# 用户登录和信息更新相关路由
|
||||
path('login/', views.user_login, name='user_login'),
|
||||
path('update_info/', views.update_user_info, name='update_user_info'),
|
||||
path('register/', views.user_register, name='user_register'),
|
||||
]
|
268
apps/user/views.py
Normal file
268
apps/user/views.py
Normal file
@ -0,0 +1,268 @@
|
||||
from django.http import JsonResponse
|
||||
# from .models import TiktokUserVideos
|
||||
import logging
|
||||
import os
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.shortcuts import render
|
||||
import json
|
||||
import requests
|
||||
import concurrent.futures
|
||||
import shutil
|
||||
import dotenv
|
||||
import random
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
# 添加logger定义
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
directory_monitoring = {}
|
||||
|
||||
# 全局变量来控制检测线程
|
||||
monitor_thread = None
|
||||
is_monitoring = False
|
||||
|
||||
@csrf_exempt
|
||||
@require_http_methods(["POST"])
|
||||
def user_login(request):
|
||||
"""用户登录接口,首次登录会返回需要填写信息的标志"""
|
||||
try:
|
||||
from .models import User
|
||||
import json
|
||||
from django.contrib.auth.hashers import check_password, make_password
|
||||
from datetime import datetime
|
||||
|
||||
data = json.loads(request.body)
|
||||
|
||||
# 获取登录参数
|
||||
email = data.get('email')
|
||||
password = data.get('password')
|
||||
|
||||
if not email or not password:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '缺少必要参数: email 或 password',
|
||||
'data': None
|
||||
}, json_dumps_params={'ensure_ascii': False})
|
||||
|
||||
# 查询用户
|
||||
try:
|
||||
user = User.objects.get(email=email)
|
||||
|
||||
# 验证密码
|
||||
# 注意:这里假设密码已经进行了哈希存储,实际使用时需要采用适当的密码验证方法
|
||||
# 如果密码未哈希存储,直接比较原始密码
|
||||
password_valid = (user.password == password)
|
||||
|
||||
if not password_valid:
|
||||
return JsonResponse({
|
||||
'code': 401,
|
||||
'message': '用户名或密码错误',
|
||||
'data': None
|
||||
}, json_dumps_params={'ensure_ascii': False})
|
||||
|
||||
# 检查是否首次登录
|
||||
is_first_login = user.is_first_login
|
||||
|
||||
# 更新最后登录时间
|
||||
user.last_login = datetime.now()
|
||||
user.save()
|
||||
|
||||
# 构造返回数据
|
||||
user_data = {
|
||||
'user_id': user.id,
|
||||
'email': user.email,
|
||||
'is_first_login': is_first_login,
|
||||
'name': user.name,
|
||||
'company': user.company
|
||||
}
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '登录成功',
|
||||
'data': user_data
|
||||
}, json_dumps_params={'ensure_ascii': False})
|
||||
|
||||
except User.DoesNotExist:
|
||||
# 用户不存在,创建新用户
|
||||
new_user = User.objects.create(
|
||||
email=email,
|
||||
password=password, # 注意:实际使用时应该哈希存储密码
|
||||
is_first_login=True,
|
||||
last_login=datetime.now()
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '登录成功',
|
||||
'data': {
|
||||
'user_id': new_user.id,
|
||||
'email': new_user.email,
|
||||
'is_first_login': True,
|
||||
'name': None,
|
||||
'company': None
|
||||
}
|
||||
}, json_dumps_params={'ensure_ascii': False})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"用户登录失败: {e}")
|
||||
import traceback
|
||||
logger.error(f"详细错误: {traceback.format_exc()}")
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'登录失败: {str(e)}',
|
||||
'data': None
|
||||
}, json_dumps_params={'ensure_ascii': False})
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@require_http_methods(["POST"])
|
||||
def update_user_info(request):
|
||||
"""更新用户信息,首次登录时填写公司和姓名"""
|
||||
try:
|
||||
from .models import User
|
||||
import json
|
||||
|
||||
data = json.loads(request.body)
|
||||
|
||||
# 获取参数
|
||||
user_id = data.get('user_id')
|
||||
company = data.get('company')
|
||||
name = data.get('name')
|
||||
|
||||
if not user_id:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '缺少必要参数: user_id',
|
||||
'data': None
|
||||
}, json_dumps_params={'ensure_ascii': False})
|
||||
|
||||
# 如果是首次登录,需要填写公司和姓名
|
||||
if not company or not name:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '首次登录需要填写公司和姓名',
|
||||
'data': None
|
||||
}, json_dumps_params={'ensure_ascii': False})
|
||||
|
||||
# 查询用户并更新信息
|
||||
try:
|
||||
user = User.objects.get(id=user_id)
|
||||
|
||||
# 更新信息
|
||||
user.company = company
|
||||
user.name = name
|
||||
user.is_first_login = False # 更新后不再是首次登录
|
||||
user.save()
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '信息更新成功',
|
||||
'data': {
|
||||
'user_id': user.id,
|
||||
'email': user.email,
|
||||
'is_first_login': False,
|
||||
'name': user.name,
|
||||
'company': user.company
|
||||
}
|
||||
}, json_dumps_params={'ensure_ascii': False})
|
||||
|
||||
except User.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': f'找不到ID为 {user_id} 的用户',
|
||||
'data': None
|
||||
}, json_dumps_params={'ensure_ascii': False})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"更新用户信息失败: {e}")
|
||||
import traceback
|
||||
logger.error(f"详细错误: {traceback.format_exc()}")
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'更新用户信息失败: {str(e)}',
|
||||
'data': None
|
||||
}, json_dumps_params={'ensure_ascii': False})
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@require_http_methods(["POST"])
|
||||
def user_register(request):
|
||||
"""用户注册接口,允许用户创建新账户,可选填写公司和姓名"""
|
||||
try:
|
||||
from .models import User
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
data = json.loads(request.body)
|
||||
|
||||
# 获取注册参数
|
||||
email = data.get('email')
|
||||
password = data.get('password')
|
||||
company = data.get('company') # 可选参数
|
||||
name = data.get('name') # 可选参数
|
||||
|
||||
# 检查必要参数
|
||||
if not email or not password:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '缺少必要参数: email 或 password',
|
||||
'data': None
|
||||
}, json_dumps_params={'ensure_ascii': False})
|
||||
|
||||
# 检查邮箱是否已注册
|
||||
if User.objects.filter(email=email).exists():
|
||||
return JsonResponse({
|
||||
'code': 409,
|
||||
'message': '该邮箱已注册',
|
||||
'data': None
|
||||
}, json_dumps_params={'ensure_ascii': False})
|
||||
|
||||
# 创建用户
|
||||
try:
|
||||
# 根据是否提供公司和姓名决定是否为首次登录
|
||||
is_first_login = not (company and name)
|
||||
|
||||
# 创建用户
|
||||
user = User.objects.create(
|
||||
email=email,
|
||||
password=password, # 注意:实际使用时应该哈希存储密码
|
||||
company=company,
|
||||
name=name,
|
||||
is_first_login=is_first_login,
|
||||
last_login=datetime.now()
|
||||
)
|
||||
|
||||
# 构造返回数据
|
||||
user_data = {
|
||||
'user_id': user.id,
|
||||
'email': user.email,
|
||||
'is_first_login': is_first_login,
|
||||
'company': user.company,
|
||||
'name': user.name
|
||||
}
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '注册成功',
|
||||
'data': user_data
|
||||
}, json_dumps_params={'ensure_ascii': False})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"创建用户失败: {e}")
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'注册失败: {str(e)}',
|
||||
'data': None
|
||||
}, json_dumps_params={'ensure_ascii': False})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"用户注册失败: {e}")
|
||||
import traceback
|
||||
logger.error(f"详细错误: {traceback.format_exc()}")
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'注册失败: {str(e)}',
|
||||
'data': None
|
||||
}, json_dumps_params={'ensure_ascii': False})
|
0
daren/__init__.py
Normal file
0
daren/__init__.py
Normal file
16
daren/asgi.py
Normal file
16
daren/asgi.py
Normal file
@ -0,0 +1,16 @@
|
||||
"""
|
||||
ASGI config for daren project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'daren.settings')
|
||||
|
||||
application = get_asgi_application()
|
176
daren/settings.py
Normal file
176
daren/settings.py
Normal file
@ -0,0 +1,176 @@
|
||||
"""
|
||||
Django settings for daren project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 5.1.5.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.1/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/5.1/ref/settings/
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-6#we0eu(p&%ejp^=vmig!43m#+@k5rsb3zv^6%623uk&e#zzgl'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django_filters',
|
||||
'django.contrib.staticfiles',
|
||||
'apps.user.apps.UserConfig',
|
||||
"apps.expertproducts.apps.ExpertproductsConfig",
|
||||
"apps.daren_detail.apps.DarenDetailConfig",
|
||||
"apps.discovery.apps.DiscoveryConfig",
|
||||
"apps.template.apps.TemplateConfig",
|
||||
"apps.brands.apps.BrandsConfig",
|
||||
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'daren.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [BASE_DIR / 'templates']
|
||||
,
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'daren.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.mysql',
|
||||
'NAME': 'daren_detail',
|
||||
'USER': 'root',
|
||||
'PASSWORD': '123456',
|
||||
'HOST': 'localhost',
|
||||
'PORT': '3306',
|
||||
'OPTIONS': {
|
||||
'charset': 'utf8mb4',
|
||||
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
|
||||
'connect_timeout': 60, # 连接超时时间
|
||||
},
|
||||
'CONN_MAX_AGE': 0, # 强制Django在每次请求后关闭连接
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/5.1/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/5.1/howto/static-files/
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
# 日志配置
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'formatters': {
|
||||
'verbose': {
|
||||
'format': '{levelname} {asctime} {module} {message}',
|
||||
'style': '{',
|
||||
},
|
||||
'simple': {
|
||||
'format': '{levelname} {message}',
|
||||
'style': '{',
|
||||
},
|
||||
},
|
||||
'handlers': {
|
||||
'console': {
|
||||
'class': 'logging.StreamHandler',
|
||||
'formatter': 'verbose',
|
||||
},
|
||||
'file': {
|
||||
'class': 'logging.FileHandler',
|
||||
'filename': os.path.join(BASE_DIR, 'logs', 'app.log'),
|
||||
'formatter': 'verbose',
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'app': {
|
||||
'handlers': ['console', 'file'],
|
||||
'level': 'INFO',
|
||||
'propagate': True,
|
||||
},
|
||||
},
|
||||
}
|
12
daren/urls.py
Normal file
12
daren/urls.py
Normal file
@ -0,0 +1,12 @@
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('api/user/', include('apps.user.urls')),
|
||||
path('api/daren_detail/', include('apps.daren_detail.urls')),
|
||||
path('api/operation/', include('apps.expertproducts.urls')),
|
||||
path('api/discovery/', include('apps.discovery.urls')),
|
||||
path('api/template/', include('apps.template.urls')),
|
||||
path('api/', include('apps.brands.urls')),
|
||||
]
|
16
daren/wsgi.py
Normal file
16
daren/wsgi.py
Normal file
@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for daren project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'daren.settings')
|
||||
|
||||
application = get_wsgi_application()
|
101
docs/api/filter_creators.md
Normal file
101
docs/api/filter_creators.md
Normal file
@ -0,0 +1,101 @@
|
||||
# 筛选达人信息接口文档
|
||||
|
||||
## 基本信息
|
||||
- **接口名称**:筛选达人信息
|
||||
- **接口URL**:`/api/daren_detail/filter_creators/`
|
||||
- **请求方式**:GET
|
||||
- **接口描述**:根据多个条件筛选达人信息,支持分页
|
||||
|
||||
## 请求参数
|
||||
### Query Parameters
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 | 示例值 |
|
||||
|--------|------|------|------|---------|
|
||||
| category | string | 否 | 达人类别 | "Beauty & Personal Care" |
|
||||
| e_commerce_level | string | 否 | 电商能力等级 | "L1", "L2", "L3" |
|
||||
| exposure_level | string | 否 | 曝光等级 | "KOC-1", "KOL-1" |
|
||||
| gmv_range | string | 否 | GMV范围 | "$0-$5k", "$5k-$25k" |
|
||||
| views_range | string | 否 | 观看量范围 | "0-100", "1k-10k" |
|
||||
| pricing | string | 否 | 价格过滤 | "$100/video" |
|
||||
| page | integer | 否 | 页码,默认1 | 1 |
|
||||
| page_size | integer | 否 | 每页数量,默认10 | 10 |
|
||||
|
||||
### GMV范围选项
|
||||
- "$0-$5k"
|
||||
- "$5k-$25k"
|
||||
- "$25k-$50k"
|
||||
- "$50k-$150k"
|
||||
- "$150k-$400k"
|
||||
- "$400k-$1500k"
|
||||
- "$1500k+"
|
||||
|
||||
### 观看量范围选项
|
||||
- "0-100"
|
||||
- "1k-10k"
|
||||
- "10k-100k"
|
||||
- "100k-250k"
|
||||
- "250k-500k"
|
||||
- "500k+"
|
||||
|
||||
## 响应参数
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "获取成功",
|
||||
"data": {
|
||||
"total_count": 100, // 总数据量
|
||||
"total_pages": 10, // 总页数
|
||||
"current_page": 1, // 当前页码
|
||||
"page_size": 10, // 每页数量
|
||||
"count": 10, // 当前页数据量
|
||||
"creators": [ // 达人列表
|
||||
{
|
||||
"Creator": {
|
||||
"name": "达人名称",
|
||||
"avatar": "头像URL",
|
||||
"active": true
|
||||
},
|
||||
"Category": "达人类别",
|
||||
"E-commerce Level": "L1",
|
||||
"Exposure Level": "KOC-1",
|
||||
"Followers": "10k",
|
||||
"GMV": "$100k",
|
||||
"Items Sold": "1k",
|
||||
"Avg. Video Views": "5k",
|
||||
"Pricing": {
|
||||
"Indv. P": "个人定价",
|
||||
"Pack. P": "套餐定价"
|
||||
},
|
||||
"# Collab": 10,
|
||||
"Latest Collab.": "最新合作",
|
||||
"E-commerce": ["平台1", "平台2"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 错误响应
|
||||
```json
|
||||
{
|
||||
"code": 500,
|
||||
"message": "筛选达人信息失败: 错误信息",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
## 示例请求
|
||||
```
|
||||
GET /api/daren_detail/filter_creators/?category=Beauty&e_commerce_level=L1&gmv_range=$5k-$25k&page=1
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
1. 所有过滤条件都是可选的,可以组合使用
|
||||
2. 分页参数默认值:page=1, page_size=10
|
||||
3. GMV和观看量的范围值需要严格按照文档中提供的选项
|
||||
4. 电商能力等级格式为"L1"到"L7"
|
||||
5. 曝光等级格式为"KOC-1", "KOC-2", "KOL-1", "KOL-2", "KOL-3"
|
||||
|
||||
## 状态码说明
|
||||
- 200: 请求成功
|
||||
- 500: 服务器内部错误
|
22
manage.py
Normal file
22
manage.py
Normal file
@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'daren.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
89
scripts/generate_test_data.py
Normal file
89
scripts/generate_test_data.py
Normal file
@ -0,0 +1,89 @@
|
||||
import os
|
||||
import django
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
import sys
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
# 设置Django环境
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'daren.settings')
|
||||
django.setup()
|
||||
|
||||
from apps.daren_detail.models import CreatorProfile
|
||||
|
||||
def generate_test_data():
|
||||
# 类别列表
|
||||
categories = [choice[0] for choice in CreatorProfile.CATEGORY_CHOICES]
|
||||
|
||||
# 曝光等级列表
|
||||
exposure_levels = [choice[0] for choice in CreatorProfile.EXPOSURE_LEVEL_CHOICES]
|
||||
|
||||
# 电商平台列表
|
||||
e_commerce_platforms = ["SUNLINK", "ARZOPA", "BELIFE", "TIKTOK", "SHOPEE", "LAZADA"]
|
||||
|
||||
# 生成50个测试数据
|
||||
for i in range(50):
|
||||
# 随机生成GMV (1-2000k)
|
||||
gmv = random.randint(1, 2000)
|
||||
|
||||
# 随机生成粉丝数 (1k-1000k)
|
||||
followers = random.randint(1000, 1000000)
|
||||
|
||||
# 随机生成平均视频观看量 (100-500k)
|
||||
avg_views = random.randint(100, 500000)
|
||||
|
||||
# 随机生成售出商品数量 (100-10000)
|
||||
items_sold = random.randint(100, 10000)
|
||||
|
||||
# 随机生成合作次数 (0-50)
|
||||
collab_count = random.randint(0, 50)
|
||||
|
||||
# 随机生成最新合作时间
|
||||
latest_collab = (datetime.now() - timedelta(days=random.randint(0, 365))).strftime("%Y-%m-%d")
|
||||
|
||||
# 随机选择2-4个电商平台
|
||||
selected_platforms = random.sample(e_commerce_platforms, random.randint(2, 4))
|
||||
|
||||
# 创建达人信息
|
||||
creator = CreatorProfile.objects.create(
|
||||
name=f"测试达人{i+1}",
|
||||
avatar_url=f"https://example.com/avatar{i+1}.jpg",
|
||||
is_active=random.choice([True, False]),
|
||||
email=f"creator{i+1}@example.com",
|
||||
instagram=f"creator{i+1}",
|
||||
tiktok_link=f"https://tiktok.com/@creator{i+1}",
|
||||
location=random.choice(["北京", "上海", "广州", "深圳", "杭州"]),
|
||||
live_schedule="每周一、三、五 20:00-22:00",
|
||||
category=random.choice(categories),
|
||||
e_commerce_level=random.randint(1, 7),
|
||||
exposure_level=random.choice(exposure_levels),
|
||||
followers=followers,
|
||||
gmv=gmv,
|
||||
items_sold=items_sold,
|
||||
avg_video_views=avg_views,
|
||||
pricing_individual=f"${random.randint(100, 1000)}/video",
|
||||
pricing_package=f"${random.randint(1000, 5000)}/package",
|
||||
collab_count=collab_count,
|
||||
latest_collab=latest_collab,
|
||||
e_commerce_platforms=selected_platforms,
|
||||
gmv_by_channel={
|
||||
"TikTok": random.randint(30, 70),
|
||||
"Instagram": random.randint(20, 50),
|
||||
"Facebook": random.randint(10, 30)
|
||||
},
|
||||
gmv_by_category={
|
||||
"服装": random.randint(20, 40),
|
||||
"美妆": random.randint(15, 35),
|
||||
"数码": random.randint(10, 30),
|
||||
"家居": random.randint(5, 25)
|
||||
},
|
||||
mcn=random.choice(["MCN机构A", "MCN机构B", "MCN机构C", None])
|
||||
)
|
||||
print(f"已创建达人: {creator.name}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("开始生成测试数据...")
|
||||
generate_test_data()
|
||||
print("测试数据生成完成!")
|
BIN
templates/contracts/商品合同.docx
Normal file
BIN
templates/contracts/商品合同.docx
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user