- Table width set to 100% - Photo URLs resolved to absolute paths using AVATAR_BACKEND_URL - Token balance now reads from global token_account table - Config: added AVATAR_BACKEND_URL setting
213 lines
7.2 KiB
Python
213 lines
7.2 KiB
Python
"""数字分身管理服务层 — 同步连接数字分身应用的 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
|
|
|
|
|
|
def _resolve_photo_url(photo_url: str) -> str:
|
|
"""将相对路径的头像 URL 补全为绝对路径"""
|
|
if not photo_url:
|
|
return ""
|
|
if photo_url.startswith(("http://", "https://")):
|
|
return photo_url
|
|
base = settings.AVATAR_BACKEND_URL
|
|
if base:
|
|
base = base.rstrip("/")
|
|
return f"{base}{photo_url}"
|
|
return photo_url
|
|
|
|
|
|
def _get_global_token_balance(db: Session) -> int:
|
|
"""获取全局 token_account 余额(单行表)"""
|
|
try:
|
|
result = db.execute(text("SELECT balance FROM token_account LIMIT 1")).fetchone()
|
|
return result.balance if result and result.balance else 0
|
|
except Exception:
|
|
return 0
|
|
|
|
|
|
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 = []
|
|
global_token = _get_global_token_balance(db)
|
|
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": _resolve_photo_url(row.photo_url),
|
|
"emoji": row.emoji or "🤖",
|
|
"status": row.status or "active",
|
|
"token_balance": global_token or (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
|
|
|
|
global_token = _get_global_token_balance(db)
|
|
return {
|
|
"id": row.id,
|
|
"name": row.name,
|
|
"display_name": row.display_name,
|
|
"description": row.description or "",
|
|
"photo_url": _resolve_photo_url(row.photo_url),
|
|
"emoji": row.emoji or "🤖",
|
|
"status": row.status or "active",
|
|
"token_balance": global_token or (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()
|