feat: add digital avatar management to admin dashboard
- Add avatars list page with pagination, search, status filter - Add avatar detail dialog showing full info and config params - Add on/off switch (active/inactive status toggle) - Backend: avatar_service.py connects to digital avatar SQLite - Backend: /api/avatars endpoints for list, detail, status update - Sidebar menu item added below dashboard - docker-compose: mount SQLite db and set AVATAR_DB_PATH env var
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
"""数字分身管理服务层 — 同步连接数字分身应用的 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):
|
||||
raise FileNotFoundError(f"数字分身数据库不存在: {db_path}")
|
||||
_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() -> Session:
|
||||
_, session = _get_engine_and_session()
|
||||
return session
|
||||
|
||||
|
||||
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()
|
||||
Reference in New Issue
Block a user