将达人添加到活动自动创建对话按状态筛选自动获取邮箱

This commit is contained in:
wanjia 2025-06-05 16:54:05 +08:00
parent 83a6049d27
commit 46e6124fb1
6 changed files with 286 additions and 1864 deletions

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.1 on 2025-06-03 09:10
# Generated by Django 5.2.1 on 2025-06-05 07:55
import django.db.models.deletion
from django.db import migrations, models
@ -7,7 +7,9 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('brands', '0002_alter_campaign_dataset_id_alter_product_dataset_id'),
('chat', '0001_initial'),
('daren_detail', '0002_delete_brandcampaign'),
('expertproducts', '0001_initial'),
]
@ -19,11 +21,13 @@ class Migration(migrations.Migration):
('conversation_id', models.CharField(db_index=True, max_length=100, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='daren_detail.creatorprofile')),
('negotiation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chats', to='expertproducts.negotiation')),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='brands.product')),
],
options={
'ordering': ['-updated_at'],
'indexes': [models.Index(fields=['negotiation', 'updated_at'], name='chat_negoti_negotia_2aa711_idx')],
'indexes': [models.Index(fields=['negotiation', 'updated_at'], name='chat_negoti_negotia_2aa711_idx'), models.Index(fields=['creator_id', 'product_id'], name='chat_negoti_creator_0506f5_idx')],
},
),
]

View File

