import json import traceback import logging import uuid from django.db import transaction from django.utils import timezone from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from django.http import Http404 from .models import FeishuTableMapping from .services.bitable_service import BitableService from .services.data_sync_service import DataSyncService from .services.gmail_extraction_service import GmailExtractionService from .services.auto_gmail_conversation_service import AutoGmailConversationService from rest_framework.permissions import IsAuthenticated from apps.gmail.models import GmailCredential, GmailConversation, AutoReplyConfig from apps.gmail.services.gmail_service import GmailService from apps.gmail.serializers import AutoReplyConfigSerializer logger = logging.getLogger(__name__) class FeishuTableRecordsView(APIView): """ 查询飞书多维表格数据的视图 """ def post(self, request, *args, **kwargs): try: # 获取前端传来的数据 data = request.data table_url = data.get('table_url') access_token = data.get('access_token') filter_exp = data.get('filter') sort = data.get('sort') page_size = data.get('page_size', 20) page_token = data.get('page_token') if not table_url or not access_token: return Response( {"error": "请提供多维表格URL和access_token"}, status=status.HTTP_400_BAD_REQUEST ) try: # 从URL中提取app_token和table_id app_token, table_id = BitableService.extract_params_from_url(table_url) # 先获取一些样本数据,检查我们能否访问多维表格 sample_data = BitableService.search_records( app_token=app_token, table_id=table_id, access_token=access_token, filter_exp=filter_exp, sort=sort, page_size=page_size, page_token=page_token ) return Response(sample_data, status=status.HTTP_200_OK) except ValueError as ve: return Response( {"error": str(ve), "details": "URL格式可能不正确"}, status=status.HTTP_400_BAD_REQUEST ) except Exception as e: error_details = traceback.format_exc() return Response( { "error": f"查询飞书多维表格失败: {str(e)}", "details": error_details[:500] # 限制错误详情长度 }, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) class FeishuDataSyncView(APIView): """ 将飞书多维表格数据同步到数据库的视图 """ def post(self, request, *args, **kwargs): try: # 获取前端传来的数据 data = request.data table_url = data.get('table_url') access_token = data.get('access_token') table_name = data.get('table_name') # 可选,自定义表名 primary_key = data.get('primary_key') # 可选,指定主键 if not table_url or not access_token: return Response( {"error": "请提供多维表格URL和access_token"}, status=status.HTTP_400_BAD_REQUEST ) # 提取参数 try: app_token, table_id = BitableService.extract_params_from_url(table_url) # 先获取一些样本数据,检查我们能否访问多维表格 sample_data = BitableService.search_records( app_token=app_token, table_id=table_id, access_token=access_token, page_size=5 ) # 执行数据同步 result = DataSyncService.sync_data_to_db( table_url=table_url, access_token=access_token, table_name=table_name, primary_key=primary_key ) # 添加样本数据到结果中 if result.get('success'): result['sample_data'] = sample_data.get('items', [])[:3] # 只返回最多3条样本数据 return Response(result, status=status.HTTP_200_OK) else: return Response(result, status=status.HTTP_500_INTERNAL_SERVER_ERROR) except ValueError as ve: return Response( {"error": str(ve), "details": "URL格式可能不正确"}, status=status.HTTP_400_BAD_REQUEST ) except Exception as e: import traceback error_details = traceback.format_exc() return Response( { "error": f"数据同步失败: {str(e)}", "details": error_details[:500] # 限制错误详情长度 }, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) class FeishuTableMappingListView(APIView): """ 获取已映射表格列表的视图 """ def get(self, request, format=None): """获取所有映射的表格列表""" mappings = FeishuTableMapping.objects.all().order_by('-last_sync_time') result = [] for mapping in mappings: result.append({ 'id': mapping.id, 'app_token': mapping.app_token, 'table_id': mapping.table_id, 'table_url': mapping.table_url, 'table_name': mapping.table_name, 'feishu_table_name': mapping.feishu_table_name, 'last_sync_time': mapping.last_sync_time.strftime('%Y-%m-%d %H:%M:%S') if mapping.last_sync_time else None, 'total_records': mapping.total_records }) return Response(result) class FeishuTableMappingDetailView(APIView): """ 单个表格映射的操作视图 """ def get_object(self, pk): try: return FeishuTableMapping.objects.get(pk=pk) except FeishuTableMapping.DoesNotExist: raise Http404 def get(self, request, pk, format=None): """获取单个映射详情""" mapping = self.get_object(pk) return Response({ 'id': mapping.id, 'app_token': mapping.app_token, 'table_id': mapping.table_id, 'table_url': mapping.table_url, 'table_name': mapping.table_name, 'feishu_table_name': mapping.feishu_table_name, 'last_sync_time': mapping.last_sync_time.strftime('%Y-%m-%d %H:%M:%S') if mapping.last_sync_time else None, 'total_records': mapping.total_records }) def post(self, request, pk, format=None): """同步单个表格数据""" mapping = self.get_object(pk) # 获取access_token access_token = request.data.get('access_token') if not access_token: return Response( {"error": "请提供access_token"}, status=status.HTTP_400_BAD_REQUEST ) # 执行数据同步 result = DataSyncService.sync_data_to_db( table_url=mapping.table_url, access_token=access_token, table_name=mapping.table_name, auto_sync=True ) if result.get('success'): return Response(result, status=status.HTTP_200_OK) else: return Response(result, status=status.HTTP_500_INTERNAL_SERVER_ERROR) def delete(self, request, pk, format=None): """删除映射关系(不删除数据表)""" mapping = self.get_object(pk) mapping.delete() return Response({"message": "映射关系已删除"}, status=status.HTTP_204_NO_CONTENT) class GmailExtractionView(APIView): """ 从飞书多维表格中提取Gmail邮箱并创建对话 """ @transaction.atomic def post(self, request, *args, **kwargs): try: # 获取请求数据 data = request.data table_url = data.get('table_url') access_token = data.get('access_token') email_field_name = data.get('email_field_name') user_email = data.get('user_email') kb_id = data.get('kb_id') # 验证必填字段 if not table_url or not access_token or not email_field_name or not user_email: return Response( {"error": "请提供必要参数: table_url, access_token, email_field_name, user_email"}, status=status.HTTP_400_BAD_REQUEST ) # 提取Gmail邮箱 gmail_emails, error = GmailExtractionService.find_duplicate_emails( db_table_name=None, # 不需要检查数据库表 feishu_table_url=table_url, access_token=access_token, email_field_name=email_field_name, user=request.user ) if error: return Response({"error": error}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) if not gmail_emails: return Response({"message": "未找到Gmail邮箱"}, status=status.HTTP_200_OK) # 创建对话 success_count, error = GmailExtractionService.create_conversations_for_emails( user=request.user, user_email=user_email, emails=gmail_emails, kb_id=kb_id ) return Response({ "success": True, "message": f"成功创建 {success_count} 个Gmail对话", "total_emails": len(gmail_emails), "gmail_emails": gmail_emails[:20] if len(gmail_emails) > 20 else gmail_emails, "error": error }, status=status.HTTP_200_OK) except Exception as e: error_details = traceback.format_exc() return Response( { "error": f"提取Gmail邮箱并创建对话失败: {str(e)}", "details": error_details[:500] }, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) class AutoGmailConversationView(APIView): """ 自动Gmail对话API,支持自动发送消息并实时接收和回复达人消息 """ permission_classes = [IsAuthenticated] def post(self, request, *args, **kwargs): """ 创建自动对话,发送第一条打招呼消息 请求参数: - user_email: 用户的Gmail邮箱(已授权) - influencer_email: 达人Gmail邮箱 - goal_description: 对话目标描述 """ try: # 获取请求数据 data = request.data user_email = data.get('user_email') influencer_email = data.get('influencer_email') goal_description = data.get('goal_description') # 验证必填参数 if not user_email or not influencer_email or not goal_description: return Response({ 'code': 400, 'message': '缺少必要参数: user_email, influencer_email, goal_description', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 获取或创建活跃对话 # 先查找现有对话 existing_conversation = GmailConversation.objects.filter( user=request.user, user_email=user_email, influencer_email=influencer_email ).first() if existing_conversation: # 激活现有对话 existing_conversation.is_active = True existing_conversation.save() logger.info(f"找到并激活现有对话: {existing_conversation.conversation_id}") # 调用服务创建自动对话 success, result, goal = AutoGmailConversationService.create_auto_conversation( user=request.user, user_email=user_email, influencer_email=influencer_email, greeting_message="", # 使用空字符串,实际消息在服务中已固定 goal_description=goal_description ) if not success: return Response({ 'code': 400, 'message': result, 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 确保对话是活跃的 conversation = GmailConversation.objects.filter(conversation_id=result).first() if conversation and not conversation.is_active: conversation.is_active = True conversation.save() logger.info(f"已将对话 {conversation.conversation_id} 设置为活跃") # 设置Gmail推送通知 notification_result, notification_error = GmailService.setup_gmail_push_notification( user=request.user, user_email=user_email ) if not notification_result and notification_error: logger.warning(f"设置Gmail推送通知失败: {notification_error},但对话创建成功") # 返回结果 return Response({ 'code': 201, 'message': '自动对话创建成功,已发送打招呼消息', 'data': { 'conversation_id': result, 'goal_id': str(goal.id) if goal else None, 'user_email': user_email, 'influencer_email': influencer_email, 'is_active': True, 'goal_description': goal_description, 'push_notification': notification_result } }, status=status.HTTP_201_CREATED) except Exception as e: logger.error(f"创建自动对话失败: {str(e)}") error_details = traceback.format_exc() return Response({ 'code': 500, 'message': f'创建自动对话失败: {str(e)}', 'data': { 'details': error_details[:500] } }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) def get(self, request, *args, **kwargs): """ 获取用户所有自动对话及其状态 """ try: # 获取用户所有对话和目标 conversations = AutoGmailConversationService.get_all_user_conversations_with_goals(request.user) return Response({ 'code': 200, 'message': '获取自动对话列表成功', 'data': { 'conversations': conversations } }) except Exception as e: logger.error(f"获取自动对话列表失败: {str(e)}") error_details = traceback.format_exc() return Response({ 'code': 500, 'message': f'获取自动对话列表失败: {str(e)}', 'data': { 'details': error_details[:500] } }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)