群发demo1
This commit is contained in:
parent
5eab14304c
commit
7898219d2c
@ -27,7 +27,6 @@ class Brand(models.Model):
|
|||||||
is_active = models.BooleanField(default=True, verbose_name='是否激活')
|
is_active = models.BooleanField(default=True, verbose_name='是否激活')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = 'brands'
|
|
||||||
verbose_name = '品牌'
|
verbose_name = '品牌'
|
||||||
verbose_name_plural = '品牌'
|
verbose_name_plural = '品牌'
|
||||||
|
|
||||||
@ -66,7 +65,6 @@ class Product(models.Model):
|
|||||||
is_active = models.BooleanField(default=True, verbose_name='是否激活')
|
is_active = models.BooleanField(default=True, verbose_name='是否激活')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = 'products'
|
|
||||||
verbose_name = '产品'
|
verbose_name = '产品'
|
||||||
verbose_name_plural = '产品'
|
verbose_name_plural = '产品'
|
||||||
unique_together = ['brand', 'name']
|
unique_together = ['brand', 'name']
|
||||||
@ -144,7 +142,6 @@ class Campaign(models.Model):
|
|||||||
is_active = models.BooleanField(default=True, verbose_name='是否激活')
|
is_active = models.BooleanField(default=True, verbose_name='是否激活')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = 'campaigns'
|
|
||||||
verbose_name = '活动'
|
verbose_name = '活动'
|
||||||
verbose_name_plural = '活动'
|
verbose_name_plural = '活动'
|
||||||
unique_together = ['brand', 'name']
|
unique_together = ['brand', 'name']
|
||||||
@ -186,7 +183,6 @@ class BrandChatSession(models.Model):
|
|||||||
is_active = models.BooleanField(default=True, verbose_name='是否激活')
|
is_active = models.BooleanField(default=True, verbose_name='是否激活')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = 'brand_chat_sessions'
|
|
||||||
verbose_name = '品牌聊天会话'
|
verbose_name = '品牌聊天会话'
|
||||||
verbose_name_plural = '品牌聊天会话'
|
verbose_name_plural = '品牌聊天会话'
|
||||||
indexes = [
|
indexes = [
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# Generated by Django 5.2.1 on 2025-05-28 09:51
|
# Generated by Django 5.2 on 2025-05-07 03:40
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
@ -15,6 +16,24 @@ class Migration(migrations.Migration):
|
|||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ConversationSummary',
|
||||||
|
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')),
|
||||||
|
('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='更新时间')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='conversation_summaries', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '对话总结',
|
||||||
|
'verbose_name_plural': '对话总结',
|
||||||
|
'db_table': 'conversation_summaries',
|
||||||
|
},
|
||||||
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='ChatHistory',
|
name='ChatHistory',
|
||||||
fields=[
|
fields=[
|
||||||
@ -32,8 +51,9 @@ class Migration(migrations.Migration):
|
|||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
|
'db_table': 'chat_history',
|
||||||
'ordering': ['created_at'],
|
'ordering': ['created_at'],
|
||||||
'indexes': [models.Index(fields=['conversation_id', 'created_at'], name='chat_chathi_convers_90ca0c_idx'), models.Index(fields=['user', 'created_at'], name='chat_chathi_user_id_326d36_idx'), models.Index(fields=['conversation_id', 'is_deleted'], name='chat_chathi_convers_bcd094_idx')],
|
'indexes': [models.Index(fields=['conversation_id', 'created_at'], name='chat_histor_convers_33721a_idx'), models.Index(fields=['user', 'created_at'], name='chat_histor_user_id_aa050a_idx'), models.Index(fields=['conversation_id', 'is_deleted'], name='chat_histor_convers_89bc43_idx')],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
18
apps/chat/migrations/0002_alter_chathistory_content.py
Normal file
18
apps/chat/migrations/0002_alter_chathistory_content.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-05-15 10:46
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('chat', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='chathistory',
|
||||||
|
name='content',
|
||||||
|
field=models.TextField(help_text='消息内容,支持存储长文本', max_length=65535),
|
||||||
|
),
|
||||||
|
]
|
18
apps/chat/migrations/0003_alter_chathistory_content.py
Normal file
18
apps/chat/migrations/0003_alter_chathistory_content.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-05-20 09:21
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('chat', '0002_alter_chathistory_content'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='chathistory',
|
||||||
|
name='content',
|
||||||
|
field=models.TextField(),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,39 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-05-21 04:30
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('chat', '0003_alter_chathistory_content'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='conversationsummary',
|
||||||
|
name='user',
|
||||||
|
),
|
||||||
|
migrations.RenameIndex(
|
||||||
|
model_name='chathistory',
|
||||||
|
new_name='chat_chathi_convers_90ca0c_idx',
|
||||||
|
old_name='chat_histor_convers_33721a_idx',
|
||||||
|
),
|
||||||
|
migrations.RenameIndex(
|
||||||
|
model_name='chathistory',
|
||||||
|
new_name='chat_chathi_user_id_326d36_idx',
|
||||||
|
old_name='chat_histor_user_id_aa050a_idx',
|
||||||
|
),
|
||||||
|
migrations.RenameIndex(
|
||||||
|
model_name='chathistory',
|
||||||
|
new_name='chat_chathi_convers_bcd094_idx',
|
||||||
|
old_name='chat_histor_convers_89bc43_idx',
|
||||||
|
),
|
||||||
|
migrations.AlterModelTable(
|
||||||
|
name='chathistory',
|
||||||
|
table=None,
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='ConversationSummary',
|
||||||
|
),
|
||||||
|
]
|
@ -14,15 +14,16 @@ from apps.knowledge_base.models import KnowledgeBase
|
|||||||
from apps.chat.models import ChatHistory
|
from apps.chat.models import ChatHistory
|
||||||
from apps.chat.serializers import ChatHistorySerializer
|
from apps.chat.serializers import ChatHistorySerializer
|
||||||
from apps.common.services.chat_service import ChatService
|
from apps.common.services.chat_service import ChatService
|
||||||
from apps.user.authentication import CustomTokenAuthentication
|
|
||||||
from apps.chat.services.chat_api import (
|
from apps.chat.services.chat_api import (
|
||||||
ExternalAPIError, stream_chat_answer, get_chat_answer, generate_conversation_title,
|
ExternalAPIError, stream_chat_answer, get_chat_answer, generate_conversation_title,
|
||||||
get_hit_test_documents, generate_conversation_title_from_deepseek
|
get_hit_test_documents, generate_conversation_title_from_deepseek
|
||||||
)
|
)
|
||||||
|
from apps.user.authentication import CustomTokenAuthentication
|
||||||
|
# from apps.permissions.services.permission_service import KnowledgeBasePermissionMixin
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# class ChatHistoryViewSet(KnowledgeBasePermissionMixin, viewsets.ModelViewSet):
|
||||||
class ChatHistoryViewSet(viewsets.ModelViewSet):
|
class ChatHistoryViewSet(viewsets.ModelViewSet):
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
authentication_classes = [CustomTokenAuthentication]
|
authentication_classes = [CustomTokenAuthentication]
|
||||||
@ -463,83 +464,83 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
|
|||||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
|
||||||
@action(detail=False, methods=['post'])
|
# @action(detail=False, methods=['post'])
|
||||||
def hit_test(self, request):
|
# def hit_test(self, request):
|
||||||
"""获取问题与知识库文档的匹配度"""
|
# """获取问题与知识库文档的匹配度"""
|
||||||
try:
|
# try:
|
||||||
data = request.data
|
# data = request.data
|
||||||
if 'question' not in data or 'dataset_id_list' not in data or not data['dataset_id_list']:
|
# if 'question' not in data or 'dataset_id_list' not in data or not data['dataset_id_list']:
|
||||||
return Response({
|
# return Response({
|
||||||
'code': 400,
|
# 'code': 400,
|
||||||
'message': '缺少必填字段: question 或 dataset_id_list',
|
# 'message': '缺少必填字段: question 或 dataset_id_list',
|
||||||
'data': None
|
# 'data': None
|
||||||
}, status=status.HTTP_400_BAD_REQUEST)
|
# }, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
question = data['question']
|
# question = data['question']
|
||||||
dataset_ids = data['dataset_id_list']
|
# dataset_ids = data['dataset_id_list']
|
||||||
if not isinstance(dataset_ids, list):
|
# if not isinstance(dataset_ids, list):
|
||||||
try:
|
# try:
|
||||||
dataset_ids = json.loads(dataset_ids)
|
# dataset_ids = json.loads(dataset_ids)
|
||||||
if not isinstance(dataset_ids, list):
|
# if not isinstance(dataset_ids, list):
|
||||||
dataset_ids = [dataset_ids]
|
# dataset_ids = [dataset_ids]
|
||||||
except (json.JSONDecodeError, TypeError):
|
# except (json.JSONDecodeError, TypeError):
|
||||||
dataset_ids = [dataset_ids]
|
# dataset_ids = [dataset_ids]
|
||||||
|
|
||||||
external_id_list = []
|
# external_id_list = []
|
||||||
for kb_id in dataset_ids:
|
# for kb_id in dataset_ids:
|
||||||
kb = KnowledgeBase.objects.filter(id=kb_id).first()
|
# kb = KnowledgeBase.objects.filter(id=kb_id).first()
|
||||||
if not kb:
|
# if not kb:
|
||||||
return Response({
|
# return Response({
|
||||||
'code': 404,
|
# 'code': 404,
|
||||||
'message': f'知识库不存在: {kb_id}',
|
# 'message': f'知识库不存在: {kb_id}',
|
||||||
'data': None
|
# 'data': None
|
||||||
}, status=status.HTTP_404_NOT_FOUND)
|
# }, status=status.HTTP_404_NOT_FOUND)
|
||||||
if not self.check_knowledge_base_permission(kb, request.user, 'read'):
|
# if not self.check_knowledge_base_permission(kb, request.user, 'read'):
|
||||||
return Response({
|
# return Response({
|
||||||
'code': 403,
|
# 'code': 403,
|
||||||
'message': f'无权访问知识库: {kb.name}',
|
# 'message': f'无权访问知识库: {kb.name}',
|
||||||
'data': None
|
# 'data': None
|
||||||
}, status=status.HTTP_403_FORBIDDEN)
|
# }, status=status.HTTP_403_FORBIDDEN)
|
||||||
if kb.external_id:
|
# if kb.external_id:
|
||||||
external_id_list.append(str(kb.external_id))
|
# external_id_list.append(str(kb.external_id))
|
||||||
|
|
||||||
if not external_id_list:
|
# if not external_id_list:
|
||||||
return Response({
|
# return Response({
|
||||||
'code': 400,
|
# 'code': 400,
|
||||||
'message': '没有有效的知识库external_id',
|
# 'message': '没有有效的知识库external_id',
|
||||||
'data': None
|
# 'data': None
|
||||||
}, status=status.HTTP_400_BAD_REQUEST)
|
# }, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
all_documents = []
|
# all_documents = []
|
||||||
for dataset_id in external_id_list:
|
# for dataset_id in external_id_list:
|
||||||
try:
|
# try:
|
||||||
doc_info = get_hit_test_documents(dataset_id, question)
|
# doc_info = get_hit_test_documents(dataset_id, question)
|
||||||
if doc_info:
|
# if doc_info:
|
||||||
all_documents.extend(doc_info)
|
# all_documents.extend(doc_info)
|
||||||
except ExternalAPIError as e:
|
# except ExternalAPIError as e:
|
||||||
logger.error(f"调用hit_test失败: 知识库ID={dataset_id}, 错误={str(e)}")
|
# logger.error(f"调用hit_test失败: 知识库ID={dataset_id}, 错误={str(e)}")
|
||||||
continue # 宽松处理,跳过失败的知识库
|
# continue # 宽松处理,跳过失败的知识库
|
||||||
|
|
||||||
all_documents = sorted(all_documents, key=lambda x: x.get('similarity', 0), reverse=True)
|
# all_documents = sorted(all_documents, key=lambda x: x.get('similarity', 0), reverse=True)
|
||||||
return Response({
|
# return Response({
|
||||||
'code': 200,
|
# 'code': 200,
|
||||||
'message': '成功',
|
# 'message': '成功',
|
||||||
'data': {
|
# 'data': {
|
||||||
'question': question,
|
# 'question': question,
|
||||||
'matched_documents': all_documents,
|
# 'matched_documents': all_documents,
|
||||||
'total_count': len(all_documents)
|
# 'total_count': len(all_documents)
|
||||||
}
|
# }
|
||||||
})
|
# })
|
||||||
|
|
||||||
except Exception as e:
|
# except Exception as e:
|
||||||
logger.error(f"hit_test接口调用失败: {str(e)}")
|
# logger.error(f"hit_test接口调用失败: {str(e)}")
|
||||||
import traceback
|
# import traceback
|
||||||
logger.error(traceback.format_exc())
|
# logger.error(traceback.format_exc())
|
||||||
return Response({
|
# return Response({
|
||||||
'code': 500,
|
# 'code': 500,
|
||||||
'message': f'hit_test接口调用失败: {str(e)}',
|
# 'message': f'hit_test接口调用失败: {str(e)}',
|
||||||
'data': None
|
# 'data': None
|
||||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
# }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
def _highlight_keyword(self, text, keyword):
|
def _highlight_keyword(self, text, keyword):
|
||||||
"""高亮关键词"""
|
"""高亮关键词"""
|
||||||
|
@ -6,11 +6,12 @@ from django.db import transaction
|
|||||||
from apps.user.models import User
|
from apps.user.models import User
|
||||||
from apps.knowledge_base.models import KnowledgeBase
|
from apps.knowledge_base.models import KnowledgeBase
|
||||||
from apps.chat.models import ChatHistory
|
from apps.chat.models import ChatHistory
|
||||||
|
# from apps.permissions.services.permission_service import KnowledgeBasePermissionMixin
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# class ChatService(KnowledgeBasePermissionMixin):
|
||||||
class ChatService():
|
class ChatService():
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create_chat_record(self, user, data, conversation_id=None):
|
def create_chat_record(self, user, data, conversation_id=None):
|
||||||
|
@ -0,0 +1,51 @@
|
|||||||
|
# # apps/common/services/notification_service.py
|
||||||
|
# import logging
|
||||||
|
# from asgiref.sync import async_to_sync
|
||||||
|
# from channels.layers import get_channel_layer
|
||||||
|
# from apps.notification.models import Notification
|
||||||
|
|
||||||
|
# logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# class NotificationService:
|
||||||
|
# def send_notification(self, user, title, content, notification_type, related_object_id, sender=None):
|
||||||
|
# """发送通知并通过WebSocket推送"""
|
||||||
|
# try:
|
||||||
|
# notification = Notification.objects.create(
|
||||||
|
# sender=sender,
|
||||||
|
# receiver=user,
|
||||||
|
# title=title,
|
||||||
|
# content=content,
|
||||||
|
# type=notification_type,
|
||||||
|
# related_resource=related_object_id,
|
||||||
|
# )
|
||||||
|
|
||||||
|
# # 准备发送到WebSocket的数据
|
||||||
|
# notification_data = {
|
||||||
|
# "id": str(notification.id),
|
||||||
|
# "title": notification.title,
|
||||||
|
# "content": notification.content,
|
||||||
|
# "type": notification.type,
|
||||||
|
# "created_at": notification.created_at.isoformat(),
|
||||||
|
# }
|
||||||
|
|
||||||
|
# # 只有当sender不为None时才添加sender信息
|
||||||
|
# if notification.sender:
|
||||||
|
# notification_data["sender"] = {
|
||||||
|
# "id": str(notification.sender.id),
|
||||||
|
# "name": notification.sender.name
|
||||||
|
# }
|
||||||
|
|
||||||
|
# channel_layer = get_channel_layer()
|
||||||
|
# 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,338 +1,338 @@
|
|||||||
# apps/common/services/permission_service.py
|
# # apps/common/services/permission_service.py
|
||||||
import logging
|
# import logging
|
||||||
from django.db import transaction
|
# from django.db import transaction
|
||||||
from django.db.models import Q
|
# from django.db.models import Q
|
||||||
from django.utils import timezone
|
# from django.utils import timezone
|
||||||
from rest_framework.exceptions import ValidationError
|
# from rest_framework.exceptions import ValidationError
|
||||||
from apps.user.models import User
|
# from apps.user.models import User
|
||||||
from apps.knowledge_base.models import KnowledgeBase
|
# from apps.knowledge_base.models import KnowledgeBase
|
||||||
|
# from apps.permissions.models import Permission, KnowledgeBasePermission as KBPermissionModel
|
||||||
|
|
||||||
|
# logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
# class PermissionService:
|
||||||
|
# def can_manage_knowledge_base(self, user, knowledge_base):
|
||||||
|
# """检查用户是否是知识库的创建者"""
|
||||||
|
# return str(knowledge_base.user_id) == str(user.id)
|
||||||
|
|
||||||
class PermissionService:
|
# def check_extend_permission(self, permission, user):
|
||||||
def can_manage_knowledge_base(self, user, knowledge_base):
|
# """检查是否有权限延长权限有效期"""
|
||||||
"""检查用户是否是知识库的创建者"""
|
# knowledge_base = permission.knowledge_base
|
||||||
return str(knowledge_base.user_id) == str(user.id)
|
# if knowledge_base.type == 'private':
|
||||||
|
# return knowledge_base.user_id == user.id
|
||||||
|
# if knowledge_base.type == 'leader':
|
||||||
|
# return user.role == 'admin'
|
||||||
|
# if knowledge_base.type == 'member':
|
||||||
|
# return user.role == 'admin' or (
|
||||||
|
# user.role == 'leader' and user.department == knowledge_base.department
|
||||||
|
# )
|
||||||
|
# return False
|
||||||
|
|
||||||
def check_extend_permission(self, permission, user):
|
# def create_permission_request(self, user, validated_data, notification_service):
|
||||||
"""检查是否有权限延长权限有效期"""
|
# """创建权限申请并发送通知"""
|
||||||
knowledge_base = permission.knowledge_base
|
# knowledge_base = validated_data['knowledge_base']
|
||||||
if knowledge_base.type == 'private':
|
# if str(knowledge_base.user_id) == str(user.id):
|
||||||
return knowledge_base.user_id == user.id
|
# raise ValidationError({
|
||||||
if knowledge_base.type == 'leader':
|
# "code": 400,
|
||||||
return user.role == 'admin'
|
# "message": "您是此知识库的创建者,无需申请权限",
|
||||||
if knowledge_base.type == 'member':
|
# "data": None
|
||||||
return user.role == 'admin' or (
|
# })
|
||||||
user.role == 'leader' and user.department == knowledge_base.department
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def create_permission_request(self, user, validated_data, notification_service):
|
# approver = User.objects.get(id=knowledge_base.user_id)
|
||||||
"""创建权限申请并发送通知"""
|
# requested_permissions = validated_data.get('permissions', {})
|
||||||
knowledge_base = validated_data['knowledge_base']
|
# expires_at = validated_data.get('expires_at')
|
||||||
if str(knowledge_base.user_id) == str(user.id):
|
|
||||||
raise ValidationError({
|
|
||||||
"code": 400,
|
|
||||||
"message": "您是此知识库的创建者,无需申请权限",
|
|
||||||
"data": None
|
|
||||||
})
|
|
||||||
|
|
||||||
approver = User.objects.get(id=knowledge_base.user_id)
|
# if not any([requested_permissions.get('can_read'),
|
||||||
requested_permissions = validated_data.get('permissions', {})
|
# requested_permissions.get('can_edit'),
|
||||||
expires_at = validated_data.get('expires_at')
|
# requested_permissions.get('can_delete')]):
|
||||||
|
# raise ValidationError("至少需要申请一种权限(读/改/删)")
|
||||||
|
|
||||||
if not any([requested_permissions.get('can_read'),
|
# if not expires_at:
|
||||||
requested_permissions.get('can_edit'),
|
# raise ValidationError("请指定权限到期时间")
|
||||||
requested_permissions.get('can_delete')]):
|
|
||||||
raise ValidationError("至少需要申请一种权限(读/改/删)")
|
|
||||||
|
|
||||||
if not expires_at:
|
# existing_request = Permission.objects.filter(
|
||||||
raise ValidationError("请指定权限到期时间")
|
# knowledge_base=knowledge_base,
|
||||||
|
# applicant=user,
|
||||||
|
# status='pending'
|
||||||
|
# ).first()
|
||||||
|
# if existing_request:
|
||||||
|
# raise ValidationError("您已有一个待处理的权限申请")
|
||||||
|
|
||||||
existing_request = Permission.objects.filter(
|
# existing_permission = Permission.objects.filter(
|
||||||
knowledge_base=knowledge_base,
|
# knowledge_base=knowledge_base,
|
||||||
applicant=user,
|
# applicant=user,
|
||||||
status='pending'
|
# status='approved',
|
||||||
).first()
|
# expires_at__gt=timezone.now()
|
||||||
if existing_request:
|
# ).first()
|
||||||
raise ValidationError("您已有一个待处理的权限申请")
|
# if existing_permission:
|
||||||
|
# raise ValidationError("您已有此知识库的访问权限")
|
||||||
|
|
||||||
existing_permission = Permission.objects.filter(
|
# with transaction.atomic():
|
||||||
knowledge_base=knowledge_base,
|
# permission = Permission.objects.create(
|
||||||
applicant=user,
|
# knowledge_base=knowledge_base,
|
||||||
status='approved',
|
# applicant=user,
|
||||||
expires_at__gt=timezone.now()
|
# approver=approver,
|
||||||
).first()
|
# permissions=requested_permissions,
|
||||||
if existing_permission:
|
# expires_at=expires_at,
|
||||||
raise ValidationError("您已有此知识库的访问权限")
|
# status='pending'
|
||||||
|
# )
|
||||||
|
|
||||||
with transaction.atomic():
|
# permission_types = []
|
||||||
permission = Permission.objects.create(
|
# if requested_permissions.get('can_read'):
|
||||||
knowledge_base=knowledge_base,
|
# permission_types.append('读取')
|
||||||
applicant=user,
|
# if requested_permissions.get('can_edit'):
|
||||||
approver=approver,
|
# permission_types.append('编辑')
|
||||||
permissions=requested_permissions,
|
# if requested_permissions.get('can_delete'):
|
||||||
expires_at=expires_at,
|
# permission_types.append('删除')
|
||||||
status='pending'
|
# permission_str = '、'.join(permission_types)
|
||||||
)
|
|
||||||
|
|
||||||
permission_types = []
|
# notification_service.send_notification(
|
||||||
if requested_permissions.get('can_read'):
|
# user=approver,
|
||||||
permission_types.append('读取')
|
# title="新的权限申请",
|
||||||
if requested_permissions.get('can_edit'):
|
# content=f"用户 {user.name} 申请了知识库 '{knowledge_base.name}' 的{permission_str}权限",
|
||||||
permission_types.append('编辑')
|
# notification_type="permission_request",
|
||||||
if requested_permissions.get('can_delete'):
|
# related_object_id=str(permission.id)
|
||||||
permission_types.append('删除')
|
# )
|
||||||
permission_str = '、'.join(permission_types)
|
|
||||||
|
|
||||||
notification_service.send_notification(
|
# return permission
|
||||||
user=approver,
|
|
||||||
title="新的权限申请",
|
|
||||||
content=f"用户 {user.name} 申请了知识库 '{knowledge_base.name}' 的{permission_str}权限",
|
|
||||||
notification_type="permission_request",
|
|
||||||
related_object_id=str(permission.id)
|
|
||||||
)
|
|
||||||
|
|
||||||
return permission
|
# def approve_permission(self, user, permission, response_message, notification_service):
|
||||||
|
# """审批权限申请"""
|
||||||
|
# if not self.can_manage_knowledge_base(user, permission.knowledge_base):
|
||||||
|
# raise ValidationError({
|
||||||
|
# 'code': 403,
|
||||||
|
# 'message': '只有知识库创建者可以审批此申请',
|
||||||
|
# 'data': None
|
||||||
|
# })
|
||||||
|
|
||||||
def approve_permission(self, user, permission, response_message, notification_service):
|
# with transaction.atomic():
|
||||||
"""审批权限申请"""
|
# permission.status = 'approved'
|
||||||
if not self.can_manage_knowledge_base(user, permission.knowledge_base):
|
# permission.approver = user
|
||||||
raise ValidationError({
|
# permission.response_message = response_message
|
||||||
'code': 403,
|
# permission.save()
|
||||||
'message': '只有知识库创建者可以审批此申请',
|
|
||||||
'data': None
|
|
||||||
})
|
|
||||||
|
|
||||||
with transaction.atomic():
|
# kb_permission = KBPermissionModel.objects.filter(
|
||||||
permission.status = 'approved'
|
# knowledge_base=permission.knowledge_base,
|
||||||
permission.approver = user
|
# user=permission.applicant
|
||||||
permission.response_message = response_message
|
# ).first()
|
||||||
permission.save()
|
|
||||||
|
|
||||||
kb_permission = KBPermissionModel.objects.filter(
|
# if kb_permission:
|
||||||
knowledge_base=permission.knowledge_base,
|
# kb_permission.can_read = permission.permissions.get('can_read', False)
|
||||||
user=permission.applicant
|
# kb_permission.can_edit = permission.permissions.get('can_edit', False)
|
||||||
).first()
|
# kb_permission.can_delete = permission.permissions.get('can_delete', False)
|
||||||
|
# kb_permission.granted_by = user
|
||||||
|
# kb_permission.status = 'active'
|
||||||
|
# kb_permission.expires_at = permission.expires_at
|
||||||
|
# kb_permission.save()
|
||||||
|
# logger.info(f"更新知识库权限记录: {kb_permission.id}")
|
||||||
|
# else:
|
||||||
|
# kb_permission = KBPermissionModel.objects.create(
|
||||||
|
# knowledge_base=permission.knowledge_base,
|
||||||
|
# user=permission.applicant,
|
||||||
|
# can_read=permission.permissions.get('can_read', False),
|
||||||
|
# can_edit=permission.permissions.get('can_edit', False),
|
||||||
|
# can_delete=permission.permissions.get('can_delete', False),
|
||||||
|
# granted_by=user,
|
||||||
|
# status='active',
|
||||||
|
# expires_at=permission.expires_at
|
||||||
|
# )
|
||||||
|
# logger.info(f"创建新的知识库权限记录: {kb_permission.id}")
|
||||||
|
|
||||||
if kb_permission:
|
# notification_service.send_notification(
|
||||||
kb_permission.can_read = permission.permissions.get('can_read', False)
|
# user=permission.applicant,
|
||||||
kb_permission.can_edit = permission.permissions.get('can_edit', False)
|
# title="权限申请已通过",
|
||||||
kb_permission.can_delete = permission.permissions.get('can_delete', False)
|
# content=f"您对知识库 '{permission.knowledge_base.name}' 的权限申请已通过",
|
||||||
kb_permission.granted_by = user
|
# notification_type="permission_approved",
|
||||||
kb_permission.status = 'active'
|
# related_object_id=str(permission.id)
|
||||||
kb_permission.expires_at = permission.expires_at
|
# )
|
||||||
kb_permission.save()
|
|
||||||
logger.info(f"更新知识库权限记录: {kb_permission.id}")
|
|
||||||
else:
|
|
||||||
kb_permission = KBPermissionModel.objects.create(
|
|
||||||
knowledge_base=permission.knowledge_base,
|
|
||||||
user=permission.applicant,
|
|
||||||
can_read=permission.permissions.get('can_read', False),
|
|
||||||
can_edit=permission.permissions.get('can_edit', False),
|
|
||||||
can_delete=permission.permissions.get('can_delete', False),
|
|
||||||
granted_by=user,
|
|
||||||
status='active',
|
|
||||||
expires_at=permission.expires_at
|
|
||||||
)
|
|
||||||
logger.info(f"创建新的知识库权限记录: {kb_permission.id}")
|
|
||||||
|
|
||||||
notification_service.send_notification(
|
# return permission
|
||||||
user=permission.applicant,
|
|
||||||
title="权限申请已通过",
|
|
||||||
content=f"您对知识库 '{permission.knowledge_base.name}' 的权限申请已通过",
|
|
||||||
notification_type="permission_approved",
|
|
||||||
related_object_id=str(permission.id)
|
|
||||||
)
|
|
||||||
|
|
||||||
return permission
|
# def reject_permission(self, user, permission, response_message, notification_service):
|
||||||
|
# """拒绝权限申请"""
|
||||||
|
# if not self.can_manage_knowledge_base(user, permission.knowledge_base):
|
||||||
|
# raise ValidationError({
|
||||||
|
# 'code': 403,
|
||||||
|
# 'message': '只有知识库创建者可以审批此申请',
|
||||||
|
# 'data': None
|
||||||
|
# })
|
||||||
|
|
||||||
def reject_permission(self, user, permission, response_message, notification_service):
|
# if permission.status != 'pending':
|
||||||
"""拒绝权限申请"""
|
# raise ValidationError({
|
||||||
if not self.can_manage_knowledge_base(user, permission.knowledge_base):
|
# 'code': 400,
|
||||||
raise ValidationError({
|
# 'message': '该申请已被处理',
|
||||||
'code': 403,
|
# 'data': None
|
||||||
'message': '只有知识库创建者可以审批此申请',
|
# })
|
||||||
'data': None
|
|
||||||
})
|
|
||||||
|
|
||||||
if permission.status != 'pending':
|
# if not response_message:
|
||||||
raise ValidationError({
|
# raise ValidationError({
|
||||||
'code': 400,
|
# 'code': 400,
|
||||||
'message': '该申请已被处理',
|
# 'message': '请填写拒绝原因',
|
||||||
'data': None
|
# 'data': None
|
||||||
})
|
# })
|
||||||
|
|
||||||
if not response_message:
|
# with transaction.atomic():
|
||||||
raise ValidationError({
|
# permission.status = 'rejected'
|
||||||
'code': 400,
|
# permission.approver = user
|
||||||
'message': '请填写拒绝原因',
|
# permission.response_message = response_message
|
||||||
'data': None
|
# permission.save()
|
||||||
})
|
|
||||||
|
|
||||||
with transaction.atomic():
|
# notification_service.send_notification(
|
||||||
permission.status = 'rejected'
|
# user=permission.applicant,
|
||||||
permission.approver = user
|
# title="权限申请已拒绝",
|
||||||
permission.response_message = response_message
|
# content=f"您对知识库 '{permission.knowledge_base.name}' 的权限申请已被拒绝\n拒绝原因:{response_message}",
|
||||||
permission.save()
|
# notification_type="permission_rejected",
|
||||||
|
# related_object_id=str(permission.id)
|
||||||
|
# )
|
||||||
|
|
||||||
notification_service.send_notification(
|
# return permission
|
||||||
user=permission.applicant,
|
|
||||||
title="权限申请已拒绝",
|
|
||||||
content=f"您对知识库 '{permission.knowledge_base.name}' 的权限申请已被拒绝\n拒绝原因:{response_message}",
|
|
||||||
notification_type="permission_rejected",
|
|
||||||
related_object_id=str(permission.id)
|
|
||||||
)
|
|
||||||
|
|
||||||
return permission
|
# def extend_permission(self, user, permission, new_expires_at, notification_service):
|
||||||
|
# """延长权限有效期"""
|
||||||
|
# if not self.check_extend_permission(permission, user):
|
||||||
|
# raise ValidationError({
|
||||||
|
# "code": 403,
|
||||||
|
# "message": "您没有权限延长此权限",
|
||||||
|
# "data": None
|
||||||
|
# })
|
||||||
|
|
||||||
def extend_permission(self, user, permission, new_expires_at, notification_service):
|
# if not new_expires_at:
|
||||||
"""延长权限有效期"""
|
# raise ValidationError({
|
||||||
if not self.check_extend_permission(permission, user):
|
# "code": 400,
|
||||||
raise ValidationError({
|
# "message": "请设置新的过期时间",
|
||||||
"code": 403,
|
# "data": None
|
||||||
"message": "您没有权限延长此权限",
|
# })
|
||||||
"data": None
|
|
||||||
})
|
|
||||||
|
|
||||||
if not new_expires_at:
|
# try:
|
||||||
raise ValidationError({
|
# new_expires_at = timezone.datetime.strptime(new_expires_at, '%Y-%m-%dT%H:%M:%SZ')
|
||||||
"code": 400,
|
# new_expires_at = timezone.make_aware(new_expires_at)
|
||||||
"message": "请设置新的过期时间",
|
# if new_expires_at <= timezone.now():
|
||||||
"data": None
|
# raise ValidationError({
|
||||||
})
|
# "code": 400,
|
||||||
|
# "message": "过期时间不能早于或等于当前时间",
|
||||||
|
# "data": None
|
||||||
|
# })
|
||||||
|
# except ValueError:
|
||||||
|
# raise ValidationError({
|
||||||
|
# "code": 400,
|
||||||
|
# "message": "过期时间格式错误,应为 ISO 格式 (YYYY-MM-DDThh:mm:ssZ)",
|
||||||
|
# "data": None
|
||||||
|
# })
|
||||||
|
|
||||||
try:
|
# with transaction.atomic():
|
||||||
new_expires_at = timezone.datetime.strptime(new_expires_at, '%Y-%m-%dT%H:%M:%SZ')
|
# permission.expires_at = new_expires_at
|
||||||
new_expires_at = timezone.make_aware(new_expires_at)
|
# permission.save()
|
||||||
if new_expires_at <= timezone.now():
|
|
||||||
raise ValidationError({
|
|
||||||
"code": 400,
|
|
||||||
"message": "过期时间不能早于或等于当前时间",
|
|
||||||
"data": None
|
|
||||||
})
|
|
||||||
except ValueError:
|
|
||||||
raise ValidationError({
|
|
||||||
"code": 400,
|
|
||||||
"message": "过期时间格式错误,应为 ISO 格式 (YYYY-MM-DDThh:mm:ssZ)",
|
|
||||||
"data": None
|
|
||||||
})
|
|
||||||
|
|
||||||
with transaction.atomic():
|
# kb_permission = KBPermissionModel.objects.get(
|
||||||
permission.expires_at = new_expires_at
|
# knowledge_base=permission.knowledge_base,
|
||||||
permission.save()
|
# user=permission.applicant
|
||||||
|
# )
|
||||||
|
# kb_permission.expires_at = new_expires_at
|
||||||
|
# kb_permission.save()
|
||||||
|
|
||||||
kb_permission = KBPermissionModel.objects.get(
|
# notification_service.send_notification(
|
||||||
knowledge_base=permission.knowledge_base,
|
# user=permission.applicant,
|
||||||
user=permission.applicant
|
# title="权限有效期延长",
|
||||||
)
|
# content=f"您对知识库 '{permission.knowledge_base.name}' 的权限有效期已延长至 {new_expires_at.strftime('%Y-%m-%d %H:%M:%S')}",
|
||||||
kb_permission.expires_at = new_expires_at
|
# notification_type="permission_extended",
|
||||||
kb_permission.save()
|
# related_object_id=str(permission.id)
|
||||||
|
# )
|
||||||
|
|
||||||
notification_service.send_notification(
|
# return permission
|
||||||
user=permission.applicant,
|
|
||||||
title="权限有效期延长",
|
|
||||||
content=f"您对知识库 '{permission.knowledge_base.name}' 的权限有效期已延长至 {new_expires_at.strftime('%Y-%m-%d %H:%M:%S')}",
|
|
||||||
notification_type="permission_extended",
|
|
||||||
related_object_id=str(permission.id)
|
|
||||||
)
|
|
||||||
|
|
||||||
return permission
|
# def update_user_permission(self, admin_user, user_id, knowledge_base_id, permissions, expires_at_str, notification_service):
|
||||||
|
# """管理员更新用户权限"""
|
||||||
|
# if admin_user.role != 'admin':
|
||||||
|
# raise ValidationError({
|
||||||
|
# 'code': 403,
|
||||||
|
# 'message': '只有管理员可以直接修改权限',
|
||||||
|
# 'data': None
|
||||||
|
# })
|
||||||
|
|
||||||
def update_user_permission(self, admin_user, user_id, knowledge_base_id, permissions, expires_at_str, notification_service):
|
# if not all([user_id, knowledge_base_id, permissions]):
|
||||||
"""管理员更新用户权限"""
|
# raise ValidationError({
|
||||||
if admin_user.role != 'admin':
|
# 'code': 400,
|
||||||
raise ValidationError({
|
# 'message': '缺少必要参数',
|
||||||
'code': 403,
|
# 'data': None
|
||||||
'message': '只有管理员可以直接修改权限',
|
# })
|
||||||
'data': None
|
|
||||||
})
|
|
||||||
|
|
||||||
if not all([user_id, knowledge_base_id, permissions]):
|
# required_permission_fields = ['can_read', 'can_edit', 'can_delete']
|
||||||
raise ValidationError({
|
# if not all(field in permissions for field in required_permission_fields):
|
||||||
'code': 400,
|
# raise ValidationError({
|
||||||
'message': '缺少必要参数',
|
# 'code': 400,
|
||||||
'data': None
|
# 'message': '权限参数格式错误,必须包含 can_read、can_edit、can_delete',
|
||||||
})
|
# 'data': None
|
||||||
|
# })
|
||||||
|
|
||||||
required_permission_fields = ['can_read', 'can_edit', 'can_delete']
|
# try:
|
||||||
if not all(field in permissions for field in required_permission_fields):
|
# user = User.objects.get(id=user_id)
|
||||||
raise ValidationError({
|
# knowledge_base = KnowledgeBase.objects.get(id=knowledge_base_id)
|
||||||
'code': 400,
|
# except User.DoesNotExist:
|
||||||
'message': '权限参数格式错误,必须包含 can_read、can_edit、can_delete',
|
# raise ValidationError({
|
||||||
'data': None
|
# 'code': 404,
|
||||||
})
|
# 'message': f'用户ID {user_id} 不存在',
|
||||||
|
# 'data': None
|
||||||
|
# })
|
||||||
|
# except KnowledgeBase.DoesNotExist:
|
||||||
|
# raise ValidationError({
|
||||||
|
# 'code': 404,
|
||||||
|
# 'message': f'知识库ID {knowledge_base_id} 不存在',
|
||||||
|
# 'data': None
|
||||||
|
# })
|
||||||
|
|
||||||
try:
|
# if knowledge_base.type == 'private' and str(knowledge_base.user_id) != str(user.id):
|
||||||
user = User.objects.get(id=user_id)
|
# raise ValidationError({
|
||||||
knowledge_base = KnowledgeBase.objects.get(id=knowledge_base_id)
|
# 'code': 403,
|
||||||
except User.DoesNotExist:
|
# 'message': '不能修改其他用户的私有知识库权限',
|
||||||
raise ValidationError({
|
# 'data': None
|
||||||
'code': 404,
|
# })
|
||||||
'message': f'用户ID {user_id} 不存在',
|
|
||||||
'data': None
|
|
||||||
})
|
|
||||||
except KnowledgeBase.DoesNotExist:
|
|
||||||
raise ValidationError({
|
|
||||||
'code': 404,
|
|
||||||
'message': f'知识库ID {knowledge_base_id} 不存在',
|
|
||||||
'data': None
|
|
||||||
})
|
|
||||||
|
|
||||||
if knowledge_base.type == 'private' and str(knowledge_base.user_id) != str(user.id):
|
# if user.role == 'member' and permissions.get('can_delete'):
|
||||||
raise ValidationError({
|
# raise ValidationError({
|
||||||
'code': 403,
|
# 'code': 400,
|
||||||
'message': '不能修改其他用户的私有知识库权限',
|
# 'message': '普通成员不能获得删除权限',
|
||||||
'data': None
|
# 'data': None
|
||||||
})
|
# })
|
||||||
|
|
||||||
if user.role == 'member' and permissions.get('can_delete'):
|
# expires_at = None
|
||||||
raise ValidationError({
|
# if expires_at_str:
|
||||||
'code': 400,
|
# try:
|
||||||
'message': '普通成员不能获得删除权限',
|
# expires_at = timezone.datetime.strptime(expires_at_str, '%Y-%m-%dT%H:%M:%SZ')
|
||||||
'data': None
|
# expires_at = timezone.make_aware(expires_at)
|
||||||
})
|
# if expires_at <= timezone.now():
|
||||||
|
# raise ValidationError({
|
||||||
|
# 'code': 400,
|
||||||
|
# 'message': '过期时间不能早于或等于当前时间',
|
||||||
|
# 'data': None
|
||||||
|
# })
|
||||||
|
# except ValueError:
|
||||||
|
# raise ValidationError({
|
||||||
|
# 'code': 400,
|
||||||
|
# 'message': '过期时间格式错误,应为 ISO 格式 (YYYY-MM-DDThh:mm:ssZ)',
|
||||||
|
# 'data': None
|
||||||
|
# })
|
||||||
|
|
||||||
expires_at = None
|
# with transaction.atomic():
|
||||||
if expires_at_str:
|
# permission, created = KBPermissionModel.objects.update_or_create(
|
||||||
try:
|
# user=user,
|
||||||
expires_at = timezone.datetime.strptime(expires_at_str, '%Y-%m-%dT%H:%M:%SZ')
|
# knowledge_base=knowledge_base,
|
||||||
expires_at = timezone.make_aware(expires_at)
|
# defaults={
|
||||||
if expires_at <= timezone.now():
|
# 'can_read': permissions.get('can_read', False),
|
||||||
raise ValidationError({
|
# 'can_edit': permissions.get('can_edit', False),
|
||||||
'code': 400,
|
# 'can_delete': permissions.get('can_delete', False),
|
||||||
'message': '过期时间不能早于或等于当前时间',
|
# 'granted_by': admin_user,
|
||||||
'data': None
|
# 'status': 'active',
|
||||||
})
|
# 'expires_at': expires_at
|
||||||
except ValueError:
|
# }
|
||||||
raise ValidationError({
|
# )
|
||||||
'code': 400,
|
|
||||||
'message': '过期时间格式错误,应为 ISO 格式 (YYYY-MM-DDThh:mm:ssZ)',
|
|
||||||
'data': None
|
|
||||||
})
|
|
||||||
|
|
||||||
with transaction.atomic():
|
# notification_service.send_notification(
|
||||||
permission, created = KBPermissionModel.objects.update_or_create(
|
# user=user,
|
||||||
user=user,
|
# title="知识库权限更新",
|
||||||
knowledge_base=knowledge_base,
|
# content=f"管理员已{created and '授予' or '更新'}您对知识库 '{knowledge_base.name}' 的权限",
|
||||||
defaults={
|
# notification_type="permission_updated",
|
||||||
'can_read': permissions.get('can_read', False),
|
# related_object_id=str(permission.id)
|
||||||
'can_edit': permissions.get('can_edit', False),
|
# )
|
||||||
'can_delete': permissions.get('can_delete', False),
|
|
||||||
'granted_by': admin_user,
|
|
||||||
'status': 'active',
|
|
||||||
'expires_at': expires_at
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
notification_service.send_notification(
|
# return permission, created
|
||||||
user=user,
|
|
||||||
title="知识库权限更新",
|
|
||||||
content=f"管理员已{created and '授予' or '更新'}您对知识库 '{knowledge_base.name}' 的权限",
|
|
||||||
notification_type="permission_updated",
|
|
||||||
related_object_id=str(permission.id)
|
|
||||||
)
|
|
||||||
|
|
||||||
return permission, created
|
|
||||||
|
|
@ -1,15 +1,11 @@
|
|||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import logging
|
|
||||||
import requests
|
import requests
|
||||||
from urllib.parse import urljoin, urlparse, parse_qs
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
# 基础URL地址
|
# 基础URL地址
|
||||||
BASE_API_URL = "https://open.feishu.cn/open-apis/"
|
BASE_API_URL = "https://open.feishu.cn/open-apis/"
|
||||||
|
|
||||||
# 获取日志记录器
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class BitableService:
|
class BitableService:
|
||||||
"""
|
"""
|
||||||
@ -36,11 +32,6 @@ class BitableService:
|
|||||||
if headers is None:
|
if headers is None:
|
||||||
headers = {}
|
headers = {}
|
||||||
|
|
||||||
# 记录请求信息,避免记录敏感信息
|
|
||||||
logger.info(f"请求飞书API: {method} {full_url}")
|
|
||||||
if params:
|
|
||||||
logger.info(f"请求参数: {params}")
|
|
||||||
|
|
||||||
response = requests.request(
|
response = requests.request(
|
||||||
method=method,
|
method=method,
|
||||||
url=full_url,
|
url=full_url,
|
||||||
@ -52,29 +43,7 @@ class BitableService:
|
|||||||
# 检查响应
|
# 检查响应
|
||||||
if not response.ok:
|
if not response.ok:
|
||||||
error_msg = f"API 请求失败: {response.status_code}, 响应: {response.text}"
|
error_msg = f"API 请求失败: {response.status_code}, 响应: {response.text}"
|
||||||
logger.error(error_msg)
|
print(error_msg)
|
||||||
|
|
||||||
# 解析错误响应
|
|
||||||
try:
|
|
||||||
error_json = response.json()
|
|
||||||
error_code = error_json.get("code")
|
|
||||||
error_msg = error_json.get("msg", "")
|
|
||||||
|
|
||||||
# 根据错误代码提供更具体的错误信息
|
|
||||||
if error_code == 91402: # NOTEXIST
|
|
||||||
error_detail = "请求的资源不存在,请检查app_token和table_id是否正确,以及应用是否有权限访问该资源"
|
|
||||||
logger.error(f"资源不存在错误: {error_detail}")
|
|
||||||
raise Exception(f"资源不存在: {error_detail}")
|
|
||||||
elif error_code == 99991663: # TOKEN_INVALID
|
|
||||||
error_detail = "访问令牌无效或已过期"
|
|
||||||
logger.error(f"令牌错误: {error_detail}")
|
|
||||||
raise Exception(f"访问令牌错误: {error_detail}")
|
|
||||||
else:
|
|
||||||
logger.error(f"飞书API错误: 代码={error_code}, 消息={error_msg}")
|
|
||||||
except ValueError:
|
|
||||||
# 响应不是有效的JSON
|
|
||||||
pass
|
|
||||||
|
|
||||||
raise Exception(error_msg)
|
raise Exception(error_msg)
|
||||||
|
|
||||||
return response.json()
|
return response.json()
|
||||||
@ -83,11 +52,6 @@ class BitableService:
|
|||||||
def extract_params_from_url(table_url):
|
def extract_params_from_url(table_url):
|
||||||
"""
|
"""
|
||||||
从URL中提取app_token和table_id
|
从URL中提取app_token和table_id
|
||||||
支持多种飞书多维表格URL格式:
|
|
||||||
1. https://xxx.feishu.cn/base/{app_token}?table={table_id}
|
|
||||||
2. https://xxx.feishu.cn/wiki/{app_token}?sheet={table_id}
|
|
||||||
3. https://xxx.feishu.cn/wiki/wikcnXXX?sheet=XXX
|
|
||||||
4. https://xxx.feishu.cn/base/bascnXXX?table=tblXXX
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
table_url: 飞书多维表格URL
|
table_url: 飞书多维表格URL
|
||||||
@ -98,76 +62,13 @@ class BitableService:
|
|||||||
Raises:
|
Raises:
|
||||||
ValueError: 如果无法从URL中提取必要参数
|
ValueError: 如果无法从URL中提取必要参数
|
||||||
"""
|
"""
|
||||||
# 记录原始URL
|
app_token_match = re.search(r'base/([^?]+)', table_url)
|
||||||
logger.info(f"解析多维表格URL: {table_url}")
|
table_id_match = re.search(r'table=([^&]+)', table_url)
|
||||||
|
|
||||||
# 处理URL中的多余空格
|
if not app_token_match or not table_id_match:
|
||||||
table_url = table_url.strip()
|
raise ValueError("无法从URL中提取必要参数,请确认URL格式正确")
|
||||||
|
|
||||||
# 解析URL
|
return app_token_match.group(1), table_id_match.group(1)
|
||||||
parsed_url = urlparse(table_url)
|
|
||||||
query_params = parse_qs(parsed_url.query)
|
|
||||||
path = parsed_url.path
|
|
||||||
|
|
||||||
logger.debug(f"URL路径: {path}, 查询参数: {query_params}")
|
|
||||||
|
|
||||||
# 尝试从查询参数中获取table_id
|
|
||||||
table_id = None
|
|
||||||
if 'table' in query_params:
|
|
||||||
table_id = query_params['table'][0].strip()
|
|
||||||
elif 'sheet' in query_params:
|
|
||||||
table_id = query_params['sheet'][0].strip()
|
|
||||||
|
|
||||||
# 尝试从路径中获取app_token
|
|
||||||
app_token = None
|
|
||||||
# 处理标准格式 /base/{app_token} 或 /wiki/{app_token}
|
|
||||||
standard_match = re.search(r'/(base|wiki)/([^/?]+)', path)
|
|
||||||
if standard_match:
|
|
||||||
app_token = standard_match.group(2).strip()
|
|
||||||
else:
|
|
||||||
# 处理简短ID格式,如 /wiki/wikcnXXX
|
|
||||||
short_id_match = re.search(r'/(base|wiki)/([a-zA-Z0-9]+)', path)
|
|
||||||
if short_id_match:
|
|
||||||
app_token = short_id_match.group(2).strip()
|
|
||||||
|
|
||||||
# 检查是否成功提取
|
|
||||||
if not app_token or not table_id:
|
|
||||||
error_msg = "无法从URL中提取必要参数,请确认URL格式正确"
|
|
||||||
logger.error(f"{error_msg}. URL: {table_url}")
|
|
||||||
raise ValueError(error_msg)
|
|
||||||
|
|
||||||
logger.info(f"成功从URL提取参数: app_token={app_token}, table_id={table_id}")
|
|
||||||
return app_token, table_id
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def validate_access(app_token, access_token):
|
|
||||||
"""
|
|
||||||
验证应用是否有权限访问多维表格
|
|
||||||
|
|
||||||
Args:
|
|
||||||
app_token: 应用令牌
|
|
||||||
access_token: 访问令牌
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 是否有权限
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# 构造请求
|
|
||||||
url = f"bitable/v1/apps/{app_token}"
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {access_token}",
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 发送请求
|
|
||||||
response = BitableService.make_request("GET", url, headers=headers)
|
|
||||||
|
|
||||||
# 检查响应
|
|
||||||
return response and "code" in response and response["code"] == 0
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"验证访问权限失败: {str(e)}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_metadata(app_token, table_id, access_token):
|
def get_metadata(app_token, table_id, access_token):
|
||||||
@ -183,10 +84,6 @@ class BitableService:
|
|||||||
dict: 表格元数据
|
dict: 表格元数据
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 先验证应用是否有权限访问
|
|
||||||
if not BitableService.validate_access(app_token, access_token):
|
|
||||||
logger.warning(f"应用无权限访问多维表格: app_token={app_token}")
|
|
||||||
|
|
||||||
# 构造请求
|
# 构造请求
|
||||||
url = f"bitable/v1/apps/{app_token}/tables/{table_id}"
|
url = f"bitable/v1/apps/{app_token}/tables/{table_id}"
|
||||||
headers = {
|
headers = {
|
||||||
@ -199,18 +96,16 @@ class BitableService:
|
|||||||
|
|
||||||
# 检查响应
|
# 检查响应
|
||||||
if response and "code" in response and response["code"] == 0:
|
if response and "code" in response and response["code"] == 0:
|
||||||
metadata = response.get("data", {}).get("table", {})
|
return response.get("data", {}).get("table", {})
|
||||||
logger.info(f"成功获取多维表格元数据: {metadata.get('name', table_id)}")
|
|
||||||
return metadata
|
|
||||||
|
|
||||||
# 发生错误
|
# 发生错误
|
||||||
error_msg = f"获取多维表格元数据失败: {json.dumps(response)}"
|
error_msg = f"获取多维表格元数据失败: {json.dumps(response)}"
|
||||||
logger.error(error_msg)
|
print(error_msg)
|
||||||
raise Exception(error_msg)
|
raise Exception(error_msg)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 如果正常API调用失败,使用替代方法
|
# 如果正常API调用失败,使用替代方法
|
||||||
logger.error(f"获取多维表格元数据失败: {str(e)}")
|
print(f"获取多维表格元数据失败: {str(e)}")
|
||||||
# 简单返回一个基本名称
|
# 简单返回一个基本名称
|
||||||
return {
|
return {
|
||||||
"name": f"table_{table_id}",
|
"name": f"table_{table_id}",
|
||||||
@ -238,8 +133,6 @@ class BitableService:
|
|||||||
Exception: 查询失败时抛出异常
|
Exception: 查询失败时抛出异常
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
logger.info(f"查询多维表格记录: app_token={app_token}, table_id={table_id}")
|
|
||||||
|
|
||||||
# 构造请求URL
|
# 构造请求URL
|
||||||
url = f"bitable/v1/apps/{app_token}/tables/{table_id}/records/search"
|
url = f"bitable/v1/apps/{app_token}/tables/{table_id}/records/search"
|
||||||
|
|
||||||
@ -266,18 +159,16 @@ class BitableService:
|
|||||||
|
|
||||||
# 检查响应
|
# 检查响应
|
||||||
if response and "code" in response and response["code"] == 0:
|
if response and "code" in response and response["code"] == 0:
|
||||||
result = response.get("data", {})
|
return response.get("data", {})
|
||||||
logger.info(f"查询成功,获取到 {len(result.get('items', []))} 条记录")
|
|
||||||
return result
|
|
||||||
|
|
||||||
# 发生错误
|
# 发生错误
|
||||||
error_msg = f"查询飞书多维表格失败: {json.dumps(response)}"
|
error_msg = f"查询飞书多维表格失败: {json.dumps(response)}"
|
||||||
logger.error(error_msg)
|
print(error_msg)
|
||||||
raise Exception(error_msg)
|
raise Exception(error_msg)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 记录详细错误
|
# 记录详细错误
|
||||||
logger.error(f"查询飞书多维表格发生错误: {str(e)}")
|
print(f"查询飞书多维表格发生错误: {str(e)}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -294,8 +185,6 @@ class BitableService:
|
|||||||
list: 字段信息列表
|
list: 字段信息列表
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
logger.info(f"获取多维表格字段: app_token={app_token}, table_id={table_id}")
|
|
||||||
|
|
||||||
# 构造请求
|
# 构造请求
|
||||||
url = f"bitable/v1/apps/{app_token}/tables/{table_id}/fields"
|
url = f"bitable/v1/apps/{app_token}/tables/{table_id}/fields"
|
||||||
headers = {
|
headers = {
|
||||||
@ -309,20 +198,17 @@ class BitableService:
|
|||||||
|
|
||||||
# 检查响应
|
# 检查响应
|
||||||
if response and "code" in response and response["code"] == 0:
|
if response and "code" in response and response["code"] == 0:
|
||||||
fields = response.get("data", {}).get("items", [])
|
return response.get("data", {}).get("items", [])
|
||||||
logger.info(f"成功获取到 {len(fields)} 个字段")
|
|
||||||
return fields
|
|
||||||
|
|
||||||
# 发生错误
|
# 发生错误
|
||||||
error_msg = f"获取多维表格字段失败: {json.dumps(response)}"
|
error_msg = f"获取多维表格字段失败: {json.dumps(response)}"
|
||||||
logger.error(error_msg)
|
print(error_msg)
|
||||||
raise Exception(error_msg)
|
raise Exception(error_msg)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 记录详细错误
|
# 记录详细错误
|
||||||
logger.error(f"获取字段信息失败: {str(e)}")
|
print(f"获取字段信息失败: {str(e)}")
|
||||||
# 如果获取失败,返回一个基本字段集
|
# 如果获取失败,返回一个基本字段集
|
||||||
logger.warning("返回默认字段集")
|
|
||||||
return [
|
return [
|
||||||
{"field_name": "title", "type": "text", "property": {}},
|
{"field_name": "title", "type": "text", "property": {}},
|
||||||
{"field_name": "description", "type": "text", "property": {}},
|
{"field_name": "description", "type": "text", "property": {}},
|
||||||
|
@ -201,42 +201,12 @@ class DataSyncService:
|
|||||||
dict: 同步结果
|
dict: 同步结果
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 记录原始URL,便于排查问题
|
|
||||||
logger.info(f"开始数据同步,URL: {table_url}")
|
|
||||||
|
|
||||||
# 提取参数
|
# 提取参数
|
||||||
try:
|
app_token, table_id = BitableService.extract_params_from_url(table_url)
|
||||||
app_token, table_id = BitableService.extract_params_from_url(table_url)
|
|
||||||
logger.info(f"成功解析URL参数: app_token={app_token}, table_id={table_id}")
|
|
||||||
except ValueError as ve:
|
|
||||||
logger.error(f"URL解析失败: {str(ve)}, URL: {table_url}")
|
|
||||||
return {
|
|
||||||
'success': False,
|
|
||||||
'error': f"URL解析失败: {str(ve)}",
|
|
||||||
'details': "URL格式可能不正确,请参考飞书多维表格文档"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 验证应用是否有权限访问该多维表格
|
|
||||||
if not BitableService.validate_access(app_token, access_token):
|
|
||||||
error_msg = f"应用无权限访问多维表格: app_token={app_token}"
|
|
||||||
logger.warning(error_msg)
|
|
||||||
return {
|
|
||||||
'success': False,
|
|
||||||
'error': error_msg,
|
|
||||||
'details': "请确认access_token是否有效,以及应用是否已被添加为表格协作者"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 1. 获取表格元数据
|
# 1. 获取表格元数据
|
||||||
try:
|
metadata = BitableService.get_metadata(app_token, table_id, access_token)
|
||||||
metadata = BitableService.get_metadata(app_token, table_id, access_token)
|
feishu_table_name = metadata.get('name', f'table_{table_id}')
|
||||||
feishu_table_name = metadata.get('name', f'table_{table_id}')
|
|
||||||
logger.info(f"获取到表格元数据: 名称={feishu_table_name}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"获取表格元数据失败: {str(e)}")
|
|
||||||
return {
|
|
||||||
'success': False,
|
|
||||||
'error': f"获取表格元数据失败: {str(e)}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 2. 获取或创建表格映射
|
# 2. 获取或创建表格映射
|
||||||
if auto_sync:
|
if auto_sync:
|
||||||
@ -244,7 +214,6 @@ class DataSyncService:
|
|||||||
try:
|
try:
|
||||||
mapping = FeishuTableMapping.objects.get(app_token=app_token, table_id=table_id)
|
mapping = FeishuTableMapping.objects.get(app_token=app_token, table_id=table_id)
|
||||||
final_table_name = mapping.table_name
|
final_table_name = mapping.table_name
|
||||||
logger.info(f"使用已有映射: {final_table_name}")
|
|
||||||
except FeishuTableMapping.DoesNotExist:
|
except FeishuTableMapping.DoesNotExist:
|
||||||
# 如果找不到映射,自动创建一个
|
# 如果找不到映射,自动创建一个
|
||||||
default_table_name = f"feishu_{feishu_table_name.lower().replace(' ', '_').replace('-', '_')}"
|
default_table_name = f"feishu_{feishu_table_name.lower().replace(' ', '_').replace('-', '_')}"
|
||||||
@ -256,7 +225,6 @@ class DataSyncService:
|
|||||||
table_name or default_table_name
|
table_name or default_table_name
|
||||||
)
|
)
|
||||||
final_table_name = mapping.table_name
|
final_table_name = mapping.table_name
|
||||||
logger.info(f"创建新映射: {final_table_name}")
|
|
||||||
else:
|
else:
|
||||||
# 非自动同步模式,优先使用用户提供的表名
|
# 非自动同步模式,优先使用用户提供的表名
|
||||||
default_table_name = f"feishu_{feishu_table_name.lower().replace(' ', '_').replace('-', '_')}"
|
default_table_name = f"feishu_{feishu_table_name.lower().replace(' ', '_').replace('-', '_')}"
|
||||||
@ -270,22 +238,12 @@ class DataSyncService:
|
|||||||
feishu_table_name,
|
feishu_table_name,
|
||||||
final_table_name
|
final_table_name
|
||||||
)
|
)
|
||||||
logger.info(f"使用表名: {final_table_name}")
|
|
||||||
|
|
||||||
# 3. 获取字段信息
|
# 3. 获取字段信息
|
||||||
try:
|
fields = BitableService.get_table_fields(app_token, table_id, access_token)
|
||||||
fields = BitableService.get_table_fields(app_token, table_id, access_token)
|
|
||||||
logger.info(f"获取到 {len(fields)} 个字段")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"获取字段信息失败: {str(e)}")
|
|
||||||
return {
|
|
||||||
'success': False,
|
|
||||||
'error': f"获取字段信息失败: {str(e)}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 4. 创建模型
|
# 4. 创建模型
|
||||||
model = DataSyncService.create_model_from_fields(final_table_name, fields)
|
model = DataSyncService.create_model_from_fields(final_table_name, fields)
|
||||||
logger.info(f"创建模型: {model.__name__}")
|
|
||||||
|
|
||||||
# 5. 检查表是否存在,不存在则创建
|
# 5. 检查表是否存在,不存在则创建
|
||||||
table_exists = DataSyncService.check_table_exists(final_table_name)
|
table_exists = DataSyncService.check_table_exists(final_table_name)
|
||||||
@ -300,92 +258,74 @@ class DataSyncService:
|
|||||||
page_token = None
|
page_token = None
|
||||||
page_size = 100
|
page_size = 100
|
||||||
|
|
||||||
try:
|
while True:
|
||||||
while True:
|
# 查询记录
|
||||||
# 查询记录
|
result = BitableService.search_records(
|
||||||
result = BitableService.search_records(
|
app_token=app_token,
|
||||||
app_token=app_token,
|
table_id=table_id,
|
||||||
table_id=table_id,
|
access_token=access_token,
|
||||||
access_token=access_token,
|
page_size=page_size,
|
||||||
page_size=page_size,
|
page_token=page_token
|
||||||
page_token=page_token
|
)
|
||||||
)
|
|
||||||
|
records = result.get('items', [])
|
||||||
records = result.get('items', [])
|
all_records.extend(records)
|
||||||
all_records.extend(records)
|
|
||||||
|
# 检查是否有更多数据
|
||||||
logger.info(f"获取第 {len(all_records) // page_size + 1} 页数据, 共 {len(records)} 条记录")
|
page_token = result.get('page_token')
|
||||||
|
if not page_token or not records:
|
||||||
# 检查是否有更多数据
|
break
|
||||||
page_token = result.get('page_token')
|
|
||||||
if not page_token or not records:
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"获取记录失败: {str(e)}")
|
|
||||||
return {
|
|
||||||
'success': False,
|
|
||||||
'error': f"获取记录失败: {str(e)}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 7. 同步数据到数据库
|
# 7. 同步数据到数据库
|
||||||
try:
|
with transaction.atomic():
|
||||||
with transaction.atomic():
|
# 统计数据
|
||||||
# 统计数据
|
created_count = 0
|
||||||
created_count = 0
|
updated_count = 0
|
||||||
updated_count = 0
|
|
||||||
|
for record in all_records:
|
||||||
|
record_id = record.get('record_id')
|
||||||
|
fields_data = record.get('fields', {})
|
||||||
|
|
||||||
for record in all_records:
|
# 准备数据
|
||||||
record_id = record.get('record_id')
|
data = {'feishu_record_id': record_id}
|
||||||
fields_data = record.get('fields', {})
|
|
||||||
|
# 处理每个字段的数据
|
||||||
|
for field_name, field_value in fields_data.items():
|
||||||
|
# 将字段名转换为Python合法标识符
|
||||||
|
db_field_name = field_name.lower().replace(' ', '_').replace('-', '_')
|
||||||
|
|
||||||
# 准备数据
|
# 跳过已保留的字段名
|
||||||
data = {'feishu_record_id': record_id}
|
if db_field_name in ['id', 'created_at', 'updated_at']:
|
||||||
|
|
||||||
# 处理每个字段的数据
|
|
||||||
for field_name, field_value in fields_data.items():
|
|
||||||
# 将字段名转换为Python合法标识符
|
|
||||||
db_field_name = field_name.lower().replace(' ', '_').replace('-', '_')
|
|
||||||
|
|
||||||
# 跳过已保留的字段名
|
|
||||||
if db_field_name in ['id', 'created_at', 'updated_at']:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 确保字段存在于模型中
|
|
||||||
if hasattr(model, db_field_name):
|
|
||||||
# 处理不同类型的字段值
|
|
||||||
if isinstance(field_value, (list, dict)):
|
|
||||||
data[db_field_name] = json.dumps(field_value)
|
|
||||||
else:
|
|
||||||
data[db_field_name] = field_value
|
|
||||||
|
|
||||||
# 尝试更新或创建记录
|
|
||||||
try:
|
|
||||||
# 总是使用 feishu_record_id 作为唯一标识符进行更新或创建
|
|
||||||
obj, created = model.objects.update_or_create(
|
|
||||||
feishu_record_id=record_id,
|
|
||||||
defaults=data
|
|
||||||
)
|
|
||||||
|
|
||||||
if created:
|
|
||||||
created_count += 1
|
|
||||||
else:
|
|
||||||
updated_count += 1
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"更新或创建记录失败: {str(e)}, 记录ID: {record_id}")
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# 确保字段存在于模型中
|
||||||
|
if hasattr(model, db_field_name):
|
||||||
|
# 处理不同类型的字段值
|
||||||
|
if isinstance(field_value, (list, dict)):
|
||||||
|
data[db_field_name] = json.dumps(field_value)
|
||||||
|
else:
|
||||||
|
data[db_field_name] = field_value
|
||||||
|
|
||||||
# 更新映射表中的记录数
|
# 尝试更新或创建记录
|
||||||
mapping.total_records = len(all_records)
|
try:
|
||||||
mapping.save(update_fields=['total_records', 'last_sync_time'])
|
# 总是使用 feishu_record_id 作为唯一标识符进行更新或创建
|
||||||
|
obj, created = model.objects.update_or_create(
|
||||||
logger.info(f"数据同步完成: 总记录数={len(all_records)}, 新增={created_count}, 更新={updated_count}")
|
feishu_record_id=record_id,
|
||||||
except Exception as e:
|
defaults=data
|
||||||
logger.error(f"数据同步到数据库失败: {str(e)}")
|
)
|
||||||
return {
|
|
||||||
'success': False,
|
if created:
|
||||||
'error': f"数据同步到数据库失败: {str(e)}"
|
created_count += 1
|
||||||
}
|
else:
|
||||||
|
updated_count += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"更新或创建记录失败: {str(e)}, 记录ID: {record_id}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 更新映射表中的记录数
|
||||||
|
mapping.total_records = len(all_records)
|
||||||
|
mapping.save(update_fields=['total_records', 'last_sync_time'])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
@ -399,8 +339,8 @@ class DataSyncService:
|
|||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"数据同步失败: {str(e)}", exc_info=True)
|
logger.error(f"数据同步失败: {str(e)}")
|
||||||
return {
|
return {
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': f"数据同步失败: {str(e)}"
|
'error': str(e)
|
||||||
}
|
}
|
1
apps/feishu/services/feishu_service.py
Normal file
1
apps/feishu/services/feishu_service.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
@ -14,7 +14,7 @@ from .services.data_sync_service import DataSyncService
|
|||||||
from .services.gmail_extraction_service import GmailExtractionService
|
from .services.gmail_extraction_service import GmailExtractionService
|
||||||
from .services.auto_gmail_conversation_service import AutoGmailConversationService
|
from .services.auto_gmail_conversation_service import AutoGmailConversationService
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from apps.gmail.models import GmailCredential, GmailConversation, AutoReplyConfig
|
from apps.gmail.models import GmailCredential, GmailConversation, AutoReplyConfig
|
||||||
from apps.gmail.services.gmail_service import GmailService
|
from apps.gmail.services.gmail_service import GmailService
|
||||||
from apps.gmail.serializers import AutoReplyConfigSerializer
|
from apps.gmail.serializers import AutoReplyConfigSerializer
|
||||||
from apps.gmail.services.goal_service import get_or_create_goal, get_conversation_summary
|
from apps.gmail.services.goal_service import get_or_create_goal, get_conversation_summary
|
||||||
@ -22,6 +22,7 @@ from apps.chat.models import ChatHistory
|
|||||||
from apps.knowledge_base.models import KnowledgeBase
|
from apps.knowledge_base.models import KnowledgeBase
|
||||||
from apps.user.authentication import CustomTokenAuthentication
|
from apps.user.authentication import CustomTokenAuthentication
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -47,64 +48,23 @@ class FeishuTableRecordsView(APIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 记录原始URL,以便于排查问题
|
|
||||||
logger.info(f"处理查询请求,URL: {table_url}")
|
|
||||||
|
|
||||||
# 从URL中提取app_token和table_id
|
# 从URL中提取app_token和table_id
|
||||||
try:
|
app_token, table_id = BitableService.extract_params_from_url(table_url)
|
||||||
app_token, table_id = BitableService.extract_params_from_url(table_url)
|
|
||||||
logger.info(f"成功解析URL参数: app_token={app_token}, table_id={table_id}")
|
|
||||||
except ValueError as ve:
|
|
||||||
logger.error(f"URL解析失败: {str(ve)}, URL: {table_url}")
|
|
||||||
return Response(
|
|
||||||
{"error": str(ve), "details": "URL格式可能不正确,请参考飞书多维表格文档"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
|
|
||||||
# 验证应用是否有权限访问该多维表格
|
|
||||||
if not BitableService.validate_access(app_token, access_token):
|
|
||||||
logger.warning(f"应用无权限访问多维表格: app_token={app_token}")
|
|
||||||
return Response(
|
|
||||||
{"error": "无权限访问该多维表格,请确认access_token是否有效,以及应用是否已被添加为表格协作者"},
|
|
||||||
status=status.HTTP_403_FORBIDDEN
|
|
||||||
)
|
|
||||||
|
|
||||||
# 先获取一些样本数据,检查我们能否访问多维表格
|
# 先获取一些样本数据,检查我们能否访问多维表格
|
||||||
try:
|
sample_data = BitableService.search_records(
|
||||||
sample_data = BitableService.search_records(
|
app_token=app_token,
|
||||||
app_token=app_token,
|
table_id=table_id,
|
||||||
table_id=table_id,
|
access_token=access_token,
|
||||||
access_token=access_token,
|
filter_exp=filter_exp,
|
||||||
filter_exp=filter_exp,
|
sort=sort,
|
||||||
sort=sort,
|
page_size=page_size,
|
||||||
page_size=page_size,
|
page_token=page_token
|
||||||
page_token=page_token
|
)
|
||||||
)
|
|
||||||
|
return Response(sample_data, status=status.HTTP_200_OK)
|
||||||
logger.info(f"成功获取多维表格数据,记录数: {len(sample_data.get('items', []))}")
|
|
||||||
return Response(sample_data, status=status.HTTP_200_OK)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"查询多维表格数据失败: {str(e)}")
|
|
||||||
# 提供更友好的错误信息
|
|
||||||
error_message = str(e)
|
|
||||||
if "资源不存在" in error_message:
|
|
||||||
return Response(
|
|
||||||
{"error": "表格资源不存在,请检查URL是否正确,以及应用是否有权限"},
|
|
||||||
status=status.HTTP_404_NOT_FOUND
|
|
||||||
)
|
|
||||||
elif "令牌错误" in error_message:
|
|
||||||
return Response(
|
|
||||||
{"error": "访问令牌无效或已过期,请重新获取"},
|
|
||||||
status=status.HTTP_401_UNAUTHORIZED
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return Response(
|
|
||||||
{"error": f"查询多维表格失败: {error_message}"},
|
|
||||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
||||||
)
|
|
||||||
|
|
||||||
except ValueError as ve:
|
except ValueError as ve:
|
||||||
logger.error(f"URL解析或参数验证失败: {str(ve)}")
|
|
||||||
return Response(
|
return Response(
|
||||||
{"error": str(ve), "details": "URL格式可能不正确"},
|
{"error": str(ve), "details": "URL格式可能不正确"},
|
||||||
status=status.HTTP_400_BAD_REQUEST
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
@ -112,7 +72,6 @@ class FeishuTableRecordsView(APIView):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_details = traceback.format_exc()
|
error_details = traceback.format_exc()
|
||||||
logger.error(f"查询飞书多维表格失败: {str(e)}\n{error_details}")
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"error": f"查询飞书多维表格失败: {str(e)}",
|
"error": f"查询飞书多维表格失败: {str(e)}",
|
||||||
@ -143,77 +102,32 @@ class FeishuDataSyncView(APIView):
|
|||||||
|
|
||||||
# 提取参数
|
# 提取参数
|
||||||
try:
|
try:
|
||||||
# 记录原始URL,以便于排查问题
|
app_token, table_id = BitableService.extract_params_from_url(table_url)
|
||||||
logger.info(f"处理数据同步请求,URL: {table_url}")
|
|
||||||
|
|
||||||
# 从URL中提取app_token和table_id
|
|
||||||
try:
|
|
||||||
app_token, table_id = BitableService.extract_params_from_url(table_url)
|
|
||||||
logger.info(f"成功解析URL参数: app_token={app_token}, table_id={table_id}")
|
|
||||||
except ValueError as ve:
|
|
||||||
logger.error(f"URL解析失败: {str(ve)}, URL: {table_url}")
|
|
||||||
return Response(
|
|
||||||
{"error": str(ve), "details": "URL格式可能不正确,请参考飞书多维表格文档"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
|
|
||||||
# 验证应用是否有权限访问该多维表格
|
|
||||||
if not BitableService.validate_access(app_token, access_token):
|
|
||||||
logger.warning(f"应用无权限访问多维表格: app_token={app_token}")
|
|
||||||
return Response(
|
|
||||||
{"error": "无权限访问该多维表格,请确认access_token是否有效,以及应用是否已被添加为表格协作者"},
|
|
||||||
status=status.HTTP_403_FORBIDDEN
|
|
||||||
)
|
|
||||||
|
|
||||||
# 先获取一些样本数据,检查我们能否访问多维表格
|
# 先获取一些样本数据,检查我们能否访问多维表格
|
||||||
try:
|
sample_data = BitableService.search_records(
|
||||||
sample_data = BitableService.search_records(
|
app_token=app_token,
|
||||||
app_token=app_token,
|
table_id=table_id,
|
||||||
table_id=table_id,
|
access_token=access_token,
|
||||||
access_token=access_token,
|
page_size=5
|
||||||
page_size=5
|
)
|
||||||
)
|
|
||||||
|
# 执行数据同步
|
||||||
logger.info(f"成功获取多维表格样本数据,记录数: {len(sample_data.get('items', []))}")
|
result = DataSyncService.sync_data_to_db(
|
||||||
|
table_url=table_url,
|
||||||
# 执行数据同步
|
access_token=access_token,
|
||||||
result = DataSyncService.sync_data_to_db(
|
table_name=table_name,
|
||||||
table_url=table_url,
|
primary_key=primary_key
|
||||||
access_token=access_token,
|
)
|
||||||
table_name=table_name,
|
|
||||||
primary_key=primary_key
|
# 添加样本数据到结果中
|
||||||
)
|
if result.get('success'):
|
||||||
|
result['sample_data'] = sample_data.get('items', [])[:3] # 只返回最多3条样本数据
|
||||||
# 添加样本数据到结果中
|
return Response(result, status=status.HTTP_200_OK)
|
||||||
if result.get('success'):
|
else:
|
||||||
result['sample_data'] = sample_data.get('items', [])[:3] # 只返回最多3条样本数据
|
return Response(result, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
logger.info(f"数据同步成功: {result}")
|
|
||||||
return Response(result, status=status.HTTP_200_OK)
|
|
||||||
else:
|
|
||||||
logger.error(f"数据同步失败: {result.get('error')}")
|
|
||||||
return Response(result, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"数据同步过程失败: {str(e)}")
|
|
||||||
# 提供更友好的错误信息
|
|
||||||
error_message = str(e)
|
|
||||||
if "资源不存在" in error_message:
|
|
||||||
return Response(
|
|
||||||
{"error": "表格资源不存在,请检查URL是否正确,以及应用是否有权限"},
|
|
||||||
status=status.HTTP_404_NOT_FOUND
|
|
||||||
)
|
|
||||||
elif "令牌错误" in error_message:
|
|
||||||
return Response(
|
|
||||||
{"error": "访问令牌无效或已过期,请重新获取"},
|
|
||||||
status=status.HTTP_401_UNAUTHORIZED
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return Response(
|
|
||||||
{"error": f"数据同步失败: {error_message}"},
|
|
||||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
||||||
)
|
|
||||||
|
|
||||||
except ValueError as ve:
|
except ValueError as ve:
|
||||||
logger.error(f"URL解析或参数验证失败: {str(ve)}")
|
|
||||||
return Response(
|
return Response(
|
||||||
{"error": str(ve), "details": "URL格式可能不正确"},
|
{"error": str(ve), "details": "URL格式可能不正确"},
|
||||||
status=status.HTTP_400_BAD_REQUEST
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
@ -222,7 +136,6 @@ class FeishuDataSyncView(APIView):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
error_details = traceback.format_exc()
|
error_details = traceback.format_exc()
|
||||||
logger.error(f"数据同步失败: {str(e)}\n{error_details}")
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"error": f"数据同步失败: {str(e)}",
|
"error": f"数据同步失败: {str(e)}",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# Generated by Django 5.2.1 on 2025-05-28 08:45
|
# Generated by Django 5.2 on 2025-05-13 06:43
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import uuid
|
import django.utils.timezone
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
@ -19,16 +19,14 @@ class Migration(migrations.Migration):
|
|||||||
name='GmailConversation',
|
name='GmailConversation',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('conversation_id', models.CharField(help_text='Unique conversation identifier', max_length=100, unique=True)),
|
('user_email', models.EmailField(help_text='用户Gmail邮箱', max_length=254)),
|
||||||
('user_email', models.EmailField(help_text="User's Gmail address", max_length=254)),
|
('influencer_email', models.EmailField(help_text='达人Gmail邮箱', max_length=254)),
|
||||||
('influencer_email', models.EmailField(help_text="Influencer's email address", max_length=254)),
|
('conversation_id', models.CharField(help_text='关联到chat_history的会话ID', max_length=100, unique=True)),
|
||||||
('title', models.CharField(help_text='Conversation title', max_length=255)),
|
('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)),
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
('updated_at', models.DateTimeField(auto_now=True)),
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
('last_sync_time', models.DateTimeField(blank=True, help_text='Last time conversation was synced with Gmail', null=True)),
|
('is_active', models.BooleanField(default=True)),
|
||||||
('is_active', models.BooleanField(default=True, help_text='Whether this conversation is active')),
|
|
||||||
('has_sent_greeting', models.BooleanField(default=False, help_text='Whether a greeting message has been sent to this conversation')),
|
|
||||||
('metadata', models.JSONField(blank=True, help_text='Additional metadata for the conversation', null=True)),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gmail_conversations', to=settings.AUTH_USER_MODEL)),
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gmail_conversations', to=settings.AUTH_USER_MODEL)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
@ -41,7 +39,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('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)),
|
('email_message_id', models.CharField(help_text='Gmail邮件ID', max_length=100)),
|
||||||
('attachment_id', models.TextField(help_text='Gmail附件的唯一标识符,可能很长')),
|
('attachment_id', models.CharField(help_text='Gmail附件ID', max_length=100)),
|
||||||
('filename', models.CharField(help_text='原始文件名', max_length=255)),
|
('filename', models.CharField(help_text='原始文件名', max_length=255)),
|
||||||
('file_path', 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)),
|
('content_type', models.CharField(help_text='MIME类型', max_length=100)),
|
||||||
@ -55,107 +53,6 @@ class Migration(migrations.Migration):
|
|||||||
'ordering': ['-created_at'],
|
'ordering': ['-created_at'],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
|
||||||
name='ConversationSummary',
|
|
||||||
fields=[
|
|
||||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
||||||
('content', models.TextField(verbose_name='摘要内容')),
|
|
||||||
('last_message_id', models.CharField(blank=True, max_length=255, null=True, verbose_name='最后处理的消息ID或ChatHistory的ID')),
|
|
||||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('updated_at', models.DateTimeField(auto_now=True)),
|
|
||||||
('conversation', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='summary', to='gmail.gmailconversation')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Gmail对话摘要',
|
|
||||||
'verbose_name_plural': 'Gmail对话摘要',
|
|
||||||
'db_table': 'gmail_conversation_summaries',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ProcessedPushNotification',
|
|
||||||
fields=[
|
|
||||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
||||||
('message_id', models.CharField(help_text='Pub/Sub消息ID', max_length=255, unique=True)),
|
|
||||||
('email_address', models.EmailField(help_text='通知关联的Gmail邮箱', max_length=254)),
|
|
||||||
('history_id', models.CharField(help_text='Gmail历史ID', max_length=100)),
|
|
||||||
('processed_at', models.DateTimeField(auto_now_add=True, help_text='处理时间')),
|
|
||||||
('is_successful', models.BooleanField(default=True, help_text='处理是否成功')),
|
|
||||||
('metadata', models.JSONField(blank=True, default=dict, help_text='额外信息')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': '已处理推送通知',
|
|
||||||
'verbose_name_plural': '已处理推送通知',
|
|
||||||
'db_table': 'gmail_processed_push_notifications',
|
|
||||||
'ordering': ['-processed_at'],
|
|
||||||
'indexes': [models.Index(fields=['message_id'], name='gmail_proce_message_912a0c_idx'), models.Index(fields=['email_address', 'history_id'], name='gmail_proce_email_a_2e3770_idx')],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='UnmatchedEmail',
|
|
||||||
fields=[
|
|
||||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
||||||
('message_id', models.CharField(help_text='Gmail邮件ID', max_length=255, unique=True)),
|
|
||||||
('user_id', models.CharField(help_text='用户ID (UUID字符串形式)', max_length=36)),
|
|
||||||
('user_email', models.EmailField(help_text='用户Gmail邮箱', max_length=254)),
|
|
||||||
('from_email', models.EmailField(help_text='发件人邮箱', max_length=254)),
|
|
||||||
('to_email', models.EmailField(help_text='收件人邮箱', max_length=254)),
|
|
||||||
('subject', models.CharField(blank=True, help_text='邮件主题', max_length=500)),
|
|
||||||
('processed_at', models.DateTimeField(auto_now_add=True, help_text='处理时间')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': '未匹配邮件',
|
|
||||||
'verbose_name_plural': '未匹配邮件',
|
|
||||||
'db_table': 'gmail_unmatched_emails',
|
|
||||||
'ordering': ['-processed_at'],
|
|
||||||
'indexes': [models.Index(fields=['message_id'], name='gmail_unmat_message_c1924d_idx'), models.Index(fields=['user_id'], name='gmail_unmat_user_id_ee06fe_idx'), models.Index(fields=['user_email', 'from_email'], name='gmail_unmat_user_em_a096af_idx')],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='UserGoal',
|
|
||||||
fields=[
|
|
||||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
||||||
('description', models.TextField(verbose_name='目标描述')),
|
|
||||||
('status', models.CharField(choices=[('pending', '待处理'), ('in_progress', '进行中'), ('completed', '已完成'), ('failed', '失败')], default='pending', max_length=20, verbose_name='目标状态')),
|
|
||||||
('is_active', models.BooleanField(default=True, verbose_name='是否激活')),
|
|
||||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('updated_at', models.DateTimeField(auto_now=True)),
|
|
||||||
('completion_time', models.DateTimeField(blank=True, null=True, verbose_name='完成时间')),
|
|
||||||
('last_activity_time', models.DateTimeField(blank=True, null=True, verbose_name='最后活动时间')),
|
|
||||||
('metadata', models.JSONField(blank=True, default=dict, help_text='存储额外信息', null=True)),
|
|
||||||
('conversation', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='goals', to='gmail.gmailconversation')),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='goals', to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': '用户目标',
|
|
||||||
'verbose_name_plural': '用户目标',
|
|
||||||
'ordering': ['-updated_at'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='AutoReplyConfig',
|
|
||||||
fields=[
|
|
||||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
||||||
('user_email', models.EmailField(help_text='用户Gmail邮箱', max_length=254)),
|
|
||||||
('influencer_email', models.EmailField(help_text='达人Gmail邮箱', max_length=254)),
|
|
||||||
('is_enabled', models.BooleanField(default=True, help_text='是否启用自动回复')),
|
|
||||||
('goal_description', models.TextField(help_text='AI回复时参考的目标', verbose_name='自动回复的目标描述')),
|
|
||||||
('reply_template', models.TextField(blank=True, help_text='回复模板(可选,为空则由AI生成)', null=True)),
|
|
||||||
('max_replies', models.IntegerField(default=5, help_text='最大自动回复次数')),
|
|
||||||
('current_replies', models.IntegerField(default=0, help_text='当前已自动回复次数')),
|
|
||||||
('last_reply_time', models.DateTimeField(blank=True, null=True)),
|
|
||||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('updated_at', models.DateTimeField(auto_now=True)),
|
|
||||||
('metadata', models.JSONField(blank=True, default=dict, help_text='存储额外信息,如已处理的消息ID等', null=True)),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='auto_reply_configs', to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Gmail自动回复配置',
|
|
||||||
'verbose_name_plural': 'Gmail自动回复配置',
|
|
||||||
'db_table': 'gmail_auto_reply_configs',
|
|
||||||
'ordering': ['-updated_at'],
|
|
||||||
'unique_together': {('user', 'user_email', 'influencer_email')},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='GmailCredential',
|
name='GmailCredential',
|
||||||
fields=[
|
fields=[
|
||||||
@ -166,7 +63,6 @@ class Migration(migrations.Migration):
|
|||||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
('updated_at', models.DateTimeField(auto_now=True)),
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
('is_valid', models.BooleanField(default=True, help_text='Whether the credential is valid')),
|
('is_valid', models.BooleanField(default=True, help_text='Whether the credential is valid')),
|
||||||
('last_history_id', models.CharField(blank=True, help_text='Last processed Gmail history ID', max_length=50, null=True)),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gmail_credentials', to=settings.AUTH_USER_MODEL)),
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gmail_credentials', to=settings.AUTH_USER_MODEL)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-05-13 08:59
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('gmail', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='gmailcredential',
|
||||||
|
name='last_history_id',
|
||||||
|
field=models.CharField(blank=True, help_text='Last processed Gmail history ID', max_length=50, null=True),
|
||||||
|
),
|
||||||
|
]
|
18
apps/gmail/migrations/0003_gmailconversation_metadata.py
Normal file
18
apps/gmail/migrations/0003_gmailconversation_metadata.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-05-13 09:03
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('gmail', '0002_gmailcredential_last_history_id'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='gmailconversation',
|
||||||
|
name='metadata',
|
||||||
|
field=models.JSONField(blank=True, default=dict, help_text='存储额外信息,如已处理的消息ID等', null=True),
|
||||||
|
),
|
||||||
|
]
|
31
apps/gmail/migrations/0004_conversationsummary.py
Normal file
31
apps/gmail/migrations/0004_conversationsummary.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-05-13 10:11
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('gmail', '0003_gmailconversation_metadata'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ConversationSummary',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('content', models.TextField(verbose_name='摘要内容')),
|
||||||
|
('last_message_id', models.CharField(blank=True, max_length=255, null=True, verbose_name='最后处理的消息ID或ChatHistory的ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('conversation', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='summary', to='gmail.gmailconversation')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Gmail对话摘要',
|
||||||
|
'verbose_name_plural': 'Gmail对话摘要',
|
||||||
|
'db_table': 'gmail_conversation_summaries',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
34
apps/gmail/migrations/0005_usergoal.py
Normal file
34
apps/gmail/migrations/0005_usergoal.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-05-14 02:52
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('gmail', '0004_conversationsummary'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UserGoal',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('description', models.TextField(verbose_name='目标描述')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('is_active', models.BooleanField(default=True, verbose_name='是否激活')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='goals', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '用户目标',
|
||||||
|
'verbose_name_plural': '用户目标',
|
||||||
|
'db_table': 'user_goals',
|
||||||
|
'ordering': ['-updated_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,39 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-05-14 09:45
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('gmail', '0005_usergoal'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='usergoal',
|
||||||
|
name='completion_time',
|
||||||
|
field=models.DateTimeField(blank=True, null=True, verbose_name='完成时间'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='usergoal',
|
||||||
|
name='conversation',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='goals', to='gmail.gmailconversation'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='usergoal',
|
||||||
|
name='last_activity_time',
|
||||||
|
field=models.DateTimeField(blank=True, null=True, verbose_name='最后活动时间'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='usergoal',
|
||||||
|
name='metadata',
|
||||||
|
field=models.JSONField(blank=True, default=dict, help_text='存储额外信息', null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='usergoal',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(choices=[('pending', '待处理'), ('in_progress', '进行中'), ('completed', '已完成'), ('failed', '失败')], default='pending', max_length=20, verbose_name='目标状态'),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-05-19 07:03
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('gmail', '0006_usergoal_completion_time_usergoal_conversation_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='gmailattachment',
|
||||||
|
name='attachment_id',
|
||||||
|
field=models.CharField(help_text='Gmail附件的唯一标识符,可能很长', max_length=255),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-05-19 07:12
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('gmail', '0007_alter_gmailattachment_attachment_id'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='gmailattachment',
|
||||||
|
name='attachment_id',
|
||||||
|
field=models.TextField(help_text='Gmail附件的唯一标识符,可能很长'),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,82 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-05-20 06:52
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('gmail', '0008_alter_gmailattachment_attachment_id'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='gmailconversation',
|
||||||
|
name='has_sent_greeting',
|
||||||
|
field=models.BooleanField(default=False, help_text='Whether a greeting message has been sent to this conversation'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='gmailconversation',
|
||||||
|
name='conversation_id',
|
||||||
|
field=models.CharField(help_text='Unique conversation identifier', max_length=100, unique=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='gmailconversation',
|
||||||
|
name='influencer_email',
|
||||||
|
field=models.EmailField(help_text="Influencer's email address", max_length=254),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='gmailconversation',
|
||||||
|
name='is_active',
|
||||||
|
field=models.BooleanField(default=True, help_text='Whether this conversation is active'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='gmailconversation',
|
||||||
|
name='last_sync_time',
|
||||||
|
field=models.DateTimeField(blank=True, help_text='Last time conversation was synced with Gmail', null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='gmailconversation',
|
||||||
|
name='metadata',
|
||||||
|
field=models.JSONField(blank=True, help_text='Additional metadata for the conversation', null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='gmailconversation',
|
||||||
|
name='title',
|
||||||
|
field=models.CharField(help_text='Conversation title', max_length=255),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='gmailconversation',
|
||||||
|
name='user_email',
|
||||||
|
field=models.EmailField(help_text="User's Gmail address", max_length=254),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='AutoReplyConfig',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('user_email', models.EmailField(help_text='用户Gmail邮箱', max_length=254)),
|
||||||
|
('influencer_email', models.EmailField(help_text='达人Gmail邮箱', max_length=254)),
|
||||||
|
('is_enabled', models.BooleanField(default=True, help_text='是否启用自动回复')),
|
||||||
|
('goal_description', models.TextField(help_text='AI回复时参考的目标', verbose_name='自动回复的目标描述')),
|
||||||
|
('reply_template', models.TextField(blank=True, help_text='回复模板(可选,为空则由AI生成)', null=True)),
|
||||||
|
('max_replies', models.IntegerField(default=5, help_text='最大自动回复次数')),
|
||||||
|
('current_replies', models.IntegerField(default=0, help_text='当前已自动回复次数')),
|
||||||
|
('last_reply_time', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('metadata', models.JSONField(blank=True, default=dict, help_text='存储额外信息,如已处理的消息ID等', null=True)),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='auto_reply_configs', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Gmail自动回复配置',
|
||||||
|
'verbose_name_plural': 'Gmail自动回复配置',
|
||||||
|
'db_table': 'gmail_auto_reply_configs',
|
||||||
|
'ordering': ['-updated_at'],
|
||||||
|
'unique_together': {('user', 'user_email', 'influencer_email')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,53 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-05-20 09:21
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('gmail', '0009_gmailconversation_has_sent_greeting_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ProcessedPushNotification',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('message_id', models.CharField(help_text='Pub/Sub消息ID', max_length=255, unique=True)),
|
||||||
|
('email_address', models.EmailField(help_text='通知关联的Gmail邮箱', max_length=254)),
|
||||||
|
('history_id', models.CharField(help_text='Gmail历史ID', max_length=100)),
|
||||||
|
('processed_at', models.DateTimeField(auto_now_add=True, help_text='处理时间')),
|
||||||
|
('is_successful', models.BooleanField(default=True, help_text='处理是否成功')),
|
||||||
|
('metadata', models.JSONField(blank=True, default=dict, help_text='额外信息')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '已处理推送通知',
|
||||||
|
'verbose_name_plural': '已处理推送通知',
|
||||||
|
'db_table': 'gmail_processed_push_notifications',
|
||||||
|
'ordering': ['-processed_at'],
|
||||||
|
'indexes': [models.Index(fields=['message_id'], name='gmail_proce_message_912a0c_idx'), models.Index(fields=['email_address', 'history_id'], name='gmail_proce_email_a_2e3770_idx')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UnmatchedEmail',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('message_id', models.CharField(help_text='Gmail邮件ID', max_length=255, unique=True)),
|
||||||
|
('user_id', models.IntegerField(help_text='用户ID')),
|
||||||
|
('user_email', models.EmailField(help_text='用户Gmail邮箱', max_length=254)),
|
||||||
|
('from_email', models.EmailField(help_text='发件人邮箱', max_length=254)),
|
||||||
|
('to_email', models.EmailField(help_text='收件人邮箱', max_length=254)),
|
||||||
|
('subject', models.CharField(blank=True, help_text='邮件主题', max_length=500)),
|
||||||
|
('processed_at', models.DateTimeField(auto_now_add=True, help_text='处理时间')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '未匹配邮件',
|
||||||
|
'verbose_name_plural': '未匹配邮件',
|
||||||
|
'db_table': 'gmail_unmatched_emails',
|
||||||
|
'ordering': ['-processed_at'],
|
||||||
|
'indexes': [models.Index(fields=['message_id'], name='gmail_unmat_message_c1924d_idx'), models.Index(fields=['user_id'], name='gmail_unmat_user_id_ee06fe_idx'), models.Index(fields=['user_email', 'from_email'], name='gmail_unmat_user_em_a096af_idx')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
18
apps/gmail/migrations/0011_alter_unmatchedemail_user_id.py
Normal file
18
apps/gmail/migrations/0011_alter_unmatchedemail_user_id.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-05-20 09:33
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('gmail', '0010_processedpushnotification_unmatchedemail'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='unmatchedemail',
|
||||||
|
name='user_id',
|
||||||
|
field=models.CharField(help_text='用户ID (UUID字符串形式)', max_length=36),
|
||||||
|
),
|
||||||
|
]
|
17
apps/gmail/migrations/0012_alter_usergoal_table.py
Normal file
17
apps/gmail/migrations/0012_alter_usergoal_table.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-05-21 04:22
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('gmail', '0011_alter_unmatchedemail_user_id'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelTable(
|
||||||
|
name='usergoal',
|
||||||
|
table=None,
|
||||||
|
),
|
||||||
|
]
|
@ -17,6 +17,7 @@ from ..models import GmailCredential, GmailConversation, GmailAttachment, Conver
|
|||||||
from apps.chat.models import ChatHistory
|
from apps.chat.models import ChatHistory
|
||||||
from apps.knowledge_base.models import KnowledgeBase
|
from apps.knowledge_base.models import KnowledgeBase
|
||||||
import requests
|
import requests
|
||||||
|
# from apps.common.services.notification_service import NotificationService
|
||||||
import threading
|
import threading
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
from apps.gmail.views import (
|
from .views import (
|
||||||
GmailAuthInitiateView,
|
GmailAuthInitiateView,
|
||||||
GmailAuthCompleteView,
|
GmailAuthCompleteView,
|
||||||
GmailConversationView,
|
GmailConversationView,
|
||||||
|
@ -35,85 +35,34 @@ class GmailAuthInitiateView(APIView):
|
|||||||
"""
|
"""
|
||||||
API 视图,用于启动 Gmail OAuth2 认证流程。
|
API 视图,用于启动 Gmail OAuth2 认证流程。
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
|
||||||
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
|
authentication_classes = [CustomTokenAuthentication]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""
|
"""
|
||||||
处理 POST 请求,启动 Gmail OAuth2 认证并返回授权 URL。
|
处理 POST 请求,启动 Gmail OAuth2 认证并返回授权 URL。
|
||||||
|
|
||||||
支持两种方式提供客户端密钥:
|
直接使用系统中已配置的客户端密钥文件。
|
||||||
1. 在请求体中提供client_secret_json字段
|
|
||||||
2. 上传名为client_secret_file的JSON文件
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
request: Django REST Framework 请求对象,包含客户端密钥 JSON 数据或文件。
|
request: Django REST Framework 请求对象。
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Response: 包含授权 URL 的 JSON 响应(成功时),或错误信息(失败时)。
|
Response: 包含授权 URL 的 JSON 响应(成功时),或错误信息(失败时)。
|
||||||
|
|
||||||
Status Codes:
|
Status Codes:
|
||||||
200: 成功生成授权 URL。
|
200: 成功生成授权 URL。
|
||||||
400: 请求数据无效。
|
|
||||||
500: 服务器内部错误(如认证服务失败)。
|
500: 服务器内部错误(如认证服务失败)。
|
||||||
"""
|
"""
|
||||||
logger.debug(f"Received auth initiate request: {request.data}")
|
logger.debug(f"Received auth initiate request from user {request.user.id}")
|
||||||
|
|
||||||
# 检查是否是文件上传方式
|
# 直接使用系统中已配置的客户端密钥文件
|
||||||
client_secret_json = None
|
client_secret_path = 'media/secret/client_secret_266164728215-v84lngbp3vgr4ulql01sqkg5vaigf4a5.apps.googleusercontent.com.json'
|
||||||
if 'client_secret_file' in request.FILES:
|
|
||||||
try:
|
|
||||||
# 读取上传的JSON文件内容
|
|
||||||
client_secret_file = request.FILES['client_secret_file']
|
|
||||||
client_secret_json = json.loads(client_secret_file.read().decode('utf-8'))
|
|
||||||
logger.info(f"从上传文件读取到客户端密钥JSON")
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
logger.error(f"解析客户端密钥JSON文件失败: {str(e)}")
|
|
||||||
return Response({
|
|
||||||
'code': 400,
|
|
||||||
'message': f'无效的JSON文件格式: {str(e)}',
|
|
||||||
'data': None
|
|
||||||
}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
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)
|
|
||||||
|
|
||||||
# 如果不是文件上传,则尝试从请求数据中提取JSON
|
|
||||||
if not client_secret_json:
|
|
||||||
serializer = GmailCredentialSerializer(data=request.data, context={'request': request})
|
|
||||||
if serializer.is_valid():
|
|
||||||
try:
|
|
||||||
# 从请求数据中提取客户端密钥 JSON
|
|
||||||
client_secret_json = serializer.validated_data['client_secret_json']
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"未提供客户端密钥JSON: {str(e)}")
|
|
||||||
return Response({
|
|
||||||
'code': 400,
|
|
||||||
'message': '请提供client_secret_json字段或上传client_secret_file文件',
|
|
||||||
'data': None
|
|
||||||
}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
else:
|
|
||||||
# 记录无效请求数据并返回错误响应
|
|
||||||
logger.warning(f"Invalid request data: {serializer.errors}")
|
|
||||||
return Response({
|
|
||||||
'code': 400,
|
|
||||||
'message': '请求数据无效,请提供client_secret_json字段或上传client_secret_file文件',
|
|
||||||
'data': serializer.errors
|
|
||||||
}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
# 如果此时仍然没有client_secret_json,返回错误
|
|
||||||
if not client_secret_json:
|
|
||||||
return Response({
|
|
||||||
'code': 400,
|
|
||||||
'message': '请提供client_secret_json字段或上传client_secret_file文件',
|
|
||||||
'data': None
|
|
||||||
}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# 读取客户端密钥文件
|
||||||
|
with open(client_secret_path, 'r') as f:
|
||||||
|
client_secret_json = json.load(f)
|
||||||
|
|
||||||
# 调用 GmailService 生成授权 URL
|
# 调用 GmailService 生成授权 URL
|
||||||
auth_url = GmailService.initiate_authentication(request.user, client_secret_json)
|
auth_url = GmailService.initiate_authentication(request.user, client_secret_json)
|
||||||
logger.info(f"Generated auth URL for user {request.user.id}")
|
logger.info(f"Generated auth URL for user {request.user.id}")
|
||||||
@ -136,19 +85,16 @@ class GmailAuthCompleteView(APIView):
|
|||||||
"""
|
"""
|
||||||
API 视图,用于完成 Gmail OAuth2 认证流程。
|
API 视图,用于完成 Gmail OAuth2 认证流程。
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
|
||||||
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
|
authentication_classes = [CustomTokenAuthentication]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""
|
"""
|
||||||
处理 POST 请求,使用授权代码完成 Gmail OAuth2 认证并保存凭证。
|
处理 POST 请求,使用授权代码完成 Gmail OAuth2 认证并保存凭证。
|
||||||
|
|
||||||
支持两种方式提供客户端密钥:
|
直接使用系统中已配置的客户端密钥文件。
|
||||||
1. 在请求体中提供client_secret_json字段
|
|
||||||
2. 上传名为client_secret_file的JSON文件
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
request: Django REST Framework 请求对象,包含授权代码和客户端密钥 JSON 或文件。
|
request: Django REST Framework 请求对象,包含授权代码。
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Response: 包含已保存凭证数据的 JSON 响应(成功时),或错误信息(失败时)。
|
Response: 包含已保存凭证数据的 JSON 响应(成功时),或错误信息(失败时)。
|
||||||
@ -158,72 +104,25 @@ class GmailAuthCompleteView(APIView):
|
|||||||
400: 请求数据无效。
|
400: 请求数据无效。
|
||||||
500: 服务器内部错误(如认证失败)。
|
500: 服务器内部错误(如认证失败)。
|
||||||
"""
|
"""
|
||||||
logger.debug(f"Received auth complete request: {request.data}")
|
logger.debug(f"Received auth complete request from user {request.user.id}")
|
||||||
|
|
||||||
# 检查是否是文件上传方式
|
# 直接使用系统中已配置的客户端密钥文件
|
||||||
client_secret_json = None
|
client_secret_path = 'media/secret/client_secret_266164728215-v84lngbp3vgr4ulql01sqkg5vaigf4a5.apps.googleusercontent.com.json'
|
||||||
if 'client_secret_file' in request.FILES:
|
|
||||||
try:
|
|
||||||
# 读取上传的JSON文件内容
|
|
||||||
client_secret_file = request.FILES['client_secret_file']
|
|
||||||
client_secret_json = json.loads(client_secret_file.read().decode('utf-8'))
|
|
||||||
logger.info(f"从上传文件读取到客户端密钥JSON")
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
logger.error(f"解析客户端密钥JSON文件失败: {str(e)}")
|
|
||||||
return Response({
|
|
||||||
'code': 400,
|
|
||||||
'message': f'无效的JSON文件格式: {str(e)}',
|
|
||||||
'data': None
|
|
||||||
}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
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)
|
|
||||||
|
|
||||||
# 获取授权码,无论是哪种方式都需要
|
# 获取授权代码
|
||||||
auth_code = request.data.get('auth_code')
|
auth_code = request.data.get('auth_code')
|
||||||
if not auth_code:
|
if not auth_code:
|
||||||
return Response({
|
return Response({
|
||||||
'code': 400,
|
'code': 400,
|
||||||
'message': '必须提供授权码',
|
'message': '缺少必要的授权代码',
|
||||||
'data': None
|
|
||||||
}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
# 如果不是文件上传,则尝试从请求数据中提取JSON
|
|
||||||
if not client_secret_json:
|
|
||||||
serializer = GmailCredentialSerializer(data=request.data, context={'request': request})
|
|
||||||
if serializer.is_valid():
|
|
||||||
try:
|
|
||||||
# 从请求数据中提取客户端密钥 JSON
|
|
||||||
client_secret_json = serializer.validated_data['client_secret_json']
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"未提供客户端密钥JSON: {str(e)}")
|
|
||||||
return Response({
|
|
||||||
'code': 400,
|
|
||||||
'message': '请提供client_secret_json字段或上传client_secret_file文件',
|
|
||||||
'data': None
|
|
||||||
}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
else:
|
|
||||||
# 记录无效请求数据并返回错误响应
|
|
||||||
logger.warning(f"Invalid request data: {serializer.errors}")
|
|
||||||
return Response({
|
|
||||||
'code': 400,
|
|
||||||
'message': '请求数据无效,请提供client_secret_json字段或上传client_secret_file文件',
|
|
||||||
'data': serializer.errors
|
|
||||||
}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
# 如果此时仍然没有client_secret_json,返回错误
|
|
||||||
if not client_secret_json:
|
|
||||||
return Response({
|
|
||||||
'code': 400,
|
|
||||||
'message': '请提供client_secret_json字段或上传client_secret_file文件',
|
|
||||||
'data': None
|
'data': None
|
||||||
}, status=status.HTTP_400_BAD_REQUEST)
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# 读取客户端密钥文件
|
||||||
|
with open(client_secret_path, 'r') as f:
|
||||||
|
client_secret_json = json.load(f)
|
||||||
|
|
||||||
# 完成认证并保存凭证
|
# 完成认证并保存凭证
|
||||||
credential = GmailService.complete_authentication(request.user, auth_code, client_secret_json)
|
credential = GmailService.complete_authentication(request.user, auth_code, client_secret_json)
|
||||||
# 序列化凭证数据以返回
|
# 序列化凭证数据以返回
|
||||||
@ -525,9 +424,9 @@ class GmailConversationView(APIView):
|
|||||||
"""
|
"""
|
||||||
API视图,用于获取和保存Gmail对话。
|
API视图,用于获取和保存Gmail对话。
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
|
||||||
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
|
authentication_classes = [CustomTokenAuthentication]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""
|
"""
|
||||||
处理POST请求,获取Gmail对话并保存到聊天历史。
|
处理POST请求,获取Gmail对话并保存到聊天历史。
|
||||||
@ -630,9 +529,9 @@ class GmailAttachmentListView(APIView):
|
|||||||
"""
|
"""
|
||||||
API视图,用于获取Gmail附件列表。
|
API视图,用于获取Gmail附件列表。
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
|
||||||
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
|
authentication_classes = [CustomTokenAuthentication]
|
||||||
|
|
||||||
def get(self, request, conversation_id=None):
|
def get(self, request, conversation_id=None):
|
||||||
"""
|
"""
|
||||||
处理GET请求,获取指定对话的附件列表。
|
处理GET请求,获取指定对话的附件列表。
|
||||||
@ -679,9 +578,9 @@ class GmailPubSubView(APIView):
|
|||||||
"""
|
"""
|
||||||
API视图,用于设置Gmail的Pub/Sub实时通知。
|
API视图,用于设置Gmail的Pub/Sub实时通知。
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
|
||||||
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
|
authentication_classes = [CustomTokenAuthentication]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""
|
"""
|
||||||
处理POST请求,为用户的Gmail账户设置Pub/Sub推送通知。
|
处理POST请求,为用户的Gmail账户设置Pub/Sub推送通知。
|
||||||
@ -761,9 +660,9 @@ class GmailPubSubView(APIView):
|
|||||||
credentials = request.user.gmail_credentials.filter(is_valid=True)
|
credentials = request.user.gmail_credentials.filter(is_valid=True)
|
||||||
|
|
||||||
# 构建响应数据
|
# 构建响应数据
|
||||||
user = []
|
accounts = []
|
||||||
for cred in credentials:
|
for cred in credentials:
|
||||||
user.append({
|
accounts.append({
|
||||||
'id': cred.id,
|
'id': cred.id,
|
||||||
'email': cred.email,
|
'email': cred.email,
|
||||||
'is_default': cred.is_default
|
'is_default': cred.is_default
|
||||||
@ -772,7 +671,7 @@ class GmailPubSubView(APIView):
|
|||||||
return Response({
|
return Response({
|
||||||
'code': 200,
|
'code': 200,
|
||||||
'message': '获取账户列表成功',
|
'message': '获取账户列表成功',
|
||||||
'data': {'user': user}
|
'data': {'accounts': accounts}
|
||||||
}, status=status.HTTP_200_OK)
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
@ -780,9 +679,9 @@ class GmailSendEmailView(APIView):
|
|||||||
"""
|
"""
|
||||||
API视图,用于发送Gmail邮件(支持附件)。
|
API视图,用于发送Gmail邮件(支持附件)。
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
|
||||||
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
|
authentication_classes = [CustomTokenAuthentication]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""
|
"""
|
||||||
处理POST请求,发送Gmail邮件。
|
处理POST请求,发送Gmail邮件。
|
||||||
@ -915,9 +814,9 @@ class GmailSendEmailView(APIView):
|
|||||||
credentials = request.user.gmail_credentials.filter(is_valid=True)
|
credentials = request.user.gmail_credentials.filter(is_valid=True)
|
||||||
|
|
||||||
# 构建响应数据
|
# 构建响应数据
|
||||||
user = []
|
accounts = []
|
||||||
for cred in credentials:
|
for cred in credentials:
|
||||||
user.append({
|
accounts.append({
|
||||||
'id': cred.id,
|
'id': cred.id,
|
||||||
'email': cred.email,
|
'email': cred.email,
|
||||||
'is_default': cred.is_default
|
'is_default': cred.is_default
|
||||||
@ -926,7 +825,7 @@ class GmailSendEmailView(APIView):
|
|||||||
return Response({
|
return Response({
|
||||||
'code': 200,
|
'code': 200,
|
||||||
'message': '获取账户列表成功',
|
'message': '获取账户列表成功',
|
||||||
'data': {'user': user}
|
'data': {'accounts': accounts}
|
||||||
}, status=status.HTTP_200_OK)
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
@ -1012,9 +911,9 @@ class GmailConversationSummaryView(APIView):
|
|||||||
"""
|
"""
|
||||||
API视图,用于获取Gmail对话的总结。
|
API视图,用于获取Gmail对话的总结。
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
|
||||||
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
|
authentication_classes = [CustomTokenAuthentication]
|
||||||
|
|
||||||
def get(self, request, conversation_id=None):
|
def get(self, request, conversation_id=None):
|
||||||
"""
|
"""
|
||||||
处理GET请求,获取指定Gmail对话的总结。
|
处理GET请求,获取指定Gmail对话的总结。
|
||||||
@ -1094,7 +993,8 @@ class GmailGoalView(APIView):
|
|||||||
用户目标API - 支持用户为每个对话设置不同的目标
|
用户目标API - 支持用户为每个对话设置不同的目标
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
|
authentication_classes = [CustomTokenAuthentication]
|
||||||
|
|
||||||
def get(self, request, conversation_id=None):
|
def get(self, request, conversation_id=None):
|
||||||
"""
|
"""
|
||||||
获取用户的目标
|
获取用户的目标
|
||||||
@ -1172,6 +1072,13 @@ class GmailGoalView(APIView):
|
|||||||
'data': None
|
'data': None
|
||||||
}, status=status.HTTP_404_NOT_FOUND)
|
}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
# 检查权限
|
||||||
|
# if conversation.user.id != request.user.id:
|
||||||
|
# return Response({
|
||||||
|
# 'code': 403,
|
||||||
|
# 'message': '无权限访问此对话',
|
||||||
|
# 'data': None
|
||||||
|
# }, status=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
# 查找现有目标
|
# 查找现有目标
|
||||||
existing_goal = UserGoal.objects.filter(
|
existing_goal = UserGoal.objects.filter(
|
||||||
@ -1274,7 +1181,8 @@ class SimpleRecommendedReplyView(APIView):
|
|||||||
通过conversation_id一键获取目标、对话摘要和推荐回复
|
通过conversation_id一键获取目标、对话摘要和推荐回复
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
|
authentication_classes = [CustomTokenAuthentication]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""
|
"""
|
||||||
直接通过conversation_id获取推荐回复
|
直接通过conversation_id获取推荐回复
|
||||||
@ -1312,12 +1220,12 @@ class SimpleRecommendedReplyView(APIView):
|
|||||||
'data': None
|
'data': None
|
||||||
}, status=status.HTTP_404_NOT_FOUND)
|
}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
if conversation.user != request.user:
|
# if conversation.user != request.user:
|
||||||
return Response({
|
# return Response({
|
||||||
'code': 403,
|
# 'code': 403,
|
||||||
'message': '无权限访问该对话',
|
# 'message': '无权限访问该对话',
|
||||||
'data': None
|
# 'data': None
|
||||||
}, status=status.HTTP_403_FORBIDDEN)
|
# }, status=status.HTTP_403_FORBIDDEN)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"查找对话失败: {str(e)}")
|
logger.error(f"查找对话失败: {str(e)}")
|
||||||
return Response({
|
return Response({
|
||||||
@ -1399,8 +1307,8 @@ class GmailExportView(APIView):
|
|||||||
"""
|
"""
|
||||||
API视图,用于导出已回复的达人Gmail列表为Excel文件。
|
API视图,用于导出已回复的达人Gmail列表为Excel文件。
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
|
||||||
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
|
authentication_classes = [CustomTokenAuthentication]
|
||||||
|
|
||||||
def get(self, request, format=None):
|
def get(self, request, format=None):
|
||||||
"""
|
"""
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.2.1 on 2025-05-28 08:33
|
# Generated by Django 5.2 on 2025-05-07 03:40
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import uuid
|
import uuid
|
||||||
@ -31,7 +31,8 @@ class Migration(migrations.Migration):
|
|||||||
('update_time', models.DateTimeField(auto_now=True)),
|
('update_time', models.DateTimeField(auto_now=True)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'indexes': [models.Index(fields=['type'], name='knowledge_b_type_7a8dcc_idx'), models.Index(fields=['department'], name='knowledge_b_departm_8f1cf3_idx'), models.Index(fields=['group'], name='knowledge_b_group_ae45d4_idx')],
|
'db_table': 'knowledge_bases',
|
||||||
|
'indexes': [models.Index(fields=['type'], name='knowledge_b_type_0439e7_idx'), models.Index(fields=['department'], name='knowledge_b_departm_e739fd_idx'), models.Index(fields=['group'], name='knowledge_b_group_3dcf34_idx')],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
@ -50,7 +51,8 @@ class Migration(migrations.Migration):
|
|||||||
options={
|
options={
|
||||||
'verbose_name': '知识库文档',
|
'verbose_name': '知识库文档',
|
||||||
'verbose_name_plural': '知识库文档',
|
'verbose_name_plural': '知识库文档',
|
||||||
'indexes': [models.Index(fields=['knowledge_base', 'status'], name='knowledge_b_knowled_6653e6_idx'), models.Index(fields=['document_id'], name='knowledge_b_documen_1fd896_idx'), models.Index(fields=['external_id'], name='knowledge_b_externa_ae1a97_idx')],
|
'db_table': 'knowledge_base_documents',
|
||||||
|
'indexes': [models.Index(fields=['knowledge_base', 'status'], name='knowledge_b_knowled_a4db1b_idx'), models.Index(fields=['document_id'], name='knowledge_b_documen_dab90f_idx'), models.Index(fields=['external_id'], name='knowledge_b_externa_b0060c_idx')],
|
||||||
'unique_together': {('knowledge_base', 'document_id')},
|
'unique_together': {('knowledge_base', 'document_id')},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -0,0 +1,51 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-05-21 04:30
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('knowledge_base', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameIndex(
|
||||||
|
model_name='knowledgebase',
|
||||||
|
new_name='knowledge_b_type_7a8dcc_idx',
|
||||||
|
old_name='knowledge_b_type_0439e7_idx',
|
||||||
|
),
|
||||||
|
migrations.RenameIndex(
|
||||||
|
model_name='knowledgebase',
|
||||||
|
new_name='knowledge_b_departm_8f1cf3_idx',
|
||||||
|
old_name='knowledge_b_departm_e739fd_idx',
|
||||||
|
),
|
||||||
|
migrations.RenameIndex(
|
||||||
|
model_name='knowledgebase',
|
||||||
|
new_name='knowledge_b_group_ae45d4_idx',
|
||||||
|
old_name='knowledge_b_group_3dcf34_idx',
|
||||||
|
),
|
||||||
|
migrations.RenameIndex(
|
||||||
|
model_name='knowledgebasedocument',
|
||||||
|
new_name='knowledge_b_knowled_6653e6_idx',
|
||||||
|
old_name='knowledge_b_knowled_a4db1b_idx',
|
||||||
|
),
|
||||||
|
migrations.RenameIndex(
|
||||||
|
model_name='knowledgebasedocument',
|
||||||
|
new_name='knowledge_b_documen_1fd896_idx',
|
||||||
|
old_name='knowledge_b_documen_dab90f_idx',
|
||||||
|
),
|
||||||
|
migrations.RenameIndex(
|
||||||
|
model_name='knowledgebasedocument',
|
||||||
|
new_name='knowledge_b_externa_ae1a97_idx',
|
||||||
|
old_name='knowledge_b_externa_b0060c_idx',
|
||||||
|
),
|
||||||
|
migrations.AlterModelTable(
|
||||||
|
name='knowledgebase',
|
||||||
|
table=None,
|
||||||
|
),
|
||||||
|
migrations.AlterModelTable(
|
||||||
|
name='knowledgebasedocument',
|
||||||
|
table=None,
|
||||||
|
),
|
||||||
|
]
|
@ -34,6 +34,34 @@ class KnowledgeBase(models.Model):
|
|||||||
create_time = models.DateTimeField(auto_now_add=True)
|
create_time = models.DateTimeField(auto_now_add=True)
|
||||||
update_time = models.DateTimeField(auto_now=True)
|
update_time = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
# def is_owner(self, user):
|
||||||
|
# """检查用户是否是所有者(通过权限表检查)"""
|
||||||
|
# from apps.permissions.models import KnowledgeBasePermission
|
||||||
|
# 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 apps.user.models import User
|
||||||
|
# from apps.permissions.models import KnowledgeBasePermission
|
||||||
|
# # 获取创建者
|
||||||
|
# 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):
|
def calculate_stats(self):
|
||||||
"""计算文档统计信息"""
|
"""计算文档统计信息"""
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
from django.urls import path, include
|
||||||
|
from rest_framework.routers import DefaultRouter
|
||||||
|
from apps.knowledge_base.views import KnowledgeBaseViewSet
|
||||||
|
|
||||||
|
router = DefaultRouter()
|
||||||
|
router.register(r'', KnowledgeBaseViewSet, basename='knowledge-base') # 移除 'knowledge-bases'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', include(router.urls)),
|
||||||
|
]
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user