@ -6,6 +6,8 @@ from itertools import count
from apps.user.models import User
from apps.knowledge_base.models import KnowledgeBase
from apps.expertproducts.models import Negotiation
from apps.daren_detail.models import CreatorProfile # 导入CreatorProfile模型
from apps.brands.models import Product # 导入Product模型
class ChatHistory(models.Model):
"""聊天历史记录"""
@ -125,6 +127,8 @@ class NegotiationChat(models.Model):
"""谈判对话关联表"""
negotiation = models.ForeignKey(Negotiation, on_delete=models.CASCADE, related_name='chats')
conversation_id = models.CharField(max_length=100, unique=True, db_index=True)
creator = models.ForeignKey(CreatorProfile, on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@ -132,6 +136,7 @@ class NegotiationChat(models.Model):
ordering = ['-updated_at']
indexes = [
models.Index(fields=['negotiation', 'updated_at']),
models.Index(fields=['creator_id', 'product_id']),
]
def __str__(self):

View File

@ -130,7 +130,7 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
@action(detail=False, methods=['get'])
def conversation_detail(self, request):
"""获取特定对话的详细信息,支持按status筛选并返回creator和product信息"""
"""获取特定对话的详细信息,并返回creator和product信息"""
try:
conversation_id = request.query_params.get('conversation_id')
if not conversation_id:
@ -140,9 +140,6 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 获取状态筛选参数
status_filter = request.query_params.get('status')
# 查找对话记录
chat_records = ChatHistory.objects.filter(
conversation_id=conversation_id,
@ -159,7 +156,7 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
# 尝试查找关联的谈判记录
negotiation_chat = NegotiationChat.objects.filter(
conversation_id=conversation_id
).first()
).select_related('negotiation').first()
# 准备基本的返回数据
result = {
@ -171,13 +168,15 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
if negotiation_chat:
negotiation = negotiation_chat.negotiation
# 如果有status筛选匹配谈判状态
if status_filter and negotiation.status != status_filter:
return Response({
'code': 404,
'message': f'没有找到状态为 {status_filter} 的对话',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
# 获取关联的达人信息
creator_id = negotiation_chat.creator_id
from apps.daren_detail.models import CreatorProfile
creator = CreatorProfile.objects.filter(id=creator_id).first()
# 获取关联的产品信息
product_id = negotiation_chat.product_id
from apps.brands.models import Product
product = Product.objects.filter(id=product_id).first()
# 添加谈判相关信息
result['negotiation'] = {
@ -186,15 +185,16 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
'current_round': negotiation.current_round,
'context': negotiation.context,
'creator': {
'id': str(negotiation.creator.id),
'name': negotiation.creator.name if hasattr(negotiation.creator, 'name') else "未知",
'profile': negotiation.creator.profile if hasattr(negotiation.creator, 'profile') else {}
},
'id': creator.id if creator else None,
'name': creator.name if creator else "未知",
'email': creator.email if creator and hasattr(creator, 'email') else None
} if creator else {"id": None, "name": "未知"},
'product': {
'id': str(negotiation.product.id),
'name': negotiation.product.name if hasattr(negotiation.product, 'name') else "未知",
'description': negotiation.product.description if hasattr(negotiation.product, 'description') else ""
}
'id': str(product.id) if product else None,
'name': product.name if product else "未知",
'description': product.description if product and hasattr(product, 'description') else "",
'brand': product.brand.name if product and hasattr(product, 'brand') else ""
} if product else {"id": None, "name": "未知"}
}
# 整理对话消息
@ -296,7 +296,9 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
# 创建谈判关联
NegotiationChat.objects.create(
negotiation=negotiation,
conversation_id=conversation_id
conversation_id=conversation_id,
creator_id=creator.id,
product_id=negotiation.product.id
)
# 准备metadata
@ -879,7 +881,7 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 查找关联的谈判信息
# 查找关联的谈判聊天记录
negotiation_chat = NegotiationChat.objects.filter(
conversation_id=conversation_id
).first()
@ -887,15 +889,12 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
if not negotiation_chat:
return Response({
'code': 404,
'message': f'找不到与对话ID关联的谈判: {conversation_id}',
'message': f'找不到与对话ID关联的谈判聊天记录: {conversation_id}',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
# 获取谈判中的达人信息
negotiation = negotiation_chat.negotiation
creator_id = negotiation.creator.id
# 直接从CreatorProfile模型中获取达人邮箱
# 获取达人ID并查询达人信息
creator_id = negotiation_chat.creator_id
from apps.daren_detail.models import CreatorProfile
creator = CreatorProfile.objects.filter(id=creator_id).first()
@ -905,7 +904,8 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
'message': f'找不到达人信息: {creator_id}',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
# 获取达人邮箱
creator_email = creator.email
if not creator_email:
@ -976,8 +976,8 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
'to': creator_email,
'from': from_email,
'subject': subject,
'negotiation_id': str(negotiation.id),
'status': negotiation.status,
'negotiation_id': str(negotiation_chat.negotiation.id) if negotiation_chat.negotiation else None,
'status': negotiation_chat.negotiation.status if negotiation_chat.negotiation else None,
'has_attachments': len(attachments) > 0
}
)
@ -1008,3 +1008,101 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
'message': f'发送邮件给达人失败: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@action(detail=False, methods=['get'])
def get_negotiation_chats(self, request):
"""获取谈判聊天列表返回NegotiationChat表的数据及关联的达人和产品信息"""
try:
# 获取分页和筛选参数
page = int(request.query_params.get('page', 1))
page_size = int(request.query_params.get('page_size', 10))
status_filter = request.query_params.get('status')
# 查询NegotiationChat表
query = NegotiationChat.objects.all().select_related('negotiation')
# 如果提供了status参数则过滤谈判状态
if status_filter:
query = query.filter(negotiation__status=status_filter)
# 获取总数量
total = query.count()
# 分页
start = (page - 1) * page_size
end = start + page_size
chats = query.order_by('-updated_at')[start:end]
results = []
for chat in chats:
# 获取关联的谈判
negotiation = chat.negotiation
# 获取关联的达人信息
creator_id = chat.creator_id
from apps.daren_detail.models import CreatorProfile
creator = CreatorProfile.objects.filter(id=creator_id).first()
# 获取关联的产品信息
product_id = chat.product_id
from apps.brands.models import Product
product = Product.objects.filter(id=product_id).first()
# 获取最新的聊天记录
latest_message = ChatHistory.objects.filter(
conversation_id=chat.conversation_id,
is_deleted=False
).order_by('-created_at').first()
# 计算聊天记录数量
message_count = ChatHistory.objects.filter(
conversation_id=chat.conversation_id,
is_deleted=False
).count()
# 构建响应数据
chat_data = {
'conversation_id': chat.conversation_id,
'negotiation_id': str(negotiation.id),
'negotiation_status': negotiation.status,
'current_round': negotiation.current_round,
'context': negotiation.context,
'creator': {
'id': creator.id if creator else None,
'name': creator.name if creator else "未知",
'email': creator.email if creator and hasattr(creator, 'email') else None,
} if creator else {"id": None, "name": "未知"},
'product': {
'id': str(product.id) if product else None,
'name': product.name if product else "未知",
'description': product.description if product and hasattr(product, 'description') else "",
'brand': product.brand.name if product and hasattr(product, 'brand') else ""
} if product else {"id": None, "name": "未知"},
'message_count': message_count,
'last_message': latest_message.content if latest_message else '',
'last_time': latest_message.created_at.strftime('%Y-%m-%d %H:%M:%S') if latest_message else chat.updated_at.strftime('%Y-%m-%d %H:%M:%S'),
'created_at': chat.created_at.strftime('%Y-%m-%d %H:%M:%S'),
'updated_at': chat.updated_at.strftime('%Y-%m-%d %H:%M:%S')
}
results.append(chat_data)
return Response({
'code': 200,
'message': '获取成功',
'data': {
'total': total,
'page': page,
'page_size': page_size,
'results': results
}
})
except Exception as e:
logger.error(f"获取谈判聊天列表失败: {str(e)}")
import traceback
logger.error(traceback.format_exc())
return Response({
'code': 500,
'message': f'获取谈判聊天列表失败: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

View File

@ -260,6 +260,8 @@ class CreatorProfile(models.Model):
def __str__(self):
return f"{self.name}"
class CreatorCampaign(models.Model):
"""达人-活动关联模型"""
creator = models.ForeignKey('CreatorProfile', on_delete=models.CASCADE, verbose_name="达人",

View File

@ -21,6 +21,9 @@ from .models import (
FollowerMetrics, TrendMetrics, CreatorVideo
)
from apps.brands.models import Campaign, Brand
from apps.expertproducts.models import Negotiation
from apps.chat.models import NegotiationChat
from django.db import connection
dotenv.load_dotenv()
@ -371,17 +374,22 @@ def get_campaigns(request):
@csrf_exempt
@require_http_methods(["POST"])
def add_to_campaign(request):
"""添加达人到营销活动(保留原有达人)"""
"""添加达人到营销活动 - 创建达人与产品的谈判关系"""
try:
from .models import CreatorProfile, CreatorCampaign
from apps.brands.models import Campaign
from apps.brands.models import Campaign, Product, Brand
from apps.expertproducts.models import Negotiation
from apps.chat.models import NegotiationChat
from django.db import connection
import json
import uuid
data = json.loads(request.body)
# 获取必要参数
campaign_id = data.get('campaign_id')
creator_ids = data.get('creator_ids', [])
brand_id = data.get('brand_id') # 可选的品牌ID参数
if not campaign_id or not creator_ids:
return JsonResponse({
@ -392,12 +400,14 @@ def add_to_campaign(request):
# 检查活动是否存在
try:
# 直接使用原始ID字符串查询
# 使用Django ORM进行查询而不是raw SQL
logger.info(f"尝试查找活动ID: {campaign_id}")
campaign = Campaign.objects.raw('SELECT * FROM campaigns WHERE id = %s AND is_active = 1', [campaign_id])[0]
# Campaign ID可能是整数而不是UUID直接使用原始ID进行查询
campaign = Campaign.objects.get(id=campaign_id, is_active=True)
logger.info(f"找到活动: {campaign.name}, Active: {campaign.is_active}")
except IndexError:
except Campaign.DoesNotExist:
logger.warning(f"找不到ID为 {campaign_id} 的活动")
return JsonResponse({
'code': 404,
@ -412,46 +422,148 @@ def add_to_campaign(request):
'data': None
}, json_dumps_params={'ensure_ascii': False})
# 获取已存在的达人关联
existing_creators = set(CreatorCampaign.objects.filter(campaign=campaign).values_list('creator_id', flat=True))
logger.info(f"活动 {campaign_id} 已有 {len(existing_creators)} 个达人关联")
# 添加新达人到活动
# 查询活动关联的所有产品
# 通过ORM的ManyToMany关系查询
products = list(campaign.link_product.all())
logger.info(f"活动 {campaign_id} 关联了 {len(products)} 个产品")
# 如果没有产品关联到活动,则尝试直接查询关联表
if not products:
logger.info(f"尝试直接查询关联表获取活动 {campaign_id} 的产品")
# 查询活动-产品关联表
with connection.cursor() as cursor:
cursor.execute(
"""
SELECT product_id FROM brands_campaign_link_product
WHERE campaign_id = %s
""",
[campaign_id]
)
product_ids = [row[0] for row in cursor.fetchall()]
if product_ids:
# 获取这些产品
products = list(Product.objects.filter(id__in=product_ids))
logger.info(f"从关联表找到 {len(products)} 个产品")
# 记录要添加的达人
added_creators = []
added_count = 0
skipped_count = 0
already_exists_count = 0
added_creators = []
# 为新添加的达人创建谈判和对话
negotiations_created = []
# 处理每个达人
for creator_id in creator_ids:
try:
# 移除is_active检查
# 询达人信息
creator = CreatorProfile.objects.get(id=creator_id)
logger.info(f"找到达人: {creator.name}")
# 检查是否已存在关联
if creator.id in existing_creators:
already_exists_count += 1
logger.info(f"达人 {creator_id} 已经存在于活动 {campaign_id}")
continue
# 创建新的关联
creator_campaign = CreatorCampaign.objects.create(
creator=creator,
campaign=campaign,
status='pending'
)
# 将达人添加到返回列表
added_count += 1
added_creators.append({
'id': creator.id,
'name': creator.name
})
# 为每个产品创建谈判记录和对话
for product in products:
# 检查是否已存在谈判
existing_negotiation = Negotiation.objects.filter(creator=creator, product=product).first()
if not existing_negotiation:
# 创建新的谈判记录
negotiation = Negotiation.objects.create(
creator=creator,
product=product,
status='brand_review', # 初始状态为品牌回顾
current_round=1,
context={}
)
logger.info(f"已创建谈判记录: 谈判ID={negotiation.id}, 达人ID={creator.id}, 产品ID={product.id}")
else:
# 使用已存在的谈判记录
negotiation = existing_negotiation
logger.info(f"使用已存在的谈判记录: 谈判ID={negotiation.id}")
# 直接创建NegotiationChat记录不检查是否已存在
try:
# 创建对话ID并关联
conversation_id = str(uuid.uuid4())
logger.info(f"准备创建对话关联,参数: negotiation={negotiation.id}, conversation_id={conversation_id}, creator_id={creator.id}, product_id={product.id}")
negotiation_chat = NegotiationChat.objects.create(
negotiation=negotiation,
conversation_id=conversation_id,
creator_id=creator.id,
product_id=product.id
)
logger.info(f"成功创建对话关联: ID={negotiation_chat.id}, conversation_id={negotiation_chat.conversation_id}")
negotiations_created.append({
'negotiation_id': negotiation.id,
'creator_id': creator.id,
'product_id': product.id,
'conversation_id': conversation_id
})
except Exception as e:
logger.error(f"创建对话关联失败: {str(e)}")
# 继续处理,不中断流程
logger.info(f"为达人 {creator.name}(ID:{creator.id}) 和产品 {product.name}(ID:{product.id}) {'创建' if not existing_negotiation else '更新'}谈判对话ID: {conversation_id if 'conversation_id' in locals() else 'N/A'}")
# 将达人与活动关联 - 可选操作
try:
# 检查是否已存在关联
existing_relation = CreatorCampaign.objects.filter(creator=creator, campaign=campaign).exists()
if not existing_relation:
CreatorCampaign.objects.create(
creator=creator,
campaign=campaign,
status='pending'
)
logger.info(f"已将达人 {creator.name} 关联到活动 {campaign.name}")
else:
already_exists_count += 1
logger.info(f"达人 {creator.name} 已经关联到活动 {campaign.name}")
except Exception as e:
logger.warning(f"关联达人到活动时出错: {str(e)}")
# 继续处理,不中断流程
except CreatorProfile.DoesNotExist:
skipped_count += 1
logger.warning(f"找不到ID为 {creator_id} 的达人")
# 如果没有找到产品,返回警告
if not products:
return JsonResponse({
'code': 200,
'message': '操作完成,但活动没有关联产品',
'data': {
'campaign': {
'id': str(campaign.id),
'name': campaign.name
},
'added_creators': added_creators,
'stats': {
'added': added_count,
'skipped': skipped_count,
'already_exists': already_exists_count,
'products_found': 0
},
'negotiations_created': negotiations_created,
'warning': '活动没有关联产品,无法创建谈判'
}
}, json_dumps_params={'ensure_ascii': False})
return JsonResponse({
'code': 200,
'message': '成功添加达人到活动',
'message': '成功添加达人并创建谈判关系',
'data': {
'campaign': {
'id': str(campaign.id),
@ -461,18 +573,20 @@ def add_to_campaign(request):
'stats': {
'added': added_count,
'skipped': skipped_count,
'already_exists': already_exists_count
}
'already_exists': already_exists_count,
'products_found': len(products)
},
'negotiations_created': negotiations_created
}
}, json_dumps_params={'ensure_ascii': False})
except Exception as e:
logger.error(f"更新活动达人失败: {e}")
logger.error(f"处理失败: {e}")
import traceback
logger.error(f"详细错误: {traceback.format_exc()}")
return JsonResponse({
'code': 500,
'message': f'更新活动达人失败: {str(e)}',
'message': f'处理失败: {str(e)}',
'data': None
}, json_dumps_params={'ensure_ascii': False})

1801
nohup.out

File diff suppressed because it is too large Load Diff