"""数字分身管理服务层 — 同步连接数字分身应用的 SQLite 数据库""" import os from typing import Optional, Tuple from sqlalchemy import create_engine, text from sqlalchemy.orm import sessionmaker, Session from app.core.config import settings _engine = None _SessionLocal: Optional[sessionmaker] = None def _get_engine_and_session(): global _engine, _SessionLocal if _engine is None: db_path = settings.AVATAR_DB_PATH if not db_path: # 默认路径:从 backend/app/core/ 向上三级 base = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) db_path = os.path.join(base, "digital-avatar-app", "backend", "avatar.db") if not os.path.isabs(db_path): db_path = os.path.abspath(db_path) if not os.path.exists(db_path): # 数据库不存在时返回 None,由调用方处理 return None, None _engine = create_engine( f"sqlite:///{db_path}", connect_args={"check_same_thread": False}, ) _SessionLocal = sessionmaker(bind=_engine, autoflush=False, expire_on_commit=False) return _engine, _SessionLocal() def get_session() -> Optional[Session]: _, session = _get_engine_and_session() return session def is_available() -> bool: """检查数字分身数据库是否可用""" engine, _ = _get_engine_and_session() return engine is not None class AvatarService: @staticmethod def list_avatars( db: Session, page: int = 1, page_size: int = 20, keyword: Optional[str] = None, status: Optional[str] = None, ) -> Tuple[int, list]: """分页查询所有数字分身(含归属用户信息)""" where_clauses = [] params = {} if keyword: where_clauses.append( "(a.name LIKE :kw OR a.display_name LIKE :kw)" ) params["kw"] = f"%{keyword}%" if status: where_clauses.append("a.status = :status") params["status"] = status where_sql = "" if where_clauses: where_sql = "WHERE " + " AND ".join(where_clauses) # 计数 count_sql = f"SELECT COUNT(*) FROM avatars a {where_sql}" total = db.execute(text(count_sql), params).scalar() or 0 # 分页查询 offset = (page - 1) * page_size params["limit"] = page_size params["offset"] = offset query = text(f""" SELECT a.id, a.name, a.display_name, a.description, a.photo_url, a.emoji, a.status, a.token_balance, a.config, a.created_at, a.updated_at, u.nickname AS owner_nickname, u.phone AS owner_phone FROM avatars a LEFT JOIN users u ON a.owner_id = u.huihui_user_id {where_sql} ORDER BY a.created_at DESC LIMIT :limit OFFSET :offset """) rows = db.execute(query, params).fetchall() items = [] for row in rows: config = {} if row.config: if isinstance(row.config, str): import json try: config = json.loads(row.config) except (json.JSONDecodeError, ValueError): config = {} elif isinstance(row.config, dict): config = row.config items.append({ "id": row.id, "name": row.name, "display_name": row.display_name, "description": row.description or "", "photo_url": row.photo_url or "", "emoji": row.emoji or "🤖", "status": row.status or "active", "token_balance": row.token_balance or 0, "config": config, "owner_nickname": row.owner_nickname or "", "owner_phone": row.owner_phone or "", "created_at": str(row.created_at) if row.created_at else "", "updated_at": str(row.updated_at) if row.updated_at else "", }) return total, items @staticmethod def get_avatar(db: Session, avatar_id: str) -> Optional[dict]: """获取单个数字分身详情""" query = text(""" SELECT a.id, a.name, a.display_name, a.description, a.photo_url, a.emoji, a.status, a.token_balance, a.config, a.created_at, a.updated_at, u.nickname AS owner_nickname, u.phone AS owner_phone FROM avatars a LEFT JOIN users u ON a.owner_id = u.huihui_user_id WHERE a.id = :avatar_id """) row = db.execute(query, {"avatar_id": avatar_id}).fetchone() if not row: return None config = {} if row.config: if isinstance(row.config, str): import json try: config = json.loads(row.config) except (json.JSONDecodeError, ValueError): config = {} elif isinstance(row.config, dict): config = row.config return { "id": row.id, "name": row.name, "display_name": row.display_name, "description": row.description or "", "photo_url": row.photo_url or "", "emoji": row.emoji or "🤖", "status": row.status or "active", "token_balance": row.token_balance or 0, "config": config, "owner_nickname": row.owner_nickname or "", "owner_phone": row.owner_phone or "", "created_at": str(row.created_at) if row.created_at else "", "updated_at": str(row.updated_at) if row.updated_at else "", } @staticmethod def update_status(db: Session, avatar_id: str, status: str) -> Optional[dict]: """更新数字分身状态(开关机)""" query = text(""" UPDATE avatars SET status = :status, updated_at = datetime('now') WHERE id = :avatar_id """) result = db.execute(query, {"status": status, "avatar_id": avatar_id}) db.commit() if result.rowcount == 0: return None return AvatarService.get_avatar(db, avatar_id) avatar_service = AvatarService()