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=( "已清空该账号下的对话、记忆、故事、章节、订单等业务数据,并已尝试删除关联的对象存储文件;" "所有登录会话已失效,请重新登录" ), )