群发demo1

This commit is contained in:
wanjia 2025-05-29 17:21:16 +08:00
parent 5eab14304c
commit 7898219d2c
33 changed files with 2425 additions and 1029 deletions

View File

@ -27,7 +27,6 @@ class Brand(models.Model):
is_active = models.BooleanField(default=True, verbose_name='是否激活') is_active = models.BooleanField(default=True, verbose_name='是否激活')
class Meta: class Meta:
db_table = 'brands'
verbose_name = '品牌' verbose_name = '品牌'
verbose_name_plural = '品牌' verbose_name_plural = '品牌'
@ -66,7 +65,6 @@ class Product(models.Model):
is_active = models.BooleanField(default=True, verbose_name='是否激活') is_active = models.BooleanField(default=True, verbose_name='是否激活')
class Meta: class Meta:
db_table = 'products'
verbose_name = '产品' verbose_name = '产品'
verbose_name_plural = '产品' verbose_name_plural = '产品'
unique_together = ['brand', 'name'] unique_together = ['brand', 'name']
@ -144,7 +142,6 @@ class Campaign(models.Model):
is_active = models.BooleanField(default=True, verbose_name='是否激活') is_active = models.BooleanField(default=True, verbose_name='是否激活')
class Meta: class Meta:
db_table = 'campaigns'
verbose_name = '活动' verbose_name = '活动'
verbose_name_plural = '活动' verbose_name_plural = '活动'
unique_together = ['brand', 'name'] unique_together = ['brand', 'name']
@ -186,7 +183,6 @@ class BrandChatSession(models.Model):
is_active = models.BooleanField(default=True, verbose_name='是否激活') is_active = models.BooleanField(default=True, verbose_name='是否激活')
class Meta: class Meta:
db_table = 'brand_chat_sessions'
verbose_name = '品牌聊天会话' verbose_name = '品牌聊天会话'
verbose_name_plural = '品牌聊天会话' verbose_name_plural = '品牌聊天会话'
indexes = [ indexes = [

View File

@ -1,6 +1,7 @@
# Generated by Django 5.2.1 on 2025-05-28 09:51 # Generated by Django 5.2 on 2025-05-07 03:40
import django.db.models.deletion import django.db.models.deletion
import uuid
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
@ -15,6 +16,24 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
migrations.CreateModel(
name='ConversationSummary',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('talent_email', models.EmailField(max_length=254, verbose_name='达人邮箱')),
('conversation_id', models.CharField(max_length=100, verbose_name='对话ID')),
('summary', models.TextField(verbose_name='对话总结')),
('is_active', models.BooleanField(default=True, verbose_name='是否激活')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='conversation_summaries', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': '对话总结',
'verbose_name_plural': '对话总结',
'db_table': 'conversation_summaries',
},
),
migrations.CreateModel( migrations.CreateModel(
name='ChatHistory', name='ChatHistory',
fields=[ fields=[
@ -32,8 +51,9 @@ class Migration(migrations.Migration):
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
], ],
options={ options={
'db_table': 'chat_history',
'ordering': ['created_at'], 'ordering': ['created_at'],
'indexes': [models.Index(fields=['conversation_id', 'created_at'], name='chat_chathi_convers_90ca0c_idx'), models.Index(fields=['user', 'created_at'], name='chat_chathi_user_id_326d36_idx'), models.Index(fields=['conversation_id', 'is_deleted'], name='chat_chathi_convers_bcd094_idx')], 'indexes': [models.Index(fields=['conversation_id', 'created_at'], name='chat_histor_convers_33721a_idx'), models.Index(fields=['user', 'created_at'], name='chat_histor_user_id_aa050a_idx'), models.Index(fields=['conversation_id', 'is_deleted'], name='chat_histor_convers_89bc43_idx')],
}, },
), ),
] ]

View 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),
),
]

View 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(),
),
]

View File

@ -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',
),
]

View File

