1.0.0初始化源代码
This commit is contained in:
361
backend/app/services/virtual_user_service.py
Normal file
361
backend/app/services/virtual_user_service.py
Normal file
@@ -0,0 +1,361 @@
|
||||
"""
|
||||
虚拟用户管理服务
|
||||
"""
|
||||
import logging
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, func, Date
|
||||
|
||||
from app.models.virtual_user import VirtualUser, VirtualUserPersona, ActivityLevel, UserStatus
|
||||
from app.models.interaction import InteractionRecord, InteractionType
|
||||
from app.services.ai_service import ai_service
|
||||
from app.core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VirtualUserService:
|
||||
"""虚拟用户服务类"""
|
||||
|
||||
# 预设写作风格库
|
||||
WRITING_STYLES = [
|
||||
"幽默风趣",
|
||||
"严肃理性",
|
||||
"文艺清新",
|
||||
"吐槽犀利",
|
||||
"感性温暖",
|
||||
"客观中立",
|
||||
"激情澎湃",
|
||||
"冷静分析",
|
||||
"活泼可爱",
|
||||
"深沉内敛"
|
||||
]
|
||||
|
||||
# 昵称前缀和后缀
|
||||
NICKNAME_PREFIXES = ["清风", "星辰", "云端", "晨曦", "暮色", "流年", "初心", "远方"]
|
||||
NICKNAME_SUFFIXES = ["行者", "旅人", "追梦", "时光", "记忆", "印象", "故事", "传奇"]
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
def get_user_by_id(self, user_id: int) -> Optional[VirtualUser]:
|
||||
"""根据 ID 获取用户"""
|
||||
return self.db.query(VirtualUser).filter(VirtualUser.id == user_id).first()
|
||||
|
||||
def get_user_by_username(self, username: str) -> Optional[VirtualUser]:
|
||||
"""根据用户名获取用户"""
|
||||
return self.db.query(VirtualUser).filter(VirtualUser.username == username).first()
|
||||
|
||||
def get_users(
|
||||
self,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
status: Optional[UserStatus] = None,
|
||||
search: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取用户列表
|
||||
:param page: 页码
|
||||
:param page_size: 每页数量
|
||||
:param status: 状态筛选
|
||||
:param search: 搜索关键词
|
||||
:return: 用户列表和总数
|
||||
"""
|
||||
query = self.db.query(VirtualUser)
|
||||
|
||||
if status:
|
||||
query = query.filter(VirtualUser.status == status)
|
||||
|
||||
if search:
|
||||
query = query.filter(
|
||||
or_(
|
||||
VirtualUser.nickname.like(f"%{search}%"),
|
||||
VirtualUser.username.like(f"%{search}%")
|
||||
)
|
||||
)
|
||||
|
||||
total = query.count()
|
||||
users = query.order_by(VirtualUser.created_at.desc()).offset(
|
||||
(page - 1) * page_size
|
||||
).limit(page_size).all()
|
||||
|
||||
return {"total": total, "items": users}
|
||||
|
||||
def create_user(
|
||||
self,
|
||||
username: str,
|
||||
password: str,
|
||||
nickname: str,
|
||||
writing_style: Optional[str] = None,
|
||||
activity_level: ActivityLevel = ActivityLevel.MEDIUM,
|
||||
avatar_url: Optional[str] = None,
|
||||
persona_description: Optional[str] = None
|
||||
) -> Optional[VirtualUser]:
|
||||
"""
|
||||
创建虚拟用户
|
||||
:param username: 用户名
|
||||
:param password: 密码
|
||||
:param nickname: 昵称
|
||||
:param writing_style: 写作风格
|
||||
:param activity_level: 活跃度
|
||||
:param avatar_url: 头像 URL
|
||||
:param persona_description: 人格描述
|
||||
:return: 创建的用户
|
||||
"""
|
||||
# 检查用户名是否已存在
|
||||
existing = self.get_user_by_username(username)
|
||||
if existing:
|
||||
logger.error(f"Username already exists: {username}")
|
||||
return None
|
||||
|
||||
user = VirtualUser(
|
||||
username=username,
|
||||
password=password, # TODO: 加密存储
|
||||
nickname=nickname,
|
||||
writing_style=writing_style or self._random_writing_style(),
|
||||
activity_level=activity_level,
|
||||
avatar_url=avatar_url or self._generate_avatar_url(),
|
||||
persona_description=persona_description
|
||||
)
|
||||
|
||||
self.db.add(user)
|
||||
self.db.commit()
|
||||
self.db.refresh(user)
|
||||
|
||||
logger.info(f"Virtual user created: {username}")
|
||||
return user
|
||||
|
||||
def generate_users(
|
||||
self,
|
||||
count: int,
|
||||
writing_styles: Optional[List[str]] = None,
|
||||
activity_levels: Optional[List[ActivityLevel]] = None,
|
||||
generate_persona: bool = True
|
||||
) -> List[VirtualUser]:
|
||||
"""
|
||||
批量生成虚拟用户
|
||||
:param count: 生成数量
|
||||
:param writing_styles: 写作风格列表
|
||||
:param activity_levels: 活跃度级别列表
|
||||
:param generate_persona: 是否生成 AI 人格描述
|
||||
:return: 生成的用户列表
|
||||
"""
|
||||
import random
|
||||
|
||||
styles = writing_styles or self.WRITING_STYLES
|
||||
levels = activity_levels or [ActivityLevel.LOW, ActivityLevel.MEDIUM, ActivityLevel.HIGH]
|
||||
|
||||
created_users = []
|
||||
|
||||
for i in range(count):
|
||||
# 生成唯一用户名
|
||||
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
|
||||
username = f"user_{timestamp}_{i}"
|
||||
|
||||
# 随机生成昵称
|
||||
prefix = random.choice(self.NICKNAME_PREFIXES)
|
||||
suffix = random.choice(self.NICKNAME_SUFFIXES)
|
||||
nickname = f"{prefix}{suffix}{random.randint(100, 999)}"
|
||||
|
||||
# 随机密码
|
||||
password = f"pwd_{random.randint(100000, 999999)}"
|
||||
|
||||
# 随机写作风格
|
||||
writing_style = random.choice(styles)
|
||||
|
||||
# 随机活跃度
|
||||
activity_level = random.choice(levels)
|
||||
|
||||
# 生成头像
|
||||
avatar_url = self._generate_avatar_url()
|
||||
|
||||
# AI 生成人格描述
|
||||
persona_description = None
|
||||
if generate_persona:
|
||||
persona_description = self._generate_persona_description(
|
||||
writing_style,
|
||||
activity_level
|
||||
)
|
||||
|
||||
user = self.create_user(
|
||||
username=username,
|
||||
password=password,
|
||||
nickname=nickname,
|
||||
writing_style=writing_style,
|
||||
activity_level=activity_level,
|
||||
avatar_url=avatar_url,
|
||||
persona_description=persona_description
|
||||
)
|
||||
|
||||
if user:
|
||||
created_users.append(user)
|
||||
|
||||
logger.info(f"Generated {len(created_users)} virtual users")
|
||||
return created_users
|
||||
|
||||
def _generate_persona_description(
|
||||
self,
|
||||
writing_style: str,
|
||||
activity_level: ActivityLevel
|
||||
) -> str:
|
||||
"""AI 生成人格描述"""
|
||||
import asyncio
|
||||
|
||||
prompt = f"""请为一位虚拟用户生成人格描述,要求:
|
||||
- 写作风格:{writing_style}
|
||||
- 活跃度:{activity_level.value}
|
||||
|
||||
请用 50-100 字描述这个人的性格特点、兴趣爱好、说话方式等。直接输出描述内容。"""
|
||||
|
||||
try:
|
||||
# 同步调用异步方法
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
result = loop.run_until_complete(ai_service._call_ai_api(prompt))
|
||||
loop.close()
|
||||
|
||||
if result and result.get("content"):
|
||||
return result["content"]
|
||||
except Exception as e:
|
||||
logger.error(f"Generate persona description error: {e}")
|
||||
|
||||
return f"这是一位{writing_style}的虚拟用户,活跃度{activity_level.value}。"
|
||||
|
||||
def _random_writing_style(self) -> str:
|
||||
"""随机选择写作风格"""
|
||||
import random
|
||||
return random.choice(self.WRITING_STYLES)
|
||||
|
||||
def _generate_avatar_url(self) -> str:
|
||||
"""生成随机头像 URL(使用第三方头像 API)"""
|
||||
import random
|
||||
# 使用 DiceBear 头像 API
|
||||
seed = f"avatar_{datetime.now().timestamp()}_{random.randint(1000, 9999)}"
|
||||
return f"https://api.dicebear.com/7.x/avataaars/svg?seed={seed}"
|
||||
|
||||
def update_user(
|
||||
self,
|
||||
user_id: int,
|
||||
**kwargs
|
||||
) -> Optional[VirtualUser]:
|
||||
"""更新用户信息"""
|
||||
user = self.get_user_by_id(user_id)
|
||||
if not user:
|
||||
return None
|
||||
|
||||
for key, value in kwargs.items():
|
||||
if hasattr(user, key) and value is not None:
|
||||
setattr(user, key, value)
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(user)
|
||||
return user
|
||||
|
||||
def delete_user(self, user_id: int) -> bool:
|
||||
"""删除用户"""
|
||||
user = self.get_user_by_id(user_id)
|
||||
if not user:
|
||||
return False
|
||||
|
||||
self.db.delete(user)
|
||||
self.db.commit()
|
||||
logger.info(f"Virtual user deleted: {user_id}")
|
||||
return True
|
||||
|
||||
def import_users_from_excel(
|
||||
self,
|
||||
users_data: List[Dict[str, Any]],
|
||||
generate_persona: bool = True
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
从 Excel 导入虚拟用户
|
||||
:param users_data: 用户数据列表
|
||||
:param generate_persona: 是否生成 AI 人格描述
|
||||
:return: 导入结果
|
||||
"""
|
||||
success_count = 0
|
||||
failed_count = 0
|
||||
created_users = []
|
||||
|
||||
for user_data in users_data:
|
||||
try:
|
||||
username = user_data.get("username")
|
||||
password = user_data.get("password")
|
||||
nickname = user_data.get("nickname", "")
|
||||
|
||||
if not username or not password:
|
||||
logger.warning(f"Missing username or password: {user_data}")
|
||||
failed_count += 1
|
||||
continue
|
||||
|
||||
# 如果昵称为空,生成一个
|
||||
if not nickname:
|
||||
nickname = f"用户{username}"
|
||||
|
||||
writing_style = user_data.get("writing_style")
|
||||
activity_level_str = user_data.get("activity_level", "medium")
|
||||
|
||||
# 转换活跃度枚举
|
||||
try:
|
||||
activity_level = ActivityLevel(activity_level_str.lower())
|
||||
except ValueError:
|
||||
activity_level = ActivityLevel.MEDIUM
|
||||
|
||||
user = self.create_user(
|
||||
username=username,
|
||||
password=password,
|
||||
nickname=nickname,
|
||||
writing_style=writing_style,
|
||||
activity_level=activity_level
|
||||
)
|
||||
|
||||
if user:
|
||||
success_count += 1
|
||||
created_users.append(user)
|
||||
else:
|
||||
failed_count += 1
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Import user error: {e}")
|
||||
failed_count += 1
|
||||
|
||||
return {
|
||||
"success_count": success_count,
|
||||
"failed_count": failed_count,
|
||||
"created_users": created_users
|
||||
}
|
||||
|
||||
def get_user_stats(self, user_id: int) -> Dict[str, Any]:
|
||||
"""获取用户统计信息"""
|
||||
user = self.get_user_by_id(user_id)
|
||||
if not user:
|
||||
return {}
|
||||
|
||||
today = datetime.now().date()
|
||||
|
||||
# 统计今日互动
|
||||
today_interactions = self.db.query(InteractionRecord).filter(
|
||||
and_(
|
||||
InteractionRecord.virtual_user_id == user_id,
|
||||
func.date(InteractionRecord.execution_time) == today
|
||||
)
|
||||
).all()
|
||||
|
||||
today_comments = sum(1 for i in today_interactions if i.interaction_type == InteractionType.COMMENT)
|
||||
today_replies = sum(1 for i in today_interactions if i.interaction_type == InteractionType.REPLY)
|
||||
|
||||
return {
|
||||
"user_id": user_id,
|
||||
"nickname": user.nickname,
|
||||
"total_interactions": user.total_interactions,
|
||||
"today_comments": today_comments,
|
||||
"today_replies": today_replies,
|
||||
"last_interaction_time": user.last_interaction_time
|
||||
}
|
||||
|
||||
|
||||
# 工厂函数
|
||||
def get_virtual_user_service(db: Session) -> VirtualUserService:
|
||||
"""获取虚拟用户服务实例"""
|
||||
return VirtualUserService(db)
|
||||
Reference in New Issue
Block a user