增加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):
|
||||
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:
|
||||
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])
|
||||
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
|
||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||
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 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
|
||||
from apps.common.services.permission_service import PermissionService
|
||||
import aiohttp
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -1,8 +1,7 @@
|
||||
# apps/chat/routing.py
|
||||
from django.urls import re_path
|
||||
from apps.chat.consumers import ChatConsumer, ChatStreamConsumer
|
||||
from apps.chat.consumers import ChatStreamConsumer
|
||||
|
||||
websocket_urlpatterns = [
|
||||
re_path(r'ws/chat/$', ChatConsumer.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.chat.models import ChatHistory
|
||||
from apps.permissions.services.permission_service import KnowledgeBasePermissionMixin
|
||||
from django.db.models import Q
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
import logging
|
||||
from asgiref.sync import async_to_sync
|
||||
from channels.layers import get_channel_layer
|
||||
from apps.message.models import Notification
|
||||
from apps.notification.models import Notification
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -19,25 +19,33 @@ class NotificationService:
|
||||
related_resource=related_object_id,
|
||||
)
|
||||
|
||||
channel_layer = get_channel_layer()
|
||||
async_to_sync(channel_layer.group_send)(
|
||||
f"notification_user_{user.id}",
|
||||
{
|
||||
"type": "notification",
|
||||
"data": {
|
||||
# 准备发送到WebSocket的数据
|
||||
notification_data = {
|
||||
"id": str(notification.id),
|
||||
"title": notification.title,
|
||||
"content": notification.content,
|
||||
"type": notification.type,
|
||||
"created_at": notification.created_at.isoformat(),
|
||||
"sender": {
|
||||
}
|
||||
|
||||
# 只有当sender不为None时才添加sender信息
|
||||
if notification.sender:
|
||||
notification_data["sender"] = {
|
||||
"id": str(notification.sender.id),
|
||||
"name": notification.sender.name
|
||||
} if notification.sender else None
|
||||
}
|
||||
|
||||
channel_layer = get_channel_layer()
|
||||
async_to_sync(channel_layer.group_send)(
|
||||
f"notification_user_{user.id}",
|
||||
{
|
||||
"type": "notification",
|
||||
"data": notification_data
|
||||
}
|
||||
)
|
||||
return notification
|
||||
except Exception as 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 uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
@ -11,72 +10,24 @@ class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('chat', '0001_initial'),
|
||||
('knowledge_base', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
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(
|
||||
name='GmailCredential',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('gmail_email', models.EmailField(default='your_default_email@example.com', max_length=255, verbose_name='Gmail邮箱')),
|
||||
('name', models.CharField(default='默认Gmail', max_length=100, verbose_name='名称')),
|
||||
('credentials', models.TextField(blank=True, null=True, verbose_name='凭证JSON')),
|
||||
('token_path', models.CharField(blank=True, max_length=255, null=True, verbose_name='令牌路径')),
|
||||
('is_default', models.BooleanField(default=False, verbose_name='是否默认')),
|
||||
('last_history_id', models.CharField(blank=True, max_length=100, null=True, verbose_name='最后历史ID')),
|
||||
('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='需要重新授权')),
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('email', models.EmailField(help_text='Gmail email address', max_length=254, unique=True)),
|
||||
('credentials', models.TextField(help_text='Serialized OAuth2 credentials (JSON)')),
|
||||
('is_default', models.BooleanField(default=False, help_text='Default Gmail account for user')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('is_valid', models.BooleanField(default=True, help_text='Whether the credential is valid')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gmail_credentials', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Gmail凭证',
|
||||
'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')},
|
||||
'unique_together': {('user', '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.utils import timezone
|
||||
import uuid
|
||||
from apps.accounts.models import User
|
||||
from apps.knowledge_base.models import KnowledgeBase
|
||||
from apps.chat.models import ChatHistory # 更新导入路径
|
||||
import json
|
||||
import os
|
||||
from django.utils import timezone
|
||||
|
||||
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')
|
||||
gmail_email = models.EmailField(verbose_name='Gmail邮箱', max_length=255, default='your_default_email@example.com')
|
||||
name = models.CharField(verbose_name='名称', max_length=100, default='默认Gmail')
|
||||
credentials = models.TextField(verbose_name='凭证JSON', blank=True, null=True)
|
||||
token_path = models.CharField(verbose_name='令牌路径', max_length=255, blank=True, null=True)
|
||||
is_default = models.BooleanField(verbose_name='是否默认', default=False)
|
||||
last_history_id = models.CharField(verbose_name='最后历史ID', max_length=100, blank=True, null=True)
|
||||
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})"
|
||||
email = models.EmailField(unique=True, help_text="Gmail email address")
|
||||
credentials = models.TextField(help_text="Serialized OAuth2 credentials (JSON)")
|
||||
is_default = models.BooleanField(default=False, help_text="Default Gmail account for user")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
is_valid = models.BooleanField(default=True, help_text="Whether the credential is valid")
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Gmail凭证'
|
||||
verbose_name_plural = 'Gmail凭证'
|
||||
unique_together = ('user', 'gmail_email')
|
||||
ordering = ['-is_default', '-updated_at']
|
||||
unique_together = ('user', 'email')
|
||||
|
||||
class GmailTalentMapping(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_talent_mappings')
|
||||
talent_email = models.EmailField(verbose_name='达人邮箱')
|
||||
knowledge_base = models.ForeignKey(KnowledgeBase, on_delete=models.CASCADE, related_name='gmail_mappings')
|
||||
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='更新时间')
|
||||
def set_credentials(self, credentials):
|
||||
self.credentials = json.dumps({
|
||||
'token': credentials.token,
|
||||
'refresh_token': credentials.refresh_token,
|
||||
'token_uri': credentials.token_uri,
|
||||
'client_id': credentials.client_id,
|
||||
'client_secret': credentials.client_secret,
|
||||
'scopes': credentials.scopes
|
||||
})
|
||||
self.is_valid = True
|
||||
|
||||
class Meta:
|
||||
db_table = 'gmail_talent_mappings'
|
||||
unique_together = ['user', 'talent_email']
|
||||
verbose_name = 'Gmail达人映射'
|
||||
verbose_name_plural = 'Gmail达人映射'
|
||||
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.talent_email}"
|
||||
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:
|
||||
ordering = ['-updated_at']
|
||||
unique_together = ('user', 'user_email', 'influencer_email')
|
||||
|
||||
class GmailAttachment(models.Model):
|
||||
"""Gmail附件模型"""
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
chat_message = models.ForeignKey(ChatHistory, on_delete=models.CASCADE, related_name='gmail_attachments')
|
||||
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='创建时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'gmail_attachments'
|
||||
verbose_name = 'Gmail附件'
|
||||
verbose_name_plural = 'Gmail附件'
|
||||
"""Gmail附件记录"""
|
||||
conversation = models.ForeignKey(GmailConversation, on_delete=models.CASCADE, related_name='attachments')
|
||||
email_message_id = models.CharField(max_length=100, help_text="Gmail邮件ID")
|
||||
attachment_id = models.CharField(max_length=100, help_text="Gmail附件ID")
|
||||
filename = models.CharField(max_length=255, help_text="原始文件名")
|
||||
file_path = models.CharField(max_length=255, help_text="保存在服务器上的路径")
|
||||
content_type = models.CharField(max_length=100, help_text="MIME类型")
|
||||
size = models.IntegerField(default=0, help_text="文件大小(字节)")
|
||||
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.filesize} bytes)"
|
||||
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:
|
||||
ordering = ['-created_at']
|
@ -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):
|
||||
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.utils import timezone
|
||||
import uuid
|
||||
@ -18,7 +18,7 @@ class Notification(models.Model):
|
||||
type = models.CharField(max_length=20, choices=NOTIFICATION_TYPES)
|
||||
title = models.CharField(max_length=100)
|
||||
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')
|
||||
is_read = models.BooleanField(default=False)
|
||||
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 apps.message.models import Notification
|
||||
from apps.notification.models import Notification
|
||||
from apps.accounts.models import User
|
||||
|
||||
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 rest_framework.routers import DefaultRouter
|
||||
from apps.message.views import NotificationViewSet
|
||||
from apps.notification.views import NotificationViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
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.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.decorators import action
|
||||
from apps.message.models import Notification
|
||||
from apps.message.serializers import NotificationSerializer
|
||||
from apps.notification.models import Notification
|
||||
from apps.notification.serializers import NotificationSerializer
|
||||
|
||||
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
|
||||
from apps.accounts.models import User
|
||||
from apps.knowledge_base.models import KnowledgeBase
|
||||
from apps.message.models import Notification
|
||||
from apps.notification.models import Notification
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -8,19 +8,27 @@ https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'daren_project.settings')
|
||||
|
||||
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||
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({
|
||||
"http": get_asgi_application(),
|
||||
"http": django_asgi_app,
|
||||
"websocket": AuthMiddlewareStack(
|
||||
URLRouter(
|
||||
websocket_urlpatterns
|
||||
chat_websocket_urlpatterns + notification_websocket_urlpatterns
|
||||
)
|
||||
),
|
||||
})
|
||||
|
@ -46,10 +46,12 @@ INSTALLED_APPS = [
|
||||
'apps.knowledge_base',
|
||||
'apps.chat',
|
||||
'apps.permissions',
|
||||
'apps.message',
|
||||
'apps.notification',
|
||||
'apps.gmail',
|
||||
'apps.feishu',
|
||||
'apps.common',
|
||||
'apps.brands',
|
||||
'apps.operation',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
@ -143,14 +145,20 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
# REST Framework 配置
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||
# 'rest_framework.authentication.SessionAuthentication',
|
||||
'rest_framework.authentication.TokenAuthentication',
|
||||
# 'rest_framework.authentication.SessionAuthentication',
|
||||
],
|
||||
'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)
|
||||
ASGI_APPLICATION = 'daren_project.asgi.application'
|
||||
CHANNEL_LAYERS = {
|
||||
@ -169,3 +177,19 @@ API_BASE_URL = 'http://81.69.223.133:48329'
|
||||
SILICON_CLOUD_API_KEY = 'sk-xqbujijjqqmlmlvkhvxeogqjtzslnhdtqxqgiyuhwpoqcjvf'
|
||||
GMAIL_WEBHOOK_URL = 'https://27b3-180-159-100-165.ngrok-free.app/api/user/gmail/webhook/'
|
||||
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/chat-history/', include('apps.chat.urls')),
|
||||
path('api/permissions/', include('apps.permissions.urls')),
|
||||
path('api/message/', include('apps.message.urls')),
|
||||
# path('api/gmail/', include('apps.gmail.urls')),
|
||||
path('api/notification/', include('apps.notification.urls')),
|
||||
path('api/gmail/', include('apps.gmail.urls')),
|
||||
# path('api/feishu/', include('apps.feishu.urls')),
|
||||
path('api/', include('apps.brands.urls')),
|
||||
path('api/operation/', include('apps.operation.urls')),
|
||||
]
|
@ -8,9 +8,26 @@ https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
import django
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
# 首先设置 Django 设置模块
|
||||
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