952 lines
40 KiB
Python
952 lines
40 KiB
Python
from itertools import count
|
||
from django.contrib.auth.models import AbstractUser
|
||
from django.db import models
|
||
from django.utils import timezone
|
||
from django.core.exceptions import ValidationError
|
||
import uuid
|
||
import logging
|
||
from django.contrib.auth import get_user_model
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
class User(AbstractUser):
|
||
"""自定义用户模型"""
|
||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||
|
||
ROLE_CHOICES = (
|
||
('admin', '管理员'),
|
||
('leader', '组长'),
|
||
('member', '组员'),
|
||
)
|
||
|
||
name = models.CharField(max_length=150, verbose_name='真实姓名', default='未设置')
|
||
role = models.CharField(max_length=20, choices=ROLE_CHOICES, verbose_name='角色', default='member')
|
||
department = models.CharField(max_length=100, verbose_name='部门', default='未分配')
|
||
group = models.CharField(max_length=100, null=True, blank=True, verbose_name='小组')
|
||
|
||
class Meta:
|
||
db_table = 'users'
|
||
verbose_name = '用户'
|
||
verbose_name_plural = '用户'
|
||
|
||
def __str__(self):
|
||
return f"{self.username}({self.name})"
|
||
|
||
def can_manage_department(self):
|
||
"""检查是否可以管理部门"""
|
||
return self.role in ['admin', 'leader']
|
||
|
||
def can_manage_knowledge_base(self, knowledge_base):
|
||
"""检查是否可以管理知识库"""
|
||
if self.role == 'admin':
|
||
return knowledge_base.type != 'private' # 管理员不能管理私人知识库
|
||
if self.role == 'leader' and self.department == knowledge_base.department:
|
||
return knowledge_base.type == 'member' # 组长只能管理本部门的成员知识库
|
||
return knowledge_base.user_id == str(self.id) # 用户可以管理自己创建的知识库
|
||
|
||
def has_access_permission(self, knowledge_base):
|
||
"""检查用户是否有权限访问知识库"""
|
||
|
||
# 1. 如果是私人知识库
|
||
if knowledge_base.type == 'private':
|
||
# 创建者直接允许
|
||
if str(knowledge_base.user_id) == str(self.id):
|
||
return True
|
||
# 其他人需要申请权限
|
||
return Permission.objects.filter(
|
||
resource_type='knowledge',
|
||
resource_id=str(knowledge_base.id),
|
||
applicant=self,
|
||
status='approved',
|
||
expires_at__gt=timezone.now()
|
||
).exists()
|
||
|
||
# 2. 如果是管理级知识库
|
||
if knowledge_base.type == 'admin':
|
||
# 管理员直接允许
|
||
if self.role == 'admin':
|
||
return True
|
||
# 其他人需要申请权限
|
||
return Permission.objects.filter(
|
||
resource_type='knowledge',
|
||
resource_id=str(knowledge_base.id),
|
||
applicant=self,
|
||
status='approved'
|
||
).exists()
|
||
|
||
# 3. 如果是部门级知识库
|
||
if knowledge_base.type == 'leader':
|
||
# 同部门的管理员和组长可以访问
|
||
if self.department == knowledge_base.department:
|
||
return self.role in ['admin', 'leader']
|
||
return False
|
||
|
||
# 4. 如果是成员级知识库
|
||
if knowledge_base.type == 'member':
|
||
# 同部门的所有人可以访问
|
||
return self.department == knowledge_base.department
|
||
|
||
class Data(models.Model):
|
||
"""统一的数据模型"""
|
||
DATA_TYPES = (
|
||
('admin', '管理员数据'),
|
||
('leader', '组长数据'),
|
||
('member', '组员数据'),
|
||
)
|
||
|
||
# 基本信息
|
||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||
name = models.CharField(max_length=100, help_text="数据名称")
|
||
desc = models.TextField(blank=True, help_text="数据描述")
|
||
type = models.CharField(max_length=10, choices=DATA_TYPES)
|
||
meta = models.JSONField(default=dict, blank=True, help_text="元数据")
|
||
|
||
# 关联信息
|
||
user_id = models.UUIDField(help_text="创建者ID")
|
||
department = models.CharField(max_length=50, blank=True)
|
||
|
||
# 统计信息
|
||
char_length = models.IntegerField(null=True, blank=True, help_text="字符长度")
|
||
document_count = models.IntegerField(null=True, blank=True, help_text="文档数量")
|
||
application_mapping_count = models.IntegerField(default=0, help_text="应用映射数量")
|
||
|
||
# 时间信息
|
||
create_time = models.DateTimeField(auto_now_add=True)
|
||
update_time = models.DateTimeField(auto_now=True)
|
||
|
||
class Meta:
|
||
db_table = 'user_data'
|
||
indexes = [
|
||
models.Index(fields=['type', 'user_id']),
|
||
models.Index(fields=['create_time']),
|
||
]
|
||
|
||
def __str__(self):
|
||
return f"{self.name} ({self.get_type_display()})"
|
||
|
||
def publish(self):
|
||
"""发布数据"""
|
||
if self.status == 'draft':
|
||
self.status = 'published'
|
||
self.published_at = timezone.now()
|
||
self.save()
|
||
logger.info(f"Data {self.id} published by {self.owner.username}")
|
||
|
||
def archive(self):
|
||
"""归档数据"""
|
||
if self.status == 'published':
|
||
self.status = 'archived'
|
||
self.archived_at = timezone.now()
|
||
self.save()
|
||
logger.info(f"Data {self.id} archived by {self.owner.username}")
|
||
|
||
class Notification(models.Model):
|
||
"""通知模型"""
|
||
NOTIFICATION_TYPES = (
|
||
('permission_request', '权限申请'),
|
||
('permission_approved', '权限批准'),
|
||
('permission_rejected', '权限拒绝'),
|
||
('permission_expired', '权限过期'),
|
||
('system_notice', '系统通知'),
|
||
)
|
||
|
||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||
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')
|
||
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
|
||
created_at = models.DateTimeField(auto_now_add=True)
|
||
|
||
class Meta:
|
||
ordering = ['-created_at']
|
||
indexes = [
|
||
models.Index(fields=['receiver', '-created_at']),
|
||
models.Index(fields=['type', 'is_read']),
|
||
]
|
||
|
||
def __str__(self):
|
||
return f"{self.get_type_display()} - {self.title}"
|
||
|
||
class Permission(models.Model):
|
||
"""权限申请模型"""
|
||
STATUS_CHOICES = (
|
||
('pending', '待审批'),
|
||
('approved', '已批准'),
|
||
('rejected', '已拒绝'),
|
||
)
|
||
|
||
knowledge_base = models.ForeignKey(
|
||
'KnowledgeBase',
|
||
on_delete=models.CASCADE,
|
||
related_name='permissions',
|
||
verbose_name='知识库'
|
||
)
|
||
applicant = models.ForeignKey(
|
||
'User',
|
||
on_delete=models.CASCADE,
|
||
related_name='permission_applications',
|
||
verbose_name='申请人'
|
||
)
|
||
approver = models.ForeignKey(
|
||
'User',
|
||
on_delete=models.SET_NULL,
|
||
null=True,
|
||
blank=True,
|
||
related_name='permission_approvals',
|
||
verbose_name='审批人'
|
||
)
|
||
|
||
permissions = models.JSONField(default=dict, verbose_name='权限配置') # {"can_read": true, "can_edit": false, "can_delete": false}
|
||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', verbose_name='状态')
|
||
reason = models.TextField(verbose_name='申请原因')
|
||
response_message = models.TextField(null=True, blank=True, verbose_name='审批意见')
|
||
expires_at = models.DateTimeField(null=True, blank=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 = '权限申请'
|
||
ordering = ['-created_at']
|
||
|
||
def __str__(self):
|
||
return f"{self.applicant} 申请 {self.knowledge_base} 的权限"
|
||
|
||
def clean(self):
|
||
"""验证权限申请的合法性"""
|
||
try:
|
||
# 检查是否是自己的知识库
|
||
if str(self.knowledge_base.user_id) == str(self.applicant.id):
|
||
raise ValidationError('不能申请访问自己的知识库')
|
||
except KnowledgeBase.DoesNotExist:
|
||
raise ValidationError('知识库不存在')
|
||
|
||
def save(self, *args, **kwargs):
|
||
self.clean()
|
||
super().save(*args, **kwargs)
|
||
|
||
def approve(self, approver, response_message=''):
|
||
"""批准权限申请"""
|
||
if self.status == 'pending':
|
||
self.status = 'approved'
|
||
self.approver = approver
|
||
self.response_message = response_message
|
||
self.save()
|
||
|
||
def reject(self, approver, response_message=''):
|
||
"""拒绝权限申请"""
|
||
if self.status == 'pending':
|
||
self.status = 'rejected'
|
||
self.approver = approver
|
||
self.response_message = response_message
|
||
self.save()
|
||
|
||
def check_expiration(self):
|
||
"""检查权限是否过期"""
|
||
if self.status == 'approved' and self.expires_at and self.expires_at < timezone.now():
|
||
self.status = 'expired'
|
||
self.save()
|
||
logger.info(f"Permission {self.id} expired")
|
||
|
||
def send_notification(self, notification_type, title, content):
|
||
"""发送通知"""
|
||
Notification.objects.create(
|
||
type=notification_type,
|
||
title=title,
|
||
content=content,
|
||
sender=self.applicant,
|
||
receiver=self.approver if notification_type == 'permission_request' else self.applicant,
|
||
related_resource=str(self.id)
|
||
)
|
||
|
||
def notify_approver(self):
|
||
"""通知审批人"""
|
||
self.send_notification(
|
||
'permission_request',
|
||
f'新的权限申请 - {self.get_resource_type_display()}',
|
||
f'用户 {self.applicant.username} 申请访问 {self.get_resource_type_display()} 的权限'
|
||
)
|
||
|
||
def notify_applicant(self, status):
|
||
"""通知申请人审批结果"""
|
||
notification_type = 'permission_approved' if status == 'approved' else 'permission_rejected'
|
||
title = f'权限申请{self.get_status_display()} - {self.get_resource_type_display()}'
|
||
content = f'您申请的 {self.get_resource_type_display()} 权限已{self.get_status_display()}'
|
||
self.send_notification(notification_type, title, content)
|
||
|
||
class ChatHistory(models.Model):
|
||
"""聊天历史记录"""
|
||
ROLE_CHOICES = [
|
||
('user', '用户'),
|
||
('assistant', 'AI助手'),
|
||
('system', '系统')
|
||
]
|
||
|
||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||
# 保留与主知识库的关联
|
||
knowledge_base = models.ForeignKey('KnowledgeBase', on_delete=models.CASCADE)
|
||
# 用于标识知识库组合的对话
|
||
conversation_id = models.CharField(max_length=100, db_index=True)
|
||
# 对话标题
|
||
title = models.CharField(max_length=100, null=True, blank=True, default='New chat', help_text="对话标题")
|
||
parent_id = models.CharField(max_length=100, null=True, blank=True)
|
||
role = models.CharField(max_length=20, choices=ROLE_CHOICES)
|
||
content = models.TextField()
|
||
tokens = models.IntegerField(default=0, help_text="消息token数")
|
||
# 扩展metadata字段,用于存储知识库组合信息
|
||
metadata = models.JSONField(default=dict, blank=True, help_text="""
|
||
{
|
||
'model_id': 'xxx',
|
||
'dataset_id_list': ['id1', 'id2', ...],
|
||
'dataset_external_id_list': ['ext1', 'ext2', ...],
|
||
'primary_knowledge_base': 'id1'
|
||
}
|
||
""")
|
||
created_at = models.DateTimeField(auto_now_add=True)
|
||
is_deleted = models.BooleanField(default=False)
|
||
|
||
class Meta:
|
||
db_table = 'chat_history'
|
||
ordering = ['created_at']
|
||
indexes = [
|
||
models.Index(fields=['conversation_id', 'created_at']),
|
||
models.Index(fields=['user', 'created_at']),
|
||
# 添加新的索引以支持知识库组合查询
|
||
models.Index(fields=['conversation_id', 'is_deleted']),
|
||
]
|
||
|
||
def __str__(self):
|
||
return f"{self.user.username} - {self.knowledge_base.name} - {self.created_at}"
|
||
|
||
@classmethod
|
||
def get_conversation(cls, conversation_id):
|
||
"""获取完整对话历史"""
|
||
return cls.objects.filter(
|
||
conversation_id=conversation_id,
|
||
is_deleted=False
|
||
).order_by('created_at')
|
||
|
||
@classmethod
|
||
def get_conversations_by_knowledge_bases(cls, dataset_ids, user):
|
||
"""根据知识库组合获取对话历史"""
|
||
# 对知识库ID列表排序以确保一致性
|
||
sorted_kb_ids = sorted(dataset_ids)
|
||
conversation_id = str(uuid.uuid5(
|
||
uuid.NAMESPACE_DNS,
|
||
'-'.join(sorted_kb_ids)
|
||
))
|
||
|
||
return cls.objects.filter(
|
||
conversation_id=conversation_id,
|
||
user=user,
|
||
is_deleted=False
|
||
).order_by('created_at')
|
||
|
||
@classmethod
|
||
def get_knowledge_base_combinations(cls, user):
|
||
"""获取用户的所有知识库组合"""
|
||
return cls.objects.filter(
|
||
user=user,
|
||
is_deleted=False
|
||
).values('conversation_id').annotate(
|
||
last_message=max('created_at'),
|
||
message_count=count('id')
|
||
).values(
|
||
'conversation_id',
|
||
'last_message',
|
||
'message_count',
|
||
'metadata'
|
||
).order_by('-last_message')
|
||
|
||
def get_knowledge_bases(self):
|
||
"""获取此消息关联的所有知识库"""
|
||
if self.metadata and 'dataset_id_list' in self.metadata:
|
||
return KnowledgeBase.objects.filter(
|
||
id__in=self.metadata['dataset_id_list']
|
||
)
|
||
return KnowledgeBase.objects.filter(id=self.knowledge_base.id)
|
||
|
||
def soft_delete(self):
|
||
"""软删除消息"""
|
||
self.is_deleted = True
|
||
self.save()
|
||
|
||
def to_dict(self):
|
||
"""转换为字典格式"""
|
||
return {
|
||
'id': str(self.id),
|
||
'conversation_id': self.conversation_id,
|
||
'role': self.role,
|
||
'content': self.content,
|
||
'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S'),
|
||
'metadata': self.metadata,
|
||
'knowledge_bases': [
|
||
{
|
||
'id': str(kb.id),
|
||
'name': kb.name,
|
||
'type': kb.type
|
||
} for kb in self.get_knowledge_bases()
|
||
]
|
||
}
|
||
|
||
class UserProfile(models.Model):
|
||
"""用户档案模型"""
|
||
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
|
||
department = models.CharField(max_length=100, blank=True, help_text="部门")
|
||
group = models.CharField(max_length=100, blank=True, help_text="小组")
|
||
auto_recommend_reply = models.BooleanField(default=False, help_text="是否启用自动推荐回复功能")
|
||
|
||
class Meta:
|
||
db_table = 'user_profiles'
|
||
|
||
def __str__(self):
|
||
return f"{self.user.username}的个人资料"
|
||
|
||
class KnowledgeBasePermission(models.Model):
|
||
"""知识库权限模型 - 实现知识库和用户的多对多关系"""
|
||
STATUS_CHOICES = (
|
||
('active', '生效中'),
|
||
('expired', '已过期'),
|
||
('revoked', '已撤销'),
|
||
)
|
||
|
||
knowledge_base = models.ForeignKey(
|
||
'KnowledgeBase',
|
||
on_delete=models.CASCADE,
|
||
related_name='user_permissions',
|
||
verbose_name='知识库'
|
||
)
|
||
user = models.ForeignKey(
|
||
'User',
|
||
on_delete=models.CASCADE,
|
||
related_name='knowledge_base_permissions',
|
||
verbose_name='用户'
|
||
)
|
||
|
||
# 基础权限
|
||
can_read = models.BooleanField(default=False, verbose_name='查看权限')
|
||
can_edit = models.BooleanField(default=False, verbose_name='修改权限')
|
||
can_delete = models.BooleanField(default=False, verbose_name='删除权限')
|
||
|
||
# 权限状态
|
||
status = models.CharField(
|
||
max_length=10,
|
||
choices=STATUS_CHOICES,
|
||
default='active',
|
||
verbose_name='状态'
|
||
)
|
||
|
||
# 授权信息
|
||
granted_by = models.ForeignKey(
|
||
'User',
|
||
on_delete=models.SET_NULL,
|
||
null=True,
|
||
related_name='granted_permissions',
|
||
verbose_name='授权人'
|
||
)
|
||
granted_at = models.DateTimeField(auto_now_add=True, verbose_name='授权时间')
|
||
expires_at = models.DateTimeField(null=True, blank=True, verbose_name='过期时间')
|
||
|
||
class Meta:
|
||
app_label = 'user_management'
|
||
db_table = 'knowledge_base_permissions'
|
||
unique_together = ['knowledge_base', 'user']
|
||
indexes = [
|
||
models.Index(fields=['knowledge_base', 'user', 'status']),
|
||
]
|
||
verbose_name = '知识库权限'
|
||
verbose_name_plural = '知识库权限'
|
||
|
||
def __str__(self):
|
||
return f"{self.user.username} - {self.knowledge_base.name}"
|
||
|
||
def is_valid(self):
|
||
"""检查权限是否有效"""
|
||
if self.status != 'active':
|
||
return False
|
||
if self.expires_at and self.expires_at < timezone.now():
|
||
self.status = 'expired'
|
||
self.save()
|
||
return False
|
||
return True
|
||
|
||
class KnowledgeBase(models.Model):
|
||
"""知识库模型"""
|
||
KNOWLEDGE_BASE_TYPES = [
|
||
('admin', '管理级知识库'),
|
||
('leader', '部门级知识库'),
|
||
('member', '成员级知识库'),
|
||
('private', '私有知识库'),
|
||
('secret', '公司级别私密知识库'),
|
||
]
|
||
|
||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||
user_id = models.UUIDField(verbose_name='创建者ID')
|
||
name = models.CharField(max_length=100, unique=True, verbose_name='知识库名称')
|
||
desc = models.TextField(verbose_name='知识库描述', null=True, blank=True)
|
||
type = models.CharField(
|
||
max_length=20,
|
||
choices=KNOWLEDGE_BASE_TYPES,
|
||
default='private',
|
||
verbose_name='知识库类型'
|
||
)
|
||
department = models.CharField(max_length=50, null=True, blank=True)
|
||
group = models.CharField(max_length=50, null=True, blank=True)
|
||
documents = models.JSONField(default=list)
|
||
char_length = models.IntegerField(default=0)
|
||
document_count = models.IntegerField(default=0)
|
||
external_id = models.UUIDField(null=True, blank=True)
|
||
create_time = models.DateTimeField(auto_now_add=True)
|
||
update_time = models.DateTimeField(auto_now=True)
|
||
|
||
def is_owner(self, user):
|
||
"""检查用户是否是所有者(通过权限表检查)"""
|
||
return str(user.id) == str(self.user_id) or KnowledgeBasePermission.objects.filter(
|
||
knowledge_base=self,
|
||
user=user,
|
||
can_read=True,
|
||
can_edit=True,
|
||
can_delete=True,
|
||
status='active'
|
||
).exists()
|
||
|
||
def get_owners(self):
|
||
"""获取所有所有者(包括创建者和具有完整权限的用户)"""
|
||
from .models import User
|
||
# 获取创建者
|
||
owners = [self.user_id]
|
||
# 获取具有完整权限的用户
|
||
permission_owners = KnowledgeBasePermission.objects.filter(
|
||
knowledge_base=self,
|
||
can_read=True,
|
||
can_edit=True,
|
||
can_delete=True,
|
||
status='active'
|
||
).values_list('user_id', flat=True)
|
||
owners.extend(permission_owners)
|
||
return User.objects.filter(id__in=set(owners))
|
||
|
||
def calculate_stats(self):
|
||
"""计算文档统计信息"""
|
||
total_chars = 0
|
||
doc_count = 0
|
||
|
||
if self.documents:
|
||
doc_count = len(self.documents)
|
||
for doc in self.documents:
|
||
if 'paragraphs' in doc:
|
||
for para in doc['paragraphs']:
|
||
if 'content' in para:
|
||
total_chars += len(para['content'])
|
||
if 'title' in para:
|
||
total_chars += len(para['title'])
|
||
|
||
return doc_count, total_chars
|
||
|
||
def save(self, *args, **kwargs):
|
||
"""重写保存方法"""
|
||
# 只在创建时计算统计信息
|
||
if not self.pk: # 如果是新实例
|
||
doc_count, char_count = self.calculate_stats()
|
||
self.document_count = doc_count
|
||
self.char_length = char_count
|
||
|
||
# 直接调用Model的save方法
|
||
models.Model.save(self, *args, **kwargs)
|
||
|
||
@classmethod
|
||
def update_external_id(cls, instance_id, external_id):
|
||
"""更新外部ID的静态方法"""
|
||
cls.objects.filter(id=instance_id).update(external_id=external_id)
|
||
|
||
class Meta:
|
||
db_table = 'knowledge_bases'
|
||
indexes = [
|
||
models.Index(fields=['type']),
|
||
models.Index(fields=['department']),
|
||
models.Index(fields=['group'])
|
||
]
|
||
|
||
def __str__(self):
|
||
return f"{self.name} ({self.get_type_display()})"
|
||
|
||
def clean(self):
|
||
"""验证知识库类型与创建者权限是否匹配"""
|
||
try:
|
||
user = User.objects.get(id=self.user_id)
|
||
if user.role == 'member' and self.type != 'private':
|
||
raise ValidationError('组员只能创建私人知识库')
|
||
if user.role == 'leader' and self.type not in ['member', 'private']:
|
||
raise ValidationError('组长只能创建成员级或私人知识库')
|
||
except User.DoesNotExist:
|
||
raise ValidationError('创建者不存在')
|
||
|
||
def to_response_dict(self):
|
||
"""转换为API响应格式"""
|
||
return {
|
||
"create_time": self.create_time.isoformat(),
|
||
"update_time": self.update_time.isoformat(),
|
||
"id": str(self.id),
|
||
"name": self.name,
|
||
"desc": self.desc,
|
||
"type": self.type,
|
||
"meta": self.meta,
|
||
"user_id": str(self.user_id),
|
||
"embedding_mode_id": str(self.embedding_mode_id),
|
||
"char_length": self.char_length,
|
||
"application_mapping_count": self.application_mapping_count,
|
||
"document_count": self.document_count
|
||
}
|
||
|
||
def to_external_format(self):
|
||
"""转换为外部接口格式"""
|
||
return {
|
||
"name": self.name,
|
||
"desc": self.desc,
|
||
"documents": self.documents
|
||
}
|
||
|
||
@classmethod
|
||
def from_external_format(cls, data, user_id, embedding_mode_id):
|
||
"""从外部接口格式创建实例"""
|
||
return cls(
|
||
name=data["name"],
|
||
desc=data["desc"],
|
||
documents=data["documents"],
|
||
user_id=user_id,
|
||
embedding_mode_id=embedding_mode_id,
|
||
document_count=len(data["documents"]) if data.get("documents") else 0
|
||
)
|
||
|
||
class FeishuCreator(models.Model):
|
||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||
record_id = models.CharField(max_length=100, unique=True, verbose_name='飞书记录ID')
|
||
|
||
# 对接人信息
|
||
contact_person = models.CharField(max_length=50, blank=True, verbose_name='对接人')
|
||
|
||
# 基本账号信息
|
||
handle = models.TextField(blank=True, verbose_name='Handle')
|
||
tiktok_url = models.TextField(blank=True, verbose_name='链接')
|
||
fans_count = models.CharField(max_length=50, blank=True, verbose_name='粉丝数')
|
||
gmv = models.CharField(max_length=100, blank=True, verbose_name='GMV')
|
||
|
||
# 联系方式
|
||
email = models.EmailField(blank=True, verbose_name='邮箱')
|
||
phone = models.CharField(max_length=50, blank=True, verbose_name='手机号|WhatsApp')
|
||
|
||
# 账号属性和报价
|
||
account_type = models.CharField(max_length=50, blank=True, verbose_name='账号属性')
|
||
price_quote = models.TextField(blank=True, verbose_name='报价')
|
||
response_speed = models.CharField(max_length=50, blank=True, verbose_name='回复速度')
|
||
|
||
# 合作相关
|
||
cooperation_intention = models.CharField(max_length=50, blank=True, verbose_name='合作意向')
|
||
payment_method = models.CharField(max_length=50, blank=True, verbose_name='支付方式')
|
||
payment_account = models.CharField(max_length=100, blank=True, verbose_name='收款账号')
|
||
address = models.TextField(blank=True, verbose_name='收件地址')
|
||
has_ooin = models.CharField(max_length=10, blank=True, verbose_name='签约OOIN?')
|
||
|
||
# 渠道和进度
|
||
source = models.CharField(max_length=100, blank=True, verbose_name='渠道来源')
|
||
contact_status = models.CharField(max_length=50, blank=True, verbose_name='建联进度')
|
||
cooperation_brands = models.JSONField(default=list, blank=True, verbose_name='合作品牌')
|
||
|
||
# 品类信息
|
||
system_categories = models.CharField(max_length=100, blank=True, verbose_name='系统展示的带货品类')
|
||
actual_categories = models.CharField(max_length=100, blank=True, verbose_name='实际高播放量带货品类')
|
||
human_categories = models.CharField(max_length=100, blank=True, verbose_name='达人标想要货品类')
|
||
|
||
# 其他信息
|
||
creator_base = models.CharField(max_length=100, blank=True, verbose_name='达人base')
|
||
notes = models.TextField(blank=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 = 'feishu_creators'
|
||
verbose_name = '创作者数据'
|
||
verbose_name_plural = '创作者数据'
|
||
|
||
class KnowledgeBaseDocument(models.Model):
|
||
"""知识库文档关联模型"""
|
||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||
knowledge_base = models.ForeignKey(
|
||
'KnowledgeBase',
|
||
on_delete=models.CASCADE,
|
||
related_name='kb_documents',
|
||
verbose_name='知识库'
|
||
)
|
||
document_id = models.CharField(max_length=100, verbose_name='文档ID')
|
||
document_name = models.CharField(max_length=255, verbose_name='文档名称')
|
||
external_id = models.CharField(max_length=100, verbose_name='外部文档ID')
|
||
uploader_name = models.CharField(max_length=100, default="未知用户", verbose_name='上传者姓名')
|
||
status = models.CharField(
|
||
max_length=20,
|
||
default='active',
|
||
choices=[
|
||
('active', '有效'),
|
||
('deleted', '已删除')
|
||
],
|
||
verbose_name='状态'
|
||
)
|
||
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||
update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||
|
||
class Meta:
|
||
db_table = 'knowledge_base_documents'
|
||
unique_together = ['knowledge_base', 'document_id']
|
||
indexes = [
|
||
models.Index(fields=['knowledge_base', 'status']),
|
||
models.Index(fields=['document_id']),
|
||
models.Index(fields=['external_id'])
|
||
]
|
||
verbose_name = '知识库文档'
|
||
verbose_name_plural = '知识库文档'
|
||
|
||
def __str__(self):
|
||
return f"{self.knowledge_base.name} - {self.document_name}"
|
||
|
||
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})"
|
||
|
||
class Meta:
|
||
verbose_name = 'Gmail凭证'
|
||
verbose_name_plural = 'Gmail凭证'
|
||
unique_together = ('user', 'gmail_email')
|
||
ordering = ['-is_default', '-updated_at']
|
||
|
||
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='更新时间')
|
||
|
||
class Meta:
|
||
db_table = 'gmail_talent_mappings'
|
||
unique_together = ['user', 'talent_email']
|
||
verbose_name = 'Gmail达人映射'
|
||
verbose_name_plural = 'Gmail达人映射'
|
||
|
||
def __str__(self):
|
||
return f"{self.user.username} - {self.talent_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附件'
|
||
|
||
def __str__(self):
|
||
return f"{self.filename} ({self.filesize} bytes)"
|
||
|
||
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]}..."
|
||
|
||
class ConversationSummary(models.Model):
|
||
"""对话总结模型"""
|
||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='conversation_summaries')
|
||
talent_email = models.EmailField(verbose_name='达人邮箱')
|
||
conversation_id = models.CharField(max_length=100, verbose_name='对话ID')
|
||
summary = 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 = 'conversation_summaries'
|
||
verbose_name = '对话总结'
|
||
verbose_name_plural = '对话总结'
|
||
|
||
def __str__(self):
|
||
return f"{self.user.username}与{self.talent_email}的对话总结"
|
||
|
||
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(blank=True, null=True, 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='账号头像')
|
||
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)
|
||
|
||
class GmailNotificationQueue(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_notification_queue')
|
||
gmail_credential = models.ForeignKey(GmailCredential, on_delete=models.CASCADE, related_name='notification_queue')
|
||
email = models.EmailField(verbose_name='邮箱', max_length=255)
|
||
history_id = models.CharField(verbose_name='历史ID', max_length=100)
|
||
notification_data = models.TextField(verbose_name='通知数据', blank=True, null=True)
|
||
processed = models.BooleanField(verbose_name='是否已处理', default=False)
|
||
success = models.BooleanField(verbose_name='处理是否成功', default=False)
|
||
error_message = models.CharField(verbose_name='错误信息', max_length=255, blank=True, null=True)
|
||
created_at = models.DateTimeField(verbose_name='创建时间', auto_now_add=True)
|
||
processed_at = models.DateTimeField(verbose_name='处理时间', blank=True, null=True)
|
||
|
||
def __str__(self):
|
||
return f"通知 {self.id} - {self.email} - {self.created_at}"
|
||
|
||
class Meta:
|
||
verbose_name = 'Gmail通知队列'
|
||
verbose_name_plural = 'Gmail通知队列'
|
||
ordering = ['processed', 'created_at'] |