diff --git a/apps/chat/migrations/0002_negotiationchat.py b/apps/chat/migrations/0002_negotiationchat.py new file mode 100644 index 0000000..524e9d6 --- /dev/null +++ b/apps/chat/migrations/0002_negotiationchat.py @@ -0,0 +1,29 @@ +# Generated by Django 5.2.1 on 2025-06-03 09:10 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('chat', '0001_initial'), + ('expertproducts', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='NegotiationChat', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('conversation_id', models.CharField(db_index=True, max_length=100, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('negotiation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chats', to='expertproducts.negotiation')), + ], + options={ + 'ordering': ['-updated_at'], + 'indexes': [models.Index(fields=['negotiation', 'updated_at'], name='chat_negoti_negotia_2aa711_idx')], + }, + ), + ] diff --git a/apps/chat/models.py b/apps/chat/models.py index 3328741..7590fb9 100644 --- a/apps/chat/models.py +++ b/apps/chat/models.py @@ -5,6 +5,7 @@ import uuid from itertools import count from apps.user.models import User from apps.knowledge_base.models import KnowledgeBase +from apps.expertproducts.models import Negotiation class ChatHistory(models.Model): """聊天历史记录""" @@ -119,3 +120,19 @@ class ChatHistory(models.Model): } for kb in self.get_knowledge_bases() ] } + +class NegotiationChat(models.Model): + """谈判对话关联表""" + negotiation = models.ForeignKey(Negotiation, on_delete=models.CASCADE, related_name='chats') + conversation_id = models.CharField(max_length=100, unique=True, db_index=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ['-updated_at'] + indexes = [ + models.Index(fields=['negotiation', 'updated_at']), + ] + + def __str__(self): + return f"谈判 {self.negotiation.id} - 对话 {self.conversation_id}" diff --git a/apps/chat/views.py b/apps/chat/views.py index 382bfda..4c08733 100644 --- a/apps/chat/views.py +++ b/apps/chat/views.py @@ -11,9 +11,10 @@ from rest_framework.response import Response from rest_framework.decorators import action from apps.user.models import User from apps.knowledge_base.models import KnowledgeBase -from apps.chat.models import ChatHistory +from apps.chat.models import ChatHistory, NegotiationChat from apps.chat.serializers import ChatHistorySerializer from apps.common.services.chat_service import ChatService +from apps.expertproducts.models import Negotiation from apps.chat.services.chat_api import ( ExternalAPIError, stream_chat_answer, get_chat_answer, generate_conversation_title, get_hit_test_documents, generate_conversation_title_from_deepseek @@ -30,17 +31,33 @@ class ChatHistoryViewSet(viewsets.ModelViewSet): serializer_class = ChatHistorySerializer queryset = ChatHistory.objects.all() + def check_knowledge_base_permission(self, kb, user, permission_type): + """ + 兼容方法:始终返回True表示有权限 + 此方法替代了原来的权限检查,现在我们不再检查知识库权限 + """ + return True + def get_queryset(self): - """确保用户只能看到自己的未删除的聊天记录以及有权限的知识库关联的聊天记录""" + """确保用户只能看到自己的未删除的聊天记录""" user = self.request.user - accessible_kb_ids = [ - kb.id for kb in KnowledgeBase.objects.all() - if self.check_knowledge_base_permission(kb, user, 'read') - ] - return ChatHistory.objects.filter( - Q(user=user) | Q(knowledge_base_id__in=accessible_kb_ids), + queryset = ChatHistory.objects.filter( + user=user, is_deleted=False ) + + # 获取status筛选参数 + status_filter = self.request.query_params.get('status') + if status_filter: + # 查找与谈判关联的对话ID + negotiation_chats = NegotiationChat.objects.filter( + negotiation__status=status_filter + ).values_list('conversation_id', flat=True) + + # 筛选这些对话ID对应的聊天记录 + queryset = queryset.filter(conversation_id__in=negotiation_chats) + + return queryset def list(self, request): """获取对话列表概览""" @@ -113,7 +130,7 @@ class ChatHistoryViewSet(viewsets.ModelViewSet): @action(detail=False, methods=['get']) def conversation_detail(self, request): - """获取特定对话的详细信息""" + """获取特定对话的详细信息,支持按status筛选并返回creator和product信息""" try: conversation_id = request.query_params.get('conversation_id') if not conversation_id: @@ -123,9 +140,73 @@ class ChatHistoryViewSet(viewsets.ModelViewSet): 'data': None }, status=status.HTTP_400_BAD_REQUEST) - chat_service = ChatService() - result = chat_service.get_conversation_detail(request.user, conversation_id) + # 获取状态筛选参数 + status_filter = request.query_params.get('status') + # 查找对话记录 + chat_records = ChatHistory.objects.filter( + conversation_id=conversation_id, + is_deleted=False + ).order_by('created_at') + + if not chat_records.exists(): + return Response({ + 'code': 404, + 'message': '对话不存在或已被删除', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + # 尝试查找关联的谈判记录 + negotiation_chat = NegotiationChat.objects.filter( + conversation_id=conversation_id + ).first() + + # 准备基本的返回数据 + result = { + 'conversation_id': conversation_id, + 'messages': [] + } + + # 如果存在谈判关联,添加谈判相关信息 + if negotiation_chat: + negotiation = negotiation_chat.negotiation + + # 如果有status筛选,匹配谈判状态 + if status_filter and negotiation.status != status_filter: + return Response({ + 'code': 404, + 'message': f'没有找到状态为 {status_filter} 的对话', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + # 添加谈判相关信息 + result['negotiation'] = { + 'id': str(negotiation.id), + 'status': negotiation.status, + 'current_round': negotiation.current_round, + 'context': negotiation.context, + 'creator': { + 'id': str(negotiation.creator.id), + 'name': negotiation.creator.name if hasattr(negotiation.creator, 'name') else "未知", + 'profile': negotiation.creator.profile if hasattr(negotiation.creator, 'profile') else {} + }, + 'product': { + 'id': str(negotiation.product.id), + 'name': negotiation.product.name if hasattr(negotiation.product, 'name') else "未知", + 'description': negotiation.product.description if hasattr(negotiation.product, 'description') else "" + } + } + + # 整理对话消息 + for record in chat_records: + result['messages'].append({ + 'id': str(record.id), + 'role': record.role, + 'content': record.content, + 'created_at': record.created_at.strftime('%Y-%m-%d %H:%M:%S'), + 'metadata': record.metadata + }) + return Response({ 'code': 200, 'message': '获取成功', @@ -153,10 +234,8 @@ class ChatHistoryViewSet(viewsets.ModelViewSet): """获取用户可访问的知识库列表""" try: user = request.user - accessible_datasets = [ - dataset for dataset in KnowledgeBase.objects.all() - if self.check_knowledge_base_permission(dataset, user, 'read') - ] + # 直接返回所有知识库,不做权限检查 + accessible_datasets = KnowledgeBase.objects.all() return Response({ 'code': 200, @@ -183,83 +262,49 @@ class ChatHistoryViewSet(viewsets.ModelViewSet): @action(detail=False, methods=['post']) def create_conversation(self, request): - """创建会话 - 先选择知识库创建会话ID,不发送问题""" + """创建会话 - 关联到谈判ID""" try: data = request.data - # 检查知识库ID:支持dataset_id或dataset_id_list格式 - dataset_ids = [] - if 'dataset_id' in data: - dataset_id = data['dataset_id'] - # 直接使用标准UUID格式 - dataset_ids.append(str(dataset_id)) - elif 'dataset_id_list' in data and isinstance(data['dataset_id_list'], (list, str)): - # 处理可能的字符串格式 - if isinstance(data['dataset_id_list'], str): - try: - # 尝试解析JSON字符串 - dataset_list = json.loads(data['dataset_id_list']) - if isinstance(dataset_list, list): - dataset_ids = [str(id) for id in dataset_list] - except json.JSONDecodeError: - # 如果解析失败,可能是单个ID - dataset_ids = [str(data['dataset_id_list'])] - else: - # 如果已经是列表,直接使用标准UUID格式 - dataset_ids = [str(id) for id in data['dataset_id_list']] - else: + # 检查谈判ID + negotiation_id = data.get('negotiation_id') + if not negotiation_id: return Response({ 'code': 400, - 'message': '缺少必填字段: dataset_id 或 dataset_id_list', + 'message': '缺少必填字段: negotiation_id', 'data': None }, status=status.HTTP_400_BAD_REQUEST) - if not dataset_ids: + # 验证谈判存在性 + try: + negotiation = Negotiation.objects.get(id=negotiation_id) + except Negotiation.DoesNotExist: return Response({ - 'code': 400, - 'message': '至少需要提供一个知识库ID', + 'code': 404, + 'message': f'谈判记录不存在: {negotiation_id}', 'data': None - }, status=status.HTTP_400_BAD_REQUEST) + }, status=status.HTTP_404_NOT_FOUND) - # 验证所有知识库 - user = request.user - knowledge_bases = [] # 存储所有知识库对象 - - for kb_id in dataset_ids: - try: - knowledge_base = KnowledgeBase.objects.filter(id=kb_id).first() - if not knowledge_base: - return Response({ - 'code': 404, - 'message': f'知识库不存在: {kb_id}', - 'data': None - }, status=status.HTTP_404_NOT_FOUND) - - knowledge_bases.append(knowledge_base) - - # 使用统一的权限检查方法 - if not self.check_knowledge_base_permission(knowledge_base, user, 'read'): - return Response({ - 'code': 403, - 'message': f'无权访问知识库: {knowledge_base.name}', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) - - except Exception as e: - return Response({ - 'code': 400, - 'message': f'处理知识库ID出错: {str(e)}', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) + # 获取达人邮箱 + creator = negotiation.creator + creator_email = creator.email if hasattr(creator, 'email') else None # 创建一个新的会话ID conversation_id = str(uuid.uuid4()) logger.info(f"创建新的会话ID: {conversation_id}") - # 准备metadata (仍然保存知识库名称用于内部处理) + # 创建谈判关联 + NegotiationChat.objects.create( + negotiation=negotiation, + conversation_id=conversation_id + ) + + # 准备metadata metadata = { - 'dataset_id_list': [str(id) for id in dataset_ids], - 'dataset_names': [kb.name for kb in knowledge_bases] + 'negotiation_id': str(negotiation_id), + 'status': negotiation.status, + 'creator_id': str(creator.id), + 'creator_email': creator_email } return Response({ @@ -267,7 +312,13 @@ class ChatHistoryViewSet(viewsets.ModelViewSet): 'message': '会话创建成功', 'data': { 'conversation_id': conversation_id, - 'dataset_id_list': metadata['dataset_id_list'] + 'negotiation_id': str(negotiation_id), + 'status': negotiation.status, + 'creator': { + 'id': str(creator.id), + 'name': creator.name if hasattr(creator, 'name') else "未知", + 'email': creator_email + } } }) @@ -279,7 +330,7 @@ class ChatHistoryViewSet(viewsets.ModelViewSet): 'message': f'创建会话失败: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - + def create(self, request): """创建聊天记录""" try: @@ -630,6 +681,9 @@ class ChatHistoryViewSet(viewsets.ModelViewSet): end_date = request.query_params.get('end_date') page = int(request.query_params.get('page', 1)) page_size = int(request.query_params.get('page_size', 10)) + + # 获取status筛选参数 + status_filter = request.query_params.get('status') query = self.get_queryset() if keyword: @@ -638,14 +692,19 @@ class ChatHistoryViewSet(viewsets.ModelViewSet): Q(knowledge_base__name__icontains=keyword) ) if dataset_id: - knowledge_base = KnowledgeBase.objects.filter(id=dataset_id).first() - if knowledge_base and not self.check_knowledge_base_permission(knowledge_base, request.user, 'read'): - return Response({ - 'code': 403, - 'message': '无权访问该知识库', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) + # 移除权限检查,直接筛选知识库ID query = query.filter(knowledge_base__id=dataset_id) + + # 添加status筛选条件 + if status_filter: + # 查找与谈判关联的对话ID + negotiation_chats = NegotiationChat.objects.filter( + negotiation__status=status_filter + ).values_list('conversation_id', flat=True) + + # 筛选这些对话ID对应的聊天记录 + query = query.filter(conversation_id__in=negotiation_chats) + if start_date: query = query.filter(created_at__gte=start_date) if end_date: @@ -793,3 +852,159 @@ class ChatHistoryViewSet(viewsets.ModelViewSet): 'message': f"更新会话标题失败: {str(e)}", 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + @action(detail=False, methods=['post']) + def send_email_to_creator(self, request): + """通过对话ID发送邮件给达人""" + try: + data = request.data + conversation_id = data.get('conversation_id') + + if not conversation_id: + return Response({ + 'code': 400, + 'message': '缺少必填字段: conversation_id', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 验证邮件参数 + subject = data.get('subject', '') # 设置默认主题 + body = data.get('body') + from_email = data.get('from_email') + + if not all([body, from_email]): + return Response({ + 'code': 400, + 'message': '缺少必要的邮件参数,请提供body和from_email字段', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 查找关联的谈判信息 + negotiation_chat = NegotiationChat.objects.filter( + conversation_id=conversation_id + ).first() + + if not negotiation_chat: + return Response({ + 'code': 404, + 'message': f'找不到与对话ID关联的谈判: {conversation_id}', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + # 获取谈判中的达人信息 + negotiation = negotiation_chat.negotiation + creator_id = negotiation.creator.id + + # 直接从CreatorProfile模型中获取达人邮箱 + from apps.daren_detail.models import CreatorProfile + creator = CreatorProfile.objects.filter(id=creator_id).first() + + if not creator: + return Response({ + 'code': 404, + 'message': f'找不到达人信息: {creator_id}', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + creator_email = creator.email + + if not creator_email: + return Response({ + 'code': 404, + 'message': '达人没有关联的邮箱地址', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + # 处理附件 + attachments = [] + for file_key, file_obj in request.FILES.items(): + if file_obj: + # 保存临时文件 + import os + from django.conf import settings + + tmp_path = os.path.join(settings.MEDIA_ROOT, 'tmp', f'{file_key}_{file_obj.name}') + os.makedirs(os.path.dirname(tmp_path), exist_ok=True) + + with open(tmp_path, 'wb+') as destination: + for chunk in file_obj.chunks(): + destination.write(chunk) + + attachments.append({ + 'path': tmp_path, + 'filename': file_obj.name + }) + + # 直接调用Gmail服务发送邮件,而不是通过视图 + from apps.gmail.services.gmail_service import GmailService + + success, result = GmailService.send_email( + request.user, + from_email, + creator_email, + subject or f'与{creator.name}的谈判', + body or '', + attachments + ) + + if success: + # 发送成功后,将邮件记录添加到对话历史 + # 查询一个有效的知识库ID + default_kb = KnowledgeBase.objects.first() + if not default_kb: + logger.warning("未找到有效的知识库,无法记录邮件发送历史") + # 返回成功但提示无法记录历史 + return Response({ + 'code': 200, + 'message': '邮件发送成功,但未能记录到对话历史', + 'data': { + 'conversation_id': conversation_id, + 'creator_email': creator_email, + 'creator_name': creator.name, + 'message_id': result + } + }) + + ChatHistory.objects.create( + user=request.user, + knowledge_base=default_kb, # 使用查询到的默认知识库 + conversation_id=conversation_id, + role='system', + content=f"发送邮件给 {creator.name}: {subject}\n\n{body}", + metadata={ + 'email_sent': True, + 'to': creator_email, + 'from': from_email, + 'subject': subject, + 'negotiation_id': str(negotiation.id), + 'status': negotiation.status, + 'has_attachments': len(attachments) > 0 + } + ) + + return Response({ + 'code': 200, + 'message': '邮件发送成功', + 'data': { + 'conversation_id': conversation_id, + 'creator_email': creator_email, + 'creator_name': creator.name, + 'message_id': result + } + }) + else: + # 返回发送失败信息 + return Response({ + 'code': 500, + 'message': f'邮件发送失败: {result}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + except Exception as e: + logger.error(f"发送邮件给达人失败: {str(e)}") + logger.error(traceback.format_exc()) + return Response({ + 'code': 500, + 'message': f'发送邮件给达人失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)