Files
vps-manager/app/services/account_service.py
T

112 lines
4.5 KiB
Python

"""平台账号业务逻辑
账号与资产的关系:Asset.account 按名称引用账号(字符串,兼容历史自由文本)。
重命名账号时同步更新所有引用资产,保证两边一致。
凭证层:登录密码与 API 配置加密存储,Read 仅返回布尔标记。
"""
from typing import Dict, List
from fastapi import HTTPException, status
from sqlalchemy import func
from sqlmodel import Session, select
from app.core import crypto
from app.models.asset import Account, Asset
from app.schemas.account import AccountCreate, AccountRead, AccountUpdate
def _asset_counts(session: Session) -> Dict[str, int]:
"""按 account 名称统计引用资产数"""
rows = session.exec(
select(Asset.account, func.count(Asset.id))
.where(Asset.account.is_not(None)) # type: ignore[union-attr]
.group_by(Asset.account)
).all()
return {name: cnt for name, cnt in rows}
def _to_read(account: Account, counts: Dict[str, int]) -> AccountRead:
read = AccountRead.model_validate(account)
read.asset_count = counts.get(account.name, 0)
read.has_login_password = bool(account.login_password_encrypted)
read.has_api_config = bool(account.api_config_encrypted)
return read
def list_accounts(session: Session) -> List[AccountRead]:
counts = _asset_counts(session)
accounts = session.exec(select(Account).order_by(Account.name.asc())).all()
return [_to_read(a, counts) for a in accounts]
def _get_account(session: Session, account_id: int) -> Account:
account = session.get(Account, account_id)
if not account:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="账号不存在")
return account
def _check_name_taken(session: Session, name: str, exclude_id: int | None = None) -> None:
stmt = select(Account).where(Account.name == name)
if exclude_id is not None:
stmt = stmt.where(Account.id != exclude_id)
if session.exec(stmt).first():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=f"账号已存在:{name}"
)
def create_account(session: Session, data: AccountCreate) -> AccountRead:
name = data.name.strip()
if not name:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="账号名称不能为空")
_check_name_taken(session, name)
account = Account(name=name, platform=data.platform, remark=data.remark, login_user=data.login_user)
account.login_password_encrypted = crypto.encrypt(data.login_password)
account.api_config_encrypted = crypto.encrypt(data.api_config)
session.add(account)
session.commit()
session.refresh(account)
return _to_read(account, _asset_counts(session))
def update_account(session: Session, account_id: int, data: AccountUpdate) -> AccountRead:
account = _get_account(session, account_id)
if data.name is not None:
new_name = data.name.strip()
if not new_name:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="账号名称不能为空")
if new_name != account.name:
_check_name_taken(session, new_name, exclude_id=account.id)
# 同步更新引用该账号的资产,避免重命名后资产端失联
assets = session.exec(select(Asset).where(Asset.account == account.name)).all()
for a in assets:
a.account = new_name
session.add(a)
account.name = new_name
if data.platform is not None:
account.platform = data.platform or None
if data.remark is not None:
account.remark = data.remark or None
if data.login_user is not None:
account.login_user = data.login_user or None
# 凭证:None = 不修改;空串 = 清除;非空 = 重新加密存储
if data.login_password is not None:
account.login_password_encrypted = crypto.encrypt(data.login_password)
if data.api_config is not None:
account.api_config_encrypted = crypto.encrypt(data.api_config)
session.add(account)
session.commit()
session.refresh(account)
return _to_read(account, _asset_counts(session))
def delete_account(session: Session, account_id: int) -> Dict[str, int]:
account = _get_account(session, account_id)
affected = len(session.exec(select(Asset.id).where(Asset.account == account.name)).all())
session.delete(account)
session.commit()
# 资产端保留原账号名文本(不级联清空),由用户自行处理
return {"affected_assets": affected}