@ -14,15 +14,16 @@ from apps.knowledge_base.models import KnowledgeBase
from apps.chat.models import ChatHistory from apps.chat.models import ChatHistory
from apps.chat.serializers import ChatHistorySerializer from apps.chat.serializers import ChatHistorySerializer
from apps.common.services.chat_service import ChatService from apps.common.services.chat_service import ChatService
from apps.user.authentication import CustomTokenAuthentication
from apps.chat.services.chat_api import ( from apps.chat.services.chat_api import (
ExternalAPIError, stream_chat_answer, get_chat_answer, generate_conversation_title, ExternalAPIError, stream_chat_answer, get_chat_answer, generate_conversation_title,
get_hit_test_documents, generate_conversation_title_from_deepseek get_hit_test_documents, generate_conversation_title_from_deepseek
) )
from apps.user.authentication import CustomTokenAuthentication
# from apps.permissions.services.permission_service import KnowledgeBasePermissionMixin
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# class ChatHistoryViewSet(KnowledgeBasePermissionMixin, viewsets.ModelViewSet):
class ChatHistoryViewSet(viewsets.ModelViewSet): class ChatHistoryViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
authentication_classes = [CustomTokenAuthentication] authentication_classes = [CustomTokenAuthentication]
@ -463,83 +464,83 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@action(detail=False, methods=['post']) # @action(detail=False, methods=['post'])
def hit_test(self, request): # def hit_test(self, request):
"""获取问题与知识库文档的匹配度""" # """获取问题与知识库文档的匹配度"""
try: # try:
data = request.data # data = request.data
if 'question' not in data or 'dataset_id_list' not in data or not data['dataset_id_list']: # if 'question' not in data or 'dataset_id_list' not in data or not data['dataset_id_list']:
return Response({ # return Response({
'code': 400, # 'code': 400,
'message': '缺少必填字段: question 或 dataset_id_list', # 'message': '缺少必填字段: question 或 dataset_id_list',
'data': None # 'data': None
}, status=status.HTTP_400_BAD_REQUEST) # }, status=status.HTTP_400_BAD_REQUEST)
question = data['question'] # question = data['question']
dataset_ids = data['dataset_id_list'] # dataset_ids = data['dataset_id_list']
if not isinstance(dataset_ids, list): # if not isinstance(dataset_ids, list):
try: # try:
dataset_ids = json.loads(dataset_ids) # dataset_ids = json.loads(dataset_ids)
if not isinstance(dataset_ids, list): # if not isinstance(dataset_ids, list):
dataset_ids = [dataset_ids] # dataset_ids = [dataset_ids]
except (json.JSONDecodeError, TypeError): # except (json.JSONDecodeError, TypeError):
dataset_ids = [dataset_ids] # dataset_ids = [dataset_ids]
external_id_list = [] # external_id_list = []
for kb_id in dataset_ids: # for kb_id in dataset_ids:
kb = KnowledgeBase.objects.filter(id=kb_id).first() # kb = KnowledgeBase.objects.filter(id=kb_id).first()
if not kb: # if not kb:
return Response({ # return Response({
'code': 404, # 'code': 404,
'message': f'知识库不存在: {kb_id}', # 'message': f'知识库不存在: {kb_id}',
'data': None # 'data': None
}, status=status.HTTP_404_NOT_FOUND) # }, status=status.HTTP_404_NOT_FOUND)
if not self.check_knowledge_base_permission(kb, request.user, 'read'): # if not self.check_knowledge_base_permission(kb, request.user, 'read'):
return Response({ # return Response({
'code': 403, # 'code': 403,
'message': f'无权访问知识库: {kb.name}', # 'message': f'无权访问知识库: {kb.name}',
'data': None # 'data': None
}, status=status.HTTP_403_FORBIDDEN) # }, status=status.HTTP_403_FORBIDDEN)
if kb.external_id: # if kb.external_id:
external_id_list.append(str(kb.external_id)) # external_id_list.append(str(kb.external_id))
if not external_id_list: # if not external_id_list:
return Response({ # return Response({
'code': 400, # 'code': 400,
'message': '没有有效的知识库external_id', # 'message': '没有有效的知识库external_id',
'data': None # 'data': None
}, status=status.HTTP_400_BAD_REQUEST) # }, status=status.HTTP_400_BAD_REQUEST)
all_documents = [] # all_documents = []
for dataset_id in external_id_list: # for dataset_id in external_id_list:
try: # try:
doc_info = get_hit_test_documents(dataset_id, question) # doc_info = get_hit_test_documents(dataset_id, question)
if doc_info: # if doc_info:
all_documents.extend(doc_info) # all_documents.extend(doc_info)
except ExternalAPIError as e: # except ExternalAPIError as e:
logger.error(f"调用hit_test失败: 知识库ID={dataset_id}, 错误={str(e)}") # logger.error(f"调用hit_test失败: 知识库ID={dataset_id}, 错误={str(e)}")
continue # 宽松处理,跳过失败的知识库 # continue # 宽松处理,跳过失败的知识库
all_documents = sorted(all_documents, key=lambda x: x.get('similarity', 0), reverse=True) # all_documents = sorted(all_documents, key=lambda x: x.get('similarity', 0), reverse=True)
return Response({ # return Response({
'code': 200, # 'code': 200,
'message': '成功', # 'message': '成功',
'data': { # 'data': {
'question': question, # 'question': question,
'matched_documents': all_documents, # 'matched_documents': all_documents,
'total_count': len(all_documents) # 'total_count': len(all_documents)
} # }
}) # })
except Exception as e: # except Exception as e:
logger.error(f"hit_test接口调用失败: {str(e)}") # logger.error(f"hit_test接口调用失败: {str(e)}")
import traceback # import traceback
logger.error(traceback.format_exc()) # logger.error(traceback.format_exc())
return Response({ # return Response({
'code': 500, # 'code': 500,
'message': f'hit_test接口调用失败: {str(e)}', # 'message': f'hit_test接口调用失败: {str(e)}',
'data': None # 'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def _highlight_keyword(self, text, keyword): def _highlight_keyword(self, text, keyword):
"""高亮关键词""" """高亮关键词"""

View File

@ -6,11 +6,12 @@ from django.db import transaction
from apps.user.models import User from apps.user.models import User
from apps.knowledge_base.models import KnowledgeBase from apps.knowledge_base.models import KnowledgeBase
from apps.chat.models import ChatHistory from apps.chat.models import ChatHistory
# from apps.permissions.services.permission_service import KnowledgeBasePermissionMixin
from django.db.models import Q from django.db.models import Q
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# class ChatService(KnowledgeBasePermissionMixin):
class ChatService(): class ChatService():
@transaction.atomic @transaction.atomic
def create_chat_record(self, user, data, conversation_id=None): def create_chat_record(self, user, data, conversation_id=None):

View File

@ -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

View File

@ -1,338 +1,338 @@
# apps/common/services/permission_service.py # # apps/common/services/permission_service.py
import logging # import logging
from django.db import transaction # from django.db import transaction
from django.db.models import Q # from django.db.models import Q
from django.utils import timezone # from django.utils import timezone
from rest_framework.exceptions import ValidationError # from rest_framework.exceptions import ValidationError
from apps.user.models import User # from apps.user.models import User
from apps.knowledge_base.models import KnowledgeBase # from apps.knowledge_base.models import KnowledgeBase
# from apps.permissions.models import Permission, KnowledgeBasePermission as KBPermissionModel
# logger = logging.getLogger(__name__)
logger = logging.getLogger(__name__) # class PermissionService:
# def can_manage_knowledge_base(self, user, knowledge_base):
# """检查用户是否是知识库的创建者"""
# return str(knowledge_base.user_id) == str(user.id)
class PermissionService: # def check_extend_permission(self, permission, user):
def can_manage_knowledge_base(self, user, knowledge_base): # """检查是否有权限延长权限有效期"""
"""检查用户是否是知识库的创建者""" # knowledge_base = permission.knowledge_base
return str(knowledge_base.user_id) == str(user.id) # if knowledge_base.type == 'private':
# return knowledge_base.user_id == user.id
# if knowledge_base.type == 'leader':
# return user.role == 'admin'
# if knowledge_base.type == 'member':
# return user.role == 'admin' or (
# user.role == 'leader' and user.department == knowledge_base.department
# )
# return False
def check_extend_permission(self, permission, user): # def create_permission_request(self, user, validated_data, notification_service):
"""检查是否有权限延长权限有效期""" # """创建权限申请并发送通知"""
knowledge_base = permission.knowledge_base # knowledge_base = validated_data['knowledge_base']
if knowledge_base.type == 'private': # if str(knowledge_base.user_id) == str(user.id):
return knowledge_base.user_id == user.id # raise ValidationError({
if knowledge_base.type == 'leader': # "code": 400,
return user.role == 'admin' # "message": "您是此知识库的创建者,无需申请权限",
if knowledge_base.type == 'member': # "data": None
return user.role == 'admin' or ( # })
user.role == 'leader' and user.department == knowledge_base.department
)
return False
def create_permission_request(self, user, validated_data, notification_service): # approver = User.objects.get(id=knowledge_base.user_id)
"""创建权限申请并发送通知""" # requested_permissions = validated_data.get('permissions', {})
knowledge_base = validated_data['knowledge_base'] # expires_at = validated_data.get('expires_at')
if str(knowledge_base.user_id) == str(user.id):
raise ValidationError({
"code": 400,
"message": "您是此知识库的创建者,无需申请权限",
"data": None
})
approver = User.objects.get(id=knowledge_base.user_id) # if not any([requested_permissions.get('can_read'),
requested_permissions = validated_data.get('permissions', {}) # requested_permissions.get('can_edit'),
expires_at = validated_data.get('expires_at') # requested_permissions.get('can_delete')]):
# raise ValidationError("至少需要申请一种权限(读/改/删)")
if not any([requested_permissions.get('can_read'), # if not expires_at:
requested_permissions.get('can_edit'), # raise ValidationError("请指定权限到期时间")
requested_permissions.get('can_delete')]):
raise ValidationError("至少需要申请一种权限(读/改/删)")
if not expires_at: # existing_request = Permission.objects.filter(
raise ValidationError("请指定权限到期时间") # knowledge_base=knowledge_base,
# applicant=user,
# status='pending'
# ).first()
# if existing_request:
# raise ValidationError("您已有一个待处理的权限申请")
existing_request = Permission.objects.filter( # existing_permission = Permission.objects.filter(
knowledge_base=knowledge_base, # knowledge_base=knowledge_base,
applicant=user, # applicant=user,
status='pending' # status='approved',
).first() # expires_at__gt=timezone.now()
if existing_request: # ).first()
raise ValidationError("您已有一个待处理的权限申请") # if existing_permission:
# raise ValidationError("您已有此知识库的访问权限")
existing_permission = Permission.objects.filter( # with transaction.atomic():
knowledge_base=knowledge_base, # permission = Permission.objects.create(
applicant=user, # knowledge_base=knowledge_base,
status='approved', # applicant=user,
expires_at__gt=timezone.now() # approver=approver,
).first() # permissions=requested_permissions,
if existing_permission: # expires_at=expires_at,
raise ValidationError("您已有此知识库的访问权限") # status='pending'
# )
with transaction.atomic(): # permission_types = []
permission = Permission.objects.create( # if requested_permissions.get('can_read'):
knowledge_base=knowledge_base, # permission_types.append('读取')
applicant=user, # if requested_permissions.get('can_edit'):
approver=approver, # permission_types.append('编辑')
permissions=requested_permissions, # if requested_permissions.get('can_delete'):
expires_at=expires_at, # permission_types.append('删除')
status='pending' # permission_str = '、'.join(permission_types)
)
permission_types = [] # notification_service.send_notification(
if requested_permissions.get('can_read'): # user=approver,
permission_types.append('读取') # title="新的权限申请",
if requested_permissions.get('can_edit'): # content=f"用户 {user.name} 申请了知识库 '{knowledge_base.name}' 的{permission_str}权限",
permission_types.append('编辑') # notification_type="permission_request",
if requested_permissions.get('can_delete'): # related_object_id=str(permission.id)
permission_types.append('删除') # )
permission_str = ''.join(permission_types)
notification_service.send_notification( # return permission
user=approver,
title="新的权限申请",
content=f"用户 {user.name} 申请了知识库 '{knowledge_base.name}'{permission_str}权限",
notification_type="permission_request",
related_object_id=str(permission.id)
)
return permission # def approve_permission(self, user, permission, response_message, notification_service):
# """审批权限申请"""
# if not self.can_manage_knowledge_base(user, permission.knowledge_base):
# raise ValidationError({
# 'code': 403,
# 'message': '只有知识库创建者可以审批此申请',
# 'data': None
# })
def approve_permission(self, user, permission, response_message, notification_service): # with transaction.atomic():
"""审批权限申请""" # permission.status = 'approved'
if not self.can_manage_knowledge_base(user, permission.knowledge_base): # permission.approver = user
raise ValidationError({ # permission.response_message = response_message
'code': 403, # permission.save()
'message': '只有知识库创建者可以审批此申请',
'data': None
})
with transaction.atomic(): # kb_permission = KBPermissionModel.objects.filter(
permission.status = 'approved' # knowledge_base=permission.knowledge_base,
permission.approver = user # user=permission.applicant
permission.response_message = response_message # ).first()
permission.save()
kb_permission = KBPermissionModel.objects.filter( # if kb_permission:
knowledge_base=permission.knowledge_base, # kb_permission.can_read = permission.permissions.get('can_read', False)
user=permission.applicant # kb_permission.can_edit = permission.permissions.get('can_edit', False)
).first() # kb_permission.can_delete = permission.permissions.get('can_delete', False)
# kb_permission.granted_by = user
# kb_permission.status = 'active'
# kb_permission.expires_at = permission.expires_at
# kb_permission.save()
# logger.info(f"更新知识库权限记录: {kb_permission.id}")
# else:
# kb_permission = KBPermissionModel.objects.create(
# knowledge_base=permission.knowledge_base,
# user=permission.applicant,
# can_read=permission.permissions.get('can_read', False),
# can_edit=permission.permissions.get('can_edit', False),
# can_delete=permission.permissions.get('can_delete', False),
# granted_by=user,
# status='active',
# expires_at=permission.expires_at
# )
# logger.info(f"创建新的知识库权限记录: {kb_permission.id}")
if kb_permission: # notification_service.send_notification(
kb_permission.can_read = permission.permissions.get('can_read', False) # user=permission.applicant,
kb_permission.can_edit = permission.permissions.get('can_edit', False) # title="权限申请已通过",
kb_permission.can_delete = permission.permissions.get('can_delete', False) # content=f"您对知识库 '{permission.knowledge_base.name}' 的权限申请已通过",
kb_permission.granted_by = user # notification_type="permission_approved",
kb_permission.status = 'active' # related_object_id=str(permission.id)
kb_permission.expires_at = permission.expires_at # )
kb_permission.save()
logger.info(f"更新知识库权限记录: {kb_permission.id}")
else:
kb_permission = KBPermissionModel.objects.create(
knowledge_base=permission.knowledge_base,
user=permission.applicant,
can_read=permission.permissions.get('can_read', False),
can_edit=permission.permissions.get('can_edit', False),
can_delete=permission.permissions.get('can_delete', False),
granted_by=user,
status='active',
expires_at=permission.expires_at
)
logger.info(f"创建新的知识库权限记录: {kb_permission.id}")
notification_service.send_notification( # return permission
user=permission.applicant,
title="权限申请已通过",
content=f"您对知识库 '{permission.knowledge_base.name}' 的权限申请已通过",
notification_type="permission_approved",
related_object_id=str(permission.id)
)
return permission # def reject_permission(self, user, permission, response_message, notification_service):
# """拒绝权限申请"""
# if not self.can_manage_knowledge_base(user, permission.knowledge_base):
# raise ValidationError({
# 'code': 403,
# 'message': '只有知识库创建者可以审批此申请',
# 'data': None
# })
def reject_permission(self, user, permission, response_message, notification_service): # if permission.status != 'pending':
"""拒绝权限申请""" # raise ValidationError({
if not self.can_manage_knowledge_base(user, permission.knowledge_base): # 'code': 400,
raise ValidationError({ # 'message': '该申请已被处理',
'code': 403, # 'data': None
'message': '只有知识库创建者可以审批此申请', # })
'data': None
})
if permission.status != 'pending': # if not response_message:
raise ValidationError({ # raise ValidationError({
'code': 400, # 'code': 400,
'message': '该申请已被处理', # 'message': '请填写拒绝原因',
'data': None # 'data': None
}) # })
if not response_message: # with transaction.atomic():
raise ValidationError({ # permission.status = 'rejected'
'code': 400, # permission.approver = user
'message': '请填写拒绝原因', # permission.response_message = response_message
'data': None # permission.save()
})
with transaction.atomic(): # notification_service.send_notification(
permission.status = 'rejected' # user=permission.applicant,
permission.approver = user # title="权限申请已拒绝",
permission.response_message = response_message # content=f"您对知识库 '{permission.knowledge_base.name}' 的权限申请已被拒绝\n拒绝原因{response_message}",
permission.save() # notification_type="permission_rejected",
# related_object_id=str(permission.id)
# )
notification_service.send_notification( # return permission
user=permission.applicant,
title="权限申请已拒绝",
content=f"您对知识库 '{permission.knowledge_base.name}' 的权限申请已被拒绝\n拒绝原因:{response_message}",
notification_type="permission_rejected",
related_object_id=str(permission.id)
)
return permission # def extend_permission(self, user, permission, new_expires_at, notification_service):
# """延长权限有效期"""
# if not self.check_extend_permission(permission, user):
# raise ValidationError({
# "code": 403,
# "message": "您没有权限延长此权限",
# "data": None
# })
def extend_permission(self, user, permission, new_expires_at, notification_service): # if not new_expires_at:
"""延长权限有效期""" # raise ValidationError({
if not self.check_extend_permission(permission, user): # "code": 400,
raise ValidationError({ # "message": "请设置新的过期时间",
"code": 403, # "data": None
"message": "您没有权限延长此权限", # })
"data": None
})
if not new_expires_at: # try:
raise ValidationError({ # new_expires_at = timezone.datetime.strptime(new_expires_at, '%Y-%m-%dT%H:%M:%SZ')
"code": 400, # new_expires_at = timezone.make_aware(new_expires_at)
"message": "请设置新的过期时间", # if new_expires_at <= timezone.now():
"data": None # raise ValidationError({
}) # "code": 400,
# "message": "过期时间不能早于或等于当前时间",
# "data": None
# })
# except ValueError:
# raise ValidationError({
# "code": 400,
# "message": "过期时间格式错误,应为 ISO 格式 (YYYY-MM-DDThh:mm:ssZ)",
# "data": None
# })
try: # with transaction.atomic():
new_expires_at = timezone.datetime.strptime(new_expires_at, '%Y-%m-%dT%H:%M:%SZ') # permission.expires_at = new_expires_at
new_expires_at = timezone.make_aware(new_expires_at) # permission.save()
if new_expires_at <= timezone.now():
raise ValidationError({
"code": 400,
"message": "过期时间不能早于或等于当前时间",
"data": None
})
except ValueError:
raise ValidationError({
"code": 400,
"message": "过期时间格式错误,应为 ISO 格式 (YYYY-MM-DDThh:mm:ssZ)",
"data": None
})
with transaction.atomic(): # kb_permission = KBPermissionModel.objects.get(
permission.expires_at = new_expires_at # knowledge_base=permission.knowledge_base,
permission.save() # user=permission.applicant
# )
# kb_permission.expires_at = new_expires_at
# kb_permission.save()
kb_permission = KBPermissionModel.objects.get( # notification_service.send_notification(
knowledge_base=permission.knowledge_base, # user=permission.applicant,
user=permission.applicant # title="权限有效期延长",
) # content=f"您对知识库 '{permission.knowledge_base.name}' 的权限有效期已延长至 {new_expires_at.strftime('%Y-%m-%d %H:%M:%S')}",
kb_permission.expires_at = new_expires_at # notification_type="permission_extended",
kb_permission.save() # related_object_id=str(permission.id)
# )
notification_service.send_notification( # return permission
user=permission.applicant,
title="权限有效期延长",
content=f"您对知识库 '{permission.knowledge_base.name}' 的权限有效期已延长至 {new_expires_at.strftime('%Y-%m-%d %H:%M:%S')}",
notification_type="permission_extended",
related_object_id=str(permission.id)
)
return permission # def update_user_permission(self, admin_user, user_id, knowledge_base_id, permissions, expires_at_str, notification_service):
# """管理员更新用户权限"""
# if admin_user.role != 'admin':
# raise ValidationError({
# 'code': 403,
# 'message': '只有管理员可以直接修改权限',
# 'data': None
# })
def update_user_permission(self, admin_user, user_id, knowledge_base_id, permissions, expires_at_str, notification_service): # if not all([user_id, knowledge_base_id, permissions]):
"""管理员更新用户权限""" # raise ValidationError({
if admin_user.role != 'admin': # 'code': 400,
raise ValidationError({ # 'message': '缺少必要参数',
'code': 403, # 'data': None
'message': '只有管理员可以直接修改权限', # })
'data': None
})
if not all([user_id, knowledge_base_id, permissions]): # required_permission_fields = ['can_read', 'can_edit', 'can_delete']
raise ValidationError({ # if not all(field in permissions for field in required_permission_fields):
'code': 400, # raise ValidationError({
'message': '缺少必要参数', # 'code': 400,
'data': None # 'message': '权限参数格式错误,必须包含 can_read、can_edit、can_delete',
}) # 'data': None
# })
required_permission_fields = ['can_read', 'can_edit', 'can_delete'] # try:
if not all(field in permissions for field in required_permission_fields): # user = User.objects.get(id=user_id)
raise ValidationError({ # knowledge_base = KnowledgeBase.objects.get(id=knowledge_base_id)
'code': 400, # except User.DoesNotExist:
'message': '权限参数格式错误,必须包含 can_read、can_edit、can_delete', # raise ValidationError({
'data': None # 'code': 404,
}) # 'message': f'用户ID {user_id} 不存在',
# 'data': None
# })
# except KnowledgeBase.DoesNotExist:
# raise ValidationError({
# 'code': 404,
# 'message': f'知识库ID {knowledge_base_id} 不存在',
# 'data': None
# })
try: # if knowledge_base.type == 'private' and str(knowledge_base.user_id) != str(user.id):
user = User.objects.get(id=user_id) # raise ValidationError({
knowledge_base = KnowledgeBase.objects.get(id=knowledge_base_id) # 'code': 403,
except User.DoesNotExist: # 'message': '不能修改其他用户的私有知识库权限',
raise ValidationError({ # 'data': None
'code': 404, # })
'message': f'用户ID {user_id} 不存在',
'data': None
})
except KnowledgeBase.DoesNotExist:
raise ValidationError({
'code': 404,
'message': f'知识库ID {knowledge_base_id} 不存在',
'data': None
})
if knowledge_base.type == 'private' and str(knowledge_base.user_id) != str(user.id): # if user.role == 'member' and permissions.get('can_delete'):
raise ValidationError({ # raise ValidationError({
'code': 403, # 'code': 400,
'message': '不能修改其他用户的私有知识库权限', # 'message': '普通成员不能获得删除权限',
'data': None # 'data': None
}) # })
if user.role == 'member' and permissions.get('can_delete'): # expires_at = None
raise ValidationError({ # if expires_at_str:
'code': 400, # try:
'message': '普通成员不能获得删除权限', # expires_at = timezone.datetime.strptime(expires_at_str, '%Y-%m-%dT%H:%M:%SZ')
'data': None # expires_at = timezone.make_aware(expires_at)
}) # if expires_at <= timezone.now():
# raise ValidationError({
# 'code': 400,
# 'message': '过期时间不能早于或等于当前时间',
# 'data': None
# })
# except ValueError:
# raise ValidationError({
# 'code': 400,
# 'message': '过期时间格式错误,应为 ISO 格式 (YYYY-MM-DDThh:mm:ssZ)',
# 'data': None
# })
expires_at = None # with transaction.atomic():
if expires_at_str: # permission, created = KBPermissionModel.objects.update_or_create(
try: # user=user,
expires_at = timezone.datetime.strptime(expires_at_str, '%Y-%m-%dT%H:%M:%SZ') # knowledge_base=knowledge_base,
expires_at = timezone.make_aware(expires_at) # defaults={
if expires_at <= timezone.now(): # 'can_read': permissions.get('can_read', False),
raise ValidationError({ # 'can_edit': permissions.get('can_edit', False),
'code': 400, # 'can_delete': permissions.get('can_delete', False),
'message': '过期时间不能早于或等于当前时间', # 'granted_by': admin_user,
'data': None # 'status': 'active',
}) # 'expires_at': expires_at
except ValueError: # }
raise ValidationError({ # )
'code': 400,
'message': '过期时间格式错误,应为 ISO 格式 (YYYY-MM-DDThh:mm:ssZ)',
'data': None
})
with transaction.atomic(): # notification_service.send_notification(
permission, created = KBPermissionModel.objects.update_or_create( # user=user,
user=user, # title="知识库权限更新",
knowledge_base=knowledge_base, # content=f"管理员已{created and '授予' or '更新'}您对知识库 '{knowledge_base.name}' 的权限",
defaults={ # notification_type="permission_updated",
'can_read': permissions.get('can_read', False), # related_object_id=str(permission.id)
'can_edit': permissions.get('can_edit', False), # )
'can_delete': permissions.get('can_delete', False),
'granted_by': admin_user,
'status': 'active',
'expires_at': expires_at
}
)
notification_service.send_notification( # return permission, created
user=user,
title="知识库权限更新",
content=f"管理员已{created and '授予' or '更新'}您对知识库 '{knowledge_base.name}' 的权限",
notification_type="permission_updated",
related_object_id=str(permission.id)
)
return permission, created

View File

@ -1,15 +1,11 @@
import json import json
import re import re
import logging
import requests import requests
from urllib.parse import urljoin, urlparse, parse_qs from urllib.parse import urljoin
# 基础URL地址 # 基础URL地址
BASE_API_URL = "https://open.feishu.cn/open-apis/" BASE_API_URL = "https://open.feishu.cn/open-apis/"
# 获取日志记录器
logger = logging.getLogger(__name__)
class BitableService: class BitableService:
""" """
@ -36,11 +32,6 @@ class BitableService:
if headers is None: if headers is None:
headers = {} headers = {}
# 记录请求信息,避免记录敏感信息
logger.info(f"请求飞书API: {method} {full_url}")
if params:
logger.info(f"请求参数: {params}")
response = requests.request( response = requests.request(
method=method, method=method,
url=full_url, url=full_url,
@ -52,29 +43,7 @@ class BitableService:
# 检查响应 # 检查响应
if not response.ok: if not response.ok:
error_msg = f"API 请求失败: {response.status_code}, 响应: {response.text}" error_msg = f"API 请求失败: {response.status_code}, 响应: {response.text}"
logger.error(error_msg) print(error_msg)
# 解析错误响应
try:
error_json = response.json()
error_code = error_json.get("code")
error_msg = error_json.get("msg", "")
# 根据错误代码提供更具体的错误信息
if error_code == 91402: # NOTEXIST
error_detail = "请求的资源不存在请检查app_token和table_id是否正确以及应用是否有权限访问该资源"
logger.error(f"资源不存在错误: {error_detail}")
raise Exception(f"资源不存在: {error_detail}")
elif error_code == 99991663: # TOKEN_INVALID
error_detail = "访问令牌无效或已过期"
logger.error(f"令牌错误: {error_detail}")
raise Exception(f"访问令牌错误: {error_detail}")
else:
logger.error(f"飞书API错误: 代码={error_code}, 消息={error_msg}")
except ValueError:
# 响应不是有效的JSON
pass
raise Exception(error_msg) raise Exception(error_msg)
return response.json() return response.json()
@ -83,11 +52,6 @@ class BitableService:
def extract_params_from_url(table_url): def extract_params_from_url(table_url):
""" """
从URL中提取app_token和table_id 从URL中提取app_token和table_id
支持多种飞书多维表格URL格式
1. https://xxx.feishu.cn/base/{app_token}?table={table_id}
2. https://xxx.feishu.cn/wiki/{app_token}?sheet={table_id}
3. https://xxx.feishu.cn/wiki/wikcnXXX?sheet=XXX
4. https://xxx.feishu.cn/base/bascnXXX?table=tblXXX
Args: Args:
table_url: 飞书多维表格URL table_url: 飞书多维表格URL
@ -98,76 +62,13 @@ class BitableService:
Raises: Raises:
ValueError: 如果无法从URL中提取必要参数 ValueError: 如果无法从URL中提取必要参数
""" """
# 记录原始URL app_token_match = re.search(r'base/([^?]+)', table_url)
logger.info(f"解析多维表格URL: {table_url}") table_id_match = re.search(r'table=([^&]+)', table_url)
# 处理URL中的多余空格 if not app_token_match or not table_id_match:
table_url = table_url.strip() raise ValueError("无法从URL中提取必要参数请确认URL格式正确")
# 解析URL return app_token_match.group(1), table_id_match.group(1)
parsed_url = urlparse(table_url)
query_params = parse_qs(parsed_url.query)
path = parsed_url.path
logger.debug(f"URL路径: {path}, 查询参数: {query_params}")
# 尝试从查询参数中获取table_id
table_id = None
if 'table' in query_params:
table_id = query_params['table'][0].strip()
elif 'sheet' in query_params:
table_id = query_params['sheet'][0].strip()
# 尝试从路径中获取app_token
app_token = None
# 处理标准格式 /base/{app_token} 或 /wiki/{app_token}
standard_match = re.search(r'/(base|wiki)/([^/?]+)', path)
if standard_match:
app_token = standard_match.group(2).strip()
else:
# 处理简短ID格式如 /wiki/wikcnXXX
short_id_match = re.search(r'/(base|wiki)/([a-zA-Z0-9]+)', path)
if short_id_match:
app_token = short_id_match.group(2).strip()
# 检查是否成功提取
if not app_token or not table_id:
error_msg = "无法从URL中提取必要参数请确认URL格式正确"
logger.error(f"{error_msg}. URL: {table_url}")
raise ValueError(error_msg)
logger.info(f"成功从URL提取参数: app_token={app_token}, table_id={table_id}")
return app_token, table_id
@staticmethod
def validate_access(app_token, access_token):
"""
验证应用是否有权限访问多维表格
Args:
app_token: 应用令牌
access_token: 访问令牌
Returns:
bool: 是否有权限
"""
try:
# 构造请求
url = f"bitable/v1/apps/{app_token}"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
# 发送请求
response = BitableService.make_request("GET", url, headers=headers)
# 检查响应
return response and "code" in response and response["code"] == 0
except Exception as e:
logger.error(f"验证访问权限失败: {str(e)}")
return False
@staticmethod @staticmethod
def get_metadata(app_token, table_id, access_token): def get_metadata(app_token, table_id, access_token):
@ -183,10 +84,6 @@ class BitableService:
dict: 表格元数据 dict: 表格元数据
""" """
try: try:
# 先验证应用是否有权限访问
if not BitableService.validate_access(app_token, access_token):
logger.warning(f"应用无权限访问多维表格: app_token={app_token}")
# 构造请求 # 构造请求
url = f"bitable/v1/apps/{app_token}/tables/{table_id}" url = f"bitable/v1/apps/{app_token}/tables/{table_id}"
headers = { headers = {
@ -199,18 +96,16 @@ class BitableService:
# 检查响应 # 检查响应
if response and "code" in response and response["code"] == 0: if response and "code" in response and response["code"] == 0:
metadata = response.get("data", {}).get("table", {}) return response.get("data", {}).get("table", {})
logger.info(f"成功获取多维表格元数据: {metadata.get('name', table_id)}")
return metadata
# 发生错误 # 发生错误
error_msg = f"获取多维表格元数据失败: {json.dumps(response)}" error_msg = f"获取多维表格元数据失败: {json.dumps(response)}"
logger.error(error_msg) print(error_msg)
raise Exception(error_msg) raise Exception(error_msg)
except Exception as e: except Exception as e:
# 如果正常API调用失败使用替代方法 # 如果正常API调用失败使用替代方法
logger.error(f"获取多维表格元数据失败: {str(e)}") print(f"获取多维表格元数据失败: {str(e)}")
# 简单返回一个基本名称 # 简单返回一个基本名称
return { return {
"name": f"table_{table_id}", "name": f"table_{table_id}",
@ -238,8 +133,6 @@ class BitableService:
Exception: 查询失败时抛出异常 Exception: 查询失败时抛出异常
""" """
try: try:
logger.info(f"查询多维表格记录: app_token={app_token}, table_id={table_id}")
# 构造请求URL # 构造请求URL
url = f"bitable/v1/apps/{app_token}/tables/{table_id}/records/search" url = f"bitable/v1/apps/{app_token}/tables/{table_id}/records/search"
@ -266,18 +159,16 @@ class BitableService:
# 检查响应 # 检查响应
if response and "code" in response and response["code"] == 0: if response and "code" in response and response["code"] == 0:
result = response.get("data", {}) return response.get("data", {})
logger.info(f"查询成功,获取到 {len(result.get('items', []))} 条记录")
return result
# 发生错误 # 发生错误
error_msg = f"查询飞书多维表格失败: {json.dumps(response)}" error_msg = f"查询飞书多维表格失败: {json.dumps(response)}"
logger.error(error_msg) print(error_msg)
raise Exception(error_msg) raise Exception(error_msg)
except Exception as e: except Exception as e:
# 记录详细错误 # 记录详细错误
logger.error(f"查询飞书多维表格发生错误: {str(e)}") print(f"查询飞书多维表格发生错误: {str(e)}")
raise raise
@staticmethod @staticmethod
@ -294,8 +185,6 @@ class BitableService:
list: 字段信息列表 list: 字段信息列表
""" """
try: try:
logger.info(f"获取多维表格字段: app_token={app_token}, table_id={table_id}")
# 构造请求 # 构造请求
url = f"bitable/v1/apps/{app_token}/tables/{table_id}/fields" url = f"bitable/v1/apps/{app_token}/tables/{table_id}/fields"
headers = { headers = {
@ -309,20 +198,17 @@ class BitableService:
# 检查响应 # 检查响应
if response and "code" in response and response["code"] == 0: if response and "code" in response and response["code"] == 0:
fields = response.get("data", {}).get("items", []) return response.get("data", {}).get("items", [])
logger.info(f"成功获取到 {len(fields)} 个字段")
return fields
# 发生错误 # 发生错误
error_msg = f"获取多维表格字段失败: {json.dumps(response)}" error_msg = f"获取多维表格字段失败: {json.dumps(response)}"
logger.error(error_msg) print(error_msg)
raise Exception(error_msg) raise Exception(error_msg)
except Exception as e: except Exception as e:
# 记录详细错误 # 记录详细错误
logger.error(f"获取字段信息失败: {str(e)}") print(f"获取字段信息失败: {str(e)}")
# 如果获取失败,返回一个基本字段集 # 如果获取失败,返回一个基本字段集
logger.warning("返回默认字段集")
return [ return [
{"field_name": "title", "type": "text", "property": {}}, {"field_name": "title", "type": "text", "property": {}},
{"field_name": "description", "type": "text", "property": {}}, {"field_name": "description", "type": "text", "property": {}},

View File

@ -201,42 +201,12 @@ class DataSyncService:
dict: 同步结果 dict: 同步结果
""" """
try: try:
# 记录原始URL便于排查问题
logger.info(f"开始数据同步URL: {table_url}")
# 提取参数 # 提取参数
try: app_token, table_id = BitableService.extract_params_from_url(table_url)
app_token, table_id = BitableService.extract_params_from_url(table_url)
logger.info(f"成功解析URL参数: app_token={app_token}, table_id={table_id}")
except ValueError as ve:
logger.error(f"URL解析失败: {str(ve)}, URL: {table_url}")
return {
'success': False,
'error': f"URL解析失败: {str(ve)}",
'details': "URL格式可能不正确请参考飞书多维表格文档"
}
# 验证应用是否有权限访问该多维表格
if not BitableService.validate_access(app_token, access_token):
error_msg = f"应用无权限访问多维表格: app_token={app_token}"
logger.warning(error_msg)
return {
'success': False,
'error': error_msg,
'details': "请确认access_token是否有效以及应用是否已被添加为表格协作者"
}
# 1. 获取表格元数据 # 1. 获取表格元数据
try: metadata = BitableService.get_metadata(app_token, table_id, access_token)
metadata = BitableService.get_metadata(app_token, table_id, access_token) feishu_table_name = metadata.get('name', f'table_{table_id}')
feishu_table_name = metadata.get('name', f'table_{table_id}')
logger.info(f"获取到表格元数据: 名称={feishu_table_name}")
except Exception as e:
logger.error(f"获取表格元数据失败: {str(e)}")
return {
'success': False,
'error': f"获取表格元数据失败: {str(e)}"
}
# 2. 获取或创建表格映射 # 2. 获取或创建表格映射
if auto_sync: if auto_sync:
@ -244,7 +214,6 @@ class DataSyncService:
try: try:
mapping = FeishuTableMapping.objects.get(app_token=app_token, table_id=table_id) mapping = FeishuTableMapping.objects.get(app_token=app_token, table_id=table_id)
final_table_name = mapping.table_name final_table_name = mapping.table_name
logger.info(f"使用已有映射: {final_table_name}")
except FeishuTableMapping.DoesNotExist: except FeishuTableMapping.DoesNotExist:
# 如果找不到映射,自动创建一个 # 如果找不到映射,自动创建一个
default_table_name = f"feishu_{feishu_table_name.lower().replace(' ', '_').replace('-', '_')}" default_table_name = f"feishu_{feishu_table_name.lower().replace(' ', '_').replace('-', '_')}"
@ -256,7 +225,6 @@ class DataSyncService:
table_name or default_table_name table_name or default_table_name
) )
final_table_name = mapping.table_name final_table_name = mapping.table_name
logger.info(f"创建新映射: {final_table_name}")
else: else:
# 非自动同步模式,优先使用用户提供的表名 # 非自动同步模式,优先使用用户提供的表名
default_table_name = f"feishu_{feishu_table_name.lower().replace(' ', '_').replace('-', '_')}" default_table_name = f"feishu_{feishu_table_name.lower().replace(' ', '_').replace('-', '_')}"
@ -270,22 +238,12 @@ class DataSyncService:
feishu_table_name, feishu_table_name,
final_table_name final_table_name
) )
logger.info(f"使用表名: {final_table_name}")
# 3. 获取字段信息 # 3. 获取字段信息
try: fields = BitableService.get_table_fields(app_token, table_id, access_token)
fields = BitableService.get_table_fields(app_token, table_id, access_token)
logger.info(f"获取到 {len(fields)} 个字段")
except Exception as e:
logger.error(f"获取字段信息失败: {str(e)}")
return {
'success': False,
'error': f"获取字段信息失败: {str(e)}"
}
# 4. 创建模型 # 4. 创建模型
model = DataSyncService.create_model_from_fields(final_table_name, fields) model = DataSyncService.create_model_from_fields(final_table_name, fields)
logger.info(f"创建模型: {model.__name__}")
# 5. 检查表是否存在,不存在则创建 # 5. 检查表是否存在,不存在则创建
table_exists = DataSyncService.check_table_exists(final_table_name) table_exists = DataSyncService.check_table_exists(final_table_name)
@ -300,92 +258,74 @@ class DataSyncService:
page_token = None page_token = None
page_size = 100 page_size = 100
try: while True:
while True: # 查询记录
# 查询记录 result = BitableService.search_records(
result = BitableService.search_records( app_token=app_token,
app_token=app_token, table_id=table_id,
table_id=table_id, access_token=access_token,
access_token=access_token, page_size=page_size,
page_size=page_size, page_token=page_token
page_token=page_token )
)
records = result.get('items', [])
records = result.get('items', []) all_records.extend(records)
all_records.extend(records)
# 检查是否有更多数据
logger.info(f"获取第 {len(all_records) // page_size + 1} 页数据, 共 {len(records)} 条记录") page_token = result.get('page_token')
if not page_token or not records:
# 检查是否有更多数据 break
page_token = result.get('page_token')
if not page_token or not records:
break
except Exception as e:
logger.error(f"获取记录失败: {str(e)}")
return {
'success': False,
'error': f"获取记录失败: {str(e)}"
}
# 7. 同步数据到数据库 # 7. 同步数据到数据库
try: with transaction.atomic():
with transaction.atomic(): # 统计数据
# 统计数据 created_count = 0
created_count = 0 updated_count = 0
updated_count = 0
for record in all_records:
record_id = record.get('record_id')
fields_data = record.get('fields', {})
for record in all_records: # 准备数据
record_id = record.get('record_id') data = {'feishu_record_id': record_id}
fields_data = record.get('fields', {})
# 处理每个字段的数据
for field_name, field_value in fields_data.items():
# 将字段名转换为Python合法标识符
db_field_name = field_name.lower().replace(' ', '_').replace('-', '_')
# 准备数据 # 跳过已保留的字段名
data = {'feishu_record_id': record_id} if db_field_name in ['id', 'created_at', 'updated_at']:
# 处理每个字段的数据
for field_name, field_value in fields_data.items():
# 将字段名转换为Python合法标识符
db_field_name = field_name.lower().replace(' ', '_').replace('-', '_')
# 跳过已保留的字段名
if db_field_name in ['id', 'created_at', 'updated_at']:
continue
# 确保字段存在于模型中
if hasattr(model, db_field_name):
# 处理不同类型的字段值
if isinstance(field_value, (list, dict)):
data[db_field_name] = json.dumps(field_value)
else:
data[db_field_name] = field_value
# 尝试更新或创建记录
try:
# 总是使用 feishu_record_id 作为唯一标识符进行更新或创建
obj, created = model.objects.update_or_create(
feishu_record_id=record_id,
defaults=data
)
if created:
created_count += 1
else:
updated_count += 1
except Exception as e:
logger.error(f"更新或创建记录失败: {str(e)}, 记录ID: {record_id}")
continue continue
# 确保字段存在于模型中
if hasattr(model, db_field_name):
# 处理不同类型的字段值
if isinstance(field_value, (list, dict)):
data[db_field_name] = json.dumps(field_value)
else:
data[db_field_name] = field_value
# 更新映射表中的记录数 # 尝试更新或创建记录
mapping.total_records = len(all_records) try:
mapping.save(update_fields=['total_records', 'last_sync_time']) # 总是使用 feishu_record_id 作为唯一标识符进行更新或创建
obj, created = model.objects.update_or_create(
logger.info(f"数据同步完成: 总记录数={len(all_records)}, 新增={created_count}, 更新={updated_count}") feishu_record_id=record_id,
except Exception as e: defaults=data
logger.error(f"数据同步到数据库失败: {str(e)}") )
return {
'success': False, if created:
'error': f"数据同步到数据库失败: {str(e)}" created_count += 1
} else:
updated_count += 1
except Exception as e:
logger.error(f"更新或创建记录失败: {str(e)}, 记录ID: {record_id}")
continue
# 更新映射表中的记录数
mapping.total_records = len(all_records)
mapping.save(update_fields=['total_records', 'last_sync_time'])
return { return {
'success': True, 'success': True,
@ -399,8 +339,8 @@ class DataSyncService:
} }
except Exception as e: except Exception as e:
logger.error(f"数据同步失败: {str(e)}", exc_info=True) logger.error(f"数据同步失败: {str(e)}")
return { return {
'success': False, 'success': False,
'error': f"数据同步失败: {str(e)}" 'error': str(e)
} }

View File

@ -0,0 +1 @@

View File

@ -14,7 +14,7 @@ from .services.data_sync_service import DataSyncService
from .services.gmail_extraction_service import GmailExtractionService from .services.gmail_extraction_service import GmailExtractionService
from .services.auto_gmail_conversation_service import AutoGmailConversationService from .services.auto_gmail_conversation_service import AutoGmailConversationService
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from apps.gmail.models import GmailCredential, GmailConversation, AutoReplyConfig from apps.gmail.models import GmailCredential, GmailConversation, AutoReplyConfig
from apps.gmail.services.gmail_service import GmailService from apps.gmail.services.gmail_service import GmailService
from apps.gmail.serializers import AutoReplyConfigSerializer from apps.gmail.serializers import AutoReplyConfigSerializer
from apps.gmail.services.goal_service import get_or_create_goal, get_conversation_summary from apps.gmail.services.goal_service import get_or_create_goal, get_conversation_summary
@ -22,6 +22,7 @@ from apps.chat.models import ChatHistory
from apps.knowledge_base.models import KnowledgeBase from apps.knowledge_base.models import KnowledgeBase
from apps.user.authentication import CustomTokenAuthentication from apps.user.authentication import CustomTokenAuthentication
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -47,64 +48,23 @@ class FeishuTableRecordsView(APIView):
) )
try: try:
# 记录原始URL以便于排查问题
logger.info(f"处理查询请求URL: {table_url}")
# 从URL中提取app_token和table_id # 从URL中提取app_token和table_id
try: app_token, table_id = BitableService.extract_params_from_url(table_url)
app_token, table_id = BitableService.extract_params_from_url(table_url)
logger.info(f"成功解析URL参数: app_token={app_token}, table_id={table_id}")
except ValueError as ve:
logger.error(f"URL解析失败: {str(ve)}, URL: {table_url}")
return Response(
{"error": str(ve), "details": "URL格式可能不正确请参考飞书多维表格文档"},
status=status.HTTP_400_BAD_REQUEST
)
# 验证应用是否有权限访问该多维表格
if not BitableService.validate_access(app_token, access_token):
logger.warning(f"应用无权限访问多维表格: app_token={app_token}")
return Response(
{"error": "无权限访问该多维表格请确认access_token是否有效以及应用是否已被添加为表格协作者"},
status=status.HTTP_403_FORBIDDEN
)
# 先获取一些样本数据,检查我们能否访问多维表格 # 先获取一些样本数据,检查我们能否访问多维表格
try: sample_data = BitableService.search_records(
sample_data = BitableService.search_records( app_token=app_token,
app_token=app_token, table_id=table_id,
table_id=table_id, access_token=access_token,
access_token=access_token, filter_exp=filter_exp,
filter_exp=filter_exp, sort=sort,
sort=sort, page_size=page_size,
page_size=page_size, page_token=page_token
page_token=page_token )
)
return Response(sample_data, status=status.HTTP_200_OK)
logger.info(f"成功获取多维表格数据,记录数: {len(sample_data.get('items', []))}")
return Response(sample_data, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"查询多维表格数据失败: {str(e)}")
# 提供更友好的错误信息
error_message = str(e)
if "资源不存在" in error_message:
return Response(
{"error": "表格资源不存在请检查URL是否正确以及应用是否有权限"},
status=status.HTTP_404_NOT_FOUND
)
elif "令牌错误" in error_message:
return Response(
{"error": "访问令牌无效或已过期,请重新获取"},
status=status.HTTP_401_UNAUTHORIZED
)
else:
return Response(
{"error": f"查询多维表格失败: {error_message}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
except ValueError as ve: except ValueError as ve:
logger.error(f"URL解析或参数验证失败: {str(ve)}")
return Response( return Response(
{"error": str(ve), "details": "URL格式可能不正确"}, {"error": str(ve), "details": "URL格式可能不正确"},
status=status.HTTP_400_BAD_REQUEST status=status.HTTP_400_BAD_REQUEST
@ -112,7 +72,6 @@ class FeishuTableRecordsView(APIView):
except Exception as e: except Exception as e:
error_details = traceback.format_exc() error_details = traceback.format_exc()
logger.error(f"查询飞书多维表格失败: {str(e)}\n{error_details}")
return Response( return Response(
{ {
"error": f"查询飞书多维表格失败: {str(e)}", "error": f"查询飞书多维表格失败: {str(e)}",
@ -143,77 +102,32 @@ class FeishuDataSyncView(APIView):
# 提取参数 # 提取参数
try: try:
# 记录原始URL以便于排查问题 app_token, table_id = BitableService.extract_params_from_url(table_url)
logger.info(f"处理数据同步请求URL: {table_url}")
# 从URL中提取app_token和table_id
try:
app_token, table_id = BitableService.extract_params_from_url(table_url)
logger.info(f"成功解析URL参数: app_token={app_token}, table_id={table_id}")
except ValueError as ve:
logger.error(f"URL解析失败: {str(ve)}, URL: {table_url}")
return Response(
{"error": str(ve), "details": "URL格式可能不正确请参考飞书多维表格文档"},
status=status.HTTP_400_BAD_REQUEST
)
# 验证应用是否有权限访问该多维表格
if not BitableService.validate_access(app_token, access_token):
logger.warning(f"应用无权限访问多维表格: app_token={app_token}")
return Response(
{"error": "无权限访问该多维表格请确认access_token是否有效以及应用是否已被添加为表格协作者"},
status=status.HTTP_403_FORBIDDEN
)
# 先获取一些样本数据,检查我们能否访问多维表格 # 先获取一些样本数据,检查我们能否访问多维表格
try: sample_data = BitableService.search_records(
sample_data = BitableService.search_records( app_token=app_token,
app_token=app_token, table_id=table_id,
table_id=table_id, access_token=access_token,
access_token=access_token, page_size=5
page_size=5 )
)
# 执行数据同步
logger.info(f"成功获取多维表格样本数据,记录数: {len(sample_data.get('items', []))}") result = DataSyncService.sync_data_to_db(
table_url=table_url,
# 执行数据同步 access_token=access_token,
result = DataSyncService.sync_data_to_db( table_name=table_name,
table_url=table_url, primary_key=primary_key
access_token=access_token, )
table_name=table_name,
primary_key=primary_key # 添加样本数据到结果中
) if result.get('success'):
result['sample_data'] = sample_data.get('items', [])[:3] # 只返回最多3条样本数据
# 添加样本数据到结果中 return Response(result, status=status.HTTP_200_OK)
if result.get('success'): else:
result['sample_data'] = sample_data.get('items', [])[:3] # 只返回最多3条样本数据 return Response(result, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
logger.info(f"数据同步成功: {result}")
return Response(result, status=status.HTTP_200_OK)
else:
logger.error(f"数据同步失败: {result.get('error')}")
return Response(result, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
except Exception as e:
logger.error(f"数据同步过程失败: {str(e)}")
# 提供更友好的错误信息
error_message = str(e)
if "资源不存在" in error_message:
return Response(
{"error": "表格资源不存在请检查URL是否正确以及应用是否有权限"},
status=status.HTTP_404_NOT_FOUND
)
elif "令牌错误" in error_message:
return Response(
{"error": "访问令牌无效或已过期,请重新获取"},
status=status.HTTP_401_UNAUTHORIZED
)
else:
return Response(
{"error": f"数据同步失败: {error_message}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
except ValueError as ve: except ValueError as ve:
logger.error(f"URL解析或参数验证失败: {str(ve)}")
return Response( return Response(
{"error": str(ve), "details": "URL格式可能不正确"}, {"error": str(ve), "details": "URL格式可能不正确"},
status=status.HTTP_400_BAD_REQUEST status=status.HTTP_400_BAD_REQUEST
@ -222,7 +136,6 @@ class FeishuDataSyncView(APIView):
except Exception as e: except Exception as e:
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
logger.error(f"数据同步失败: {str(e)}\n{error_details}")
return Response( return Response(
{ {
"error": f"数据同步失败: {str(e)}", "error": f"数据同步失败: {str(e)}",

View File

@ -1,7 +1,7 @@
# Generated by Django 5.2.1 on 2025-05-28 08:45 # Generated by Django 5.2 on 2025-05-13 06:43
import django.db.models.deletion import django.db.models.deletion
import uuid import django.utils.timezone
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
@ -19,16 +19,14 @@ class Migration(migrations.Migration):
name='GmailConversation', name='GmailConversation',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('conversation_id', models.CharField(help_text='Unique conversation identifier', max_length=100, unique=True)), ('user_email', models.EmailField(help_text='用户Gmail邮箱', max_length=254)),
('user_email', models.EmailField(help_text="User's Gmail address", max_length=254)), ('influencer_email', models.EmailField(help_text='达人Gmail邮箱', max_length=254)),
('influencer_email', models.EmailField(help_text="Influencer's email address", max_length=254)), ('conversation_id', models.CharField(help_text='关联到chat_history的会话ID', max_length=100, unique=True)),
('title', models.CharField(help_text='Conversation title', max_length=255)), ('title', models.CharField(default='Gmail对话', help_text='对话标题', max_length=100)),
('last_sync_time', models.DateTimeField(default=django.utils.timezone.now, help_text='最后同步时间')),
('created_at', models.DateTimeField(auto_now_add=True)), ('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)), ('updated_at', models.DateTimeField(auto_now=True)),
('last_sync_time', models.DateTimeField(blank=True, help_text='Last time conversation was synced with Gmail', null=True)), ('is_active', models.BooleanField(default=True)),
('is_active', models.BooleanField(default=True, help_text='Whether this conversation is active')),
('has_sent_greeting', models.BooleanField(default=False, help_text='Whether a greeting message has been sent to this conversation')),
('metadata', models.JSONField(blank=True, help_text='Additional metadata for the conversation', null=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gmail_conversations', to=settings.AUTH_USER_MODEL)), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gmail_conversations', to=settings.AUTH_USER_MODEL)),
], ],
options={ options={
@ -41,7 +39,7 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email_message_id', models.CharField(help_text='Gmail邮件ID', max_length=100)), ('email_message_id', models.CharField(help_text='Gmail邮件ID', max_length=100)),
('attachment_id', models.TextField(help_text='Gmail附件的唯一标识符可能很长')), ('attachment_id', models.CharField(help_text='Gmail附件ID', max_length=100)),
('filename', models.CharField(help_text='原始文件名', max_length=255)), ('filename', models.CharField(help_text='原始文件名', max_length=255)),
('file_path', models.CharField(help_text='保存在服务器上的路径', max_length=255)), ('file_path', models.CharField(help_text='保存在服务器上的路径', max_length=255)),
('content_type', models.CharField(help_text='MIME类型', max_length=100)), ('content_type', models.CharField(help_text='MIME类型', max_length=100)),
@ -55,107 +53,6 @@ class Migration(migrations.Migration):
'ordering': ['-created_at'], 'ordering': ['-created_at'],
}, },
), ),
migrations.CreateModel(
name='ConversationSummary',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('content', models.TextField(verbose_name='摘要内容')),
('last_message_id', models.CharField(blank=True, max_length=255, null=True, verbose_name='最后处理的消息ID或ChatHistory的ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('conversation', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='summary', to='gmail.gmailconversation')),
],
options={
'verbose_name': 'Gmail对话摘要',
'verbose_name_plural': 'Gmail对话摘要',
'db_table': 'gmail_conversation_summaries',
},
),
migrations.CreateModel(
name='ProcessedPushNotification',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('message_id', models.CharField(help_text='Pub/Sub消息ID', max_length=255, unique=True)),
('email_address', models.EmailField(help_text='通知关联的Gmail邮箱', max_length=254)),
('history_id', models.CharField(help_text='Gmail历史ID', max_length=100)),
('processed_at', models.DateTimeField(auto_now_add=True, help_text='处理时间')),
('is_successful', models.BooleanField(default=True, help_text='处理是否成功')),
('metadata', models.JSONField(blank=True, default=dict, help_text='额外信息')),
],
options={
'verbose_name': '已处理推送通知',
'verbose_name_plural': '已处理推送通知',
'db_table': 'gmail_processed_push_notifications',
'ordering': ['-processed_at'],
'indexes': [models.Index(fields=['message_id'], name='gmail_proce_message_912a0c_idx'), models.Index(fields=['email_address', 'history_id'], name='gmail_proce_email_a_2e3770_idx')],
},
),
migrations.CreateModel(
name='UnmatchedEmail',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('message_id', models.CharField(help_text='Gmail邮件ID', max_length=255, unique=True)),
('user_id', models.CharField(help_text='用户ID (UUID字符串形式)', max_length=36)),
('user_email', models.EmailField(help_text='用户Gmail邮箱', max_length=254)),
('from_email', models.EmailField(help_text='发件人邮箱', max_length=254)),
('to_email', models.EmailField(help_text='收件人邮箱', max_length=254)),
('subject', models.CharField(blank=True, help_text='邮件主题', max_length=500)),
('processed_at', models.DateTimeField(auto_now_add=True, help_text='处理时间')),
],
options={
'verbose_name': '未匹配邮件',
'verbose_name_plural': '未匹配邮件',
'db_table': 'gmail_unmatched_emails',
'ordering': ['-processed_at'],
'indexes': [models.Index(fields=['message_id'], name='gmail_unmat_message_c1924d_idx'), models.Index(fields=['user_id'], name='gmail_unmat_user_id_ee06fe_idx'), models.Index(fields=['user_email', 'from_email'], name='gmail_unmat_user_em_a096af_idx')],
},
),
migrations.CreateModel(
name='UserGoal',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('description', models.TextField(verbose_name='目标描述')),
('status', models.CharField(choices=[('pending', '待处理'), ('in_progress', '进行中'), ('completed', '已完成'), ('failed', '失败')], default='pending', max_length=20, verbose_name='目标状态')),
('is_active', models.BooleanField(default=True, verbose_name='是否激活')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('completion_time', models.DateTimeField(blank=True, null=True, verbose_name='完成时间')),
('last_activity_time', models.DateTimeField(blank=True, null=True, verbose_name='最后活动时间')),
('metadata', models.JSONField(blank=True, default=dict, help_text='存储额外信息', null=True)),
('conversation', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='goals', to='gmail.gmailconversation')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='goals', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': '用户目标',
'verbose_name_plural': '用户目标',
'ordering': ['-updated_at'],
},
),
migrations.CreateModel(
name='AutoReplyConfig',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('user_email', models.EmailField(help_text='用户Gmail邮箱', max_length=254)),
('influencer_email', models.EmailField(help_text='达人Gmail邮箱', max_length=254)),
('is_enabled', models.BooleanField(default=True, help_text='是否启用自动回复')),
('goal_description', models.TextField(help_text='AI回复时参考的目标', verbose_name='自动回复的目标描述')),
('reply_template', models.TextField(blank=True, help_text='回复模板可选为空则由AI生成', null=True)),
('max_replies', models.IntegerField(default=5, help_text='最大自动回复次数')),
('current_replies', models.IntegerField(default=0, help_text='当前已自动回复次数')),
('last_reply_time', models.DateTimeField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('metadata', models.JSONField(blank=True, default=dict, help_text='存储额外信息如已处理的消息ID等', null=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='auto_reply_configs', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Gmail自动回复配置',
'verbose_name_plural': 'Gmail自动回复配置',
'db_table': 'gmail_auto_reply_configs',
'ordering': ['-updated_at'],
'unique_together': {('user', 'user_email', 'influencer_email')},
},
),
migrations.CreateModel( migrations.CreateModel(
name='GmailCredential', name='GmailCredential',
fields=[ fields=[
@ -166,7 +63,6 @@ class Migration(migrations.Migration):
('created_at', models.DateTimeField(auto_now_add=True)), ('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)), ('updated_at', models.DateTimeField(auto_now=True)),
('is_valid', models.BooleanField(default=True, help_text='Whether the credential is valid')), ('is_valid', models.BooleanField(default=True, help_text='Whether the credential is valid')),
('last_history_id', models.CharField(blank=True, help_text='Last processed Gmail history ID', max_length=50, null=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gmail_credentials', to=settings.AUTH_USER_MODEL)), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gmail_credentials', to=settings.AUTH_USER_MODEL)),
], ],
options={ options={

View File

@ -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),
),
]

View 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),
),
]

View 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',
},
),
]

View 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'],
},
),
]

View File

@ -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='目标状态'),
),
]

View File

@ -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),
),
]

View File

@ -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附件的唯一标识符可能很长'),
),
]

View File

@ -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')},
},
),
]

View File

@ -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')],
},
),
]

