operations_project/apps/feishu/views.py

525 lines
22 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
from apps.gmail.services.goal_service import get_or_create_goal, get_conversation_summary
from apps.chat.models import ChatHistory
from apps.knowledge_base.models import KnowledgeBase
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):
"""
创建自动对话根据需要发送第一条打招呼消息
2025-05-20 15:57:10 +08:00
请求参数:
- 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)
# 验证Gmail凭证
credential = GmailCredential.objects.filter(user=request.user, email=user_email).first()
if not credential:
return Response({
'code': 400,
'message': f"未找到{user_email}的Gmail授权",
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
logger.info(f"开始创建/更新Gmail自动对话: 用户={request.user.username}, 邮箱={user_email}, 达人={influencer_email}")
# 查找现有对话
2025-05-20 15:57:10 +08:00
existing_conversation = GmailConversation.objects.filter(
user=request.user,
user_email=user_email,
influencer_email=influencer_email
).first()
conversation_id = None
is_new_conversation = False
2025-05-20 15:57:10 +08:00
if existing_conversation:
# 使用现有对话
conversation = existing_conversation
conversation_id = conversation.conversation_id
# 激活对话
conversation.is_active = True
conversation.save()
logger.info(f"找到并激活现有对话: {conversation_id}, has_sent_greeting={conversation.has_sent_greeting}")
else:
# 创建新对话
conversation_id = f"gmail_{request.user.id}_{str(uuid.uuid4())[:8]}"
conversation = GmailConversation.objects.create(
user=request.user,
user_email=user_email,
influencer_email=influencer_email,
conversation_id=conversation_id,
title=f"{influencer_email} 的Gmail对话",
is_active=True,
has_sent_greeting=False, # 新创建的对话尚未发送打招呼消息
metadata={
'auto_conversation': True,
'created_at': timezone.now().isoformat()
}
)
is_new_conversation = True
logger.info(f"创建新的自动对话: {conversation_id}")
2025-05-20 15:57:10 +08:00
# 使用goal_service创建或更新目标
goal, is_new_goal = get_or_create_goal(
user=request.user,
conversation_id=conversation_id,
2025-05-20 15:57:10 +08:00
goal_description=goal_description
)
logger.info(f"目标{'创建' if is_new_goal else '更新'}成功: {goal.id if goal else 'None'}")
2025-05-20 15:57:10 +08:00
# 检查是否需要发送打招呼消息
if not conversation.has_sent_greeting:
logger.info(f"准备发送打招呼消息: {conversation_id}")
# 固定的打招呼消息
greeting_message = """Paid Collaboration Opportunity with TikTok's #1 Fragrance Brand 🌸
Hi,
I'm Vira from OOIN Media, and I'm reaching out on behalf of a top-performing fragrance brand Sttes on TikTok Shopcurrently ranked #1 in the perfume category.
This brand has already launched several viral products and is now looking to partner with select creators like you through paid collaborations to continue driving awareness and sales.
We'd love to explore a partnership and would appreciate it if you could share:
Your rate for a single TikTok video
Whether you offer bundle pricing for multiple videos
Any additional details or formats you offer (e.g. story integration, livestream add-ons, etc.)
The product has strong market traction, proven conversions, and a competitive commission structure if you're also open to affiliate partnerships.
Looking forward to the opportunity to work together and hearing your rates!
Warm regards,
Vira
OOIN Media"""
# 发送打招呼消息
subject = "Paid Collaboration Opportunity with TikTok's #1 Fragrance Brand"
logger.info(f"开始向 {influencer_email} 发送打招呼消息")
# 使用GmailService发送邮件
success, message_id = GmailService.send_email(
user=request.user,
user_email=user_email,
to_email=influencer_email,
subject=subject,
body=greeting_message
)
if success:
# 将打招呼消息保存到聊天历史 - 使用更完整的方式
try:
# 查找或创建默认知识库
knowledge_base = KnowledgeBase.objects.filter(user_id=request.user.id, type='private').first()
if knowledge_base:
# 创建聊天消息
ChatHistory.objects.create(
user=request.user,
knowledge_base=knowledge_base,
conversation_id=conversation_id,
message_id=f"greeting_{message_id}",
title=conversation.title,
role="user", # 用户发出的消息
content=greeting_message,
metadata={
'gmail_message_id': message_id,
'from': user_email,
'to': influencer_email,
'date': timezone.now().isoformat(),
'subject': subject,
'greeting': True,
'source': 'gmail'
}
)
logger.info(f"打招呼消息已保存到聊天历史: {message_id}")
else:
logger.warning("未找到默认知识库,打招呼消息未保存到聊天历史")
except Exception as chat_error:
logger.error(f"保存打招呼消息到聊天历史失败: {str(chat_error)}")
# 这里我们继续执行,不影响主流程
# 更新对话的has_sent_greeting字段
conversation.has_sent_greeting = True
conversation.save(update_fields=['has_sent_greeting', 'updated_at'])
logger.info(f"对话 {conversation_id} 已发送打招呼消息并更新了has_sent_greeting=True")
else:
logger.error(f"发送打招呼消息失败: {message_id}")
return Response({
'code': 500,
'message': f"发送打招呼消息失败: {message_id}",
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
else:
logger.info(f"对话 {conversation_id} 已经发送过打招呼消息,不再重复发送")
2025-05-20 15:57:10 +08:00
# 设置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},但对话创建/更新成功")
# 生成对话摘要(如果有足够的消息)
summary = get_conversation_summary(conversation_id)
2025-05-20 15:57:10 +08:00
# 返回结果
return Response({
'code': 201 if is_new_conversation else 200,
'message': '自动对话创建成功' if is_new_conversation else '自动对话更新成功',
2025-05-20 15:57:10 +08:00
'data': {
'conversation_id': conversation_id,
2025-05-20 15:57:10 +08:00
'goal_id': str(goal.id) if goal else None,
'user_email': user_email,
'influencer_email': influencer_email,
'is_active': True,
'has_sent_greeting': conversation.has_sent_greeting,
2025-05-20 15:57:10 +08:00
'goal_description': goal_description,
'push_notification': notification_result,
'summary': summary
2025-05-20 15:57:10 +08:00
}
}, status=status.HTTP_201_CREATED if is_new_conversation else status.HTTP_200_OK)
2025-05-20 15:57:10 +08:00
except Exception as e:
logger.error(f"创建/更新自动对话失败: {str(e)}")
2025-05-20 15:57:10 +08:00
error_details = traceback.format_exc()
return Response({
'code': 500,
'message': f'创建/更新自动对话失败: {str(e)}',
2025-05-20 15:57:10 +08:00
'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