896 lines
40 KiB
Python
896 lines
40 KiB
Python
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, CreatorConversationTracker
|
||
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
|
||
from apps.user.authentication import CustomTokenAuthentication
|
||
|
||
|
||
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]
|
||
authentication_classes = [CustomTokenAuthentication]
|
||
|
||
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)
|
||
|
||
# 验证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.name}, 邮箱={user_email}, 达人={influencer_email}")
|
||
|
||
# 查找现有对话
|
||
existing_conversation = GmailConversation.objects.filter(
|
||
user=request.user,
|
||
user_email=user_email,
|
||
influencer_email=influencer_email
|
||
).first()
|
||
|
||
conversation_id = None
|
||
is_new_conversation = False
|
||
|
||
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}")
|
||
|
||
# 使用goal_service创建或更新目标
|
||
goal, is_new_goal = get_or_create_goal(
|
||
user=request.user,
|
||
conversation_id=conversation_id,
|
||
goal_description=goal_description
|
||
)
|
||
|
||
logger.info(f"目标{'创建' if is_new_goal else '更新'}成功: {goal.id if goal else 'None'}")
|
||
|
||
# 检查是否需要发送打招呼消息
|
||
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 Shop—currently 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} 已经发送过打招呼消息,不再重复发送")
|
||
|
||
# 设置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)
|
||
|
||
# 返回结果
|
||
return Response({
|
||
'code': 201 if is_new_conversation else 200,
|
||
'message': '自动对话创建成功' if is_new_conversation else '自动对话更新成功',
|
||
'data': {
|
||
'conversation_id': conversation_id,
|
||
'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,
|
||
'goal_description': goal_description,
|
||
'push_notification': notification_result,
|
||
'summary': summary
|
||
}
|
||
}, status=status.HTTP_201_CREATED if is_new_conversation else status.HTTP_200_OK)
|
||
|
||
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)
|
||
|
||
|
||
class BatchGmailConversationView(APIView):
|
||
"""
|
||
批量Gmail自动对话API,支持根据达人库批量发送邮件并跟踪回复状态
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
authentication_classes = [CustomTokenAuthentication]
|
||
|
||
def post(self, request, *args, **kwargs):
|
||
"""
|
||
批量创建自动对话,根据达人库中的邮箱发送打招呼消息
|
||
|
||
请求参数:
|
||
- user_email: 用户的Gmail邮箱(已授权)
|
||
- creator_ids: 创作者ID列表,可选,不提供则使用creator_pool_id
|
||
- creator_pool_id: 创作者库ID,可选,不提供则使用creator_ids
|
||
- goal_description: 对话目标描述
|
||
- batch_size: 批量处理大小,默认10
|
||
"""
|
||
try:
|
||
# 获取请求数据
|
||
data = request.data
|
||
user_email = data.get('user_email')
|
||
creator_ids = data.get('creator_ids', [])
|
||
creator_pool_id = data.get('creator_pool_id')
|
||
goal_description = data.get('goal_description')
|
||
batch_size = int(data.get('batch_size', 10))
|
||
|
||
# 验证必填参数
|
||
if not user_email or not goal_description:
|
||
return Response({
|
||
'code': 400,
|
||
'message': '缺少必要参数: user_email, goal_description',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
if not creator_ids and not creator_pool_id:
|
||
return Response({
|
||
'code': 400,
|
||
'message': '请提供creator_ids或creator_pool_id其中之一',
|
||
'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)
|
||
|
||
# 获取创作者列表
|
||
from apps.daren_detail.models import CreatorProfile, PrivateCreatorPool, PrivateCreatorRelation
|
||
|
||
creators = []
|
||
if creator_ids:
|
||
creators = CreatorProfile.objects.filter(id__in=creator_ids)
|
||
elif creator_pool_id:
|
||
# 从私有池获取创作者
|
||
relations = PrivateCreatorRelation.objects.filter(
|
||
private_pool_id=creator_pool_id,
|
||
status='active'
|
||
).select_related('creator')
|
||
creators = [relation.creator for relation in relations]
|
||
|
||
if not creators:
|
||
return Response({
|
||
'code': 400,
|
||
'message': '未找到有效的创作者',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 筛选有邮箱的创作者
|
||
valid_creators = [creator for creator in creators if creator.email]
|
||
|
||
if not valid_creators:
|
||
return Response({
|
||
'code': 400,
|
||
'message': '所选创作者中没有有效的邮箱地址',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
logger.info(f"开始批量创建Gmail自动对话: 用户={request.user.name}, 邮箱={user_email}, 创作者数量={len(valid_creators)}")
|
||
|
||
# 创建结果统计
|
||
result_stats = {
|
||
'total': len(valid_creators),
|
||
'success': 0,
|
||
'failed': 0,
|
||
'skipped': 0,
|
||
'details': []
|
||
}
|
||
|
||
# 准备固定的打招呼消息
|
||
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 Shop—currently 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"""
|
||
|
||
# 按批次处理
|
||
for i in range(0, len(valid_creators), batch_size):
|
||
batch_creators = valid_creators[i:i+batch_size]
|
||
for creator in batch_creators:
|
||
# 跳过没有邮箱的创作者
|
||
if not creator.email:
|
||
result_stats['skipped'] += 1
|
||
result_stats['details'].append({
|
||
'creator_id': str(creator.id),
|
||
'creator_name': creator.name,
|
||
'status': 'skipped',
|
||
'reason': '缺少邮箱地址'
|
||
})
|
||
continue
|
||
|
||
try:
|
||
# 检查是否已存在对话
|
||
existing_tracker = CreatorConversationTracker.objects.filter(
|
||
creator_profile=creator,
|
||
user=request.user,
|
||
user_email=user_email
|
||
).first()
|
||
|
||
if existing_tracker:
|
||
# 已存在追踪记录,跳过
|
||
result_stats['skipped'] += 1
|
||
result_stats['details'].append({
|
||
'creator_id': str(creator.id),
|
||
'creator_name': creator.name,
|
||
'status': 'skipped',
|
||
'reason': f'已存在对话 {existing_tracker.conversation_id}'
|
||
})
|
||
continue
|
||
|
||
# 查找现有对话
|
||
existing_conversation = GmailConversation.objects.filter(
|
||
user=request.user,
|
||
user_email=user_email,
|
||
influencer_email=creator.email
|
||
).first()
|
||
|
||
conversation_id = None
|
||
is_new_conversation = False
|
||
|
||
if existing_conversation:
|
||
# 使用现有对话
|
||
conversation = existing_conversation
|
||
conversation_id = conversation.conversation_id
|
||
# 激活对话
|
||
conversation.is_active = True
|
||
conversation.save()
|
||
logger.info(f"找到并激活现有对话: {conversation_id}, 创作者={creator.name}")
|
||
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=creator.email,
|
||
conversation_id=conversation_id,
|
||
title=f"与 {creator.name} ({creator.email}) 的Gmail对话",
|
||
is_active=True,
|
||
has_sent_greeting=False,
|
||
metadata={
|
||
'auto_conversation': True,
|
||
'batch_process': True,
|
||
'creator_id': str(creator.id),
|
||
'created_at': timezone.now().isoformat()
|
||
}
|
||
)
|
||
is_new_conversation = True
|
||
logger.info(f"创建新的自动对话: {conversation_id}, 创作者={creator.name}")
|
||
|
||
# 使用goal_service创建或更新目标
|
||
goal, is_new_goal = get_or_create_goal(
|
||
user=request.user,
|
||
conversation_id=conversation_id,
|
||
goal_description=goal_description
|
||
)
|
||
|
||
# 创建跟踪记录
|
||
tracker = CreatorConversationTracker.objects.create(
|
||
creator_profile=creator,
|
||
conversation_id=conversation_id,
|
||
user=request.user,
|
||
user_email=user_email,
|
||
influencer_email=creator.email
|
||
)
|
||
|
||
# 检查是否需要发送打招呼消息
|
||
if not conversation.has_sent_greeting:
|
||
# 发送打招呼消息
|
||
subject = "Paid Collaboration Opportunity with TikTok's #1 Fragrance Brand"
|
||
logger.info(f"开始向 {creator.email} 发送打招呼消息")
|
||
|
||
# 使用GmailService发送邮件
|
||
success, message_id = GmailService.send_email(
|
||
user=request.user,
|
||
user_email=user_email,
|
||
to_email=creator.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': creator.email,
|
||
'date': timezone.now().isoformat(),
|
||
'subject': subject,
|
||
'greeting': True,
|
||
'source': 'gmail',
|
||
'creator_id': str(creator.id)
|
||
}
|
||
)
|
||
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'])
|
||
|
||
result_stats['success'] += 1
|
||
result_stats['details'].append({
|
||
'creator_id': str(creator.id),
|
||
'creator_name': creator.name,
|
||
'status': 'success',
|
||
'conversation_id': conversation_id,
|
||
'email': creator.email
|
||
})
|
||
else:
|
||
# 发送失败
|
||
logger.error(f"发送打招呼消息失败: {message_id}, 创作者={creator.name}")
|
||
result_stats['failed'] += 1
|
||
result_stats['details'].append({
|
||
'creator_id': str(creator.id),
|
||
'creator_name': creator.name,
|
||
'status': 'failed',
|
||
'reason': f"发送邮件失败: {message_id}"
|
||
})
|
||
else:
|
||
# 已发送过打招呼消息
|
||
logger.info(f"对话 {conversation_id} 已经发送过打招呼消息,不再重复发送")
|
||
result_stats['skipped'] += 1
|
||
result_stats['details'].append({
|
||
'creator_id': str(creator.id),
|
||
'creator_name': creator.name,
|
||
'status': 'skipped',
|
||
'reason': '已发送过打招呼消息',
|
||
'conversation_id': conversation_id
|
||
})
|
||
|
||
except Exception as creator_error:
|
||
logger.error(f"处理创作者 {creator.name} 时出错: {str(creator_error)}")
|
||
result_stats['failed'] += 1
|
||
result_stats['details'].append({
|
||
'creator_id': str(creator.id),
|
||
'creator_name': creator.name,
|
||
'status': 'error',
|
||
'reason': str(creator_error)
|
||
})
|
||
|
||
# 设置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': 200,
|
||
'message': f"批量自动对话处理完成: 成功 {result_stats['success']}, 失败 {result_stats['failed']}, 跳过 {result_stats['skipped']}",
|
||
'data': {
|
||
'stats': result_stats,
|
||
'push_notification': notification_result
|
||
}
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
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:
|
||
# 获取用户所有跟踪记录
|
||
trackers = CreatorConversationTracker.objects.filter(user=request.user).select_related('creator_profile')
|
||
|
||
# 统计数据
|
||
stats = {
|
||
'total': trackers.count(),
|
||
'replied': trackers.filter(has_replied=True).count(),
|
||
'goal_achieved': trackers.filter(goal_achieved=True).count()
|
||
}
|
||
|
||
# 整理返回数据
|
||
tracker_data = []
|
||
for tracker in trackers:
|
||
tracker_data.append({
|
||
'id': str(tracker.id),
|
||
'creator_id': str(tracker.creator_profile.id),
|
||
'creator_name': tracker.creator_profile.name,
|
||
'creator_email': tracker.influencer_email,
|
||
'conversation_id': tracker.conversation_id,
|
||
'has_replied': tracker.has_replied,
|
||
'goal_achieved': tracker.goal_achieved,
|
||
'created_at': tracker.created_at.strftime('%Y-%m-%d %H:%M:%S'),
|
||
'updated_at': tracker.updated_at.strftime('%Y-%m-%d %H:%M:%S')
|
||
})
|
||
|
||
return Response({
|
||
'code': 200,
|
||
'message': '获取跟踪状态成功',
|
||
'data': {
|
||
'stats': stats,
|
||
'trackers': tracker_data
|
||
}
|
||
})
|
||
|
||
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)
|
||
|