View 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),
),
]

View 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,
),
]

View File

@ -17,6 +17,7 @@ from ..models import GmailCredential, GmailConversation, GmailAttachment, Conver
from apps.chat.models import ChatHistory from apps.chat.models import ChatHistory
from apps.knowledge_base.models import KnowledgeBase from apps.knowledge_base.models import KnowledgeBase
import requests import requests
# from apps.common.services.notification_service import NotificationService
import threading import threading
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText

View File

@ -1,6 +1,6 @@
from django.urls import path, include from django.urls import path, include
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from apps.gmail.views import ( from .views import (
GmailAuthInitiateView, GmailAuthInitiateView,
GmailAuthCompleteView, GmailAuthCompleteView,
GmailConversationView, GmailConversationView,

View File

@ -35,85 +35,34 @@ class GmailAuthInitiateView(APIView):
""" """
API 视图用于启动 Gmail OAuth2 认证流程 API 视图用于启动 Gmail OAuth2 认证流程
""" """
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户 authentication_classes = [CustomTokenAuthentication]
def post(self, request): def post(self, request):
""" """
处理 POST 请求启动 Gmail OAuth2 认证并返回授权 URL 处理 POST 请求启动 Gmail OAuth2 认证并返回授权 URL
支持两种方式提供客户端密钥: 直接使用系统中已配置的客户端密钥文件
1. 在请求体中提供client_secret_json字段
2. 上传名为client_secret_file的JSON文件
Args: Args:
request: Django REST Framework 请求对象包含客户端密钥 JSON 数据或文件 request: Django REST Framework 请求对象
Returns: Returns:
Response: 包含授权 URL JSON 响应成功时或错误信息失败时 Response: 包含授权 URL JSON 响应成功时或错误信息失败时
Status Codes: Status Codes:
200: 成功生成授权 URL 200: 成功生成授权 URL
400: 请求数据无效
500: 服务器内部错误如认证服务失败 500: 服务器内部错误如认证服务失败
""" """
logger.debug(f"Received auth initiate request: {request.data}") logger.debug(f"Received auth initiate request from user {request.user.id}")
# 检查是否是文件上传方式 # 直接使用系统中已配置的客户端密钥文件
client_secret_json = None client_secret_path = 'media/secret/client_secret_266164728215-v84lngbp3vgr4ulql01sqkg5vaigf4a5.apps.googleusercontent.com.json'
if 'client_secret_file' in request.FILES:
try:
# 读取上传的JSON文件内容
client_secret_file = request.FILES['client_secret_file']
client_secret_json = json.loads(client_secret_file.read().decode('utf-8'))
logger.info(f"从上传文件读取到客户端密钥JSON")
except json.JSONDecodeError as e:
logger.error(f"解析客户端密钥JSON文件失败: {str(e)}")
return Response({
'code': 400,
'message': f'无效的JSON文件格式: {str(e)}',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
logger.error(f"处理上传文件失败: {str(e)}")
return Response({
'code': 500,
'message': f'处理上传文件失败: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# 如果不是文件上传则尝试从请求数据中提取JSON
if not client_secret_json:
serializer = GmailCredentialSerializer(data=request.data, context={'request': request})
if serializer.is_valid():
try:
# 从请求数据中提取客户端密钥 JSON
client_secret_json = serializer.validated_data['client_secret_json']
except Exception as e:
logger.error(f"未提供客户端密钥JSON: {str(e)}")
return Response({
'code': 400,
'message': '请提供client_secret_json字段或上传client_secret_file文件',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
else:
# 记录无效请求数据并返回错误响应
logger.warning(f"Invalid request data: {serializer.errors}")
return Response({
'code': 400,
'message': '请求数据无效请提供client_secret_json字段或上传client_secret_file文件',
'data': serializer.errors
}, status=status.HTTP_400_BAD_REQUEST)
# 如果此时仍然没有client_secret_json返回错误
if not client_secret_json:
return Response({
'code': 400,
'message': '请提供client_secret_json字段或上传client_secret_file文件',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
try: try:
# 读取客户端密钥文件
with open(client_secret_path, 'r') as f:
client_secret_json = json.load(f)
# 调用 GmailService 生成授权 URL # 调用 GmailService 生成授权 URL
auth_url = GmailService.initiate_authentication(request.user, client_secret_json) auth_url = GmailService.initiate_authentication(request.user, client_secret_json)
logger.info(f"Generated auth URL for user {request.user.id}") logger.info(f"Generated auth URL for user {request.user.id}")
@ -136,19 +85,16 @@ class GmailAuthCompleteView(APIView):
""" """
API 视图用于完成 Gmail OAuth2 认证流程 API 视图用于完成 Gmail OAuth2 认证流程
""" """
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户 authentication_classes = [CustomTokenAuthentication]
def post(self, request): def post(self, request):
""" """
处理 POST 请求使用授权代码完成 Gmail OAuth2 认证并保存凭证 处理 POST 请求使用授权代码完成 Gmail OAuth2 认证并保存凭证
支持两种方式提供客户端密钥: 直接使用系统中已配置的客户端密钥文件
1. 在请求体中提供client_secret_json字段
2. 上传名为client_secret_file的JSON文件
Args: Args:
request: Django REST Framework 请求对象包含授权代码和客户端密钥 JSON 或文件 request: Django REST Framework 请求对象包含授权代码
Returns: Returns:
Response: 包含已保存凭证数据的 JSON 响应成功时或错误信息失败时 Response: 包含已保存凭证数据的 JSON 响应成功时或错误信息失败时
@ -158,72 +104,25 @@ class GmailAuthCompleteView(APIView):
400: 请求数据无效 400: 请求数据无效
500: 服务器内部错误如认证失败 500: 服务器内部错误如认证失败
""" """
logger.debug(f"Received auth complete request: {request.data}") logger.debug(f"Received auth complete request from user {request.user.id}")
# 检查是否是文件上传方式 # 直接使用系统中已配置的客户端密钥文件
client_secret_json = None client_secret_path = 'media/secret/client_secret_266164728215-v84lngbp3vgr4ulql01sqkg5vaigf4a5.apps.googleusercontent.com.json'
if 'client_secret_file' in request.FILES:
try:
# 读取上传的JSON文件内容
client_secret_file = request.FILES['client_secret_file']
client_secret_json = json.loads(client_secret_file.read().decode('utf-8'))
logger.info(f"从上传文件读取到客户端密钥JSON")
except json.JSONDecodeError as e:
logger.error(f"解析客户端密钥JSON文件失败: {str(e)}")
return Response({
'code': 400,
'message': f'无效的JSON文件格式: {str(e)}',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
logger.error(f"处理上传文件失败: {str(e)}")
return Response({
'code': 500,
'message': f'处理上传文件失败: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# 获取授权,无论是哪种方式都需要 # 获取授权代码
auth_code = request.data.get('auth_code') auth_code = request.data.get('auth_code')
if not auth_code: if not auth_code:
return Response({ return Response({
'code': 400, 'code': 400,
'message': '必须提供授权码', 'message': '缺少必要的授权代码',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 如果不是文件上传则尝试从请求数据中提取JSON
if not client_secret_json:
serializer = GmailCredentialSerializer(data=request.data, context={'request': request})
if serializer.is_valid():
try:
# 从请求数据中提取客户端密钥 JSON
client_secret_json = serializer.validated_data['client_secret_json']
except Exception as e:
logger.error(f"未提供客户端密钥JSON: {str(e)}")
return Response({
'code': 400,
'message': '请提供client_secret_json字段或上传client_secret_file文件',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
else:
# 记录无效请求数据并返回错误响应
logger.warning(f"Invalid request data: {serializer.errors}")
return Response({
'code': 400,
'message': '请求数据无效请提供client_secret_json字段或上传client_secret_file文件',
'data': serializer.errors
}, status=status.HTTP_400_BAD_REQUEST)
# 如果此时仍然没有client_secret_json返回错误
if not client_secret_json:
return Response({
'code': 400,
'message': '请提供client_secret_json字段或上传client_secret_file文件',
'data': None 'data': None
}, status=status.HTTP_400_BAD_REQUEST) }, status=status.HTTP_400_BAD_REQUEST)
try: try:
# 读取客户端密钥文件
with open(client_secret_path, 'r') as f:
client_secret_json = json.load(f)
# 完成认证并保存凭证 # 完成认证并保存凭证
credential = GmailService.complete_authentication(request.user, auth_code, client_secret_json) credential = GmailService.complete_authentication(request.user, auth_code, client_secret_json)
# 序列化凭证数据以返回 # 序列化凭证数据以返回
@ -525,9 +424,9 @@ class GmailConversationView(APIView):
""" """
API视图用于获取和保存Gmail对话 API视图用于获取和保存Gmail对话
""" """
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户 authentication_classes = [CustomTokenAuthentication]
def post(self, request): def post(self, request):
""" """
处理POST请求获取Gmail对话并保存到聊天历史 处理POST请求获取Gmail对话并保存到聊天历史
@ -630,9 +529,9 @@ class GmailAttachmentListView(APIView):
""" """
API视图用于获取Gmail附件列表 API视图用于获取Gmail附件列表
""" """
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户 authentication_classes = [CustomTokenAuthentication]
def get(self, request, conversation_id=None): def get(self, request, conversation_id=None):
""" """
处理GET请求获取指定对话的附件列表 处理GET请求获取指定对话的附件列表
@ -679,9 +578,9 @@ class GmailPubSubView(APIView):
""" """
API视图用于设置Gmail的Pub/Sub实时通知 API视图用于设置Gmail的Pub/Sub实时通知
""" """
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户 authentication_classes = [CustomTokenAuthentication]
def post(self, request): def post(self, request):
""" """
处理POST请求为用户的Gmail账户设置Pub/Sub推送通知 处理POST请求为用户的Gmail账户设置Pub/Sub推送通知
@ -761,9 +660,9 @@ class GmailPubSubView(APIView):
credentials = request.user.gmail_credentials.filter(is_valid=True) credentials = request.user.gmail_credentials.filter(is_valid=True)
# 构建响应数据 # 构建响应数据
user = [] accounts = []
for cred in credentials: for cred in credentials:
user.append({ accounts.append({
'id': cred.id, 'id': cred.id,
'email': cred.email, 'email': cred.email,
'is_default': cred.is_default 'is_default': cred.is_default
@ -772,7 +671,7 @@ class GmailPubSubView(APIView):
return Response({ return Response({
'code': 200, 'code': 200,
'message': '获取账户列表成功', 'message': '获取账户列表成功',
'data': {'user': user} 'data': {'accounts': accounts}
}, status=status.HTTP_200_OK) }, status=status.HTTP_200_OK)
@ -780,9 +679,9 @@ class GmailSendEmailView(APIView):
""" """
API视图用于发送Gmail邮件支持附件 API视图用于发送Gmail邮件支持附件
""" """
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户 authentication_classes = [CustomTokenAuthentication]
def post(self, request): def post(self, request):
""" """
处理POST请求发送Gmail邮件 处理POST请求发送Gmail邮件
@ -915,9 +814,9 @@ class GmailSendEmailView(APIView):
credentials = request.user.gmail_credentials.filter(is_valid=True) credentials = request.user.gmail_credentials.filter(is_valid=True)
# 构建响应数据 # 构建响应数据
user = [] accounts = []
for cred in credentials: for cred in credentials:
user.append({ accounts.append({
'id': cred.id, 'id': cred.id,
'email': cred.email, 'email': cred.email,
'is_default': cred.is_default 'is_default': cred.is_default
@ -926,7 +825,7 @@ class GmailSendEmailView(APIView):
return Response({ return Response({
'code': 200, 'code': 200,
'message': '获取账户列表成功', 'message': '获取账户列表成功',
'data': {'user': user} 'data': {'accounts': accounts}
}, status=status.HTTP_200_OK) }, status=status.HTTP_200_OK)
@ -1012,9 +911,9 @@ class GmailConversationSummaryView(APIView):
""" """
API视图用于获取Gmail对话的总结 API视图用于获取Gmail对话的总结
""" """
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户 authentication_classes = [CustomTokenAuthentication]
def get(self, request, conversation_id=None): def get(self, request, conversation_id=None):
""" """
处理GET请求获取指定Gmail对话的总结 处理GET请求获取指定Gmail对话的总结
@ -1094,7 +993,8 @@ class GmailGoalView(APIView):
用户目标API - 支持用户为每个对话设置不同的目标 用户目标API - 支持用户为每个对话设置不同的目标
""" """
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户 authentication_classes = [CustomTokenAuthentication]
def get(self, request, conversation_id=None): def get(self, request, conversation_id=None):
""" """
获取用户的目标 获取用户的目标
@ -1172,6 +1072,13 @@ class GmailGoalView(APIView):
'data': None 'data': None
}, status=status.HTTP_404_NOT_FOUND) }, status=status.HTTP_404_NOT_FOUND)
# 检查权限
# if conversation.user.id != request.user.id:
# return Response({
# 'code': 403,
# 'message': '无权限访问此对话',
# 'data': None
# }, status=status.HTTP_403_FORBIDDEN)
# 查找现有目标 # 查找现有目标
existing_goal = UserGoal.objects.filter( existing_goal = UserGoal.objects.filter(
@ -1274,7 +1181,8 @@ class SimpleRecommendedReplyView(APIView):
通过conversation_id一键获取目标对话摘要和推荐回复 通过conversation_id一键获取目标对话摘要和推荐回复
""" """
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户 authentication_classes = [CustomTokenAuthentication]
def post(self, request): def post(self, request):
""" """
直接通过conversation_id获取推荐回复 直接通过conversation_id获取推荐回复
@ -1312,12 +1220,12 @@ class SimpleRecommendedReplyView(APIView):
'data': None 'data': None
}, status=status.HTTP_404_NOT_FOUND) }, status=status.HTTP_404_NOT_FOUND)
if conversation.user != request.user: # if conversation.user != request.user:
return Response({ # return Response({
'code': 403, # 'code': 403,
'message': '无权限访问该对话', # 'message': '无权限访问该对话',
'data': None # 'data': None
}, status=status.HTTP_403_FORBIDDEN) # }, status=status.HTTP_403_FORBIDDEN)
except Exception as e: except Exception as e:
logger.error(f"查找对话失败: {str(e)}") logger.error(f"查找对话失败: {str(e)}")
return Response({ return Response({
@ -1399,8 +1307,8 @@ class GmailExportView(APIView):
""" """
API视图用于导出已回复的达人Gmail列表为Excel文件 API视图用于导出已回复的达人Gmail列表为Excel文件
""" """
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户 authentication_classes = [CustomTokenAuthentication]
def get(self, request, format=None): def get(self, request, format=None):
""" """

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.1 on 2025-05-28 08:33 # Generated by Django 5.2 on 2025-05-07 03:40
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid
@ -31,7 +31,8 @@ class Migration(migrations.Migration):
('update_time', models.DateTimeField(auto_now=True)), ('update_time', models.DateTimeField(auto_now=True)),
], ],
options={ options={
'indexes': [models.Index(fields=['type'], name='knowledge_b_type_7a8dcc_idx'), models.Index(fields=['department'], name='knowledge_b_departm_8f1cf3_idx'), models.Index(fields=['group'], name='knowledge_b_group_ae45d4_idx')], 'db_table': 'knowledge_bases',
'indexes': [models.Index(fields=['type'], name='knowledge_b_type_0439e7_idx'), models.Index(fields=['department'], name='knowledge_b_departm_e739fd_idx'), models.Index(fields=['group'], name='knowledge_b_group_3dcf34_idx')],
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
@ -50,7 +51,8 @@ class Migration(migrations.Migration):
options={ options={
'verbose_name': '知识库文档', 'verbose_name': '知识库文档',
'verbose_name_plural': '知识库文档', 'verbose_name_plural': '知识库文档',
'indexes': [models.Index(fields=['knowledge_base', 'status'], name='knowledge_b_knowled_6653e6_idx'), models.Index(fields=['document_id'], name='knowledge_b_documen_1fd896_idx'), models.Index(fields=['external_id'], name='knowledge_b_externa_ae1a97_idx')], 'db_table': 'knowledge_base_documents',
'indexes': [models.Index(fields=['knowledge_base', 'status'], name='knowledge_b_knowled_a4db1b_idx'), models.Index(fields=['document_id'], name='knowledge_b_documen_dab90f_idx'), models.Index(fields=['external_id'], name='knowledge_b_externa_b0060c_idx')],
'unique_together': {('knowledge_base', 'document_id')}, 'unique_together': {('knowledge_base', 'document_id')},
}, },
), ),

View File

@ -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,
),
]

View File

@ -34,6 +34,34 @@ class KnowledgeBase(models.Model):
create_time = models.DateTimeField(auto_now_add=True) create_time = models.DateTimeField(auto_now_add=True)
update_time = models.DateTimeField(auto_now=True) update_time = models.DateTimeField(auto_now=True)
# def is_owner(self, user):
# """检查用户是否是所有者(通过权限表检查)"""
# from apps.permissions.models import KnowledgeBasePermission
# return str(user.id) == str(self.user_id) or KnowledgeBasePermission.objects.filter(
# knowledge_base=self,
# user=user,
# can_read=True,
# can_edit=True,
# can_delete=True,
# status='active'
# ).exists()
# def get_owners(self):
# """获取所有所有者(包括创建者和具有完整权限的用户)"""
# from apps.user.models import User
# from apps.permissions.models import KnowledgeBasePermission
# # 获取创建者
# owners = [self.user_id]
# # 获取具有完整权限的用户
# permission_owners = KnowledgeBasePermission.objects.filter(
# knowledge_base=self,
# can_read=True,
# can_edit=True,
# can_delete=True,
# status='active'
# ).values_list('user_id', flat=True)
# owners.extend(permission_owners)
# return User.objects.filter(id__in=set(owners))
def calculate_stats(self): def calculate_stats(self):
"""计算文档统计信息""" """计算文档统计信息"""

View File

@ -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