群发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='是否激活')
|
||||
|
||||
class Meta:
|
||||
db_table = 'brands'
|
||||
verbose_name = '品牌'
|
||||
verbose_name_plural = '品牌'
|
||||
|
||||
@ -66,7 +65,6 @@ class Product(models.Model):
|
||||
is_active = models.BooleanField(default=True, verbose_name='是否激活')
|
||||
|
||||
class Meta:
|
||||
db_table = 'products'
|
||||
verbose_name = '产品'
|
||||
verbose_name_plural = '产品'
|
||||
unique_together = ['brand', 'name']
|
||||
@ -144,7 +142,6 @@ class Campaign(models.Model):
|
||||
is_active = models.BooleanField(default=True, verbose_name='是否激活')
|
||||
|
||||
class Meta:
|
||||
db_table = 'campaigns'
|
||||
verbose_name = '活动'
|
||||
verbose_name_plural = '活动'
|
||||
unique_together = ['brand', 'name']
|
||||
@ -186,7 +183,6 @@ class BrandChatSession(models.Model):
|
||||
is_active = models.BooleanField(default=True, verbose_name='是否激活')
|
||||
|
||||
class Meta:
|
||||
db_table = 'brand_chat_sessions'
|
||||
verbose_name = '品牌聊天会话'
|
||||
verbose_name_plural = '品牌聊天会话'
|
||||
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 uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
@ -15,6 +16,24 @@ class Migration(migrations.Migration):
|
||||
]
|
||||
|
||||
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(
|
||||
name='ChatHistory',
|
||||
fields=[
|
||||
@ -32,8 +51,9 @@ class Migration(migrations.Migration):
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'chat_history',
|
||||
'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.serializers import ChatHistorySerializer
|
||||
from apps.common.services.chat_service import ChatService
|
||||
from apps.user.authentication import CustomTokenAuthentication
|
||||
from apps.chat.services.chat_api import (
|
||||
ExternalAPIError, stream_chat_answer, get_chat_answer, generate_conversation_title,
|
||||
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__)
|
||||
|
||||
# class ChatHistoryViewSet(KnowledgeBasePermissionMixin, viewsets.ModelViewSet):
|
||||
class ChatHistoryViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = [IsAuthenticated]
|
||||
authentication_classes = [CustomTokenAuthentication]
|
||||
@ -463,83 +464,83 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
@action(detail=False, methods=['post'])
|
||||
def hit_test(self, request):
|
||||
"""获取问题与知识库文档的匹配度"""
|
||||
try:
|
||||
data = request.data
|
||||
if 'question' not in data or 'dataset_id_list' not in data or not data['dataset_id_list']:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '缺少必填字段: question 或 dataset_id_list',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
# @action(detail=False, methods=['post'])
|
||||
# def hit_test(self, request):
|
||||
# """获取问题与知识库文档的匹配度"""
|
||||
# try:
|
||||
# data = request.data
|
||||
# if 'question' not in data or 'dataset_id_list' not in data or not data['dataset_id_list']:
|
||||
# return Response({
|
||||
# 'code': 400,
|
||||
# 'message': '缺少必填字段: question 或 dataset_id_list',
|
||||
# 'data': None
|
||||
# }, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
question = data['question']
|
||||
dataset_ids = data['dataset_id_list']
|
||||
if not isinstance(dataset_ids, list):
|
||||
try:
|
||||
dataset_ids = json.loads(dataset_ids)
|
||||
if not isinstance(dataset_ids, list):
|
||||
dataset_ids = [dataset_ids]
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
dataset_ids = [dataset_ids]
|
||||
# question = data['question']
|
||||
# dataset_ids = data['dataset_id_list']
|
||||
# if not isinstance(dataset_ids, list):
|
||||
# try:
|
||||
# dataset_ids = json.loads(dataset_ids)
|
||||
# if not isinstance(dataset_ids, list):
|
||||
# dataset_ids = [dataset_ids]
|
||||
# except (json.JSONDecodeError, TypeError):
|
||||
# dataset_ids = [dataset_ids]
|
||||
|
||||
external_id_list = []
|
||||
for kb_id in dataset_ids:
|
||||
kb = KnowledgeBase.objects.filter(id=kb_id).first()
|
||||
if not kb:
|
||||
return Response({
|
||||
'code': 404,
|
||||
'message': f'知识库不存在: {kb_id}',
|
||||
'data': None
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
if not self.check_knowledge_base_permission(kb, request.user, 'read'):
|
||||
return Response({
|
||||
'code': 403,
|
||||
'message': f'无权访问知识库: {kb.name}',
|
||||
'data': None
|
||||
}, status=status.HTTP_403_FORBIDDEN)
|
||||
if kb.external_id:
|
||||
external_id_list.append(str(kb.external_id))
|
||||
# external_id_list = []
|
||||
# for kb_id in dataset_ids:
|
||||
# kb = KnowledgeBase.objects.filter(id=kb_id).first()
|
||||
# if not kb:
|
||||
# return Response({
|
||||
# 'code': 404,
|
||||
# 'message': f'知识库不存在: {kb_id}',
|
||||
# 'data': None
|
||||
# }, status=status.HTTP_404_NOT_FOUND)
|
||||
# if not self.check_knowledge_base_permission(kb, request.user, 'read'):
|
||||
# return Response({
|
||||
# 'code': 403,
|
||||
# 'message': f'无权访问知识库: {kb.name}',
|
||||
# 'data': None
|
||||
# }, status=status.HTTP_403_FORBIDDEN)
|
||||
# if kb.external_id:
|
||||
# external_id_list.append(str(kb.external_id))
|
||||
|
||||
if not external_id_list:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '没有有效的知识库external_id',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
# if not external_id_list:
|
||||
# return Response({
|
||||
# 'code': 400,
|
||||
# 'message': '没有有效的知识库external_id',
|
||||
# 'data': None
|
||||
# }, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
all_documents = []
|
||||
for dataset_id in external_id_list:
|
||||
try:
|
||||
doc_info = get_hit_test_documents(dataset_id, question)
|
||||
if doc_info:
|
||||
all_documents.extend(doc_info)
|
||||
except ExternalAPIError as e:
|
||||
logger.error(f"调用hit_test失败: 知识库ID={dataset_id}, 错误={str(e)}")
|
||||
continue # 宽松处理,跳过失败的知识库
|
||||
# all_documents = []
|
||||
# for dataset_id in external_id_list:
|
||||
# try:
|
||||
# doc_info = get_hit_test_documents(dataset_id, question)
|
||||
# if doc_info:
|
||||
# all_documents.extend(doc_info)
|
||||
# except ExternalAPIError as e:
|
||||
# logger.error(f"调用hit_test失败: 知识库ID={dataset_id}, 错误={str(e)}")
|
||||
# continue # 宽松处理,跳过失败的知识库
|
||||
|
||||
all_documents = sorted(all_documents, key=lambda x: x.get('similarity', 0), reverse=True)
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': '成功',
|
||||
'data': {
|
||||
'question': question,
|
||||
'matched_documents': all_documents,
|
||||
'total_count': len(all_documents)
|
||||
}
|
||||
})
|
||||
# all_documents = sorted(all_documents, key=lambda x: x.get('similarity', 0), reverse=True)
|
||||
# return Response({
|
||||
# 'code': 200,
|
||||
# 'message': '成功',
|
||||
# 'data': {
|
||||
# 'question': question,
|
||||
# 'matched_documents': all_documents,
|
||||
# 'total_count': len(all_documents)
|
||||
# }
|
||||
# })
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"hit_test接口调用失败: {str(e)}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': f'hit_test接口调用失败: {str(e)}',
|
||||
'data': None
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
# except Exception as e:
|
||||
# logger.error(f"hit_test接口调用失败: {str(e)}")
|
||||
# import traceback
|
||||
# logger.error(traceback.format_exc())
|
||||
# return Response({
|
||||
# 'code': 500,
|
||||
# 'message': f'hit_test接口调用失败: {str(e)}',
|
||||
# 'data': None
|
||||
# }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
def _highlight_keyword(self, text, keyword):
|
||||
"""高亮关键词"""
|
||||
|
@ -6,11 +6,12 @@ from django.db import transaction
|
||||
from apps.user.models import User
|
||||
from apps.knowledge_base.models import KnowledgeBase
|
||||
from apps.chat.models import ChatHistory
|
||||
|
||||
# from apps.permissions.services.permission_service import KnowledgeBasePermissionMixin
|
||||
from django.db.models import Q
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# class ChatService(KnowledgeBasePermissionMixin):
|
||||
class ChatService():
|
||||
@transaction.atomic
|
||||
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
|
||||
import logging
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from apps.user.models import User
|
||||
from apps.knowledge_base.models import KnowledgeBase
|
||||
# # apps/common/services/permission_service.py
|
||||
# import logging
|
||||
# from django.db import transaction
|
||||
# from django.db.models import Q
|
||||
# from django.utils import timezone
|
||||
# from rest_framework.exceptions import ValidationError
|
||||
# from apps.user.models import User
|
||||
# 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 can_manage_knowledge_base(self, user, knowledge_base):
|
||||
"""检查用户是否是知识库的创建者"""
|
||||
return str(knowledge_base.user_id) == str(user.id)
|
||||
# def check_extend_permission(self, permission, user):
|
||||
# """检查是否有权限延长权限有效期"""
|
||||
# knowledge_base = permission.knowledge_base
|
||||
# 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):
|
||||
"""检查是否有权限延长权限有效期"""
|
||||
knowledge_base = permission.knowledge_base
|
||||
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 create_permission_request(self, user, validated_data, notification_service):
|
||||
# """创建权限申请并发送通知"""
|
||||
# knowledge_base = validated_data['knowledge_base']
|
||||
# if str(knowledge_base.user_id) == str(user.id):
|
||||
# raise ValidationError({
|
||||
# "code": 400,
|
||||
# "message": "您是此知识库的创建者,无需申请权限",
|
||||
# "data": None
|
||||
# })
|
||||
|
||||
def create_permission_request(self, user, validated_data, notification_service):
|
||||
"""创建权限申请并发送通知"""
|
||||
knowledge_base = validated_data['knowledge_base']
|
||||
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)
|
||||
# requested_permissions = validated_data.get('permissions', {})
|
||||
# expires_at = validated_data.get('expires_at')
|
||||
|
||||
approver = User.objects.get(id=knowledge_base.user_id)
|
||||
requested_permissions = validated_data.get('permissions', {})
|
||||
expires_at = validated_data.get('expires_at')
|
||||
# if not any([requested_permissions.get('can_read'),
|
||||
# requested_permissions.get('can_edit'),
|
||||
# requested_permissions.get('can_delete')]):
|
||||
# raise ValidationError("至少需要申请一种权限(读/改/删)")
|
||||
|
||||
if not any([requested_permissions.get('can_read'),
|
||||
requested_permissions.get('can_edit'),
|
||||
requested_permissions.get('can_delete')]):
|
||||
raise ValidationError("至少需要申请一种权限(读/改/删)")
|
||||
# if not expires_at:
|
||||
# raise ValidationError("请指定权限到期时间")
|
||||
|
||||
if not expires_at:
|
||||
raise ValidationError("请指定权限到期时间")
|
||||
# existing_request = Permission.objects.filter(
|
||||
# knowledge_base=knowledge_base,
|
||||
# applicant=user,
|
||||
# status='pending'
|
||||
# ).first()
|
||||
# if existing_request:
|
||||
# raise ValidationError("您已有一个待处理的权限申请")
|
||||
|
||||
existing_request = Permission.objects.filter(
|
||||
knowledge_base=knowledge_base,
|
||||
applicant=user,
|
||||
status='pending'
|
||||
).first()
|
||||
if existing_request:
|
||||
raise ValidationError("您已有一个待处理的权限申请")
|
||||
# existing_permission = Permission.objects.filter(
|
||||
# knowledge_base=knowledge_base,
|
||||
# applicant=user,
|
||||
# status='approved',
|
||||
# expires_at__gt=timezone.now()
|
||||
# ).first()
|
||||
# if existing_permission:
|
||||
# raise ValidationError("您已有此知识库的访问权限")
|
||||
|
||||
existing_permission = Permission.objects.filter(
|
||||
knowledge_base=knowledge_base,
|
||||
applicant=user,
|
||||
status='approved',
|
||||
expires_at__gt=timezone.now()
|
||||
).first()
|
||||
if existing_permission:
|
||||
raise ValidationError("您已有此知识库的访问权限")
|
||||
# with transaction.atomic():
|
||||
# permission = Permission.objects.create(
|
||||
# knowledge_base=knowledge_base,
|
||||
# applicant=user,
|
||||
# approver=approver,
|
||||
# permissions=requested_permissions,
|
||||
# expires_at=expires_at,
|
||||
# status='pending'
|
||||
# )
|
||||
|
||||
with transaction.atomic():
|
||||
permission = Permission.objects.create(
|
||||
knowledge_base=knowledge_base,
|
||||
applicant=user,
|
||||
approver=approver,
|
||||
permissions=requested_permissions,
|
||||
expires_at=expires_at,
|
||||
status='pending'
|
||||
)
|
||||
# permission_types = []
|
||||
# if requested_permissions.get('can_read'):
|
||||
# permission_types.append('读取')
|
||||
# if requested_permissions.get('can_edit'):
|
||||
# permission_types.append('编辑')
|
||||
# if requested_permissions.get('can_delete'):
|
||||
# permission_types.append('删除')
|
||||
# permission_str = '、'.join(permission_types)
|
||||
|
||||
permission_types = []
|
||||
if requested_permissions.get('can_read'):
|
||||
permission_types.append('读取')
|
||||
if requested_permissions.get('can_edit'):
|
||||
permission_types.append('编辑')
|
||||
if requested_permissions.get('can_delete'):
|
||||
permission_types.append('删除')
|
||||
permission_str = '、'.join(permission_types)
|
||||
# notification_service.send_notification(
|
||||
# user=approver,
|
||||
# title="新的权限申请",
|
||||
# content=f"用户 {user.name} 申请了知识库 '{knowledge_base.name}' 的{permission_str}权限",
|
||||
# notification_type="permission_request",
|
||||
# related_object_id=str(permission.id)
|
||||
# )
|
||||
|
||||
notification_service.send_notification(
|
||||
user=approver,
|
||||
title="新的权限申请",
|
||||
content=f"用户 {user.name} 申请了知识库 '{knowledge_base.name}' 的{permission_str}权限",
|
||||
notification_type="permission_request",
|
||||
related_object_id=str(permission.id)
|
||||
)
|
||||
# return permission
|
||||
|
||||
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):
|
||||
"""审批权限申请"""
|
||||
if not self.can_manage_knowledge_base(user, permission.knowledge_base):
|
||||
raise ValidationError({
|
||||
'code': 403,
|
||||
'message': '只有知识库创建者可以审批此申请',
|
||||
'data': None
|
||||
})
|
||||
# with transaction.atomic():
|
||||
# permission.status = 'approved'
|
||||
# permission.approver = user
|
||||
# permission.response_message = response_message
|
||||
# permission.save()
|
||||
|
||||
with transaction.atomic():
|
||||
permission.status = 'approved'
|
||||
permission.approver = user
|
||||
permission.response_message = response_message
|
||||
permission.save()
|
||||
# kb_permission = KBPermissionModel.objects.filter(
|
||||
# knowledge_base=permission.knowledge_base,
|
||||
# user=permission.applicant
|
||||
# ).first()
|
||||
|
||||
kb_permission = KBPermissionModel.objects.filter(
|
||||
knowledge_base=permission.knowledge_base,
|
||||
user=permission.applicant
|
||||
).first()
|
||||
# if kb_permission:
|
||||
# kb_permission.can_read = permission.permissions.get('can_read', False)
|
||||
# kb_permission.can_edit = permission.permissions.get('can_edit', False)
|
||||
# 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:
|
||||
kb_permission.can_read = permission.permissions.get('can_read', False)
|
||||
kb_permission.can_edit = permission.permissions.get('can_edit', False)
|
||||
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}")
|
||||
# notification_service.send_notification(
|
||||
# user=permission.applicant,
|
||||
# title="权限申请已通过",
|
||||
# content=f"您对知识库 '{permission.knowledge_base.name}' 的权限申请已通过",
|
||||
# notification_type="permission_approved",
|
||||
# related_object_id=str(permission.id)
|
||||
# )
|
||||
|
||||
notification_service.send_notification(
|
||||
user=permission.applicant,
|
||||
title="权限申请已通过",
|
||||
content=f"您对知识库 '{permission.knowledge_base.name}' 的权限申请已通过",
|
||||
notification_type="permission_approved",
|
||||
related_object_id=str(permission.id)
|
||||
)
|
||||
# return permission
|
||||
|
||||
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 not self.can_manage_knowledge_base(user, permission.knowledge_base):
|
||||
raise ValidationError({
|
||||
'code': 403,
|
||||
'message': '只有知识库创建者可以审批此申请',
|
||||
'data': None
|
||||
})
|
||||
# if permission.status != 'pending':
|
||||
# raise ValidationError({
|
||||
# 'code': 400,
|
||||
# 'message': '该申请已被处理',
|
||||
# 'data': None
|
||||
# })
|
||||
|
||||
if permission.status != 'pending':
|
||||
raise ValidationError({
|
||||
'code': 400,
|
||||
'message': '该申请已被处理',
|
||||
'data': None
|
||||
})
|
||||
# if not response_message:
|
||||
# raise ValidationError({
|
||||
# 'code': 400,
|
||||
# 'message': '请填写拒绝原因',
|
||||
# 'data': None
|
||||
# })
|
||||
|
||||
if not response_message:
|
||||
raise ValidationError({
|
||||
'code': 400,
|
||||
'message': '请填写拒绝原因',
|
||||
'data': None
|
||||
})
|
||||
# with transaction.atomic():
|
||||
# permission.status = 'rejected'
|
||||
# permission.approver = user
|
||||
# permission.response_message = response_message
|
||||
# permission.save()
|
||||
|
||||
with transaction.atomic():
|
||||
permission.status = 'rejected'
|
||||
permission.approver = user
|
||||
permission.response_message = response_message
|
||||
permission.save()
|
||||
# notification_service.send_notification(
|
||||
# user=permission.applicant,
|
||||
# title="权限申请已拒绝",
|
||||
# content=f"您对知识库 '{permission.knowledge_base.name}' 的权限申请已被拒绝\n拒绝原因:{response_message}",
|
||||
# notification_type="permission_rejected",
|
||||
# related_object_id=str(permission.id)
|
||||
# )
|
||||
|
||||
notification_service.send_notification(
|
||||
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
|
||||
|
||||
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 self.check_extend_permission(permission, user):
|
||||
raise ValidationError({
|
||||
"code": 403,
|
||||
"message": "您没有权限延长此权限",
|
||||
"data": None
|
||||
})
|
||||
# if not new_expires_at:
|
||||
# raise ValidationError({
|
||||
# "code": 400,
|
||||
# "message": "请设置新的过期时间",
|
||||
# "data": None
|
||||
# })
|
||||
|
||||
if not new_expires_at:
|
||||
raise ValidationError({
|
||||
"code": 400,
|
||||
"message": "请设置新的过期时间",
|
||||
"data": None
|
||||
})
|
||||
# try:
|
||||
# new_expires_at = timezone.datetime.strptime(new_expires_at, '%Y-%m-%dT%H:%M:%SZ')
|
||||
# new_expires_at = timezone.make_aware(new_expires_at)
|
||||
# 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
|
||||
# })
|
||||
|
||||
try:
|
||||
new_expires_at = timezone.datetime.strptime(new_expires_at, '%Y-%m-%dT%H:%M:%SZ')
|
||||
new_expires_at = timezone.make_aware(new_expires_at)
|
||||
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():
|
||||
# permission.expires_at = new_expires_at
|
||||
# permission.save()
|
||||
|
||||
with transaction.atomic():
|
||||
permission.expires_at = new_expires_at
|
||||
permission.save()
|
||||
# kb_permission = KBPermissionModel.objects.get(
|
||||
# knowledge_base=permission.knowledge_base,
|
||||
# user=permission.applicant
|
||||
# )
|
||||
# kb_permission.expires_at = new_expires_at
|
||||
# kb_permission.save()
|
||||
|
||||
kb_permission = KBPermissionModel.objects.get(
|
||||
knowledge_base=permission.knowledge_base,
|
||||
user=permission.applicant
|
||||
)
|
||||
kb_permission.expires_at = new_expires_at
|
||||
kb_permission.save()
|
||||
# notification_service.send_notification(
|
||||
# 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)
|
||||
# )
|
||||
|
||||
notification_service.send_notification(
|
||||
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
|
||||
|
||||
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 admin_user.role != 'admin':
|
||||
raise ValidationError({
|
||||
'code': 403,
|
||||
'message': '只有管理员可以直接修改权限',
|
||||
'data': None
|
||||
})
|
||||
# if not all([user_id, knowledge_base_id, permissions]):
|
||||
# raise ValidationError({
|
||||
# 'code': 400,
|
||||
# 'message': '缺少必要参数',
|
||||
# 'data': None
|
||||
# })
|
||||
|
||||
if not all([user_id, knowledge_base_id, permissions]):
|
||||
raise ValidationError({
|
||||
'code': 400,
|
||||
'message': '缺少必要参数',
|
||||
'data': None
|
||||
})
|
||||
# required_permission_fields = ['can_read', 'can_edit', 'can_delete']
|
||||
# if not all(field in permissions for field in required_permission_fields):
|
||||
# raise ValidationError({
|
||||
# 'code': 400,
|
||||
# 'message': '权限参数格式错误,必须包含 can_read、can_edit、can_delete',
|
||||
# 'data': None
|
||||
# })
|
||||
|
||||
required_permission_fields = ['can_read', 'can_edit', 'can_delete']
|
||||
if not all(field in permissions for field in required_permission_fields):
|
||||
raise ValidationError({
|
||||
'code': 400,
|
||||
'message': '权限参数格式错误,必须包含 can_read、can_edit、can_delete',
|
||||
'data': None
|
||||
})
|
||||
# try:
|
||||
# user = User.objects.get(id=user_id)
|
||||
# knowledge_base = KnowledgeBase.objects.get(id=knowledge_base_id)
|
||||
# except User.DoesNotExist:
|
||||
# raise ValidationError({
|
||||
# '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:
|
||||
user = User.objects.get(id=user_id)
|
||||
knowledge_base = KnowledgeBase.objects.get(id=knowledge_base_id)
|
||||
except User.DoesNotExist:
|
||||
raise ValidationError({
|
||||
'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):
|
||||
# raise ValidationError({
|
||||
# 'code': 403,
|
||||
# 'message': '不能修改其他用户的私有知识库权限',
|
||||
# 'data': None
|
||||
# })
|
||||
|
||||
if knowledge_base.type == 'private' and str(knowledge_base.user_id) != str(user.id):
|
||||
raise ValidationError({
|
||||
'code': 403,
|
||||
'message': '不能修改其他用户的私有知识库权限',
|
||||
'data': None
|
||||
})
|
||||
# if user.role == 'member' and permissions.get('can_delete'):
|
||||
# raise ValidationError({
|
||||
# 'code': 400,
|
||||
# 'message': '普通成员不能获得删除权限',
|
||||
# 'data': None
|
||||
# })
|
||||
|
||||
if user.role == 'member' and permissions.get('can_delete'):
|
||||
raise ValidationError({
|
||||
'code': 400,
|
||||
'message': '普通成员不能获得删除权限',
|
||||
'data': None
|
||||
})
|
||||
# expires_at = None
|
||||
# if expires_at_str:
|
||||
# try:
|
||||
# expires_at = timezone.datetime.strptime(expires_at_str, '%Y-%m-%dT%H:%M:%SZ')
|
||||
# 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
|
||||
if expires_at_str:
|
||||
try:
|
||||
expires_at = timezone.datetime.strptime(expires_at_str, '%Y-%m-%dT%H:%M:%SZ')
|
||||
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
|
||||
})
|
||||
# with transaction.atomic():
|
||||
# permission, created = KBPermissionModel.objects.update_or_create(
|
||||
# user=user,
|
||||
# knowledge_base=knowledge_base,
|
||||
# defaults={
|
||||
# 'can_read': permissions.get('can_read', False),
|
||||
# 'can_edit': permissions.get('can_edit', False),
|
||||
# 'can_delete': permissions.get('can_delete', False),
|
||||
# 'granted_by': admin_user,
|
||||
# 'status': 'active',
|
||||
# 'expires_at': expires_at
|
||||
# }
|
||||
# )
|
||||
|
||||
with transaction.atomic():
|
||||
permission, created = KBPermissionModel.objects.update_or_create(
|
||||
user=user,
|
||||
knowledge_base=knowledge_base,
|
||||
defaults={
|
||||
'can_read': permissions.get('can_read', False),
|
||||
'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(
|
||||
# user=user,
|
||||
# title="知识库权限更新",
|
||||
# content=f"管理员已{created and '授予' or '更新'}您对知识库 '{knowledge_base.name}' 的权限",
|
||||
# notification_type="permission_updated",
|
||||
# related_object_id=str(permission.id)
|
||||
# )
|
||||
|
||||
notification_service.send_notification(
|
||||
user=user,
|
||||
title="知识库权限更新",
|
||||
content=f"管理员已{created and '授予' or '更新'}您对知识库 '{knowledge_base.name}' 的权限",
|
||||
notification_type="permission_updated",
|
||||
related_object_id=str(permission.id)
|
||||
)
|
||||
|
||||
return permission, created
|
||||
# return permission, created
|
||||
|
@ -1,15 +1,11 @@
|
||||
import json
|
||||
import re
|
||||
import logging
|
||||
import requests
|
||||
from urllib.parse import urljoin, urlparse, parse_qs
|
||||
from urllib.parse import urljoin
|
||||
|
||||
# 基础URL地址
|
||||
BASE_API_URL = "https://open.feishu.cn/open-apis/"
|
||||
|
||||
# 获取日志记录器
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BitableService:
|
||||
"""
|
||||
@ -36,11 +32,6 @@ class BitableService:
|
||||
if headers is None:
|
||||
headers = {}
|
||||
|
||||
# 记录请求信息,避免记录敏感信息
|
||||
logger.info(f"请求飞书API: {method} {full_url}")
|
||||
if params:
|
||||
logger.info(f"请求参数: {params}")
|
||||
|
||||
response = requests.request(
|
||||
method=method,
|
||||
url=full_url,
|
||||
@ -52,29 +43,7 @@ class BitableService:
|
||||
# 检查响应
|
||||
if not response.ok:
|
||||
error_msg = f"API 请求失败: {response.status_code}, 响应: {response.text}"
|
||||
logger.error(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
|
||||
|
||||
print(error_msg)
|
||||
raise Exception(error_msg)
|
||||
|
||||
return response.json()
|
||||
@ -83,11 +52,6 @@ class BitableService:
|
||||
def extract_params_from_url(table_url):
|
||||
"""
|
||||
从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:
|
||||
table_url: 飞书多维表格URL
|
||||
@ -98,76 +62,13 @@ class BitableService:
|
||||
Raises:
|
||||
ValueError: 如果无法从URL中提取必要参数
|
||||
"""
|
||||
# 记录原始URL
|
||||
logger.info(f"解析多维表格URL: {table_url}")
|
||||
app_token_match = re.search(r'base/([^?]+)', table_url)
|
||||
table_id_match = re.search(r'table=([^&]+)', table_url)
|
||||
|
||||
# 处理URL中的多余空格
|
||||
table_url = table_url.strip()
|
||||
if not app_token_match or not table_id_match:
|
||||
raise ValueError("无法从URL中提取必要参数,请确认URL格式正确")
|
||||
|
||||
# 解析URL
|
||||
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
|
||||
return app_token_match.group(1), table_id_match.group(1)
|
||||
|
||||
@staticmethod
|
||||
def get_metadata(app_token, table_id, access_token):
|
||||
@ -183,10 +84,6 @@ class BitableService:
|
||||
dict: 表格元数据
|
||||
"""
|
||||
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}"
|
||||
headers = {
|
||||
@ -199,18 +96,16 @@ class BitableService:
|
||||
|
||||
# 检查响应
|
||||
if response and "code" in response and response["code"] == 0:
|
||||
metadata = response.get("data", {}).get("table", {})
|
||||
logger.info(f"成功获取多维表格元数据: {metadata.get('name', table_id)}")
|
||||
return metadata
|
||||
return response.get("data", {}).get("table", {})
|
||||
|
||||
# 发生错误
|
||||
error_msg = f"获取多维表格元数据失败: {json.dumps(response)}"
|
||||
logger.error(error_msg)
|
||||
print(error_msg)
|
||||
raise Exception(error_msg)
|
||||
|
||||
except Exception as e:
|
||||
# 如果正常API调用失败,使用替代方法
|
||||
logger.error(f"获取多维表格元数据失败: {str(e)}")
|
||||
print(f"获取多维表格元数据失败: {str(e)}")
|
||||
# 简单返回一个基本名称
|
||||
return {
|
||||
"name": f"table_{table_id}",
|
||||
@ -238,8 +133,6 @@ class BitableService:
|
||||
Exception: 查询失败时抛出异常
|
||||
"""
|
||||
try:
|
||||
logger.info(f"查询多维表格记录: app_token={app_token}, table_id={table_id}")
|
||||
|
||||
# 构造请求URL
|
||||
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:
|
||||
result = response.get("data", {})
|
||||
logger.info(f"查询成功,获取到 {len(result.get('items', []))} 条记录")
|
||||
return result
|
||||
return response.get("data", {})
|
||||
|
||||
# 发生错误
|
||||
error_msg = f"查询飞书多维表格失败: {json.dumps(response)}"
|
||||
logger.error(error_msg)
|
||||
print(error_msg)
|
||||
raise Exception(error_msg)
|
||||
|
||||
except Exception as e:
|
||||
# 记录详细错误
|
||||
logger.error(f"查询飞书多维表格发生错误: {str(e)}")
|
||||
print(f"查询飞书多维表格发生错误: {str(e)}")
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
@ -294,8 +185,6 @@ class BitableService:
|
||||
list: 字段信息列表
|
||||
"""
|
||||
try:
|
||||
logger.info(f"获取多维表格字段: app_token={app_token}, table_id={table_id}")
|
||||
|
||||
# 构造请求
|
||||
url = f"bitable/v1/apps/{app_token}/tables/{table_id}/fields"
|
||||
headers = {
|
||||
@ -309,20 +198,17 @@ class BitableService:
|
||||
|
||||
# 检查响应
|
||||
if response and "code" in response and response["code"] == 0:
|
||||
fields = response.get("data", {}).get("items", [])
|
||||
logger.info(f"成功获取到 {len(fields)} 个字段")
|
||||
return fields
|
||||
return response.get("data", {}).get("items", [])
|
||||
|
||||
# 发生错误
|
||||
error_msg = f"获取多维表格字段失败: {json.dumps(response)}"
|
||||
logger.error(error_msg)
|
||||
print(error_msg)
|
||||
raise Exception(error_msg)
|
||||
|
||||
except Exception as e:
|
||||
# 记录详细错误
|
||||
logger.error(f"获取字段信息失败: {str(e)}")
|
||||
print(f"获取字段信息失败: {str(e)}")
|
||||
# 如果获取失败,返回一个基本字段集
|
||||
logger.warning("返回默认字段集")
|
||||
return [
|
||||
{"field_name": "title", "type": "text", "property": {}},
|
||||
{"field_name": "description", "type": "text", "property": {}},
|
||||
|
@ -201,42 +201,12 @@ class DataSyncService:
|
||||
dict: 同步结果
|
||||
"""
|
||||
try:
|
||||
# 记录原始URL,便于排查问题
|
||||
logger.info(f"开始数据同步,URL: {table_url}")
|
||||
|
||||
# 提取参数
|
||||
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 {
|
||||
'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是否有效,以及应用是否已被添加为表格协作者"
|
||||
}
|
||||
app_token, table_id = BitableService.extract_params_from_url(table_url)
|
||||
|
||||
# 1. 获取表格元数据
|
||||
try:
|
||||
metadata = BitableService.get_metadata(app_token, table_id, access_token)
|
||||
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)}"
|
||||
}
|
||||
metadata = BitableService.get_metadata(app_token, table_id, access_token)
|
||||
feishu_table_name = metadata.get('name', f'table_{table_id}')
|
||||
|
||||
# 2. 获取或创建表格映射
|
||||
if auto_sync:
|
||||
@ -244,7 +214,6 @@ class DataSyncService:
|
||||
try:
|
||||
mapping = FeishuTableMapping.objects.get(app_token=app_token, table_id=table_id)
|
||||
final_table_name = mapping.table_name
|
||||
logger.info(f"使用已有映射: {final_table_name}")
|
||||
except FeishuTableMapping.DoesNotExist:
|
||||
# 如果找不到映射,自动创建一个
|
||||
default_table_name = f"feishu_{feishu_table_name.lower().replace(' ', '_').replace('-', '_')}"
|
||||
@ -256,7 +225,6 @@ class DataSyncService:
|
||||
table_name or default_table_name
|
||||
)
|
||||
final_table_name = mapping.table_name
|
||||
logger.info(f"创建新映射: {final_table_name}")
|
||||
else:
|
||||
# 非自动同步模式,优先使用用户提供的表名
|
||||
default_table_name = f"feishu_{feishu_table_name.lower().replace(' ', '_').replace('-', '_')}"
|
||||
@ -270,22 +238,12 @@ class DataSyncService:
|
||||
feishu_table_name,
|
||||
final_table_name
|
||||
)
|
||||
logger.info(f"使用表名: {final_table_name}")
|
||||
|
||||
# 3. 获取字段信息
|
||||
try:
|
||||
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)}"
|
||||
}
|
||||
fields = BitableService.get_table_fields(app_token, table_id, access_token)
|
||||
|
||||
# 4. 创建模型
|
||||
model = DataSyncService.create_model_from_fields(final_table_name, fields)
|
||||
logger.info(f"创建模型: {model.__name__}")
|
||||
|
||||
# 5. 检查表是否存在,不存在则创建
|
||||
table_exists = DataSyncService.check_table_exists(final_table_name)
|
||||
@ -300,92 +258,74 @@ class DataSyncService:
|
||||
page_token = None
|
||||
page_size = 100
|
||||
|
||||
try:
|
||||
while True:
|
||||
# 查询记录
|
||||
result = BitableService.search_records(
|
||||
app_token=app_token,
|
||||
table_id=table_id,
|
||||
access_token=access_token,
|
||||
page_size=page_size,
|
||||
page_token=page_token
|
||||
)
|
||||
while True:
|
||||
# 查询记录
|
||||
result = BitableService.search_records(
|
||||
app_token=app_token,
|
||||
table_id=table_id,
|
||||
access_token=access_token,
|
||||
page_size=page_size,
|
||||
page_token=page_token
|
||||
)
|
||||
|
||||
records = result.get('items', [])
|
||||
all_records.extend(records)
|
||||
records = result.get('items', [])
|
||||
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
|
||||
except Exception as e:
|
||||
logger.error(f"获取记录失败: {str(e)}")
|
||||
return {
|
||||
'success': False,
|
||||
'error': f"获取记录失败: {str(e)}"
|
||||
}
|
||||
# 检查是否有更多数据
|
||||
page_token = result.get('page_token')
|
||||
if not page_token or not records:
|
||||
break
|
||||
|
||||
# 7. 同步数据到数据库
|
||||
try:
|
||||
with transaction.atomic():
|
||||
# 统计数据
|
||||
created_count = 0
|
||||
updated_count = 0
|
||||
with transaction.atomic():
|
||||
# 统计数据
|
||||
created_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')
|
||||
fields_data = record.get('fields', {})
|
||||
|
||||
# 准备数据
|
||||
data = {'feishu_record_id': record_id}
|
||||
# 准备数据
|
||||
data = {'feishu_record_id': record_id}
|
||||
|
||||
# 处理每个字段的数据
|
||||
for field_name, field_value in fields_data.items():
|
||||
# 将字段名转换为Python合法标识符
|
||||
db_field_name = field_name.lower().replace(' ', '_').replace('-', '_')
|
||||
# 处理每个字段的数据
|
||||
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}")
|
||||
# 跳过已保留的字段名
|
||||
if db_field_name in ['id', 'created_at', 'updated_at']:
|
||||
continue
|
||||
|
||||
# 更新映射表中的记录数
|
||||
mapping.total_records = len(all_records)
|
||||
mapping.save(update_fields=['total_records', 'last_sync_time'])
|
||||
# 确保字段存在于模型中
|
||||
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
|
||||
|
||||
logger.info(f"数据同步完成: 总记录数={len(all_records)}, 新增={created_count}, 更新={updated_count}")
|
||||
except Exception as e:
|
||||
logger.error(f"数据同步到数据库失败: {str(e)}")
|
||||
return {
|
||||
'success': False,
|
||||
'error': f"数据同步到数据库失败: {str(e)}"
|
||||
}
|
||||
# 尝试更新或创建记录
|
||||
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
|
||||
|
||||
# 更新映射表中的记录数
|
||||
mapping.total_records = len(all_records)
|
||||
mapping.save(update_fields=['total_records', 'last_sync_time'])
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
@ -399,8 +339,8 @@ class DataSyncService:
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"数据同步失败: {str(e)}", exc_info=True)
|
||||
logger.error(f"数据同步失败: {str(e)}")
|
||||
return {
|
||||
'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 @@
|
||||
|
@ -22,6 +22,7 @@ from apps.chat.models import ChatHistory
|
||||
from apps.knowledge_base.models import KnowledgeBase
|
||||
from apps.user.authentication import CustomTokenAuthentication
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -47,64 +48,23 @@ class FeishuTableRecordsView(APIView):
|
||||
)
|
||||
|
||||
try:
|
||||
# 记录原始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
|
||||
)
|
||||
app_token, table_id = BitableService.extract_params_from_url(table_url)
|
||||
|
||||
# 先获取一些样本数据,检查我们能否访问多维表格
|
||||
try:
|
||||
sample_data = BitableService.search_records(
|
||||
app_token=app_token,
|
||||
table_id=table_id,
|
||||
access_token=access_token,
|
||||
filter_exp=filter_exp,
|
||||
sort=sort,
|
||||
page_size=page_size,
|
||||
page_token=page_token
|
||||
)
|
||||
sample_data = BitableService.search_records(
|
||||
app_token=app_token,
|
||||
table_id=table_id,
|
||||
access_token=access_token,
|
||||
filter_exp=filter_exp,
|
||||
sort=sort,
|
||||
page_size=page_size,
|
||||
page_token=page_token
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
return Response(sample_data, status=status.HTTP_200_OK)
|
||||
|
||||
except ValueError as ve:
|
||||
logger.error(f"URL解析或参数验证失败: {str(ve)}")
|
||||
return Response(
|
||||
{"error": str(ve), "details": "URL格式可能不正确"},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
@ -112,7 +72,6 @@ class FeishuTableRecordsView(APIView):
|
||||
|
||||
except Exception as e:
|
||||
error_details = traceback.format_exc()
|
||||
logger.error(f"查询飞书多维表格失败: {str(e)}\n{error_details}")
|
||||
return Response(
|
||||
{
|
||||
"error": f"查询飞书多维表格失败: {str(e)}",
|
||||
@ -143,77 +102,32 @@ class FeishuDataSyncView(APIView):
|
||||
|
||||
# 提取参数
|
||||
try:
|
||||
# 记录原始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
|
||||
)
|
||||
app_token, table_id = BitableService.extract_params_from_url(table_url)
|
||||
|
||||
# 先获取一些样本数据,检查我们能否访问多维表格
|
||||
try:
|
||||
sample_data = BitableService.search_records(
|
||||
app_token=app_token,
|
||||
table_id=table_id,
|
||||
access_token=access_token,
|
||||
page_size=5
|
||||
)
|
||||
sample_data = BitableService.search_records(
|
||||
app_token=app_token,
|
||||
table_id=table_id,
|
||||
access_token=access_token,
|
||||
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,
|
||||
table_name=table_name,
|
||||
primary_key=primary_key
|
||||
)
|
||||
|
||||
# 执行数据同步
|
||||
result = DataSyncService.sync_data_to_db(
|
||||
table_url=table_url,
|
||||
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条样本数据
|
||||
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
|
||||
)
|
||||
# 添加样本数据到结果中
|
||||
if result.get('success'):
|
||||
result['sample_data'] = sample_data.get('items', [])[:3] # 只返回最多3条样本数据
|
||||
return Response(result, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response(result, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
except ValueError as ve:
|
||||
logger.error(f"URL解析或参数验证失败: {str(ve)}")
|
||||
return Response(
|
||||
{"error": str(ve), "details": "URL格式可能不正确"},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
@ -222,7 +136,6 @@ class FeishuDataSyncView(APIView):
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
logger.error(f"数据同步失败: {str(e)}\n{error_details}")
|
||||
return Response(
|
||||
{
|
||||
"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 uuid
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
@ -19,16 +19,14 @@ class Migration(migrations.Migration):
|
||||
name='GmailConversation',
|
||||
fields=[
|
||||
('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="User's Gmail address", max_length=254)),
|
||||
('influencer_email', models.EmailField(help_text="Influencer's email address", max_length=254)),
|
||||
('title', models.CharField(help_text='Conversation title', max_length=255)),
|
||||
('user_email', models.EmailField(help_text='用户Gmail邮箱', max_length=254)),
|
||||
('influencer_email', models.EmailField(help_text='达人Gmail邮箱', max_length=254)),
|
||||
('conversation_id', models.CharField(help_text='关联到chat_history的会话ID', max_length=100, unique=True)),
|
||||
('title', models.CharField(default='Gmail对话', help_text='对话标题', max_length=100)),
|
||||
('last_sync_time', models.DateTimeField(default=django.utils.timezone.now, help_text='最后同步时间')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('last_sync_time', models.DateTimeField(blank=True, help_text='Last time conversation was synced with Gmail', null=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)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gmail_conversations', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
@ -41,7 +39,7 @@ class Migration(migrations.Migration):
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('email_message_id', models.CharField(help_text='Gmail邮件ID', max_length=100)),
|
||||
('attachment_id', models.TextField(help_text='Gmail附件的唯一标识符,可能很长')),
|
||||
('attachment_id', models.CharField(help_text='Gmail附件ID', max_length=100)),
|
||||
('filename', models.CharField(help_text='原始文件名', max_length=255)),
|
||||
('file_path', models.CharField(help_text='保存在服务器上的路径', max_length=255)),
|
||||
('content_type', models.CharField(help_text='MIME类型', max_length=100)),
|
||||
@ -55,107 +53,6 @@ class Migration(migrations.Migration):
|
||||
'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(
|
||||
name='GmailCredential',
|
||||
fields=[
|
||||
@ -166,7 +63,6 @@ class Migration(migrations.Migration):
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('is_valid', models.BooleanField(default=True, help_text='Whether the credential is valid')),
|
||||
('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)),
|
||||
],
|
||||
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.knowledge_base.models import KnowledgeBase
|
||||
import requests
|
||||
# from apps.common.services.notification_service import NotificationService
|
||||
import threading
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
@ -1,6 +1,6 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from apps.gmail.views import (
|
||||
from .views import (
|
||||
GmailAuthInitiateView,
|
||||
GmailAuthCompleteView,
|
||||
GmailConversationView,
|
||||
|
@ -35,85 +35,34 @@ class GmailAuthInitiateView(APIView):
|
||||
"""
|
||||
API 视图,用于启动 Gmail OAuth2 认证流程。
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
|
||||
|
||||
permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
|
||||
authentication_classes = [CustomTokenAuthentication]
|
||||
def post(self, request):
|
||||
"""
|
||||
处理 POST 请求,启动 Gmail OAuth2 认证并返回授权 URL。
|
||||
|
||||
支持两种方式提供客户端密钥:
|
||||
1. 在请求体中提供client_secret_json字段
|
||||
2. 上传名为client_secret_file的JSON文件
|
||||
直接使用系统中已配置的客户端密钥文件。
|
||||
|
||||
Args:
|
||||
request: Django REST Framework 请求对象,包含客户端密钥 JSON 数据或文件。
|
||||
request: Django REST Framework 请求对象。
|
||||
|
||||
Returns:
|
||||
Response: 包含授权 URL 的 JSON 响应(成功时),或错误信息(失败时)。
|
||||
|
||||
Status Codes:
|
||||
200: 成功生成授权 URL。
|
||||
400: 请求数据无效。
|
||||
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
|
||||
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)
|
||||
# 直接使用系统中已配置的客户端密钥文件
|
||||
client_secret_path = 'media/secret/client_secret_266164728215-v84lngbp3vgr4ulql01sqkg5vaigf4a5.apps.googleusercontent.com.json'
|
||||
|
||||
try:
|
||||
# 读取客户端密钥文件
|
||||
with open(client_secret_path, 'r') as f:
|
||||
client_secret_json = json.load(f)
|
||||
|
||||
# 调用 GmailService 生成授权 URL
|
||||
auth_url = GmailService.initiate_authentication(request.user, client_secret_json)
|
||||
logger.info(f"Generated auth URL for user {request.user.id}")
|
||||
@ -136,19 +85,16 @@ class GmailAuthCompleteView(APIView):
|
||||
"""
|
||||
API 视图,用于完成 Gmail OAuth2 认证流程。
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
|
||||
|
||||
permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
|
||||
authentication_classes = [CustomTokenAuthentication]
|
||||
def post(self, request):
|
||||
"""
|
||||
处理 POST 请求,使用授权代码完成 Gmail OAuth2 认证并保存凭证。
|
||||
|
||||
支持两种方式提供客户端密钥:
|
||||
1. 在请求体中提供client_secret_json字段
|
||||
2. 上传名为client_secret_file的JSON文件
|
||||
直接使用系统中已配置的客户端密钥文件。
|
||||
|
||||
Args:
|
||||
request: Django REST Framework 请求对象,包含授权代码和客户端密钥 JSON 或文件。
|
||||
request: Django REST Framework 请求对象,包含授权代码。
|
||||
|
||||
Returns:
|
||||
Response: 包含已保存凭证数据的 JSON 响应(成功时),或错误信息(失败时)。
|
||||
@ -158,72 +104,25 @@ class GmailAuthCompleteView(APIView):
|
||||
400: 请求数据无效。
|
||||
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
|
||||
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)
|
||||
# 直接使用系统中已配置的客户端密钥文件
|
||||
client_secret_path = 'media/secret/client_secret_266164728215-v84lngbp3vgr4ulql01sqkg5vaigf4a5.apps.googleusercontent.com.json'
|
||||
|
||||
# 获取授权码,无论是哪种方式都需要
|
||||
# 获取授权代码
|
||||
auth_code = request.data.get('auth_code')
|
||||
if not auth_code:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'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文件',
|
||||
'message': '缺少必要的授权代码',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
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)
|
||||
# 序列化凭证数据以返回
|
||||
@ -525,8 +424,8 @@ class GmailConversationView(APIView):
|
||||
"""
|
||||
API视图,用于获取和保存Gmail对话。
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
|
||||
permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
|
||||
authentication_classes = [CustomTokenAuthentication]
|
||||
|
||||
def post(self, request):
|
||||
"""
|
||||
@ -630,8 +529,8 @@ class GmailAttachmentListView(APIView):
|
||||
"""
|
||||
API视图,用于获取Gmail附件列表。
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
|
||||
permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
|
||||
authentication_classes = [CustomTokenAuthentication]
|
||||
|
||||
def get(self, request, conversation_id=None):
|
||||
"""
|
||||
@ -679,8 +578,8 @@ class GmailPubSubView(APIView):
|
||||
"""
|
||||
API视图,用于设置Gmail的Pub/Sub实时通知。
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
|
||||
permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
|
||||
authentication_classes = [CustomTokenAuthentication]
|
||||
|
||||
def post(self, request):
|
||||
"""
|
||||
@ -761,9 +660,9 @@ class GmailPubSubView(APIView):
|
||||
credentials = request.user.gmail_credentials.filter(is_valid=True)
|
||||
|
||||
# 构建响应数据
|
||||
user = []
|
||||
accounts = []
|
||||
for cred in credentials:
|
||||
user.append({
|
||||
accounts.append({
|
||||
'id': cred.id,
|
||||
'email': cred.email,
|
||||
'is_default': cred.is_default
|
||||
@ -772,7 +671,7 @@ class GmailPubSubView(APIView):
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': '获取账户列表成功',
|
||||
'data': {'user': user}
|
||||
'data': {'accounts': accounts}
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@ -780,8 +679,8 @@ class GmailSendEmailView(APIView):
|
||||
"""
|
||||
API视图,用于发送Gmail邮件(支持附件)。
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
|
||||
permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
|
||||
authentication_classes = [CustomTokenAuthentication]
|
||||
|
||||
def post(self, request):
|
||||
"""
|
||||
@ -915,9 +814,9 @@ class GmailSendEmailView(APIView):
|
||||
credentials = request.user.gmail_credentials.filter(is_valid=True)
|
||||
|
||||
# 构建响应数据
|
||||
user = []
|
||||
accounts = []
|
||||
for cred in credentials:
|
||||
user.append({
|
||||
accounts.append({
|
||||
'id': cred.id,
|
||||
'email': cred.email,
|
||||
'is_default': cred.is_default
|
||||
@ -926,7 +825,7 @@ class GmailSendEmailView(APIView):
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': '获取账户列表成功',
|
||||
'data': {'user': user}
|
||||
'data': {'accounts': accounts}
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@ -1012,8 +911,8 @@ class GmailConversationSummaryView(APIView):
|
||||
"""
|
||||
API视图,用于获取Gmail对话的总结。
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
|
||||
permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
|
||||
authentication_classes = [CustomTokenAuthentication]
|
||||
|
||||
def get(self, request, conversation_id=None):
|
||||
"""
|
||||
@ -1094,7 +993,8 @@ class GmailGoalView(APIView):
|
||||
用户目标API - 支持用户为每个对话设置不同的目标
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
|
||||
authentication_classes = [CustomTokenAuthentication]
|
||||
|
||||
def get(self, request, conversation_id=None):
|
||||
"""
|
||||
获取用户的目标
|
||||
@ -1172,6 +1072,13 @@ class GmailGoalView(APIView):
|
||||
'data': None
|
||||
}, 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(
|
||||
@ -1274,7 +1181,8 @@ class SimpleRecommendedReplyView(APIView):
|
||||
通过conversation_id一键获取目标、对话摘要和推荐回复
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
|
||||
authentication_classes = [CustomTokenAuthentication]
|
||||
|
||||
def post(self, request):
|
||||
"""
|
||||
直接通过conversation_id获取推荐回复
|
||||
@ -1312,12 +1220,12 @@ class SimpleRecommendedReplyView(APIView):
|
||||
'data': None
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
if conversation.user != request.user:
|
||||
return Response({
|
||||
'code': 403,
|
||||
'message': '无权限访问该对话',
|
||||
'data': None
|
||||
}, status=status.HTTP_403_FORBIDDEN)
|
||||
# if conversation.user != request.user:
|
||||
# return Response({
|
||||
# 'code': 403,
|
||||
# 'message': '无权限访问该对话',
|
||||
# 'data': None
|
||||
# }, status=status.HTTP_403_FORBIDDEN)
|
||||
except Exception as e:
|
||||
logger.error(f"查找对话失败: {str(e)}")
|
||||
return Response({
|
||||
@ -1399,8 +1307,8 @@ class GmailExportView(APIView):
|
||||
"""
|
||||
API视图,用于导出已回复的达人Gmail列表为Excel文件。
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
|
||||
permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
|
||||
authentication_classes = [CustomTokenAuthentication]
|
||||
|
||||
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 uuid
|
||||
@ -31,7 +31,8 @@ class Migration(migrations.Migration):
|
||||
('update_time', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
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(
|
||||
@ -50,7 +51,8 @@ class Migration(migrations.Migration):
|
||||
options={
|
||||
'verbose_name': '知识库文档',
|
||||
'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')},
|
||||
},
|
||||
),
|
||||
|
@ -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)
|
||||
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):
|
||||
"""计算文档统计信息"""
|
||||
|
@ -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