1.0.0初始化源代码
This commit is contained in:
408
backend/app/services/interaction_service.py
Normal file
408
backend/app/services/interaction_service.py
Normal file
@@ -0,0 +1,408 @@
|
||||
"""
|
||||
互动执行服务
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user