增加brands模块
This commit is contained in:
parent
139f7bb83f
commit
cee0b638e8
28
apps/accounts/migrations/0003_userprofile.py
Normal file
28
apps/accounts/migrations/0003_userprofile.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-05-09 03:11
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0002_delete_userprofile'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UserProfile',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('department', models.CharField(blank=True, help_text='部门', max_length=100)),
|
||||||
|
('group', models.CharField(blank=True, help_text='小组', max_length=100)),
|
||||||
|
('auto_recommend_reply', models.BooleanField(default=False, help_text='是否启用自动推荐回复功能')),
|
||||||
|
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'user_profiles',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
16
apps/accounts/migrations/0004_delete_usergoal.py
Normal file
16
apps/accounts/migrations/0004_delete_usergoal.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-05-12 04:43
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0003_userprofile'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='UserGoal',
|
||||||
|
),
|
||||||
|
]
|
@ -98,20 +98,3 @@ class UserProfile(models.Model):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.user.username}的个人资料"
|
return f"{self.user.username}的个人资料"
|
||||||
|
|
||||||
class UserGoal(models.Model):
|
|
||||||
"""用户总目标模型"""
|
|
||||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='goals')
|
|
||||||
content = models.TextField(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:
|
|
||||||
db_table = 'user_goals'
|
|
||||||
verbose_name = '用户总目标'
|
|
||||||
verbose_name_plural = '用户总目标'
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.user.username}的总目标 - {self.content[:50]}..."
|
|
63
apps/accounts/serializers.py
Normal file
63
apps/accounts/serializers.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from apps.accounts.models import User, UserProfile
|
||||||
|
|
||||||
|
class UserProfileSerializer(serializers.ModelSerializer):
|
||||||
|
"""用户档案序列化器"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = UserProfile
|
||||||
|
fields = ['department', 'group', 'auto_recommend_reply']
|
||||||
|
|
||||||
|
|
||||||
|
class UserSerializer(serializers.ModelSerializer):
|
||||||
|
"""用户序列化器"""
|
||||||
|
profile = UserProfileSerializer(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = [
|
||||||
|
'id', 'username', 'email', 'name', 'role',
|
||||||
|
'department', 'group', 'profile', 'is_active',
|
||||||
|
'date_joined', 'last_login'
|
||||||
|
]
|
||||||
|
read_only_fields = ['id', 'date_joined', 'last_login']
|
||||||
|
|
||||||
|
|
||||||
|
class UserCreateSerializer(serializers.ModelSerializer):
|
||||||
|
"""创建用户的序列化器"""
|
||||||
|
password = serializers.CharField(write_only=True, required=True, style={'input_type': 'password'})
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = [
|
||||||
|
'id', 'username', 'email', 'name', 'password',
|
||||||
|
'role', 'department', 'group', 'is_active'
|
||||||
|
]
|
||||||
|
read_only_fields = ['id']
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
password = validated_data.pop('password')
|
||||||
|
user = User(**validated_data)
|
||||||
|
user.set_password(password)
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
# 创建用户档案
|
||||||
|
UserProfile.objects.create(
|
||||||
|
user=user,
|
||||||
|
department=user.department,
|
||||||
|
group=user.group
|
||||||
|
)
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordChangeSerializer(serializers.Serializer):
|
||||||
|
"""修改密码序列化器"""
|
||||||
|
old_password = serializers.CharField(required=True)
|
||||||
|
new_password = serializers.CharField(required=True)
|
||||||
|
|
||||||
|
def validate_old_password(self, value):
|
||||||
|
user = self.context['request'].user
|
||||||
|
if not user.check_password(value):
|
||||||
|
raise serializers.ValidationError("旧密码不正确")
|
||||||
|
return value
|
@ -228,6 +228,16 @@ def user_profile(request):
|
|||||||
|
|
||||||
for field in allowed_fields:
|
for field in allowed_fields:
|
||||||
if field in request.data:
|
if field in request.data:
|
||||||
|
# 检查name字段是否重名
|
||||||
|
if field == 'name' and request.data['name'] != user.name:
|
||||||
|
# 检查是否有其他用户使用相同name
|
||||||
|
if User.objects.filter(name=request.data['name']).exclude(id=user.id).exists():
|
||||||
|
return Response({
|
||||||
|
'code': 400,
|
||||||
|
'message': '用户名称已存在',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
setattr(user, field, request.data[field])
|
setattr(user, field, request.data[field])
|
||||||
updated_fields.append(field)
|
updated_fields.append(field)
|
||||||
|
|
||||||
|
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'
|
99
apps/brands/migrations/0001_initial.py
Normal file
99
apps/brands/migrations/0001_initial.py
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-05-09 03:55
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Brand',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('name', models.CharField(max_length=100, unique=True, verbose_name='品牌名称')),
|
||||||
|
('description', models.TextField(blank=True, null=True, verbose_name='品牌描述')),
|
||||||
|
('logo_url', models.CharField(blank=True, max_length=255, null=True, verbose_name='品牌Logo')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||||
|
('is_active', models.BooleanField(default=True, verbose_name='是否激活')),
|
||||||
|
('dataset_id_list', models.JSONField(blank=True, default=list, help_text='所有关联的知识库ID列表', verbose_name='知识库ID列表')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '品牌',
|
||||||
|
'verbose_name_plural': '品牌',
|
||||||
|
'db_table': 'brands',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Activity',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('name', models.CharField(max_length=100, verbose_name='活动名称')),
|
||||||
|
('description', models.TextField(blank=True, null=True, verbose_name='活动描述')),
|
||||||
|
('image_url', models.CharField(blank=True, max_length=255, null=True, verbose_name='活动图片')),
|
||||||
|
('start_date', models.DateTimeField(blank=True, null=True, verbose_name='开始日期')),
|
||||||
|
('end_date', models.DateTimeField(blank=True, null=True, verbose_name='结束日期')),
|
||||||
|
('dataset_id', models.CharField(help_text='外部知识库系统中的ID', max_length=100, verbose_name='知识库ID')),
|
||||||
|
('external_id', models.CharField(blank=True, help_text='外部系统中的唯一标识', max_length=100, null=True, verbose_name='外部ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||||
|
('is_active', models.BooleanField(default=True, verbose_name='是否激活')),
|
||||||
|
('brand', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activities', to='brands.brand', verbose_name='所属品牌')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '活动',
|
||||||
|
'verbose_name_plural': '活动',
|
||||||
|
'db_table': 'activities',
|
||||||
|
'indexes': [models.Index(fields=['brand'], name='activities_brand_i_9a57da_idx'), models.Index(fields=['dataset_id'], name='activities_dataset_c873ab_idx'), models.Index(fields=['is_active'], name='activities_is_acti_cff4bc_idx'), models.Index(fields=['start_date'], name='activities_start_d_fe1952_idx'), models.Index(fields=['end_date'], name='activities_end_dat_9cb2d8_idx')],
|
||||||
|
'unique_together': {('brand', 'name')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='BrandChatSession',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('session_id', models.CharField(max_length=100, unique=True, verbose_name='会话ID')),
|
||||||
|
('title', models.CharField(default='新对话', max_length=200, verbose_name='会话标题')),
|
||||||
|
('dataset_id_list', models.JSONField(blank=True, default=list, verbose_name='知识库ID列表')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||||
|
('is_active', models.BooleanField(default=True, verbose_name='是否激活')),
|
||||||
|
('brand', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chat_sessions', to='brands.brand', verbose_name='品牌')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '品牌聊天会话',
|
||||||
|
'verbose_name_plural': '品牌聊天会话',
|
||||||
|
'db_table': 'brand_chat_sessions',
|
||||||
|
'indexes': [models.Index(fields=['brand'], name='brand_chat__brand_i_83752e_idx'), models.Index(fields=['session_id'], name='brand_chat__session_4bf9b0_idx'), models.Index(fields=['created_at'], name='brand_chat__created_957266_idx')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Product',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('name', models.CharField(max_length=100, verbose_name='产品名称')),
|
||||||
|
('description', models.TextField(blank=True, null=True, verbose_name='产品描述')),
|
||||||
|
('image_url', models.CharField(blank=True, max_length=255, null=True, verbose_name='产品图片')),
|
||||||
|
('dataset_id', models.CharField(help_text='外部知识库系统中的ID', max_length=100, verbose_name='知识库ID')),
|
||||||
|
('external_id', models.CharField(blank=True, help_text='外部系统中的唯一标识', max_length=100, null=True, verbose_name='外部ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||||
|
('is_active', models.BooleanField(default=True, verbose_name='是否激活')),
|
||||||
|
('brand', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to='brands.brand', verbose_name='所属品牌')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '产品',
|
||||||
|
'verbose_name_plural': '产品',
|
||||||
|
'db_table': 'products',
|
||||||
|
'indexes': [models.Index(fields=['brand'], name='products_brand_i_0d1950_idx'), models.Index(fields=['dataset_id'], name='products_dataset_faf62a_idx'), models.Index(fields=['is_active'], name='products_is_acti_cb485f_idx')],
|
||||||
|
'unique_together': {('brand', 'name')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,194 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-05-13 02:45
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('brands', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Campaign',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('name', models.CharField(max_length=100, verbose_name='活动名称')),
|
||||||
|
('description', models.TextField(blank=True, null=True, verbose_name='活动描述')),
|
||||||
|
('image_url', models.CharField(blank=True, max_length=255, null=True, verbose_name='活动图片')),
|
||||||
|
('service', models.CharField(blank=True, max_length=100, null=True, verbose_name='服务类型')),
|
||||||
|
('creator_type', models.CharField(blank=True, max_length=100, null=True, verbose_name='创作者类型')),
|
||||||
|
('creator_level', models.CharField(blank=True, max_length=100, null=True, verbose_name='创作者等级')),
|
||||||
|
('creator_category', models.CharField(blank=True, max_length=100, null=True, verbose_name='创作者分类')),
|
||||||
|
('creators_count', models.IntegerField(default=0, verbose_name='创作者数量')),
|
||||||
|
('gmv', models.CharField(blank=True, max_length=100, null=True, verbose_name='GMV范围')),
|
||||||
|
('followers', models.CharField(blank=True, max_length=100, null=True, verbose_name='粉丝数范围')),
|
||||||
|
('views', models.CharField(blank=True, max_length=100, null=True, verbose_name='浏览量范围')),
|
||||||
|
('budget', models.CharField(blank=True, max_length=100, null=True, verbose_name='预算范围')),
|
||||||
|
('start_date', models.DateTimeField(blank=True, null=True, verbose_name='开始日期')),
|
||||||
|
('end_date', models.DateTimeField(blank=True, null=True, verbose_name='结束日期')),
|
||||||
|
('dataset_id', models.CharField(help_text='外部知识库系统中的ID', max_length=100, verbose_name='知识库ID')),
|
||||||
|
('external_id', models.CharField(blank=True, help_text='外部系统中的唯一标识', max_length=100, null=True, verbose_name='外部ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||||
|
('is_active', models.BooleanField(default=True, verbose_name='是否激活')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '活动',
|
||||||
|
'verbose_name_plural': '活动',
|
||||||
|
'db_table': 'campaigns',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='activity',
|
||||||
|
unique_together=None,
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='activity',
|
||||||
|
name='brand',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='brand',
|
||||||
|
name='campaign_id',
|
||||||
|
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='活动ID'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='brand',
|
||||||
|
name='category',
|
||||||
|
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='品牌分类'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='brand',
|
||||||
|
name='collab_count',
|
||||||
|
field=models.IntegerField(default=0, verbose_name='合作数量'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='brand',
|
||||||
|
name='creators_count',
|
||||||
|
field=models.IntegerField(default=0, verbose_name='创作者数量'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='brand',
|
||||||
|
name='shop_overall_rating',
|
||||||
|
field=models.DecimalField(decimal_places=1, default=0.0, max_digits=3, verbose_name='店铺评分'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='brand',
|
||||||
|
name='source',
|
||||||
|
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='来源'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='brand',
|
||||||
|
name='total_gmv_achieved',
|
||||||
|
field=models.DecimalField(decimal_places=2, default=0, max_digits=12, verbose_name='总GMV'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='brand',
|
||||||
|
name='total_views_achieved',
|
||||||
|
field=models.DecimalField(decimal_places=2, default=0, max_digits=12, verbose_name='总浏览量'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='available_samples',
|
||||||
|
field=models.IntegerField(default=0, verbose_name='可用样品数'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='collab_creators',
|
||||||
|
field=models.IntegerField(default=0, verbose_name='合作创作者数'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='commission_rate',
|
||||||
|
field=models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='佣金率'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='items_sold',
|
||||||
|
field=models.IntegerField(default=0, verbose_name='已售数量'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='open_collab',
|
||||||
|
field=models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='开放合作率'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='pid',
|
||||||
|
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='产品ID'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='product_rating',
|
||||||
|
field=models.DecimalField(decimal_places=1, default=0, max_digits=3, verbose_name='产品评分'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='reviews_count',
|
||||||
|
field=models.IntegerField(default=0, verbose_name='评价数量'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='sales_price_max',
|
||||||
|
field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='最高销售价'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='sales_price_min',
|
||||||
|
field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='最低销售价'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='stock',
|
||||||
|
field=models.IntegerField(default=0, verbose_name='库存'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='tiktok_shop',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='是否TikTok商店'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='product',
|
||||||
|
index=models.Index(fields=['pid'], name='products_pid_99aab2_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='campaign',
|
||||||
|
name='brand',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='campaigns', to='brands.brand', verbose_name='所属品牌'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='campaign',
|
||||||
|
name='link_product',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='campaigns', to='brands.product', verbose_name='关联产品'),
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='Activity',
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='campaign',
|
||||||
|
index=models.Index(fields=['brand'], name='campaigns_brand_i_c2d4bd_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='campaign',
|
||||||
|
index=models.Index(fields=['dataset_id'], name='campaigns_dataset_bfbb68_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='campaign',
|
||||||
|
index=models.Index(fields=['is_active'], name='campaigns_is_acti_6c57d0_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='campaign',
|
||||||
|
index=models.Index(fields=['start_date'], name='campaigns_start_d_5c2c6b_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='campaign',
|
||||||
|
index=models.Index(fields=['end_date'], name='campaigns_end_dat_6aaba4_idx'),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='campaign',
|
||||||
|
unique_together={('brand', 'name')},
|
||||||
|
),
|
||||||
|
]
|
3
apps/brands/migrations/__init__.py
Normal file
3
apps/brands/migrations/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
181
apps/brands/models.py
Normal file
181
apps/brands/models.py
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
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=100, verbose_name='活动名称')
|
||||||
|
description = models.TextField(blank=True, null=True, verbose_name='活动描述')
|
||||||
|
image_url = models.CharField(max_length=255, blank=True, null=True, verbose_name='活动图片')
|
||||||
|
|
||||||
|
# 活动相关字段
|
||||||
|
service = models.CharField(max_length=100, blank=True, null=True, verbose_name='服务类型')
|
||||||
|
creator_type = models.CharField(max_length=100, blank=True, null=True, verbose_name='创作者类型')
|
||||||
|
creator_level = models.CharField(max_length=100, blank=True, null=True, verbose_name='创作者等级')
|
||||||
|
creator_category = models.CharField(max_length=100, blank=True, null=True, verbose_name='创作者分类')
|
||||||
|
creators_count = models.IntegerField(default=0, verbose_name='创作者数量')
|
||||||
|
gmv = models.CharField(max_length=100, blank=True, null=True, verbose_name='GMV范围')
|
||||||
|
followers = models.CharField(max_length=100, blank=True, null=True, verbose_name='粉丝数范围')
|
||||||
|
views = models.CharField(max_length=100, blank=True, null=True, verbose_name='浏览量范围')
|
||||||
|
budget = models.CharField(max_length=100, blank=True, null=True, verbose_name='预算范围')
|
||||||
|
link_product = models.ManyToManyField(Product, blank=True, related_name='campaigns', verbose_name='关联产品')
|
||||||
|
|
||||||
|
# 时间信息
|
||||||
|
start_date = models.DateTimeField(blank=True, null=True, verbose_name='开始日期')
|
||||||
|
end_date = models.DateTimeField(blank=True, null=True, verbose_name='结束日期')
|
||||||
|
|
||||||
|
# 知识库信息
|
||||||
|
dataset_id = models.CharField(max_length=100, verbose_name='知识库ID',
|
||||||
|
help_text='外部知识库系统中的ID')
|
||||||
|
external_id = models.CharField(max_length=100, blank=True, null=True, verbose_name='外部ID',
|
||||||
|
help_text='外部系统中的唯一标识')
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||||
|
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||||
|
is_active = models.BooleanField(default=True, verbose_name='是否激活')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
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']),
|
||||||
|
]
|
||||||
|
|
||||||
|
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 @@
|
|||||||
|
|
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)),
|
||||||
|
]
|
322
apps/brands/views.py
Normal file
322
apps/brands/views.py
Normal file
@ -0,0 +1,322 @@
|
|||||||
|
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 rest_framework.permissions import IsAuthenticated
|
||||||
|
|
||||||
|
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
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
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
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
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
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
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
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
|
serializer = self.get_serializer(queryset, many=True)
|
||||||
|
return api_response(data=serializer.data)
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
serializer = self.get_serializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
self.perform_create(serializer)
|
||||||
|
return api_response(data=serializer.data)
|
||||||
|
return api_response(code=400, message="创建失败", data=serializer.errors)
|
||||||
|
|
||||||
|
def retrieve(self, request, *args, **kwargs):
|
||||||
|
instance = self.get_object()
|
||||||
|
serializer = self.get_serializer(instance)
|
||||||
|
return api_response(data=serializer.data)
|
||||||
|
|
||||||
|
def update(self, request, *args, **kwargs):
|
||||||
|
partial = kwargs.pop('partial', False)
|
||||||
|
instance = self.get_object()
|
||||||
|
serializer = self.get_serializer(instance, data=request.data, partial=partial)
|
||||||
|
if serializer.is_valid():
|
||||||
|
self.perform_update(serializer)
|
||||||
|
return api_response(data=serializer.data)
|
||||||
|
return api_response(code=400, message="更新失败", data=serializer.errors)
|
||||||
|
|
||||||
|
def destroy(self, request, *args, **kwargs):
|
||||||
|
instance = self.get_object()
|
||||||
|
self.perform_destroy(instance)
|
||||||
|
return api_response(message="删除成功", data=None)
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
# 创建聊天会话时,可以设置使用特定品牌下的所有知识库
|
||||||
|
chat_session = serializer.save()
|
||||||
|
|
||||||
|
# 如果没有提供dataset_id_list,则使用品牌的dataset_id_list
|
||||||
|
if not chat_session.dataset_id_list:
|
||||||
|
brand = chat_session.brand
|
||||||
|
chat_session.dataset_id_list = brand.dataset_id_list
|
||||||
|
chat_session.save(update_fields=['dataset_id_list', 'updated_at'])
|
@ -1,17 +1,16 @@
|
|||||||
# apps/chat/consumers.py
|
# apps/chat/consumers.py
|
||||||
|
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||||
import json
|
import json
|
||||||
|
from channels.db import database_sync_to_async
|
||||||
|
from apps.knowledge_base.models import KnowledgeBase
|
||||||
|
from apps.chat.models import ChatHistory
|
||||||
|
from rest_framework.authtoken.models import Token
|
||||||
|
from django.conf import settings
|
||||||
import logging
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
|
||||||
from channels.db import database_sync_to_async
|
|
||||||
from rest_framework.authtoken.models import Token
|
|
||||||
from urllib.parse import parse_qs
|
|
||||||
from apps.chat.models import ChatHistory
|
|
||||||
from apps.knowledge_base.models import KnowledgeBase
|
|
||||||
from django.conf import settings
|
|
||||||
import aiohttp
|
|
||||||
import uuid
|
import uuid
|
||||||
from apps.common.services.permission_service import PermissionService
|
import aiohttp
|
||||||
|
from urllib.parse import parse_qs
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
# apps/chat/routing.py
|
# apps/chat/routing.py
|
||||||
from django.urls import re_path
|
from django.urls import re_path
|
||||||
from apps.chat.consumers import ChatConsumer, ChatStreamConsumer
|
from apps.chat.consumers import ChatStreamConsumer
|
||||||
|
|
||||||
websocket_urlpatterns = [
|
websocket_urlpatterns = [
|
||||||
re_path(r'ws/chat/$', ChatConsumer.as_asgi()),
|
|
||||||
re_path(r'ws/chat/stream/$', ChatStreamConsumer.as_asgi()),
|
re_path(r'ws/chat/stream/$', ChatStreamConsumer.as_asgi()),
|
||||||
]
|
]
|
||||||
|
@ -7,6 +7,7 @@ from apps.accounts.models import User
|
|||||||
from apps.knowledge_base.models import KnowledgeBase
|
from apps.knowledge_base.models import KnowledgeBase
|
||||||
from apps.chat.models import ChatHistory
|
from apps.chat.models import ChatHistory
|
||||||
from apps.permissions.services.permission_service import KnowledgeBasePermissionMixin
|
from apps.permissions.services.permission_service import KnowledgeBasePermissionMixin
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import logging
|
import logging
|
||||||
from asgiref.sync import async_to_sync
|
from asgiref.sync import async_to_sync
|
||||||
from channels.layers import get_channel_layer
|
from channels.layers import get_channel_layer
|
||||||
from apps.message.models import Notification
|
from apps.notification.models import Notification
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -19,25 +19,33 @@ class NotificationService:
|
|||||||
related_resource=related_object_id,
|
related_resource=related_object_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 准备发送到WebSocket的数据
|
||||||
|
notification_data = {
|
||||||
|
"id": str(notification.id),
|
||||||
|
"title": notification.title,
|
||||||
|
"content": notification.content,
|
||||||
|
"type": notification.type,
|
||||||
|
"created_at": notification.created_at.isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
# 只有当sender不为None时才添加sender信息
|
||||||
|
if notification.sender:
|
||||||
|
notification_data["sender"] = {
|
||||||
|
"id": str(notification.sender.id),
|
||||||
|
"name": notification.sender.name
|
||||||
|
}
|
||||||
|
|
||||||
channel_layer = get_channel_layer()
|
channel_layer = get_channel_layer()
|
||||||
async_to_sync(channel_layer.group_send)(
|
async_to_sync(channel_layer.group_send)(
|
||||||
f"notification_user_{user.id}",
|
f"notification_user_{user.id}",
|
||||||
{
|
{
|
||||||
"type": "notification",
|
"type": "notification",
|
||||||
"data": {
|
"data": notification_data
|
||||||
"id": str(notification.id),
|
|
||||||
"title": notification.title,
|
|
||||||
"content": notification.content,
|
|
||||||
"type": notification.type,
|
|
||||||
"created_at": notification.created_at.isoformat(),
|
|
||||||
"sender": {
|
|
||||||
"id": str(notification.sender.id),
|
|
||||||
"name": notification.sender.name
|
|
||||||
} if notification.sender else None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
return notification
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"发送通知失败: {str(e)}")
|
logger.error(f"发送通知失败: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
|||||||
# Generated by Django 5.2 on 2025-05-07 03:40
|
# Generated by Django 5.2 on 2025-05-12 06:56
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import uuid
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
@ -11,72 +10,24 @@ class Migration(migrations.Migration):
|
|||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('chat', '0001_initial'),
|
|
||||||
('knowledge_base', '0001_initial'),
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
|
||||||
name='GmailAttachment',
|
|
||||||
fields=[
|
|
||||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
||||||
('gmail_message_id', models.CharField(max_length=100, verbose_name='Gmail消息ID')),
|
|
||||||
('filename', models.CharField(max_length=255, verbose_name='文件名')),
|
|
||||||
('filepath', models.CharField(max_length=500, verbose_name='文件路径')),
|
|
||||||
('mimetype', models.CharField(max_length=100, verbose_name='MIME类型')),
|
|
||||||
('filesize', models.IntegerField(default=0, verbose_name='文件大小')),
|
|
||||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
|
||||||
('chat_message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gmail_attachments', to='chat.chathistory')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Gmail附件',
|
|
||||||
'verbose_name_plural': 'Gmail附件',
|
|
||||||
'db_table': 'gmail_attachments',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='GmailCredential',
|
name='GmailCredential',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('gmail_email', models.EmailField(default='your_default_email@example.com', max_length=255, verbose_name='Gmail邮箱')),
|
('email', models.EmailField(help_text='Gmail email address', max_length=254, unique=True)),
|
||||||
('name', models.CharField(default='默认Gmail', max_length=100, verbose_name='名称')),
|
('credentials', models.TextField(help_text='Serialized OAuth2 credentials (JSON)')),
|
||||||
('credentials', models.TextField(blank=True, null=True, verbose_name='凭证JSON')),
|
('is_default', models.BooleanField(default=False, help_text='Default Gmail account for user')),
|
||||||
('token_path', models.CharField(blank=True, max_length=255, null=True, verbose_name='令牌路径')),
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
('is_default', models.BooleanField(default=False, verbose_name='是否默认')),
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
('last_history_id', models.CharField(blank=True, max_length=100, null=True, verbose_name='最后历史ID')),
|
('is_valid', models.BooleanField(default=True, help_text='Whether the credential is valid')),
|
||||||
('watch_expiration', 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='更新时间')),
|
|
||||||
('gmail_credential_id', models.CharField(blank=True, max_length=255, null=True, verbose_name='Gmail凭证ID')),
|
|
||||||
('needs_reauth', models.BooleanField(default=False, verbose_name='需要重新授权')),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gmail_credentials', to=settings.AUTH_USER_MODEL)),
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gmail_credentials', to=settings.AUTH_USER_MODEL)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'Gmail凭证',
|
'unique_together': {('user', 'email')},
|
||||||
'verbose_name_plural': 'Gmail凭证',
|
|
||||||
'ordering': ['-is_default', '-updated_at'],
|
|
||||||
'unique_together': {('user', 'gmail_email')},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='GmailTalentMapping',
|
|
||||||
fields=[
|
|
||||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
||||||
('talent_email', models.EmailField(max_length=254, verbose_name='达人邮箱')),
|
|
||||||
('conversation_id', models.CharField(max_length=100, verbose_name='对话ID')),
|
|
||||||
('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='更新时间')),
|
|
||||||
('knowledge_base', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gmail_mappings', to='knowledge_base.knowledgebase')),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gmail_talent_mappings', to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Gmail达人映射',
|
|
||||||
'verbose_name_plural': 'Gmail达人映射',
|
|
||||||
'db_table': 'gmail_talent_mappings',
|
|
||||||
'unique_together': {('user', 'talent_email')},
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -0,0 +1,55 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-05-12 08:22
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('gmail', '0001_initial'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='GmailConversation',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('user_email', models.EmailField(help_text='用户Gmail邮箱', max_length=254)),
|
||||||
|
('influencer_email', models.EmailField(help_text='达人Gmail邮箱', max_length=254)),
|
||||||
|
('conversation_id', models.CharField(help_text='关联到chat_history的会话ID', max_length=100, unique=True)),
|
||||||
|
('title', models.CharField(default='Gmail对话', help_text='对话标题', max_length=100)),
|
||||||
|
('last_sync_time', models.DateTimeField(default=django.utils.timezone.now, help_text='最后同步时间')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('is_active', models.BooleanField(default=True)),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gmail_conversations', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['-updated_at'],
|
||||||
|
'unique_together': {('user', 'user_email', 'influencer_email')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='GmailAttachment',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('email_message_id', models.CharField(help_text='Gmail邮件ID', max_length=100)),
|
||||||
|
('attachment_id', models.CharField(help_text='Gmail附件ID', max_length=100)),
|
||||||
|
('filename', models.CharField(help_text='原始文件名', max_length=255)),
|
||||||
|
('file_path', models.CharField(help_text='保存在服务器上的路径', max_length=255)),
|
||||||
|
('content_type', models.CharField(help_text='MIME类型', max_length=100)),
|
||||||
|
('size', models.IntegerField(default=0, help_text='文件大小(字节)')),
|
||||||
|
('sender_email', models.EmailField(help_text='发送者邮箱', max_length=254)),
|
||||||
|
('chat_message_id', models.CharField(blank=True, help_text='关联到ChatHistory的消息ID', max_length=100, null=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='gmail.gmailconversation')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['-created_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -1,72 +1,90 @@
|
|||||||
# apps/gmail/models.py
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
|
||||||
import uuid
|
|
||||||
from apps.accounts.models import User
|
from apps.accounts.models import User
|
||||||
from apps.knowledge_base.models import KnowledgeBase
|
import json
|
||||||
from apps.chat.models import ChatHistory # 更新导入路径
|
import os
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
class GmailCredential(models.Model):
|
class GmailCredential(models.Model):
|
||||||
"""Gmail账号凭证"""
|
|
||||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='gmail_credentials')
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='gmail_credentials')
|
||||||
gmail_email = models.EmailField(verbose_name='Gmail邮箱', max_length=255, default='your_default_email@example.com')
|
email = models.EmailField(unique=True, help_text="Gmail email address")
|
||||||
name = models.CharField(verbose_name='名称', max_length=100, default='默认Gmail')
|
credentials = models.TextField(help_text="Serialized OAuth2 credentials (JSON)")
|
||||||
credentials = models.TextField(verbose_name='凭证JSON', blank=True, null=True)
|
is_default = models.BooleanField(default=False, help_text="Default Gmail account for user")
|
||||||
token_path = models.CharField(verbose_name='令牌路径', max_length=255, blank=True, null=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
is_default = models.BooleanField(verbose_name='是否默认', default=False)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
last_history_id = models.CharField(verbose_name='最后历史ID', max_length=100, blank=True, null=True)
|
is_valid = models.BooleanField(default=True, help_text="Whether the credential is valid")
|
||||||
watch_expiration = models.DateTimeField(verbose_name='监听过期时间', blank=True, null=True)
|
|
||||||
is_active = models.BooleanField(verbose_name='是否活跃', default=True)
|
|
||||||
created_at = models.DateTimeField(verbose_name='创建时间', auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(verbose_name='更新时间', auto_now=True)
|
|
||||||
gmail_credential_id = models.CharField(verbose_name='Gmail凭证ID', max_length=255, blank=True, null=True)
|
|
||||||
needs_reauth = models.BooleanField(verbose_name='需要重新授权', default=False)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.name} ({self.gmail_email})"
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = 'Gmail凭证'
|
|
||||||
verbose_name_plural = 'Gmail凭证'
|
|
||||||
unique_together = ('user', 'gmail_email')
|
|
||||||
ordering = ['-is_default', '-updated_at']
|
|
||||||
|
|
||||||
class GmailTalentMapping(models.Model):
|
class Meta:
|
||||||
"""Gmail达人映射关系模型"""
|
unique_together = ('user', 'email')
|
||||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='gmail_talent_mappings')
|
def set_credentials(self, credentials):
|
||||||
talent_email = models.EmailField(verbose_name='达人邮箱')
|
self.credentials = json.dumps({
|
||||||
knowledge_base = models.ForeignKey(KnowledgeBase, on_delete=models.CASCADE, related_name='gmail_mappings')
|
'token': credentials.token,
|
||||||
conversation_id = models.CharField(max_length=100, verbose_name='对话ID')
|
'refresh_token': credentials.refresh_token,
|
||||||
is_active = models.BooleanField(default=True, verbose_name='是否激活')
|
'token_uri': credentials.token_uri,
|
||||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
'client_id': credentials.client_id,
|
||||||
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
'client_secret': credentials.client_secret,
|
||||||
|
'scopes': credentials.scopes
|
||||||
|
})
|
||||||
|
self.is_valid = True
|
||||||
|
|
||||||
|
def get_credentials(self):
|
||||||
|
from google.oauth2.credentials import Credentials
|
||||||
|
creds_data = json.loads(self.credentials)
|
||||||
|
return Credentials(
|
||||||
|
token=creds_data['token'],
|
||||||
|
refresh_token=creds_data['refresh_token'],
|
||||||
|
token_uri=creds_data['token_uri'],
|
||||||
|
client_id=creds_data['client_id'],
|
||||||
|
client_secret=creds_data['client_secret'],
|
||||||
|
scopes=creds_data['scopes']
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.user.username} - {self.email}"
|
||||||
|
|
||||||
|
class GmailConversation(models.Model):
|
||||||
|
"""Gmail对话记录,跟踪用户和达人之间的邮件交互"""
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='gmail_conversations')
|
||||||
|
user_email = models.EmailField(help_text="用户Gmail邮箱")
|
||||||
|
influencer_email = models.EmailField(help_text="达人Gmail邮箱")
|
||||||
|
conversation_id = models.CharField(max_length=100, unique=True, help_text="关联到chat_history的会话ID")
|
||||||
|
title = models.CharField(max_length=100, default="Gmail对话", help_text="对话标题")
|
||||||
|
last_sync_time = models.DateTimeField(default=timezone.now, help_text="最后同步时间")
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.user.username}: {self.user_email} - {self.influencer_email}"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = 'gmail_talent_mappings'
|
ordering = ['-updated_at']
|
||||||
unique_together = ['user', 'talent_email']
|
unique_together = ('user', 'user_email', 'influencer_email')
|
||||||
verbose_name = 'Gmail达人映射'
|
|
||||||
verbose_name_plural = 'Gmail达人映射'
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.user.username} - {self.talent_email}"
|
|
||||||
|
|
||||||
class GmailAttachment(models.Model):
|
class GmailAttachment(models.Model):
|
||||||
"""Gmail附件模型"""
|
"""Gmail附件记录"""
|
||||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
conversation = models.ForeignKey(GmailConversation, on_delete=models.CASCADE, related_name='attachments')
|
||||||
chat_message = models.ForeignKey(ChatHistory, on_delete=models.CASCADE, related_name='gmail_attachments')
|
email_message_id = models.CharField(max_length=100, help_text="Gmail邮件ID")
|
||||||
gmail_message_id = models.CharField(max_length=100, verbose_name='Gmail消息ID')
|
attachment_id = models.CharField(max_length=100, help_text="Gmail附件ID")
|
||||||
filename = models.CharField(max_length=255, verbose_name='文件名')
|
filename = models.CharField(max_length=255, help_text="原始文件名")
|
||||||
filepath = models.CharField(max_length=500, verbose_name='文件路径')
|
file_path = models.CharField(max_length=255, help_text="保存在服务器上的路径")
|
||||||
mimetype = models.CharField(max_length=100, verbose_name='MIME类型')
|
content_type = models.CharField(max_length=100, help_text="MIME类型")
|
||||||
filesize = models.IntegerField(default=0, verbose_name='文件大小')
|
size = models.IntegerField(default=0, help_text="文件大小(字节)")
|
||||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
sender_email = models.EmailField(help_text="发送者邮箱")
|
||||||
|
chat_message_id = models.CharField(max_length=100, blank=True, null=True, help_text="关联到ChatHistory的消息ID")
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.filename} ({self.size} bytes)"
|
||||||
|
|
||||||
|
def get_file_extension(self):
|
||||||
|
"""获取文件扩展名"""
|
||||||
|
_, ext = os.path.splitext(self.filename)
|
||||||
|
return ext.lower()
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
"""获取文件URL"""
|
||||||
|
return f"/media/gmail_attachments/{os.path.basename(self.file_path)}"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = 'gmail_attachments'
|
ordering = ['-created_at']
|
||||||
verbose_name = 'Gmail附件'
|
|
||||||
verbose_name_plural = 'Gmail附件'
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.filename} ({self.filesize} bytes)"
|
|
@ -0,0 +1,69 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from .models import GmailCredential
|
||||||
|
import json
|
||||||
|
|
||||||
|
class GmailCredentialSerializer(serializers.ModelSerializer):
|
||||||
|
client_secret_json = serializers.JSONField(write_only=True, required=False, allow_null=True)
|
||||||
|
client_secret_file = serializers.FileField(write_only=True, required=False, allow_null=True)
|
||||||
|
auth_code = serializers.CharField(write_only=True, required=False, allow_blank=True)
|
||||||
|
email = serializers.EmailField(required=False, allow_blank=True) # Make email optional
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = GmailCredential
|
||||||
|
fields = ['id', 'email', 'is_default', 'created_at', 'updated_at', 'is_valid',
|
||||||
|
'client_secret_json', 'client_secret_file', 'auth_code']
|
||||||
|
read_only_fields = ['created_at', 'updated_at', 'is_valid']
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
"""Validate client_secret input (either JSON or file)."""
|
||||||
|
client_secret_json = data.get('client_secret_json')
|
||||||
|
client_secret_file = data.get('client_secret_file')
|
||||||
|
auth_code = data.get('auth_code')
|
||||||
|
|
||||||
|
# For auth initiation, only client_secret is required
|
||||||
|
if not auth_code: # Initiation phase
|
||||||
|
if not client_secret_json and not client_secret_file:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
"Either client_secret_json or client_secret_file is required."
|
||||||
|
)
|
||||||
|
if client_secret_json and client_secret_file:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
"Provide only one of client_secret_json or client_secret_file."
|
||||||
|
)
|
||||||
|
|
||||||
|
# For auth completion, both auth_code and client_secret are required
|
||||||
|
if auth_code and not (client_secret_json or client_secret_file):
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
"client_secret_json or client_secret_file is required with auth_code."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parse client_secret_json if provided
|
||||||
|
if client_secret_json:
|
||||||
|
try:
|
||||||
|
json.dumps(client_secret_json)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
raise serializers.ValidationError("client_secret_json must be valid JSON.")
|
||||||
|
|
||||||
|
# Parse client_secret_file if provided
|
||||||
|
if client_secret_file:
|
||||||
|
try:
|
||||||
|
content = client_secret_file.read().decode('utf-8')
|
||||||
|
client_secret_json = json.loads(content)
|
||||||
|
data['client_secret_json'] = client_secret_json
|
||||||
|
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||||
|
raise serializers.ValidationError("client_secret_file must contain valid JSON.")
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def validate_email(self, value):
|
||||||
|
"""Ensure email is unique for the user (only for completion)."""
|
||||||
|
if not value: # Email is optional during initiation
|
||||||
|
return value
|
||||||
|
user = self.context['request'].user
|
||||||
|
if self.instance: # Update case
|
||||||
|
if GmailCredential.objects.filter(user=user, email=value).exclude(id=self.instance.id).exists():
|
||||||
|
raise serializers.ValidationError("This Gmail account is already added.")
|
||||||
|
else: # Create case
|
||||||
|
if GmailCredential.objects.filter(user=user, email=value).exists():
|
||||||
|
raise serializers.ValidationError("This Gmail account is already added.")
|
||||||
|
return value
|
@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
1084
apps/gmail/services/gmail_service.py
Normal file
1084
apps/gmail/services/gmail_service.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,27 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from .views import (
|
||||||
|
GmailAuthInitiateView,
|
||||||
|
GmailAuthCompleteView,
|
||||||
|
GmailCredentialListView,
|
||||||
|
GmailCredentialDetailView,
|
||||||
|
GmailConversationView,
|
||||||
|
GmailAttachmentListView,
|
||||||
|
GmailPubSubView,
|
||||||
|
GmailNotificationStartView,
|
||||||
|
GmailSendEmailView
|
||||||
|
)
|
||||||
|
|
||||||
|
app_name = 'gmail'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('auth/initiate/', GmailAuthInitiateView.as_view(), name='auth_initiate'),
|
||||||
|
path('auth/complete/', GmailAuthCompleteView.as_view(), name='auth_complete'),
|
||||||
|
path('credentials/', GmailCredentialListView.as_view(), name='credential_list'),
|
||||||
|
path('credentials/<int:pk>/', GmailCredentialDetailView.as_view(), name='credential_detail'),
|
||||||
|
path('conversations/', GmailConversationView.as_view(), name='conversation_list'),
|
||||||
|
path('attachments/', GmailAttachmentListView.as_view(), name='attachment_list'),
|
||||||
|
path('attachments/<str:conversation_id>/', GmailAttachmentListView.as_view(), name='attachment_list_by_conversation'),
|
||||||
|
path('notifications/setup/', GmailPubSubView.as_view(), name='pubsub_setup'),
|
||||||
|
path('notifications/start/', GmailNotificationStartView.as_view(), name='notification_start'),
|
||||||
|
path('send/', GmailSendEmailView.as_view(), name='send_email'),
|
||||||
|
]
|
@ -1,3 +1,598 @@
|
|||||||
from django.shortcuts import render
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework import status
|
||||||
|
from .serializers import GmailCredentialSerializer
|
||||||
|
from .services.gmail_service import GmailService
|
||||||
|
from .models import GmailCredential, GmailConversation, GmailAttachment
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.files.storage import default_storage
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
|
||||||
# Create your views here.
|
# 配置日志记录器,用于记录视图操作的调试、警告和错误信息
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class GmailAuthInitiateView(APIView):
|
||||||
|
"""
|
||||||
|
API 视图,用于启动 Gmail OAuth2 认证流程。
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
"""
|
||||||
|
处理 POST 请求,启动 Gmail OAuth2 认证并返回授权 URL。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: Django REST Framework 请求对象,包含客户端密钥 JSON 数据。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: 包含授权 URL 的 JSON 响应(成功时),或错误信息(失败时)。
|
||||||
|
|
||||||
|
Status Codes:
|
||||||
|
200: 成功生成授权 URL。
|
||||||
|
400: 请求数据无效。
|
||||||
|
500: 服务器内部错误(如认证服务失败)。
|
||||||
|
"""
|
||||||
|
logger.debug(f"Received auth initiate request: {request.data}")
|
||||||
|
serializer = GmailCredentialSerializer(data=request.data, context={'request': request})
|
||||||
|
if serializer.is_valid():
|
||||||
|
try:
|
||||||
|
# 从请求数据中提取客户端密钥 JSON
|
||||||
|
client_secret_json = serializer.validated_data['client_secret_json']
|
||||||
|
# 调用 GmailService 生成授权 URL
|
||||||
|
auth_url = GmailService.initiate_authentication(request.user, client_secret_json)
|
||||||
|
logger.info(f"Generated auth URL for user {request.user.id}")
|
||||||
|
return Response({'auth_url': auth_url}, status=status.HTTP_200_OK)
|
||||||
|
except Exception as e:
|
||||||
|
# 记录错误并返回服务器错误响应
|
||||||
|
logger.error(f"Error initiating authentication for user {request.user.id}: {str(e)}")
|
||||||
|
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
# 记录无效请求数据并返回错误响应
|
||||||
|
logger.warning(f"Invalid request data: {serializer.errors}")
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
class GmailAuthCompleteView(APIView):
|
||||||
|
"""
|
||||||
|
API 视图,用于完成 Gmail OAuth2 认证流程。
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
"""
|
||||||
|
处理 POST 请求,使用授权代码完成 Gmail OAuth2 认证并保存凭证。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: Django REST Framework 请求对象,包含授权代码和客户端密钥 JSON。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: 包含已保存凭证数据的 JSON 响应(成功时),或错误信息(失败时)。
|
||||||
|
|
||||||
|
Status Codes:
|
||||||
|
201: 成功保存凭证。
|
||||||
|
400: 请求数据无效。
|
||||||
|
500: 服务器内部错误(如认证失败)。
|
||||||
|
"""
|
||||||
|
logger.debug(f"Received auth complete request: {request.data}")
|
||||||
|
serializer = GmailCredentialSerializer(data=request.data, context={'request': request})
|
||||||
|
if serializer.is_valid():
|
||||||
|
try:
|
||||||
|
# 提取授权代码和客户端密钥 JSON
|
||||||
|
auth_code = serializer.validated_data['auth_code']
|
||||||
|
client_secret_json = serializer.validated_data['client_secret_json']
|
||||||
|
# 完成认证并保存凭证
|
||||||
|
credential = GmailService.complete_authentication(request.user, auth_code, client_secret_json)
|
||||||
|
# 序列化凭证数据以返回
|
||||||
|
serializer = GmailCredentialSerializer(credential, context={'request': request})
|
||||||
|
logger.info(f"Authentication completed for user {request.user.id}, email: {credential.email}")
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
except Exception as e:
|
||||||
|
# 记录错误并返回服务器错误响应
|
||||||
|
logger.error(f"Error completing authentication for user {request.user.id}: {str(e)}")
|
||||||
|
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
# 记录无效请求数据并返回错误响应
|
||||||
|
logger.warning(f"Invalid request data: {serializer.errors}")
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
class GmailCredentialListView(APIView):
|
||||||
|
"""
|
||||||
|
API 视图,用于列出用户的所有 Gmail 凭证。
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""
|
||||||
|
处理 GET 请求,返回用户的所有 Gmail 凭证列表。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: Django REST Framework 请求对象。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: 包含凭证列表的 JSON 响应。
|
||||||
|
|
||||||
|
Status Codes:
|
||||||
|
200: 成功返回凭证列表。
|
||||||
|
"""
|
||||||
|
# 获取用户关联的所有 Gmail 凭证
|
||||||
|
credentials = request.user.gmail_credentials.all()
|
||||||
|
# 序列化凭证数据
|
||||||
|
serializer = GmailCredentialSerializer(credentials, many=True, context={'request': request})
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class GmailCredentialDetailView(APIView):
|
||||||
|
"""
|
||||||
|
API 视图,用于管理特定 Gmail 凭证的获取、更新和删除。
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
|
||||||
|
|
||||||
|
def get(self, request, pk):
|
||||||
|
"""
|
||||||
|
处理 GET 请求,获取特定 Gmail 凭证的详细信息。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: Django REST Framework 请求对象。
|
||||||
|
pk: 凭证的主键 ID。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: 包含凭证详细信息的 JSON 响应。
|
||||||
|
|
||||||
|
Status Codes:
|
||||||
|
200: 成功返回凭证信息。
|
||||||
|
404: 未找到指定凭证。
|
||||||
|
"""
|
||||||
|
# 获取用户拥有的指定凭证,未找到则返回 404
|
||||||
|
credential = get_object_or_404(GmailCredential, pk=pk, user=request.user)
|
||||||
|
serializer = GmailCredentialSerializer(credential, context={'request': request})
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def patch(self, request, pk):
|
||||||
|
"""
|
||||||
|
处理 PATCH 请求,更新特定 Gmail 凭证(如设置为默认凭证)。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: Django REST Framework 请求对象,包含更新数据。
|
||||||
|
pk: 凭证的主键 ID。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: 包含更新后凭证数据的 JSON 响应,或错误信息。
|
||||||
|
|
||||||
|
Status Codes:
|
||||||
|
200: 成功更新凭证。
|
||||||
|
400: 请求数据无效。
|
||||||
|
404: 未找到指定凭证。
|
||||||
|
"""
|
||||||
|
# 获取用户拥有的指定凭证
|
||||||
|
credential = get_object_or_404(GmailCredential, pk=pk, user=request.user)
|
||||||
|
serializer = GmailCredentialSerializer(credential, data=request.data, partial=True, context={'request': request})
|
||||||
|
if serializer.is_valid():
|
||||||
|
# 如果设置为默认凭证,清除其他凭证的默认状态
|
||||||
|
if serializer.validated_data.get('is_default', False):
|
||||||
|
GmailCredential.objects.filter(user=request.user).exclude(id=credential.id).update(is_default=False)
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
# 返回无效数据错误
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def delete(self, request, pk):
|
||||||
|
"""
|
||||||
|
处理 DELETE 请求,删除特定 Gmail 凭证。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: Django REST Framework 请求对象。
|
||||||
|
pk: 凭证的主键 ID。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: 空响应,表示删除成功。
|
||||||
|
|
||||||
|
Status Codes:
|
||||||
|
204: 成功删除凭证。
|
||||||
|
404: 未找到指定凭证。
|
||||||
|
"""
|
||||||
|
# 获取并删除用户拥有的指定凭证
|
||||||
|
credential = get_object_or_404(GmailCredential, pk=pk, user=request.user)
|
||||||
|
credential.delete()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
class GmailConversationView(APIView):
|
||||||
|
"""
|
||||||
|
API视图,用于获取和保存Gmail对话。
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
"""
|
||||||
|
处理POST请求,获取Gmail对话并保存到聊天历史。
|
||||||
|
|
||||||
|
请求参数:
|
||||||
|
user_email: 用户Gmail邮箱
|
||||||
|
influencer_email: 达人Gmail邮箱
|
||||||
|
kb_id: [可选] 知识库ID,不提供则使用默认知识库
|
||||||
|
|
||||||
|
返回:
|
||||||
|
conversation_id: 创建的会话ID
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 验证必填参数
|
||||||
|
user_email = request.data.get('user_email')
|
||||||
|
influencer_email = request.data.get('influencer_email')
|
||||||
|
|
||||||
|
if not user_email or not influencer_email:
|
||||||
|
return Response({
|
||||||
|
'code': 400,
|
||||||
|
'message': '缺少必填参数: user_email 或 influencer_email',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# 可选参数
|
||||||
|
kb_id = request.data.get('kb_id')
|
||||||
|
|
||||||
|
# 调用服务保存对话
|
||||||
|
conversation_id, error = GmailService.save_conversations_to_chat(
|
||||||
|
request.user,
|
||||||
|
user_email,
|
||||||
|
influencer_email,
|
||||||
|
kb_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if error:
|
||||||
|
return Response({
|
||||||
|
'code': 400,
|
||||||
|
'message': error,
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 200,
|
||||||
|
'message': '获取Gmail对话成功',
|
||||||
|
'data': {
|
||||||
|
'conversation_id': conversation_id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取Gmail对话失败: {str(e)}")
|
||||||
|
return Response({
|
||||||
|
'code': 500,
|
||||||
|
'message': f'获取Gmail对话失败: {str(e)}',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""
|
||||||
|
处理GET请求,获取用户的Gmail对话列表。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
conversations = GmailConversation.objects.filter(user=request.user, is_active=True)
|
||||||
|
|
||||||
|
data = []
|
||||||
|
for conversation in conversations:
|
||||||
|
# 获取附件计数
|
||||||
|
attachments_count = GmailAttachment.objects.filter(
|
||||||
|
conversation=conversation
|
||||||
|
).count()
|
||||||
|
|
||||||
|
data.append({
|
||||||
|
'id': str(conversation.id),
|
||||||
|
'conversation_id': conversation.conversation_id,
|
||||||
|
'user_email': conversation.user_email,
|
||||||
|
'influencer_email': conversation.influencer_email,
|
||||||
|
'title': conversation.title,
|
||||||
|
'last_sync_time': conversation.last_sync_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
'created_at': conversation.created_at.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
'attachments_count': attachments_count
|
||||||
|
})
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 200,
|
||||||
|
'message': '获取对话列表成功',
|
||||||
|
'data': data
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取对话列表失败: {str(e)}")
|
||||||
|
return Response({
|
||||||
|
'code': 500,
|
||||||
|
'message': f'获取对话列表失败: {str(e)}',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
class GmailAttachmentListView(APIView):
|
||||||
|
"""
|
||||||
|
API视图,用于获取Gmail附件列表。
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
|
||||||
|
|
||||||
|
def get(self, request, conversation_id=None):
|
||||||
|
"""
|
||||||
|
处理GET请求,获取指定对话的附件列表。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if conversation_id:
|
||||||
|
# 获取指定对话的附件
|
||||||
|
conversation = get_object_or_404(GmailConversation, conversation_id=conversation_id, user=request.user)
|
||||||
|
attachments = GmailAttachment.objects.filter(conversation=conversation)
|
||||||
|
else:
|
||||||
|
# 获取用户的所有附件
|
||||||
|
conversations = GmailConversation.objects.filter(user=request.user, is_active=True)
|
||||||
|
attachments = GmailAttachment.objects.filter(conversation__in=conversations)
|
||||||
|
|
||||||
|
data = []
|
||||||
|
for attachment in attachments:
|
||||||
|
data.append({
|
||||||
|
'id': str(attachment.id),
|
||||||
|
'conversation_id': attachment.conversation.conversation_id,
|
||||||
|
'filename': attachment.filename,
|
||||||
|
'content_type': attachment.content_type,
|
||||||
|
'size': attachment.size,
|
||||||
|
'sender_email': attachment.sender_email,
|
||||||
|
'created_at': attachment.created_at.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
'url': attachment.get_absolute_url()
|
||||||
|
})
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 200,
|
||||||
|
'message': '获取附件列表成功',
|
||||||
|
'data': data
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取附件列表失败: {str(e)}")
|
||||||
|
return Response({
|
||||||
|
'code': 500,
|
||||||
|
'message': f'获取附件列表失败: {str(e)}',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
|
||||||
|
class GmailPubSubView(APIView):
|
||||||
|
"""
|
||||||
|
API视图,用于设置Gmail的Pub/Sub实时通知。
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
"""
|
||||||
|
处理POST请求,为用户的Gmail账户设置Pub/Sub推送通知。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: Django REST Framework请求对象,包含Gmail邮箱信息。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: 设置结果的JSON响应。
|
||||||
|
|
||||||
|
Status Codes:
|
||||||
|
200: 成功设置Pub/Sub通知。
|
||||||
|
400: 请求数据无效。
|
||||||
|
404: 未找到指定Gmail凭证。
|
||||||
|
500: 服务器内部错误。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 获取请求参数
|
||||||
|
email = request.data.get('email')
|
||||||
|
|
||||||
|
if not email:
|
||||||
|
return Response({'error': '必须提供Gmail邮箱地址'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# 检查用户是否有此Gmail账户的凭证
|
||||||
|
credential = GmailCredential.objects.filter(user=request.user, email=email).first()
|
||||||
|
if not credential:
|
||||||
|
return Response({'error': f'未找到{email}的授权信息'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
# 设置Pub/Sub通知
|
||||||
|
success, error = GmailService.setup_gmail_push_notification(request.user, email)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return Response({'message': f'已成功为{email}设置实时通知'}, status=status.HTTP_200_OK)
|
||||||
|
else:
|
||||||
|
return Response({'error': error}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"设置Gmail Pub/Sub通知失败: {str(e)}")
|
||||||
|
return Response({'error': f'设置Gmail实时通知失败: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""
|
||||||
|
处理GET请求,获取用户所有已设置Pub/Sub通知的Gmail账户。
|
||||||
|
|
||||||
|
这个方法目前仅返回用户的所有Gmail凭证,未来可以扩展为返回推送通知的详细状态。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: Django REST Framework请求对象。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: 包含Gmail账户列表的JSON响应。
|
||||||
|
|
||||||
|
Status Codes:
|
||||||
|
200: 成功返回账户列表。
|
||||||
|
"""
|
||||||
|
# 获取用户所有Gmail凭证
|
||||||
|
credentials = request.user.gmail_credentials.filter(is_valid=True)
|
||||||
|
|
||||||
|
# 构建响应数据
|
||||||
|
accounts = []
|
||||||
|
for cred in credentials:
|
||||||
|
accounts.append({
|
||||||
|
'id': cred.id,
|
||||||
|
'email': cred.email,
|
||||||
|
'is_default': cred.is_default
|
||||||
|
})
|
||||||
|
|
||||||
|
return Response({'accounts': accounts}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
class GmailNotificationStartView(APIView):
|
||||||
|
"""
|
||||||
|
API视图,用于启动Gmail Pub/Sub监听器。
|
||||||
|
通常由系统管理员或后台任务调用,而非普通用户。
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated] # 可根据需要更改为更严格的权限
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
"""
|
||||||
|
处理POST请求,启动Gmail Pub/Sub监听器。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: Django REST Framework请求对象。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: 启动结果的JSON响应。
|
||||||
|
|
||||||
|
Status Codes:
|
||||||
|
200: 成功启动监听器。
|
||||||
|
500: 服务器内部错误。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 可选:指定要监听的用户ID
|
||||||
|
user_id = request.data.get('user_id')
|
||||||
|
|
||||||
|
# 在后台线程中启动监听器
|
||||||
|
thread = GmailService.start_pubsub_listener_thread(user_id)
|
||||||
|
|
||||||
|
return Response({'message': '已成功启动Gmail实时通知监听器'}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"启动Gmail Pub/Sub监听器失败: {str(e)}")
|
||||||
|
return Response({'error': f'启动Gmail实时通知监听器失败: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
class GmailSendEmailView(APIView):
|
||||||
|
"""
|
||||||
|
API视图,用于发送Gmail邮件(支持附件)。
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
"""
|
||||||
|
处理POST请求,发送Gmail邮件。
|
||||||
|
|
||||||
|
请求应包含以下字段:
|
||||||
|
- email: 发件人Gmail邮箱
|
||||||
|
- to: 收件人邮箱
|
||||||
|
- subject: 邮件主题
|
||||||
|
- body: 邮件正文
|
||||||
|
- attachments: 附件文件IDs列表 (可选)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: Django REST Framework请求对象。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: 发送结果的JSON响应。
|
||||||
|
|
||||||
|
Status Codes:
|
||||||
|
200: 成功发送邮件。
|
||||||
|
400: 请求数据无效。
|
||||||
|
404: 未找到Gmail凭证。
|
||||||
|
500: 服务器内部错误。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 获取请求参数
|
||||||
|
user_email = request.data.get('email')
|
||||||
|
to_email = request.data.get('to')
|
||||||
|
subject = request.data.get('subject')
|
||||||
|
body = request.data.get('body')
|
||||||
|
attachment_ids = request.data.get('attachments', [])
|
||||||
|
|
||||||
|
# 验证必填字段
|
||||||
|
if not all([user_email, to_email, subject]):
|
||||||
|
return Response({
|
||||||
|
'error': '缺少必要参数,请提供email、to和subject字段'
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# 检查是否有此Gmail账户的凭证
|
||||||
|
credential = GmailCredential.objects.filter(
|
||||||
|
user=request.user,
|
||||||
|
email=user_email,
|
||||||
|
is_valid=True
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not credential:
|
||||||
|
return Response({
|
||||||
|
'error': f'未找到{user_email}的有效授权信息'
|
||||||
|
}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
# 处理附件
|
||||||
|
attachments = []
|
||||||
|
if attachment_ids and isinstance(attachment_ids, list):
|
||||||
|
for file_id in attachment_ids:
|
||||||
|
# 查找已上传的文件
|
||||||
|
file_obj = request.FILES.get(f'file_{file_id}')
|
||||||
|
if file_obj:
|
||||||
|
# 保存临时文件
|
||||||
|
tmp_path = os.path.join(settings.MEDIA_ROOT, 'tmp', f'{file_id}_{file_obj.name}')
|
||||||
|
os.makedirs(os.path.dirname(tmp_path), exist_ok=True)
|
||||||
|
|
||||||
|
with open(tmp_path, 'wb+') as destination:
|
||||||
|
for chunk in file_obj.chunks():
|
||||||
|
destination.write(chunk)
|
||||||
|
|
||||||
|
attachments.append({
|
||||||
|
'path': tmp_path,
|
||||||
|
'filename': file_obj.name
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
# 检查是否为已有的Gmail附件ID
|
||||||
|
try:
|
||||||
|
attachment = GmailAttachment.objects.get(id=file_id)
|
||||||
|
if attachment.conversation.user_id == request.user.id:
|
||||||
|
attachments.append({
|
||||||
|
'path': attachment.file_path,
|
||||||
|
'filename': attachment.filename
|
||||||
|
})
|
||||||
|
except (GmailAttachment.DoesNotExist, ValueError):
|
||||||
|
logger.warning(f"无法找到附件: {file_id}")
|
||||||
|
|
||||||
|
# 发送邮件
|
||||||
|
success, result = GmailService.send_email(
|
||||||
|
request.user,
|
||||||
|
user_email,
|
||||||
|
to_email,
|
||||||
|
subject,
|
||||||
|
body or '',
|
||||||
|
attachments
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return Response({
|
||||||
|
'message': '邮件发送成功',
|
||||||
|
'message_id': result
|
||||||
|
}, status=status.HTTP_200_OK)
|
||||||
|
else:
|
||||||
|
return Response({
|
||||||
|
'error': result
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"发送Gmail邮件失败: {str(e)}")
|
||||||
|
return Response({
|
||||||
|
'error': f'发送Gmail邮件失败: {str(e)}'
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""
|
||||||
|
处理GET请求,获取用户可用于发送邮件的Gmail账户列表。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: Django REST Framework请求对象。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: 包含Gmail账户列表的JSON响应。
|
||||||
|
|
||||||
|
Status Codes:
|
||||||
|
200: 成功返回账户列表。
|
||||||
|
"""
|
||||||
|
# 获取用户所有可用Gmail凭证
|
||||||
|
credentials = request.user.gmail_credentials.filter(is_valid=True)
|
||||||
|
|
||||||
|
# 构建响应数据
|
||||||
|
accounts = []
|
||||||
|
for cred in credentials:
|
||||||
|
accounts.append({
|
||||||
|
'id': cred.id,
|
||||||
|
'email': cred.email,
|
||||||
|
'is_default': cred.is_default
|
||||||
|
})
|
||||||
|
|
||||||
|
return Response({'accounts': accounts}, status=status.HTTP_200_OK)
|
||||||
|
@ -1,72 +0,0 @@
|
|||||||
# apps/message/consumers.py
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
|
||||||
from channels.db import database_sync_to_async
|
|
||||||
from rest_framework.authtoken.models import Token
|
|
||||||
from urllib.parse import parse_qs
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
class NotificationConsumer(AsyncWebsocketConsumer):
|
|
||||||
async def connect(self):
|
|
||||||
"""建立WebSocket连接"""
|
|
||||||
try:
|
|
||||||
# 从URL参数中获取token
|
|
||||||
query_string = self.scope.get('query_string', b'').decode()
|
|
||||||
query_params = parse_qs(query_string)
|
|
||||||
token_key = query_params.get('token', [''])[0]
|
|
||||||
|
|
||||||
if not token_key:
|
|
||||||
logger.warning("WebSocket连接尝试,但没有提供token")
|
|
||||||
await self.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
# 验证token
|
|
||||||
self.user = await self.get_user_from_token(token_key)
|
|
||||||
if not self.user:
|
|
||||||
logger.warning(f"WebSocket连接尝试,但token无效: {token_key}")
|
|
||||||
await self.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
# 为用户创建专属房间
|
|
||||||
self.room_name = f"notification_user_{self.user.id}"
|
|
||||||
await self.channel_layer.group_add(
|
|
||||||
self.room_name,
|
|
||||||
self.channel_name
|
|
||||||
)
|
|
||||||
await self.accept()
|
|
||||||
logger.info(f"用户 {self.user.username} WebSocket连接成功")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"WebSocket连接错误: {str(e)}")
|
|
||||||
await self.close()
|
|
||||||
|
|
||||||
@database_sync_to_async
|
|
||||||
def get_user_from_token(self, token_key):
|
|
||||||
try:
|
|
||||||
token = Token.objects.select_related('user').get(key=token_key)
|
|
||||||
return token.user
|
|
||||||
except Token.DoesNotExist:
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def disconnect(self, close_code):
|
|
||||||
"""断开WebSocket连接"""
|
|
||||||
try:
|
|
||||||
if hasattr(self, 'room_name'):
|
|
||||||
await self.channel_layer.group_discard(
|
|
||||||
self.room_name,
|
|
||||||
self.channel_name
|
|
||||||
)
|
|
||||||
logger.info(f"用户 {self.user.username} 已断开连接,关闭代码: {close_code}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"断开连接时发生错误: {str(e)}")
|
|
||||||
|
|
||||||
async def notification(self, event):
|
|
||||||
"""处理并发送通知消息"""
|
|
||||||
try:
|
|
||||||
await self.send(text_data=json.dumps(event))
|
|
||||||
logger.info(f"已发送通知给用户 {self.user.username}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"发送通知消息时发生错误: {str(e)}")
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
|||||||
# apps/message/routing.py
|
|
||||||
from django.urls import re_path
|
|
||||||
from apps.message.consumers import NotificationConsumer
|
|
||||||
from apps.chat.consumers import ChatStreamConsumer # 直接导入已有的ChatStreamConsumer
|
|
||||||
import logging
|
|
||||||
|
|
||||||
websocket_urlpatterns = [
|
|
||||||
re_path(r'^ws/notifications/$', NotificationConsumer.as_asgi()),
|
|
||||||
re_path(r'^ws/chat/stream/$', ChatStreamConsumer.as_asgi()),
|
|
||||||
]
|
|
||||||
|
|
@ -3,4 +3,4 @@ from django.apps import AppConfig
|
|||||||
|
|
||||||
class MessageConfig(AppConfig):
|
class MessageConfig(AppConfig):
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
name = 'apps.message'
|
name = 'apps.notification'
|
61
apps/notification/consumers.py
Normal file
61
apps/notification/consumers.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
# apps/notification/consumers.py
|
||||||
|
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||||
|
import json
|
||||||
|
from channels.db import database_sync_to_async
|
||||||
|
from rest_framework.authtoken.models import Token
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class NotificationConsumer(AsyncWebsocketConsumer):
|
||||||
|
async def connect(self):
|
||||||
|
# 获取token参数
|
||||||
|
query_string = self.scope.get('query_string', b'').decode()
|
||||||
|
query_params = dict(param.split('=') for param in query_string.split('&') if '=' in param)
|
||||||
|
token_key = query_params.get('token', None)
|
||||||
|
|
||||||
|
if token_key:
|
||||||
|
# 使用token获取用户
|
||||||
|
self.user = await self.get_user_from_token(token_key)
|
||||||
|
if not self.user:
|
||||||
|
logger.error(f"Invalid token: {token_key}")
|
||||||
|
await self.close()
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# 使用scope中的用户(如果有认证)
|
||||||
|
self.user = self.scope.get('user')
|
||||||
|
if not self.user or not self.user.is_authenticated:
|
||||||
|
logger.error("No valid authentication in WebSocket connection")
|
||||||
|
await self.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(f"WebSocket connected for user: {self.user.id}")
|
||||||
|
self.group_name = f"notification_user_{self.user.id}"
|
||||||
|
await self.channel_layer.group_add(
|
||||||
|
self.group_name,
|
||||||
|
self.channel_name
|
||||||
|
)
|
||||||
|
await self.accept()
|
||||||
|
|
||||||
|
@database_sync_to_async
|
||||||
|
def get_user_from_token(self, token_key):
|
||||||
|
try:
|
||||||
|
token = Token.objects.select_related('user').get(key=token_key)
|
||||||
|
return token.user
|
||||||
|
except Token.DoesNotExist:
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error authenticating token: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def disconnect(self, close_code):
|
||||||
|
logger.info(f"WebSocket disconnected with code: {close_code}")
|
||||||
|
if hasattr(self, 'group_name'):
|
||||||
|
await self.channel_layer.group_discard(
|
||||||
|
self.group_name,
|
||||||
|
self.channel_name
|
||||||
|
)
|
||||||
|
|
||||||
|
async def notification(self, event):
|
||||||
|
"""处理通知事件"""
|
||||||
|
await self.send(text_data=json.dumps(event['data']))
|
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-05-09 03:11
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('notification', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameIndex(
|
||||||
|
model_name='notification',
|
||||||
|
new_name='notificatio_receive_6f29eb_idx',
|
||||||
|
old_name='message_not_receive_e8d006_idx',
|
||||||
|
),
|
||||||
|
migrations.RenameIndex(
|
||||||
|
model_name='notification',
|
||||||
|
new_name='notificatio_type_83c189_idx',
|
||||||
|
old_name='message_not_type_a0b1e3_idx',
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,21 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-05-09 08:35
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('notification', '0002_rename_message_not_receive_e8d006_idx_notificatio_receive_6f29eb_idx_and_more'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='notification',
|
||||||
|
name='sender',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sent_notifications', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
]
|
@ -1,4 +1,4 @@
|
|||||||
# apps/message/models.py
|
# apps/notification/models.py
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
import uuid
|
import uuid
|
||||||
@ -18,7 +18,7 @@ class Notification(models.Model):
|
|||||||
type = models.CharField(max_length=20, choices=NOTIFICATION_TYPES)
|
type = models.CharField(max_length=20, choices=NOTIFICATION_TYPES)
|
||||||
title = models.CharField(max_length=100)
|
title = models.CharField(max_length=100)
|
||||||
content = models.TextField()
|
content = models.TextField()
|
||||||
sender = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sent_notifications')
|
sender = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL, related_name='sent_notifications')
|
||||||
receiver = models.ForeignKey(User, on_delete=models.CASCADE, related_name='received_notifications')
|
receiver = models.ForeignKey(User, on_delete=models.CASCADE, related_name='received_notifications')
|
||||||
is_read = models.BooleanField(default=False)
|
is_read = models.BooleanField(default=False)
|
||||||
related_resource = models.CharField(max_length=100, blank=True) # 相关资源ID
|
related_resource = models.CharField(max_length=100, blank=True) # 相关资源ID
|
9
apps/notification/routing.py
Normal file
9
apps/notification/routing.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# apps/notification/routing.py
|
||||||
|
from django.urls import re_path
|
||||||
|
from apps.notification.consumers import NotificationConsumer
|
||||||
|
import logging
|
||||||
|
|
||||||
|
websocket_urlpatterns = [
|
||||||
|
re_path(r'^ws/notifications/$', NotificationConsumer.as_asgi()),
|
||||||
|
]
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
# apps/message/serializers.py
|
# apps/notification/serializers.py
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from apps.message.models import Notification
|
from apps.notification.models import Notification
|
||||||
from apps.accounts.models import User
|
from apps.accounts.models import User
|
||||||
|
|
||||||
class NotificationSerializer(serializers.ModelSerializer):
|
class NotificationSerializer(serializers.ModelSerializer):
|
0
apps/notification/services/__init__.py
Normal file
0
apps/notification/services/__init__.py
Normal file
3
apps/notification/tests.py
Normal file
3
apps/notification/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
@ -1,7 +1,7 @@
|
|||||||
# apps/message/urls.py
|
# apps/notification/urls.py
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
from apps.message.views import NotificationViewSet
|
from apps.notification.views import NotificationViewSet
|
||||||
|
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
router.register(r'', NotificationViewSet, basename='notification')
|
router.register(r'', NotificationViewSet, basename='notification')
|
@ -1,10 +1,10 @@
|
|||||||
# apps/message/views.py
|
# apps/notification/views.py
|
||||||
from rest_framework import viewsets, status
|
from rest_framework import viewsets, status
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from apps.message.models import Notification
|
from apps.notification.models import Notification
|
||||||
from apps.message.serializers import NotificationSerializer
|
from apps.notification.serializers import NotificationSerializer
|
||||||
|
|
||||||
class NotificationViewSet(viewsets.ModelViewSet):
|
class NotificationViewSet(viewsets.ModelViewSet):
|
||||||
"""通知视图集"""
|
"""通知视图集"""
|
0
apps/operation/__init__.py
Normal file
0
apps/operation/__init__.py
Normal file
36
apps/operation/admin.py
Normal file
36
apps/operation/admin.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from .models import OperatorAccount, PlatformAccount, Video
|
||||||
|
|
||||||
|
@admin.register(OperatorAccount)
|
||||||
|
class OperatorAccountAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('username', 'real_name', 'email', 'phone', 'position', 'department', 'is_active', 'created_at')
|
||||||
|
list_filter = ('position', 'department', 'is_active')
|
||||||
|
search_fields = ('username', 'real_name', 'email', 'phone')
|
||||||
|
date_hierarchy = 'created_at'
|
||||||
|
readonly_fields = ('created_at', 'updated_at')
|
||||||
|
|
||||||
|
@admin.register(PlatformAccount)
|
||||||
|
class PlatformAccountAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('account_name', 'platform_name', 'operator', 'status', 'followers_count', 'last_posting', 'created_at')
|
||||||
|
list_filter = ('platform_name', 'status')
|
||||||
|
search_fields = ('account_name', 'account_id', 'description')
|
||||||
|
date_hierarchy = 'created_at'
|
||||||
|
readonly_fields = ('created_at', 'updated_at')
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
"""优化查询,减少数据库查询次数"""
|
||||||
|
queryset = super().get_queryset(request)
|
||||||
|
return queryset.select_related('operator')
|
||||||
|
|
||||||
|
@admin.register(Video)
|
||||||
|
class VideoAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('title', 'platform_account', 'status', 'views_count', 'likes_count', 'publish_time', 'created_at')
|
||||||
|
list_filter = ('status', 'created_at', 'publish_time')
|
||||||
|
search_fields = ('title', 'description', 'tags')
|
||||||
|
date_hierarchy = 'created_at'
|
||||||
|
readonly_fields = ('created_at', 'updated_at')
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
"""优化查询,减少数据库查询次数"""
|
||||||
|
queryset = super().get_queryset(request)
|
||||||
|
return queryset.select_related('platform_account', 'platform_account__operator')
|
6
apps/operation/apps.py
Normal file
6
apps/operation/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class OperationConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'apps.operation'
|
88
apps/operation/migrations/0001_initial.py
Normal file
88
apps/operation/migrations/0001_initial.py
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
# Generated by Django 5.1.5 on 2025-05-12 08:55
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='OperatorAccount',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True, verbose_name='UUID')),
|
||||||
|
('username', models.CharField(max_length=100, unique=True, verbose_name='用户名')),
|
||||||
|
('password', models.CharField(max_length=255, verbose_name='密码')),
|
||||||
|
('real_name', models.CharField(max_length=50, verbose_name='真实姓名')),
|
||||||
|
('email', models.EmailField(max_length=254, verbose_name='邮箱')),
|
||||||
|
('phone', models.CharField(max_length=15, verbose_name='电话')),
|
||||||
|
('position', models.CharField(choices=[('editor', '编辑'), ('planner', '策划'), ('operator', '运营'), ('admin', '管理员')], max_length=20, verbose_name='工作定位')),
|
||||||
|
('department', models.CharField(max_length=50, 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': '运营账号',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PlatformAccount',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('platform_name', models.CharField(choices=[('youtube', 'YouTube'), ('tiktok', 'TikTok'), ('twitter', 'Twitter/X'), ('instagram', 'Instagram'), ('facebook', 'Facebook'), ('bilibili', 'Bilibili')], max_length=20, verbose_name='平台名称')),
|
||||||
|
('account_name', models.CharField(max_length=100, verbose_name='账号名称')),
|
||||||
|
('account_id', models.CharField(max_length=100, verbose_name='账号ID')),
|
||||||
|
('status', models.CharField(choices=[('active', '正常'), ('restricted', '限流'), ('suspended', '封禁'), ('inactive', '未激活')], default='active', max_length=20, verbose_name='账号状态')),
|
||||||
|
('followers_count', models.IntegerField(default=0, verbose_name='粉丝数')),
|
||||||
|
('account_url', models.URLField(verbose_name='账号链接')),
|
||||||
|
('description', models.TextField(blank=True, null=True, verbose_name='账号描述')),
|
||||||
|
('tags', models.CharField(blank=True, help_text='用逗号分隔的标签列表', max_length=255, null=True, verbose_name='标签')),
|
||||||
|
('profile_image', models.URLField(blank=True, null=True, verbose_name='头像URL')),
|
||||||
|
('last_posting', 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='更新时间')),
|
||||||
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='最后登录时间')),
|
||||||
|
('operator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='platform_accounts', to='operation.operatoraccount', verbose_name='关联运营')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '平台账号',
|
||||||
|
'verbose_name_plural': '平台账号',
|
||||||
|
'unique_together': {('platform_name', 'account_id')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Video',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.CharField(max_length=200, verbose_name='视频标题')),
|
||||||
|
('description', models.TextField(blank=True, null=True, verbose_name='视频描述')),
|
||||||
|
('video_url', models.URLField(blank=True, null=True, verbose_name='视频地址')),
|
||||||
|
('local_path', models.CharField(blank=True, max_length=255, null=True, verbose_name='本地路径')),
|
||||||
|
('thumbnail_url', models.URLField(blank=True, null=True, verbose_name='缩略图地址')),
|
||||||
|
('status', models.CharField(choices=[('draft', '草稿'), ('scheduled', '已排期'), ('published', '已发布'), ('failed', '发布失败'), ('deleted', '已删除')], default='draft', max_length=20, verbose_name='发布状态')),
|
||||||
|
('views_count', models.IntegerField(default=0, verbose_name='播放次数')),
|
||||||
|
('likes_count', models.IntegerField(default=0, verbose_name='点赞数')),
|
||||||
|
('comments_count', models.IntegerField(default=0, verbose_name='评论数')),
|
||||||
|
('shares_count', models.IntegerField(default=0, verbose_name='分享数')),
|
||||||
|
('tags', models.CharField(blank=True, max_length=500, null=True, verbose_name='标签')),
|
||||||
|
('publish_time', models.DateTimeField(blank=True, null=True, verbose_name='发布时间')),
|
||||||
|
('scheduled_time', 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='更新时间')),
|
||||||
|
('platform_account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='videos', to='operation.platformaccount', verbose_name='发布账号')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '视频',
|
||||||
|
'verbose_name_plural': '视频',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
0
apps/operation/migrations/__init__.py
Normal file
0
apps/operation/migrations/__init__.py
Normal file
126
apps/operation/models.py
Normal file
126
apps/operation/models.py
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
from django.db import models
|
||||||
|
import uuid
|
||||||
|
from django.utils import timezone
|
||||||
|
from apps.knowledge_base.models import KnowledgeBase, KnowledgeBaseDocument
|
||||||
|
from apps.accounts.models import User
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
|
|
||||||
|
# 我们可以在这里添加额外的模型或关系,但现在使用user_management中的现有模型
|
||||||
|
|
||||||
|
# 从user_management迁移过来的模型
|
||||||
|
class OperatorAccount(models.Model):
|
||||||
|
"""运营账号信息表"""
|
||||||
|
|
||||||
|
id = models.AutoField(primary_key=True) # 保留自动递增的ID字段
|
||||||
|
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, verbose_name='UUID')
|
||||||
|
|
||||||
|
POSITION_CHOICES = [
|
||||||
|
('editor', '编辑'),
|
||||||
|
('planner', '策划'),
|
||||||
|
('operator', '运营'),
|
||||||
|
('admin', '管理员'),
|
||||||
|
]
|
||||||
|
|
||||||
|
username = models.CharField(max_length=100, unique=True, verbose_name='用户名')
|
||||||
|
password = models.CharField(max_length=255, verbose_name='密码')
|
||||||
|
real_name = models.CharField(max_length=50, verbose_name='真实姓名')
|
||||||
|
email = models.EmailField(verbose_name='邮箱')
|
||||||
|
phone = models.CharField(max_length=15, verbose_name='电话')
|
||||||
|
position = models.CharField(max_length=20, choices=POSITION_CHOICES, verbose_name='工作定位')
|
||||||
|
department = models.CharField(max_length=50, 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 = '运营账号'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.real_name} ({self.username})"
|
||||||
|
|
||||||
|
class PlatformAccount(models.Model):
|
||||||
|
"""平台账号信息表"""
|
||||||
|
|
||||||
|
STATUS_CHOICES = [
|
||||||
|
('active', '正常'),
|
||||||
|
('restricted', '限流'),
|
||||||
|
('suspended', '封禁'),
|
||||||
|
('inactive', '未激活'),
|
||||||
|
]
|
||||||
|
|
||||||
|
PLATFORM_CHOICES = [
|
||||||
|
('youtube', 'YouTube'),
|
||||||
|
('tiktok', 'TikTok'),
|
||||||
|
('twitter', 'Twitter/X'),
|
||||||
|
('instagram', 'Instagram'),
|
||||||
|
('facebook', 'Facebook'),
|
||||||
|
('bilibili', 'Bilibili'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operator = models.ForeignKey(OperatorAccount, on_delete=models.CASCADE, related_name='platform_accounts', verbose_name='关联运营')
|
||||||
|
platform_name = models.CharField(max_length=20, choices=PLATFORM_CHOICES, verbose_name='平台名称')
|
||||||
|
account_name = models.CharField(max_length=100, verbose_name='账号名称')
|
||||||
|
account_id = models.CharField(max_length=100, verbose_name='账号ID')
|
||||||
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='active', verbose_name='账号状态')
|
||||||
|
followers_count = models.IntegerField(default=0, verbose_name='粉丝数')
|
||||||
|
account_url = models.URLField(verbose_name='账号链接')
|
||||||
|
description = models.TextField(blank=True, null=True, verbose_name='账号描述')
|
||||||
|
|
||||||
|
# 新增字段
|
||||||
|
tags = models.CharField(max_length=255, blank=True, null=True, verbose_name='标签', help_text='用逗号分隔的标签列表')
|
||||||
|
profile_image = models.URLField(blank=True, null=True, verbose_name='头像URL')
|
||||||
|
last_posting = 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='更新时间')
|
||||||
|
last_login = models.DateTimeField(blank=True, null=True, verbose_name='最后登录时间')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = '平台账号'
|
||||||
|
verbose_name_plural = '平台账号'
|
||||||
|
unique_together = ('platform_name', 'account_id')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.account_name} ({self.platform_name})"
|
||||||
|
|
||||||
|
class Video(models.Model):
|
||||||
|
"""视频信息表"""
|
||||||
|
|
||||||
|
STATUS_CHOICES = [
|
||||||
|
('draft', '草稿'),
|
||||||
|
('scheduled', '已排期'),
|
||||||
|
('published', '已发布'),
|
||||||
|
('failed', '发布失败'),
|
||||||
|
('deleted', '已删除'),
|
||||||
|
]
|
||||||
|
|
||||||
|
platform_account = models.ForeignKey(PlatformAccount, on_delete=models.CASCADE, related_name='videos', verbose_name='发布账号')
|
||||||
|
title = models.CharField(max_length=200, verbose_name='视频标题')
|
||||||
|
description = models.TextField(blank=True, null=True, verbose_name='视频描述')
|
||||||
|
video_url = models.URLField(blank=True, null=True, verbose_name='视频地址')
|
||||||
|
local_path = models.CharField(max_length=255, blank=True, null=True, verbose_name='本地路径')
|
||||||
|
thumbnail_url = models.URLField(blank=True, null=True, verbose_name='缩略图地址')
|
||||||
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft', verbose_name='发布状态')
|
||||||
|
views_count = models.IntegerField(default=0, verbose_name='播放次数')
|
||||||
|
likes_count = models.IntegerField(default=0, verbose_name='点赞数')
|
||||||
|
comments_count = models.IntegerField(default=0, verbose_name='评论数')
|
||||||
|
shares_count = models.IntegerField(default=0, verbose_name='分享数')
|
||||||
|
tags = models.CharField(max_length=500, blank=True, null=True, verbose_name='标签')
|
||||||
|
publish_time = models.DateTimeField(blank=True, null=True, verbose_name='发布时间')
|
||||||
|
scheduled_time = 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 = '视频'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.status == 'published' and not self.publish_time:
|
||||||
|
self.publish_time = timezone.now()
|
||||||
|
super().save(*args, **kwargs)
|
23
apps/operation/pagination.py
Normal file
23
apps/operation/pagination.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from rest_framework.pagination import PageNumberPagination
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
class CustomPagination(PageNumberPagination):
|
||||||
|
"""自定义分页器,返回格式为 {code, message, data}"""
|
||||||
|
page_size = 10
|
||||||
|
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,
|
||||||
|
"page": self.page.number,
|
||||||
|
"pages": self.page.paginator.num_pages,
|
||||||
|
"page_size": self.page_size
|
||||||
|
}
|
||||||
|
})
|
102
apps/operation/serializers.py
Normal file
102
apps/operation/serializers.py
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from .models import OperatorAccount, PlatformAccount, Video
|
||||||
|
from apps.knowledge_base.models import KnowledgeBase, KnowledgeBaseDocument
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
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 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']
|
3
apps/operation/tests.py
Normal file
3
apps/operation/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
12
apps/operation/urls.py
Normal file
12
apps/operation/urls.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
from django.urls import path, include
|
||||||
|
from rest_framework.routers import DefaultRouter
|
||||||
|
from .views import OperatorAccountViewSet, PlatformAccountViewSet, VideoViewSet
|
||||||
|
|
||||||
|
router = DefaultRouter()
|
||||||
|
router.register(r'operators', OperatorAccountViewSet)
|
||||||
|
router.register(r'platforms', PlatformAccountViewSet)
|
||||||
|
router.register(r'videos', VideoViewSet)
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', include(router.urls)),
|
||||||
|
]
|
1095
apps/operation/views.py
Normal file
1095
apps/operation/views.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -6,7 +6,7 @@ import uuid
|
|||||||
import logging
|
import logging
|
||||||
from apps.accounts.models import User
|
from apps.accounts.models import User
|
||||||
from apps.knowledge_base.models import KnowledgeBase
|
from apps.knowledge_base.models import KnowledgeBase
|
||||||
from apps.message.models import Notification
|
from apps.notification.models import Notification
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -8,19 +8,27 @@ https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'daren_project.settings')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
from django.core.asgi import get_asgi_application
|
from django.core.asgi import get_asgi_application
|
||||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||||
from channels.auth import AuthMiddlewareStack
|
from channels.auth import AuthMiddlewareStack
|
||||||
from apps.message.routing import websocket_urlpatterns # WebSocket 路由
|
# WebSocket 路由
|
||||||
|
|
||||||
|
django_asgi_app = get_asgi_application()
|
||||||
|
|
||||||
|
from apps.chat.routing import websocket_urlpatterns as chat_websocket_urlpatterns # WebSocket 路由
|
||||||
|
from apps.notification.routing import websocket_urlpatterns as notification_websocket_urlpatterns # WebSocket 路由
|
||||||
|
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'daren_project.settings')
|
|
||||||
|
|
||||||
application = ProtocolTypeRouter({
|
application = ProtocolTypeRouter({
|
||||||
"http": get_asgi_application(),
|
"http": django_asgi_app,
|
||||||
"websocket": AuthMiddlewareStack(
|
"websocket": AuthMiddlewareStack(
|
||||||
URLRouter(
|
URLRouter(
|
||||||
websocket_urlpatterns
|
chat_websocket_urlpatterns + notification_websocket_urlpatterns
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
@ -46,10 +46,12 @@ INSTALLED_APPS = [
|
|||||||
'apps.knowledge_base',
|
'apps.knowledge_base',
|
||||||
'apps.chat',
|
'apps.chat',
|
||||||
'apps.permissions',
|
'apps.permissions',
|
||||||
'apps.message',
|
'apps.notification',
|
||||||
'apps.gmail',
|
'apps.gmail',
|
||||||
'apps.feishu',
|
'apps.feishu',
|
||||||
'apps.common',
|
'apps.common',
|
||||||
|
'apps.brands',
|
||||||
|
'apps.operation',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
@ -143,14 +145,20 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
|||||||
# REST Framework 配置
|
# REST Framework 配置
|
||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||||
# 'rest_framework.authentication.SessionAuthentication',
|
|
||||||
'rest_framework.authentication.TokenAuthentication',
|
'rest_framework.authentication.TokenAuthentication',
|
||||||
|
# 'rest_framework.authentication.SessionAuthentication',
|
||||||
],
|
],
|
||||||
'DEFAULT_PERMISSION_CLASSES': [
|
'DEFAULT_PERMISSION_CLASSES': [
|
||||||
'rest_framework.permissions.IsAuthenticated',
|
'rest_framework.permissions.AllowAny',
|
||||||
|
],
|
||||||
|
'DEFAULT_PARSER_CLASSES': [
|
||||||
|
'rest_framework.parsers.JSONParser',
|
||||||
|
'rest_framework.parsers.FormParser',
|
||||||
|
'rest_framework.parsers.MultiPartParser'
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Channels 配置(WebSocket)
|
# Channels 配置(WebSocket)
|
||||||
ASGI_APPLICATION = 'daren_project.asgi.application'
|
ASGI_APPLICATION = 'daren_project.asgi.application'
|
||||||
CHANNEL_LAYERS = {
|
CHANNEL_LAYERS = {
|
||||||
@ -169,3 +177,19 @@ API_BASE_URL = 'http://81.69.223.133:48329'
|
|||||||
SILICON_CLOUD_API_KEY = 'sk-xqbujijjqqmlmlvkhvxeogqjtzslnhdtqxqgiyuhwpoqcjvf'
|
SILICON_CLOUD_API_KEY = 'sk-xqbujijjqqmlmlvkhvxeogqjtzslnhdtqxqgiyuhwpoqcjvf'
|
||||||
GMAIL_WEBHOOK_URL = 'https://27b3-180-159-100-165.ngrok-free.app/api/user/gmail/webhook/'
|
GMAIL_WEBHOOK_URL = 'https://27b3-180-159-100-165.ngrok-free.app/api/user/gmail/webhook/'
|
||||||
APPLICATION_ID = 'd5d11efa-ea9a-11ef-9933-0242ac120006'
|
APPLICATION_ID = 'd5d11efa-ea9a-11ef-9933-0242ac120006'
|
||||||
|
|
||||||
|
|
||||||
|
# 全局代理设置
|
||||||
|
# 格式为 'http://主机名:端口号',例如:'http://127.0.0.1:7890'
|
||||||
|
# 此代理将应用于所有HTTP/HTTPS请求和Gmail API请求
|
||||||
|
# 如果代理不可用,请将此值设为None或注释掉此行
|
||||||
|
PROXY_URL = 'http://127.0.0.1:7890'
|
||||||
|
|
||||||
|
|
||||||
|
# Gmail Pub/Sub相关设置
|
||||||
|
GOOGLE_CLOUD_PROJECT_ID = 'your-project-id' # 替换为您的Google Cloud项目ID
|
||||||
|
GMAIL_PUBSUB_TOPIC = 'projects/{project_id}/topics/gmail-notifications'
|
||||||
|
GMAIL_PUBSUB_SUBSCRIPTION = 'projects/{project_id}/subscriptions/gmail-notifications-sub'
|
||||||
|
|
||||||
|
# 设置允许使用Google Pub/Sub的应用列表
|
||||||
|
INSTALLED_APPS += ['google.cloud.pubsub']
|
@ -23,7 +23,9 @@ urlpatterns = [
|
|||||||
path('api/knowledge-bases/', include('apps.knowledge_base.urls')),
|
path('api/knowledge-bases/', include('apps.knowledge_base.urls')),
|
||||||
path('api/chat-history/', include('apps.chat.urls')),
|
path('api/chat-history/', include('apps.chat.urls')),
|
||||||
path('api/permissions/', include('apps.permissions.urls')),
|
path('api/permissions/', include('apps.permissions.urls')),
|
||||||
path('api/message/', include('apps.message.urls')),
|
path('api/notification/', include('apps.notification.urls')),
|
||||||
# path('api/gmail/', include('apps.gmail.urls')),
|
path('api/gmail/', include('apps.gmail.urls')),
|
||||||
# path('api/feishu/', include('apps.feishu.urls')),
|
# path('api/feishu/', include('apps.feishu.urls')),
|
||||||
|
path('api/', include('apps.brands.urls')),
|
||||||
|
path('api/operation/', include('apps.operation.urls')),
|
||||||
]
|
]
|
@ -8,9 +8,26 @@ https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import django
|
||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application
|
# 首先设置 Django 设置模块
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'daren_project.settings')
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'daren_project.settings')
|
||||||
|
django.setup() # 添加这行来初始化 Django
|
||||||
|
|
||||||
application = get_wsgi_application()
|
# 然后再导入其他模块
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||||
|
from channels.auth import AuthMiddlewareStack
|
||||||
|
from channels.security.websocket import AllowedHostsOriginValidator
|
||||||
|
from apps.chat.routing import websocket_urlpatterns
|
||||||
|
from apps.common.middlewares import TokenAuthMiddleware
|
||||||
|
|
||||||
|
# 使用TokenAuthMiddleware代替AuthMiddlewareStack
|
||||||
|
application = ProtocolTypeRouter({
|
||||||
|
"http": get_asgi_application(),
|
||||||
|
"websocket": AllowedHostsOriginValidator(
|
||||||
|
TokenAuthMiddleware(
|
||||||
|
URLRouter(websocket_urlpatterns)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
})
|
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
Loading…
Reference in New Issue
Block a user