Files
huihuiSquare/backend/app/services/interaction_service.py
yuqianqian10204095yu cebc0a288f 1.0.0初始化源代码
2026-03-23 15:40:36 +08:00

409 lines
14 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 logging
import random
from typing import Optional, List, Dict, Any
from datetime import datetime, timedelta
from sqlalchemy.orm import Session
from sqlalchemy import and_, func
from app.models.virtual_user import VirtualUser, ActivityLevel, UserStatus
from app.models.interaction import InteractionRecord, InteractionType, InteractionStatus
from app.models.token_usage import TokenUsage
from app.models.news_cache import NewsCache
from app.services.huihui_api_service import huihui_api_service
from app.services.ai_service import ai_service
from app.core.config import settings
logger = logging.getLogger(__name__)
class InteractionService:
"""互动执行服务类"""
def __init__(self, db: Session):
self.db = db
async def execute_interaction(
self,
virtual_user_id: int,
interaction_type: Optional[InteractionType] = None,
news_id: Optional[str] = None
) -> Optional[InteractionRecord]:
"""
执行单次互动
:param virtual_user_id: 虚拟用户 ID
:param interaction_type: 互动类型(不传则随机)
:param news_id: 新闻 ID不传则随机选择
:return: 互动记录
"""
# 获取虚拟用户
user = self.db.query(VirtualUser).filter(VirtualUser.id == virtual_user_id).first()
if not user:
logger.error(f"Virtual user not found: {virtual_user_id}")
return None
# 检查用户状态
if user.status != UserStatus.ACTIVE:
logger.warning(f"Virtual user is not active: {virtual_user_id}")
return None
# 检查是否已登录
if not user.is_logged_in or not user.session_token:
logger.warning(f"Virtual user not logged in: {virtual_user_id}")
# TODO: 自动登录
return None
# 检查今日限额
if not self._check_daily_limit(user, interaction_type):
logger.warning(f"Daily limit reached for user {virtual_user_id}")
return None
# 选择新闻
if not news_id:
news_id = await self._select_news(user)
if not news_id:
logger.warning("No news available for interaction")
return None
# 获取新闻详情
news = self.db.query(NewsCache).filter(NewsCache.news_id == news_id).first()
if not news:
# 从 API 获取
news_detail = await huihui_api_service.get_news_detail(news_id)
if news_detail:
news = self._cache_news(news_detail)
if not news:
logger.error(f"Cannot get news detail: {news_id}")
return None
# 确定互动类型
if not interaction_type:
interaction_type = self._random_interaction_type()
# 创建互动记录
record = InteractionRecord(
virtual_user_id=virtual_user_id,
news_id=news_id,
news_title=news.title if news else "",
interaction_type=interaction_type,
status=InteractionStatus.PENDING
)
self.db.add(record)
self.db.commit()
self.db.refresh(record)
try:
# 执行互动
if interaction_type == InteractionType.COMMENT:
result = await self._execute_comment(user, news, record)
elif interaction_type == InteractionType.REPLY:
result = await self._execute_reply(user, news, record)
elif interaction_type == InteractionType.LIKE:
result = await self._execute_like(user, news, record)
elif interaction_type == InteractionType.FAVORITE:
result = await self._execute_favorite(user, news, record)
elif interaction_type == InteractionType.SHARE:
result = await self._execute_share(user, news, record)
else:
logger.error(f"Unknown interaction type: {interaction_type}")
return None
if result:
record.status = InteractionStatus.SUCCESS
record.api_response = str(result)
# 更新用户统计
user.total_interactions += 1
if interaction_type == InteractionType.COMMENT:
user.today_comments += 1
elif interaction_type == InteractionType.REPLY:
user.today_replies += 1
user.last_interaction_time = datetime.now()
self.db.commit()
logger.info(f"Interaction executed successfully: user={user.id}, type={interaction_type}")
return record
else:
record.status = InteractionStatus.FAILED
record.error_message = "API call failed"
self.db.commit()
return None
except Exception as e:
logger.error(f"Execute interaction error: {e}")
record.status = InteractionStatus.FAILED
record.error_message = str(e)
self.db.commit()
return None
async def _execute_comment(
self,
user: VirtualUser,
news: NewsCache,
record: InteractionRecord
) -> Optional[Dict[str, Any]]:
"""执行评论"""
# AI 生成评论内容
ai_result = await ai_service.generate_comment(
news_content=news.content or news.summary or news.title,
writing_style=user.writing_style or "普通",
persona_description=user.persona_description
)
if not ai_result or not ai_result.get("content"):
logger.error("AI generate comment failed")
return None
# 记录 Token 使用
self._record_token_usage(
virtual_user_id=user.id,
interaction_id=record.id,
tokens_used=ai_result.get("tokens_used", 0),
ai_model=ai_result.get("model", "unknown"),
action_type="generate_comment",
tokens_prompt=ai_result.get("tokens_prompt", 0),
tokens_completion=ai_result.get("tokens_completion", 0)
)
# 调用接口提交评论
result = await huihui_api_service.create_comment(
news_id=news.news_id,
content=ai_result["content"],
session_token=user.session_token
)
if result:
record.content = ai_result["content"]
record.tokens_used = ai_result.get("tokens_used", 0)
record.ai_model_used = ai_result.get("model")
return result
async def _execute_reply(
self,
user: VirtualUser,
news: NewsCache,
record: InteractionRecord
) -> Optional[Dict[str, Any]]:
"""执行回复"""
# 获取评论列表
comments = await huihui_api_service.get_comments(news_id=news.news_id)
if not comments or len(comments) == 0:
logger.warning(f"No comments available for news {news.news_id}")
return None
# 随机选择一条评论进行回复
target_comment = random.choice(comments)
record.target_comment_id = target_comment.get("id")
# AI 生成回复内容
ai_result = await ai_service.generate_reply(
original_comment=target_comment.get("content", ""),
news_content=news.content or news.summary or news.title,
writing_style=user.writing_style or "普通",
persona_description=user.persona_description
)
if not ai_result or not ai_result.get("content"):
logger.error("AI generate reply failed")
return None
# 记录 Token 使用
self._record_token_usage(
virtual_user_id=user.id,
interaction_id=record.id,
tokens_used=ai_result.get("tokens_used", 0),
ai_model=ai_result.get("model", "unknown"),
action_type="generate_reply"
)
# 调用接口提交回复
result = await huihui_api_service.create_reply(
comment_id=target_comment.get("id"),
content=ai_result["content"],
session_token=user.session_token
)
if result:
record.content = ai_result["content"]
record.tokens_used = ai_result.get("tokens_used", 0)
return result
async def _execute_like(
self,
user: VirtualUser,
news: NewsCache,
record: InteractionRecord
) -> Optional[Dict[str, Any]]:
"""执行点赞"""
# 获取评论列表
comments = await huihui_api_service.get_comments(news_id=news.news_id)
if not comments or len(comments) == 0:
logger.warning(f"No comments available for like")
return None
# 随机选择一条评论点赞
target_comment = random.choice(comments)
record.target_comment_id = target_comment.get("id")
result = await huihui_api_service.like_comment(
comment_id=target_comment.get("id"),
session_token=user.session_token
)
return result
async def _execute_favorite(
self,
user: VirtualUser,
news: NewsCache,
record: InteractionRecord
) -> Optional[Dict[str, Any]]:
"""执行收藏"""
result = await huihui_api_service.favorite_news(
news_id=news.news_id,
session_token=user.session_token
)
return result
async def _execute_share(
self,
user: VirtualUser,
news: NewsCache,
record: InteractionRecord
) -> Optional[Dict[str, Any]]:
"""执行转发"""
result = await huihui_api_service.share_news(
news_id=news.news_id,
session_token=user.session_token
)
return result
def _check_daily_limit(
self,
user: VirtualUser,
interaction_type: Optional[InteractionType]
) -> bool:
"""检查每日限额"""
today = datetime.now().date()
# 统计今日互动
today_records = self.db.query(InteractionRecord).filter(
and_(
InteractionRecord.virtual_user_id == user.id,
func.date(InteractionRecord.execution_time) == today,
InteractionRecord.status == InteractionStatus.SUCCESS
)
).all()
today_comments = sum(1 for r in today_records if r.interaction_type == InteractionType.COMMENT)
today_replies = sum(1 for r in today_records if r.interaction_type == InteractionType.REPLY)
# 检查评论限额
if interaction_type == InteractionType.COMMENT:
if today_comments >= settings.MAX_COMMENTS_PER_USER_PER_DAY:
return False
# 检查回复限额
if interaction_type == InteractionType.REPLY:
if today_replies >= settings.MAX_REPLIES_PER_USER_PER_DAY:
return False
return True
def _random_interaction_type(self) -> InteractionType:
"""随机选择互动类型"""
rand = random.random()
# 根据概率决定互动类型
if rand < settings.LIKE_PROBABILITY:
return InteractionType.LIKE
elif rand < settings.LIKE_PROBABILITY + settings.FAVORITE_PROBABILITY:
return InteractionType.FAVORITE
elif rand < settings.LIKE_PROBABILITY + settings.FAVORITE_PROBABILITY + settings.SHARE_PROBABILITY:
return InteractionType.SHARE
else:
return InteractionType.COMMENT
async def _select_news(self, user: VirtualUser) -> Optional[str]:
"""选择新闻"""
# 优先选择未互动过的新闻
cached_news = self.db.query(NewsCache).order_by(
NewsCache.created_at.desc()
).limit(50).all()
if not cached_news:
# 从 API 获取
news_list = await huihui_api_service.get_news_list(page=1, page_size=20)
if news_list:
for news_data in news_list:
self._cache_news(news_data)
cached_news = self.db.query(NewsCache).order_by(
NewsCache.created_at.desc()
).limit(50).all()
if not cached_news:
return None
# 随机选择一篇
return random.choice(cached_news).news_id
def _cache_news(self, news_data: Dict[str, Any]) -> Optional[NewsCache]:
"""缓存新闻"""
news = NewsCache(
news_id=str(news_data.get("id")),
title=news_data.get("title", ""),
summary=news_data.get("summary", ""),
content=news_data.get("content", ""),
source=news_data.get("source", ""),
author=news_data.get("author", ""),
category=news_data.get("category", "")
)
self.db.add(news)
self.db.commit()
self.db.refresh(news)
return news
def _record_token_usage(
self,
virtual_user_id: int,
interaction_id: int,
tokens_used: int,
ai_model: str,
action_type: str,
tokens_prompt: int = 0,
tokens_completion: int = 0
):
"""记录 Token 使用"""
usage = TokenUsage(
virtual_user_id=virtual_user_id,
interaction_id=interaction_id,
tokens_used=tokens_used,
tokens_prompt=tokens_prompt,
tokens_completion=tokens_completion,
ai_model=ai_model,
action_type=action_type
)
self.db.add(usage)
self.db.commit()
logger.info(f"Token usage recorded: {tokens_used} tokens for user {virtual_user_id}")
# 工厂函数
def get_interaction_service(db: Session) -> InteractionService:
"""获取互动服务实例"""
return InteractionService(db)