operations_project/apps/feishu/views.py

419 lines
16 KiB
Python
Raw Normal View History

2025-05-20 15:57:10 +08:00
import json
import traceback
import logging
2025-05-07 18:01:48 +08:00
import uuid
2025-05-20 15:57:10 +08:00
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
2025-05-07 18:01:48 +08:00
2025-05-20 15:57:10 +08:00
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
2025-05-07 18:01:48 +08:00
2025-05-20 15:57:10 +08:00
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
})
2025-05-07 18:01:48 +08:00
2025-05-20 15:57:10 +08:00
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)
2025-05-07 18:01:48 +08:00
2025-05-20 15:57:10 +08:00
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]
2025-05-07 18:01:48 +08:00
2025-05-20 15:57:10 +08:00
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)
2025-05-07 18:01:48 +08:00
2025-05-20 15:57:10 +08:00
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)
2025-05-07 18:01:48 +08:00