Files
life-echo/api/app/features/user/service.py
Kevin a3f61fcc0f feat(api+app): 对话阶段化、回忆录流水线与客户端会话体验
- DB: segments 用户输入文本(Alembic 0002)
- Chat: 阶段检测/阶段提示/回复限制,编排与访谈/画像 prompts 调整
- Memoir: 忠实度检查 agent,叙事与分类等链路更新
- Core: agent 日志、Alembic 启动、LangChain/日志/配置等
- Story: time_hints;Memory 检索与相关测试
- Expo: 助手头像、会话页与消息拆分、实时会话与文案/i18n
- Docs/scripts/tests: 迁移脚本、LLM JSON/记忆检索文档、新增单测
2026-03-26 12:13:36 +08:00

154 lines
5.4 KiB
Python

from datetime import timedelta
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.db import utc_now
from app.core.logging import get_logger
from app.core.redis import redis_service
from app.core.task_tracker import task_tracker
from app.features.user import repo
from app.features.user.models import User
from app.features.user.schemas import (
PURGE_USER_DATA_CONFIRMATION,
PurgeUserDataResponse,
TestSubscriptionResponse,
UpdateUserProfileRequest,
UserProfileResponse,
)
from app.ports.storage import ObjectStorage
logger = get_logger(__name__)
def _user_to_profile(user: User) -> UserProfileResponse:
return UserProfileResponse(
id=user.id,
phone=user.phone,
email=user.email,
nickname=user.nickname,
avatar_url=user.avatar_url,
subscription_type=user.subscription_type,
created_at=user.created_at.isoformat(),
birth_year=user.birth_year,
birth_place=user.birth_place,
grew_up_place=user.grew_up_place,
occupation=user.occupation,
)
class UserService:
def __init__(self, db: AsyncSession):
self._db = db
async def update_profile(
self, user_id: str, body: UpdateUserProfileRequest
) -> UserProfileResponse:
user = await repo.get_user_by_id(user_id, self._db)
if not user:
raise ValueError("用户不存在")
if body.birth_year is not None:
user.birth_year = body.birth_year
if body.birth_place is not None:
user.birth_place = body.birth_place
if body.grew_up_place is not None:
user.grew_up_place = body.grew_up_place
if body.occupation is not None:
user.occupation = body.occupation
await self._db.commit()
await self._db.refresh(user)
return _user_to_profile(user)
async def toggle_test_subscription(
self, user_id: str, action: str, plan_id: str
) -> TestSubscriptionResponse:
user = await repo.get_user_by_id(user_id, self._db)
if not user:
raise ValueError("用户不存在")
now = utc_now()
if action == "activate":
user.subscription_type = plan_id
user.subscription_expires_at = now + timedelta(days=365)
await self._db.commit()
return TestSubscriptionResponse(
success=True,
message=f"已开启测试订阅:{plan_id}",
subscription_type=plan_id,
)
user.subscription_type = "free"
user.subscription_expires_at = None
await self._db.commit()
return TestSubscriptionResponse(
success=True,
message="已关闭测试订阅,恢复免费体验版",
subscription_type="free",
)
async def purge_all_user_data(
self,
user_id: str,
*,
confirmation: str,
object_storage: ObjectStorage | None = None,
) -> PurgeUserDataResponse:
"""物理删除该用户业务数据(不含 users 账号行);提交后再清 Redis / 任务追踪 / 锁 key。"""
if confirmation != PURGE_USER_DATA_CONFIRMATION:
raise ValueError("确认文案不正确,请按提示完整输入口令")
user = await repo.get_user_by_id(user_id, self._db)
if not user:
raise ValueError("用户不存在")
storage_keys = await repo.collect_object_storage_keys_before_purge(
self._db, user_id
)
conv_ids, chapter_ids, story_ids = await repo.collect_purge_context(
self._db, user_id
)
await repo.purge_user_related_rows(self._db, user_id)
await self._db.commit()
if object_storage and storage_keys:
for key in storage_keys:
try:
object_storage.delete(key)
except Exception as e:
logger.warning(
"对象存储删除失败 user_id={} key={} err={}", user_id, key, e
)
for cid in conv_ids:
try:
await redis_service.clear_conversation_history(cid)
except Exception as e:
logger.warning(
"清空会话 Redis 历史失败 conversation_id={} err={}", cid, e
)
try:
await task_tracker.clear_user_tasks(user_id)
except Exception as e:
logger.warning("清空用户任务追踪失败 user_id={} err={}", user_id, e)
try:
await redis_service.delete_keys_matching_pattern(
f"lock:chapter:{user_id}:*"
)
for ch_id in chapter_ids:
await redis_service.delete_keys_matching_pattern(
f"lock:chapter-images:{ch_id}"
)
for sid in story_ids:
await redis_service.delete_keys_matching_pattern(
f"lock:story-image:{sid}"
)
except Exception as e:
logger.warning("清理 Redis 锁 key 失败 user_id={} err={}", user_id, e)
return PurgeUserDataResponse(
success=True,
message=(
"已清空该账号下的对话、记忆、故事、章节、订单等业务数据,并已尝试删除关联的对象存储文件;"
"所有登录会话已失效,请重新登录"
),
)