operations_project/apps/feishu/views.py
2025-05-20 15:57:10 +08:00

419 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)