refactor: make model sessions explicit

This commit is contained in:
jxxghp
2026-08-23 23:33:07 +08:00
parent 820582ab12
commit 6e69258e3c
65 changed files with 1299 additions and 2010 deletions
+24 -42
View File
@@ -1,7 +1,7 @@
"""
ORM 基类与数据访问基类。
Base 提供声明式基类与兼容行为(字典转换、旧增删改查便利方法)
Base 提供声明式基类与显式会话增删改查原语
DbOper 是各业务 Oper 的基类,持有一个可注入的会话。
"""
from collections.abc import Awaitable, Callable
@@ -12,12 +12,6 @@ from sqlalchemy import (CursorResult, Executable, Identity, Integer, Sequence,
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import DeclarativeBase, Mapped, Session, declared_attr, mapped_column
from app.db.decorators import (
legacy_async_db_query,
legacy_async_db_update,
legacy_db_query,
legacy_db_update,
)
from app.db.uow import run_async_transaction, run_sync_transaction
from app.runtime.config import settings
@@ -70,98 +64,87 @@ class Base(DeclarativeBase): # type: ignore[misc] # SQLAlchemy 无 py.typed
继承本类的模型一律使用 mapped_column() + Mapped[] 注解;确需非映射的类级属性时
用 ClassVar 显式声明,而不是把这个标志加回来。
create/get/update/delete/list/truncate 及其异步版本仅保留旧插件 ABI。宿主新代码应由
Application Command 定义事务边界,经显式 Session 调用 Oper,不得新增对这些方法的依赖。
create/get/update/delete/list/truncate 及其异步版本都是显式会话原语:只在调用方
Session 中暂存或查询,不自行创建、提交、回滚或关闭事务。宿主业务代码应通过 Oper
或 Application Command 使用这些能力,插件不得直接依赖宿主模型。
"""
# 由 get_id_column() 在各模型中提供实际的列定义,这里只声明类型供 IDE 使用
id: Mapped[int]
@legacy_db_update
def create(self, db: Session) -> None:
"""兼容旧插件调用:新增当前模型并提交"""
"""在调用方同步事务中暂存当前模型。"""
db.add(self)
@legacy_async_db_update
async def async_create(self, db: AsyncSession) -> Self:
"""兼容旧插件调用异步新增当前模型刷新主键并提交"""
"""调用异步事务中暂存当前模型刷新主键。"""
db.add(self)
await db.flush()
return self
@classmethod
@legacy_db_query
def get(cls, db: Session, rid: int) -> Optional[Self]:
"""兼容旧插件调用:按主键查询当前模型。"""
"""在调用方同步会话中按主键查询当前模型。"""
return cast(
Optional[Self],
db.execute(select(cls).where(and_(cls.id == rid))).scalars().first(),
)
@classmethod
@legacy_async_db_query
async def async_get(cls, db: AsyncSession, rid: int) -> Optional[Self]:
"""兼容旧插件调用异步按主键查询当前模型。"""
"""调用异步会话中按主键查询当前模型。"""
result = await db.execute(select(cls).where(and_(cls.id == rid)))
return cast(Optional[Self], result.scalars().first())
@legacy_db_update
def update(self, db: Session, payload: dict[str, Any]) -> None:
"""兼容旧插件调用:更新当前模型字段并提交"""
"""在调用方同步事务中更新当前模型字段。"""
for key, value in payload.items():
setattr(self, key, value)
if inspect(self).detached:
db.add(self)
@legacy_async_db_update
async def async_update(
self,
db: AsyncSession,
payload: dict[str, Any],
) -> None:
"""兼容旧插件调用异步更新当前模型字段并提交"""
"""调用异步事务中更新当前模型字段。"""
for key, value in payload.items():
setattr(self, key, value)
if inspect(self).detached:
db.add(self)
@classmethod
@legacy_db_update
def delete(cls, db: Session, rid: Any) -> None:
"""兼容旧插件调用:按主键删除当前模型并提交"""
"""在调用方同步事务中按主键删除当前模型。"""
db.execute(delete(cls).where(and_(cls.id == rid)))
@classmethod
@legacy_async_db_update
async def async_delete(cls, db: AsyncSession, rid: Any) -> None:
"""兼容旧插件调用异步按主键删除当前模型并提交"""
"""调用异步事务中按主键删除当前模型。"""
result = await db.execute(select(cls).where(and_(cls.id == rid)))
user = result.scalars().first()
if user:
await db.delete(user)
@classmethod
@legacy_db_update
def truncate(cls, db: Session) -> None:
"""兼容旧插件调用:清空当前模型表并提交"""
"""在调用方同步事务中清空当前模型表。"""
db.execute(delete(cls))
@classmethod
@legacy_async_db_update
async def async_truncate(cls, db: AsyncSession) -> None:
"""兼容旧插件调用异步清空当前模型表并提交"""
"""调用异步事务中清空当前模型表。"""
await db.execute(delete(cls))
@classmethod
@legacy_db_query
def list(cls, db: Session) -> List[Self]:
"""兼容旧插件调用:查询当前模型的全部记录。"""
"""在调用方同步会话中查询当前模型的全部记录。"""
return list(db.execute(select(cls)).scalars().all())
@classmethod
@legacy_async_db_query
async def async_list(cls, db: AsyncSession) -> List[Self]:
"""兼容旧插件调用异步查询当前模型的全部记录。"""
"""调用异步会话中查询当前模型的全部记录。"""
result = await db.execute(select(cls))
return list(result.scalars().all())
@@ -183,19 +166,19 @@ class DbOper:
"""
def __init__(self, db: Optional[Union[Session, AsyncSession]] = None):
"""保存调用方会话;无会话写入由组合根兼容事务执行器承接。"""
"""保存调用方会话;无会话调用由组合根事务执行器承接。"""
self._db = db
def _execute_sync_write(self, operation: Callable[[Session], T]) -> T:
"""在当前同步会话暂存,或委托组合根创建兼容事务。"""
"""在当前同步会话暂存,或委托组合根创建事务。"""
if self._db is None or isinstance(self._db, AsyncSession):
# 旧调用可能在同一 Oper 上混用同步/异步方法;跨会话类型时使用匹配的
# 兼容事务,不能把 AsyncSession 交给同步 SQLAlchemy API。
# 独立事务,不能把 AsyncSession 交给同步 SQLAlchemy API。
return run_sync_transaction(operation)
return operation(self._db)
def _execute_sync_query(self, operation: Callable[[Session], T]) -> T:
"""在当前同步会话查询,或委托组合根创建一次性兼容会话。"""
"""在当前同步会话查询,或委托组合根创建一次性会话。"""
if self._db is None or isinstance(self._db, AsyncSession):
return run_sync_transaction(operation)
return operation(self._db)
@@ -204,10 +187,9 @@ class DbOper:
self,
operation: Callable[[AsyncSession], Awaitable[T]],
) -> T:
"""在当前异步会话暂存,或委托组合根创建兼容事务。"""
"""在当前异步会话暂存,或委托组合根创建事务。"""
if self._db is None or isinstance(self._db, Session):
# 与查询装饰器的历史行为一致:同步会话不会被错误传入异步模型写入,
# 而是由组合根另开匹配的异步事务。
# 同步会话不会被错误传入异步模型写入,而是由组合根另开匹配的异步事务。
return await run_async_transaction(operation)
return await operation(self._db)
@@ -215,13 +197,13 @@ class DbOper:
self,
operation: Callable[[AsyncSession], Awaitable[T]],
) -> T:
"""在当前异步会话查询,或委托组合根创建一次性兼容会话。"""
"""在当前异步会话查询,或委托组合根创建一次性会话。"""
if self._db is None or isinstance(self._db, Session):
return await run_async_transaction(operation)
return await operation(self._db)
def _stage_create(self, model: TModel) -> TModel:
"""显式同步事务中暂存新模型,不触发 Base 的兼容提交装饰器"""
"""调用方或组合根持有的同步事务中暂存新模型"""
def stage(session: Session) -> TModel:
"""把模型加入当前同步会话。"""
session.add(model)
+2 -151
View File
@@ -5,8 +5,8 @@
未显式传入会话时自动创建,并在结束时归还——异步路径经 async_session_scope 收口,
连接池与配额都在那里生效。
收尾故障(rollback / close / __aexit__ 自身抛异常)一律只记日志、不上抛,正式装饰器
和 legacy 兼容壳的处理一致。理由与代价都要写明,别当成漏写的 raise
收尾故障(rollback / close / __aexit__ 自身抛异常)一律只记日志、不上抛。理由与代价
都要写明,别当成漏写的 raise
- 连接断开、事务已失效这类故障恰恰最容易发生在「出错之后」的收尾阶段。裸写收尾语句时
它一抛错就顶替掉原始异常,调用方看到的只剩「connection reset」,业务异常连类型都被
@@ -16,8 +16,6 @@
SQLAlchemy 归还连接时已在池层吞掉异常并 invalidate 坏连接,再把释放故障升级成调用方
的异常,只会让一次已经落库的写入看起来像失败,诱发重复提交。
"""
from functools import wraps
from inspect import Parameter, signature
from typing import Any, Awaitable, Callable, Optional, TypeVar
from sqlalchemy.ext.asyncio import AsyncSession
@@ -279,150 +277,3 @@ def async_db_query(func: Callable[..., Awaitable[_R]]) -> Callable[..., Awaitabl
return result
return wrapper
def legacy_db_query(func: Callable[..., _R]) -> Callable[..., _R]:
"""保留旧 Model 查询 ABI,同时让新调用方复用显式 Session。
旧插件通常省略 ``db``,直接把业务参数放在第一个位置;通用 ``db_query``
装饰器只适用于固定的 ``(db, ...)`` 形状,不能把这类位置参数直接套进去。
这里按签名插入会话,避免丢失旧插件传入的第一个业务参数。
"""
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> _R:
db = _get_args_db(args, kwargs)
if db is not None:
return func(*args, **kwargs)
session = ScopedSession()
call_args, call_kwargs = _inject_legacy_db(func, args, kwargs, session)
try:
return func(*call_args, **call_kwargs)
finally:
try:
session.close()
except Exception as close_err: # noqa: BLE001 释放故障不得改变旧 ABI 返回值
logger.error(f"释放数据库会话失败:{close_err}")
return wrapper
def legacy_async_db_query(
func: Callable[..., Awaitable[_R]],
) -> Callable[..., Awaitable[_R]]:
"""保留旧 Model 异步查询 ABI,同时让新调用方复用显式 AsyncSession。"""
@wraps(func)
async def wrapper(*args: Any, **kwargs: Any) -> _R:
db = _get_args_async_db(args, kwargs)
if db is not None:
return await func(*args, **kwargs)
async with async_session_scope() as session:
call_args, call_kwargs = _inject_legacy_db(func, args, kwargs, session)
return await func(*call_args, **call_kwargs)
return wrapper
def legacy_db_update(func: Callable[..., _R]) -> Callable[..., _R]:
"""保留旧 Model 同步写 ABI,并维持历史自动提交语义。
该装饰器只供已经公开的 Model/Base 方法兼容仓外插件。宿主新写路径必须
通过 Application Command、显式 Session 和 UnitOfWork 完成事务收口。
"""
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> _R:
db = _get_args_db(args, kwargs)
owns_session = db is None
if db is None:
db = ScopedSession()
args, kwargs = _inject_legacy_db(func, args, kwargs, db)
try:
result = func(*args, **kwargs)
db.commit()
return result
except Exception:
try:
db.rollback()
except Exception as rollback_err: # noqa: BLE001 回滚失败不能掩盖原始异常
logger.error(f"事务回滚失败,原始异常将原样上抛:{rollback_err}")
raise
finally:
if owns_session:
try:
db.close()
except Exception as close_err: # noqa: BLE001 释放故障不得改变旧 ABI 结果
logger.error(f"释放数据库会话失败:{close_err}")
return wrapper
def legacy_async_db_update(
func: Callable[..., Awaitable[_R]],
) -> Callable[..., Awaitable[_R]]:
"""保留旧 Model 异步写 ABI,并维持历史自动提交语义。
该装饰器只承接既有兼容面;新宿主代码不得用它创建隐式事务。
"""
@wraps(func)
async def wrapper(*args: Any, **kwargs: Any) -> _R:
db = _get_args_async_db(args, kwargs)
owns_session = db is None
scope = None
if db is None:
scope = async_session_scope()
db = await scope.__aenter__()
args, kwargs = _inject_legacy_db(func, args, kwargs, db)
try:
result = await func(*args, **kwargs)
await db.commit()
return result
except Exception:
try:
await db.rollback()
except Exception as rollback_err: # noqa: BLE001 回滚失败不能掩盖原始异常
logger.error(f"事务回滚失败,原始异常将原样上抛:{rollback_err}")
raise
finally:
if owns_session and scope is not None:
try:
await scope.__aexit__(None, None, None)
except Exception as close_err: # noqa: BLE001 释放故障不得改变旧 ABI 结果
logger.error(f"释放数据库会话失败:{close_err}")
return wrapper
def _inject_legacy_db(
func: Callable[..., _R],
args: tuple[Any, ...],
kwargs: dict[str, Any],
db: Any,
) -> tuple[tuple[Any, ...], dict[str, Any]]:
"""按旧 Model 方法签名注入兼容会话,不吞掉位置业务参数。"""
call_args = list(args)
call_kwargs = dict(kwargs)
parameters = list(signature(func).parameters.values())
db_index = next(
(index for index, parameter in enumerate(parameters) if parameter.name == "db"),
None,
)
if "db" in call_kwargs:
call_kwargs["db"] = db
return tuple(call_args), call_kwargs
if db_index is None:
# 兼容没有显式 db 参数的极旧函数,保持调用失败方式与普通 Python 一致。
return tuple(call_args), {"db": db, **call_kwargs}
if db_index < len(call_args) and call_args[db_index] is None:
call_args[db_index] = db
elif db_index < len(parameters) and parameters[db_index].kind is Parameter.POSITIONAL_ONLY:
call_args.insert(db_index, db)
elif db_index <= len(call_args):
call_args.insert(db_index, db)
else:
call_kwargs["db"] = db
return tuple(call_args), call_kwargs
-5
View File
@@ -5,7 +5,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import Base, get_id_column
from app.db.decorators import legacy_async_db_query, legacy_db_query
class AgentChat(Base):
@@ -50,7 +49,6 @@ class AgentChat(Base):
)
@classmethod
@legacy_db_query
def get_by_session(
cls, db: Session, session_id: str, user_id: Optional[str] = None
) -> Optional["AgentChat"]:
@@ -63,7 +61,6 @@ class AgentChat(Base):
return db.execute(statement.order_by(cls.id.desc())).scalars().first()
@classmethod
@legacy_async_db_query
async def async_get_by_session(
cls, db: AsyncSession, session_id: str, user_id: Optional[str] = None
) -> Optional["AgentChat"]:
@@ -77,7 +74,6 @@ class AgentChat(Base):
return result.scalars().first()
@classmethod
@legacy_db_query
def list_by_page(
cls,
db: Session,
@@ -103,7 +99,6 @@ class AgentChat(Base):
).scalars().all())
@classmethod
@legacy_async_db_query
async def async_list_by_page(
cls,
db: AsyncSession,
+11 -31
View File
@@ -4,7 +4,6 @@ from sqlalchemy import Boolean, Index, Integer, String, Text, select, update
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import Base, execute_dml, get_id_column
from app.db.decorators import legacy_db_query
def _get_for_user_statement(
@@ -85,47 +84,28 @@ class AgentTask(Base):
return task.id
@classmethod
@legacy_db_query
def get_for_user(
cls,
db: Session | int | None = None,
task_id: int | None = None,
db: Session,
task_id: int,
user_id: Optional[str] = None,
) -> Optional["AgentTask"]:
"""
按任务 ID 和可选用户 ID 查询,并保留无 Session 的旧插件调用方式。
"""
if task_id is None and isinstance(db, int):
task_id, db = db, None
if task_id is None:
raise TypeError("task_id is required")
def query(session: Session) -> Optional["AgentTask"]:
"""在给定会话中读取单个 Agent 任务。"""
return session.execute(
_get_for_user_statement(cls, task_id=task_id, user_id=user_id)
).scalars().first()
return query(db)
"""在调用方会话中按任务 ID 和可选用户 ID 查询。"""
return db.execute(
_get_for_user_statement(cls, task_id=task_id, user_id=user_id)
).scalars().first()
@classmethod
@legacy_db_query
def list_for_user(
cls,
db: Session | None = None,
db: Session,
user_id: Optional[str] = None,
enabled: Optional[bool] = None,
) -> list["AgentTask"]:
"""
按用户和启用状态查询,并保留无 Session 的旧插件调用方式。
"""
def query(session: Session) -> list["AgentTask"]:
"""在给定会话中读取 Agent 任务列表。"""
return list(session.execute(
_list_for_user_statement(cls, user_id=user_id, enabled=enabled)
).scalars().all())
return query(db)
"""在调用方会话中按用户和启用状态查询。"""
return list(db.execute(
_list_for_user_statement(cls, user_id=user_id, enabled=enabled)
).scalars().all())
@classmethod
def update_task(
-3
View File
@@ -4,7 +4,6 @@ from sqlalchemy import Index, Integer, String, Text, delete, select, update
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import Base, execute_dml, get_id_column
from app.db.decorators import legacy_db_query
from app.db.models.agenttask import AgentTask
@@ -249,7 +248,6 @@ class AgentTaskRun(Base):
return True
@classmethod
@legacy_db_query
def get_by_run_id(
cls,
db: Session,
@@ -261,7 +259,6 @@ class AgentTaskRun(Base):
).scalars().first()
@classmethod
@legacy_db_query
def list_for_task(
cls,
db: Session,
-17
View File
@@ -6,7 +6,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import Base, execute_dml, get_id_column
from app.db.decorators import legacy_async_db_query, legacy_db_query
from app.db.models._constraints import media_identity_constraint
from app.schemas.types import MediaSource
@@ -77,7 +76,6 @@ class DownloadHistory(Base):
)
@classmethod
@legacy_db_query
def get_by_hash(cls, db: Session, download_hash: str):
return db.execute(
select(DownloadHistory)
@@ -86,7 +84,6 @@ class DownloadHistory(Base):
).scalars().first()
@classmethod
@legacy_db_query
def get_by_hashes(cls, db: Session, download_hashes: List[str]):
"""
批量查询多个下载任务的最新历史记录,避免在上层形成 N+1 查询。
@@ -119,7 +116,6 @@ class DownloadHistory(Base):
]
@classmethod
@legacy_db_query
def get_by_media_identity(
cls, db: Session, media_source: MediaSource, media_id: str,
music_type: Optional[str] = None,
@@ -136,7 +132,6 @@ class DownloadHistory(Base):
return list(db.execute(statement).scalars().all())
@classmethod
@legacy_db_query
def list_by_page(
cls, db: Session, page: int = 1, count: int = 30
):
@@ -148,7 +143,6 @@ class DownloadHistory(Base):
).scalars().all())
@classmethod
@legacy_async_db_query
async def async_list_by_page(
cls, db: AsyncSession, page: int = 1, count: int = 30
):
@@ -161,7 +155,6 @@ class DownloadHistory(Base):
return list(result.scalars().all())
@classmethod
@legacy_async_db_query
async def async_list_by_title(
cls,
db: AsyncSession,
@@ -177,13 +170,11 @@ class DownloadHistory(Base):
return list(result.scalars().all())
@classmethod
@legacy_async_db_query
async def async_count(cls, db: AsyncSession):
result = await db.execute(select(func.count(cls.id)))
return result.scalar()
@classmethod
@legacy_async_db_query
async def async_count_by_title(cls, db: AsyncSession, title: str):
result = await db.execute(
select(func.count(cls.id)).filter(_title_like(cls.title, title))
@@ -191,14 +182,12 @@ class DownloadHistory(Base):
return result.scalar()
@classmethod
@legacy_db_query
def get_by_path(cls, db: Session, path: str):
return db.execute(
select(DownloadHistory).where(DownloadHistory.path == path)
).scalars().first()
@classmethod
@legacy_db_query
def get_last_by(
cls,
db: Session,
@@ -237,7 +226,6 @@ class DownloadHistory(Base):
@classmethod
@legacy_db_query
def list_by_user_date(cls, db: Session, date: str, username: Optional[str] = None):
"""
查询某用户某时间之前的下载历史。
@@ -256,7 +244,6 @@ class DownloadHistory(Base):
).scalars().all())
@classmethod
@legacy_db_query
def list_by_date(
cls,
db: Session,
@@ -282,7 +269,6 @@ class DownloadHistory(Base):
).scalars().all())
@classmethod
@legacy_db_query
def list_by_type(cls, db: Session, mtype: str, days: int):
return list(db.execute(
select(DownloadHistory).where(
@@ -345,7 +331,6 @@ class DownloadFiles(Base):
)
@classmethod
@legacy_db_query
def get_by_hash(cls, db: Session, download_hash: str, state: Optional[int] = None):
statement = select(cls).where(cls.download_hash == download_hash)
if state is not None:
@@ -353,7 +338,6 @@ class DownloadFiles(Base):
return list(db.execute(statement).scalars().all())
@classmethod
@legacy_db_query
def get_by_fullpath(cls, db: Session, fullpath: str, all_files: bool = False):
result = db.execute(
select(cls).where(cls.fullpath == fullpath).order_by(cls.id.desc())
@@ -361,7 +345,6 @@ class DownloadFiles(Base):
return list(result.all()) if all_files else result.first()
@classmethod
@legacy_db_query
def get_by_savepath(cls, db: Session, savepath: str):
return list(db.execute(select(cls).where(cls.savepath == savepath)).scalars().all())
-8
View File
@@ -7,7 +7,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import Base, execute_dml, get_id_column
from app.db.decorators import legacy_async_db_query, legacy_db_query
from app.db.models._constraints import media_identity_constraint
from app.schemas.types import MediaSource
@@ -53,12 +52,10 @@ class MediaServerItem(Base):
)
@classmethod
@legacy_db_query
def get_by_itemid(cls, db: Session, item_id: str):
return db.execute(select(cls).where(cls.item_id == item_id)).scalars().first()
@classmethod
@legacy_db_query
def get_by_server_itemid(cls, db: Session, server: str, item_id: str):
return db.execute(
select(cls).where(cls.server == server, cls.item_id == item_id)
@@ -97,7 +94,6 @@ class MediaServerItem(Base):
)
@classmethod
@legacy_db_query
def exist_by_media_identity(
cls, db: Session, media_source: MediaSource, media_id: str, mtype: str,
):
@@ -109,7 +105,6 @@ class MediaServerItem(Base):
)).scalars().first()
@classmethod
@legacy_db_query
def exists_by_title(cls, db: Session, title: str, mtype: str, year: str):
statement = select(cls).where(cls.title == title)
if mtype:
@@ -119,13 +114,11 @@ class MediaServerItem(Base):
return db.execute(statement).scalars().first()
@classmethod
@legacy_async_db_query
async def async_get_by_itemid(cls, db: AsyncSession, item_id: str):
result = await db.execute(select(cls).filter(cls.item_id == item_id))
return result.scalars().first()
@classmethod
@legacy_async_db_query
async def async_exist_by_media_identity(
cls, db: AsyncSession, media_source: MediaSource, media_id: str, mtype: str,
):
@@ -138,7 +131,6 @@ class MediaServerItem(Base):
return result.scalars().first()
@classmethod
@legacy_async_db_query
async def async_exists_by_title(cls, db: AsyncSession, title: str, mtype: str, year: str):
if not mtype and not year:
result = await db.execute(select(cls).filter(cls.title == title))
+45 -73
View File
@@ -5,7 +5,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import Base, execute_dml, get_id_column
from app.db.decorators import legacy_async_db_query, legacy_db_query
class Message(Base):
@@ -49,33 +48,25 @@ class Message(Base):
return self.to_dict()
@classmethod
@legacy_db_query
def list_by_page(
cls,
db: Session | None = None,
db: Session,
page: int = 1,
count: int = 30,
) -> List["Message"]:
"""
分页获取消息记录,兼容显式会话和旧插件无会话调用。
"""
def query(session: Session) -> List["Message"]:
"""在给定同步会话中执行消息分页查询。"""
return list(session.execute(
select(cls)
.order_by(cls.reg_time.desc(), cls.id.desc())
.offset((page - 1) * count)
.limit(count)
).scalars().all())
return query(db)
"""在调用方同步会话中分页获取消息记录。"""
return list(db.execute(
select(cls)
.order_by(cls.reg_time.desc(), cls.id.desc())
.offset((page - 1) * count)
.limit(count)
).scalars().all())
@classmethod
@legacy_db_query
def exists_by_source(
cls,
db: Session | str | None = None,
source: str | None = None,
db: Session,
source: str,
) -> bool:
"""
判断指定来源标识的消息记录是否存在。
@@ -84,44 +75,29 @@ class Message(Base):
:param source: 消息来源唯一标识
:return: 是否存在匹配记录
"""
if source is None and isinstance(db, str):
source, db = db, None
if source is None:
raise TypeError("source is required")
def query(session: Session) -> bool:
"""在给定同步会话中执行来源存在性查询。"""
return session.execute(
select(cls.id).where(cls.source == source).limit(1)
).scalars().first() is not None
return query(db)
return db.execute(
select(cls.id).where(cls.source == source).limit(1)
).scalars().first() is not None
@classmethod
@legacy_async_db_query
async def async_list_by_page(
cls, db: AsyncSession | None = None, page: int = 1, count: int = 30
cls, db: AsyncSession, page: int = 1, count: int = 30
) -> List["Message"]:
"""
异步分页获取消息记录。
"""
async def query(session: AsyncSession) -> List["Message"]:
"""在给定异步会话中执行消息分页查询。"""
result = await session.execute(
select(cls)
.order_by(cls.reg_time.desc(), cls.id.desc())
.offset((page - 1) * count)
.limit(count)
)
return list(result.scalars().all())
return await query(db)
result = await db.execute(
select(cls)
.order_by(cls.reg_time.desc(), cls.id.desc())
.offset((page - 1) * count)
.limit(count)
)
return list(result.scalars().all())
@classmethod
@legacy_async_db_query
async def async_list_sent_by_page(
cls,
db: AsyncSession | None = None,
db: AsyncSession,
page: int = 1,
count: int = 30,
all_clear_before: Optional[str] = None,
@@ -131,35 +107,31 @@ class Message(Base):
"""
分页获取系统发送的通知消息。
"""
async def query(session: AsyncSession) -> List["Message"]:
"""在给定异步会话中执行通知消息分页查询。"""
statement = select(cls).where(cls.action == 1)
if all_clear_before:
statement = statement.where(cls.reg_time > all_clear_before)
if system_clear_before:
statement = statement.where(
or_(
and_(cls.image.isnot(None), cls.image != ""),
cls.reg_time > system_clear_before,
)
statement = select(cls).where(cls.action == 1)
if all_clear_before:
statement = statement.where(cls.reg_time > all_clear_before)
if system_clear_before:
statement = statement.where(
or_(
and_(cls.image.isnot(None), cls.image != ""),
cls.reg_time > system_clear_before,
)
if media_clear_before:
statement = statement.where(
or_(
cls.image.is_(None),
cls.image == "",
cls.reg_time > media_clear_before,
)
)
result = await session.execute(
statement
.order_by(cls.reg_time.desc(), cls.id.desc())
.offset((page - 1) * count)
.limit(count)
)
return list(result.scalars().all())
return await query(db)
if media_clear_before:
statement = statement.where(
or_(
cls.image.is_(None),
cls.image == "",
cls.reg_time > media_clear_before,
)
)
result = await db.execute(
statement
.order_by(cls.reg_time.desc(), cls.id.desc())
.offset((page - 1) * count)
.limit(count)
)
return list(result.scalars().all())
@classmethod
def delete_before(
+16 -44
View File
@@ -5,10 +5,6 @@ from sqlalchemy.orm import Mapped, Session, mapped_column
from datetime import datetime
from app.db.base import Base, get_id_column
from app.db.decorators import (
legacy_async_db_query,
legacy_db_query,
)
def _get_by_user_id_statement(model: type["PassKey"], user_id: int):
@@ -54,75 +50,51 @@ class PassKey(Base):
transports: Mapped[Optional[str]] = mapped_column(String, nullable=True)
@classmethod
@legacy_db_query
def get_by_user_id(
cls,
db: Session | int | None = None,
user_id: int | None = None,
db: Session,
user_id: int,
):
"""获取用户的所有 PassKey,并保留无 Session 的旧插件调用方式"""
if user_id is None and isinstance(db, int):
user_id, db = db, None
if user_id is None:
raise TypeError("user_id is required")
def query(session: Session):
"""在给定会话中执行启用凭证查询。"""
return list(session.execute(
_get_by_user_id_statement(cls, user_id)
).scalars().all())
return query(db)
"""在调用方 Session 中获取用户的所有启用 PassKey。"""
return list(db.execute(
_get_by_user_id_statement(cls, user_id)
).scalars().all())
@classmethod
@legacy_async_db_query
async def async_get_by_user_id(cls, db: AsyncSession, user_id: int):
"""异步获取用户的所有 PassKey,并保留旧插件无 Session 调用"""
"""在调用方 AsyncSession 中获取用户的所有启用 PassKey。"""
result = await db.execute(
_get_by_user_id_statement(cls, user_id)
)
return list(result.scalars().all())
@classmethod
@legacy_db_query
def get_by_credential_id(
cls,
db: Session | str | None = None,
credential_id: str | None = None,
db: Session,
credential_id: str,
):
"""按凭证 ID 获取 PassKey,并保留无 Session 的旧插件调用方式"""
if credential_id is None and isinstance(db, str):
credential_id, db = db, None
if credential_id is None:
raise TypeError("credential_id is required")
def query(session: Session):
"""在给定会话中执行启用凭证查询。"""
return session.execute(
_get_by_credential_id_statement(cls, credential_id)
).scalars().first()
return query(db)
"""在调用方 Session 中按凭证 ID 获取启用 PassKey。"""
return db.execute(
_get_by_credential_id_statement(cls, credential_id)
).scalars().first()
@classmethod
@legacy_async_db_query
async def async_get_by_credential_id(cls, db: AsyncSession, credential_id: str):
"""异步根据凭证 ID 获取 PassKey,并保留旧插件无 Session 调用"""
"""在调用方 AsyncSession 中根据凭证 ID 获取启用 PassKey。"""
result = await db.execute(
_get_by_credential_id_statement(cls, credential_id)
)
return result.scalars().first()
@classmethod
@legacy_db_query
def get_by_id(cls, db: Session, passkey_id: int):
"""根据 ID 获取 PassKey,并保留旧插件无 Session 调用"""
"""在调用方 Session 中根据 ID 获取 PassKey。"""
return db.execute(select(cls).where(cls.id == passkey_id)).scalars().first()
@classmethod
@legacy_async_db_query
async def async_get_by_id(cls, db: AsyncSession, passkey_id: int):
"""异步根据 ID 获取 PassKey,并保留旧插件无 Session 调用"""
"""在调用方 AsyncSession 中根据 ID 获取 PassKey"""
result = await db.execute(
select(cls).filter(cls.id == passkey_id)
)
+12 -31
View File
@@ -4,7 +4,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import get_id_column, Base
from app.db.decorators import legacy_async_db_query, legacy_db_query
class PluginData(Base):
@@ -21,44 +20,32 @@ class PluginData(Base):
)
@classmethod
@legacy_db_query
def get_plugin_data(cls, db: Session | None = None, plugin_id: str | None = None):
"""在调用方 Session 中读取插件全部数据,并兼容旧无会话入口。"""
if plugin_id is None:
raise TypeError("plugin_id is required")
def get_plugin_data(cls, db: Session, plugin_id: str):
"""在调用方 Session 中读取插件全部数据。"""
return list(db.execute(select(cls).where(cls.plugin_id == plugin_id)).scalars().all())
@classmethod
@legacy_async_db_query
async def async_get_plugin_data(
cls, db: AsyncSession | None = None, plugin_id: str | None = None
cls, db: AsyncSession, plugin_id: str
):
"""在调用方 AsyncSession 中读取插件全部数据,并兼容旧无会话入口"""
if plugin_id is None:
raise TypeError("plugin_id is required")
"""在调用方 AsyncSession 中读取插件全部数据。"""
result = await db.execute(select(cls).where(cls.plugin_id == plugin_id))
return list(result.scalars().all())
@classmethod
@legacy_db_query
def get_plugin_data_by_key(
cls, db: Session | None = None, plugin_id: str | None = None, key: str | None = None
cls, db: Session, plugin_id: str, key: str
):
"""在调用方 Session 中按键读取插件数据,并兼容旧无会话入口"""
if plugin_id is None or key is None:
raise TypeError("plugin_id and key are required")
"""在调用方 Session 中按键读取插件数据。"""
return db.execute(
select(cls).where(cls.plugin_id == plugin_id, cls.key == key)
).scalars().first()
@classmethod
@legacy_async_db_query
async def async_get_plugin_data_by_key(
cls, db: AsyncSession | None = None, plugin_id: str | None = None, key: str | None = None
cls, db: AsyncSession, plugin_id: str, key: str
):
"""在调用方 AsyncSession 中按键读取插件数据,并兼容旧无会话入口"""
if plugin_id is None or key is None:
raise TypeError("plugin_id and key are required")
"""在调用方 AsyncSession 中按键读取插件数据。"""
result = await db.execute(
select(cls).where(cls.plugin_id == plugin_id, cls.key == key)
)
@@ -75,22 +62,16 @@ class PluginData(Base):
db.execute(delete(cls).where(cls.plugin_id == plugin_id))
@classmethod
@legacy_db_query
def get_plugin_data_by_plugin_id(
cls, db: Session | None = None, plugin_id: str | None = None
cls, db: Session, plugin_id: str
):
"""在调用方 Session 中按插件 ID 读取数据,并兼容旧无会话入口"""
if plugin_id is None:
raise TypeError("plugin_id is required")
"""在调用方 Session 中按插件 ID 读取数据。"""
return list(db.execute(select(cls).where(cls.plugin_id == plugin_id)).scalars().all())
@classmethod
@legacy_async_db_query
async def async_get_plugin_data_by_plugin_id(
cls, db: AsyncSession | None = None, plugin_id: str | None = None
cls, db: AsyncSession, plugin_id: str
):
"""在调用方 AsyncSession 中按插件 ID 读取数据,并兼容旧无会话入口"""
if plugin_id is None:
raise TypeError("plugin_id is required")
"""在调用方 AsyncSession 中按插件 ID 读取数据。"""
result = await db.execute(select(cls).where(cls.plugin_id == plugin_id))
return list(result.scalars().all())
+35 -92
View File
@@ -6,7 +6,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import Base, get_id_column
from app.db.decorators import legacy_async_db_query, legacy_db_query
class Site(Base):
@@ -58,122 +57,66 @@ class Site(Base):
downloader: Mapped[Optional[str]] = mapped_column(String)
@classmethod
@legacy_db_query
def get_by_domain(cls, db: Session | str | None = None, domain: str | None = None):
"""按域名查询站点,兼容显式会话和旧插件无会话调用。"""
if domain is None and isinstance(db, str):
domain, db = db, None
if domain is None:
raise TypeError("domain is required")
def query(session: Session):
"""在给定同步会话中执行域名查询。"""
return session.execute(select(cls).where(cls.domain == domain)).scalars().first()
return query(db)
def get_by_domain(cls, db: Session, domain: str):
"""在调用方 Session 中按域名查询站点。"""
return db.execute(select(cls).where(cls.domain == domain)).scalars().first()
@classmethod
@legacy_async_db_query
async def async_get_by_domain(
cls,
db: AsyncSession | str | None = None,
domain: str | None = None,
db: AsyncSession,
domain: str,
):
"""异步按域名查询站点,兼容显式会话和旧插件无会话调用"""
if domain is None and isinstance(db, str):
domain, db = db, None
if domain is None:
raise TypeError("domain is required")
async def query(session: AsyncSession):
"""在给定异步会话中执行域名查询。"""
result = await session.execute(select(cls).where(cls.domain == domain))
return result.scalar_one_or_none()
return await query(db)
"""在调用方 AsyncSession 中按域名查询站点"""
result = await db.execute(select(cls).where(cls.domain == domain))
return result.scalar_one_or_none()
@classmethod
@legacy_async_db_query
async def async_get_by_name(
cls,
db: AsyncSession | str | None = None,
name: str | None = None,
db: AsyncSession,
name: str,
):
"""异步按站点名称查询,兼容显式会话和旧插件无会话调用"""
if name is None and isinstance(db, str):
name, db = db, None
if name is None:
raise TypeError("name is required")
async def query(session: AsyncSession):
"""在给定异步会话中执行名称查询。"""
result = await session.execute(select(cls).where(cls.name == name))
return result.scalar_one_or_none()
return await query(db)
"""在调用方 AsyncSession 中按站点名称查询"""
result = await db.execute(select(cls).where(cls.name == name))
return result.scalar_one_or_none()
@classmethod
@legacy_db_query
def get_actives(cls, db: Session | None = None):
"""查询启用站点,兼容显式会话和旧插件无会话调用。"""
def query(session: Session):
"""在给定同步会话中执行启用站点查询。"""
return list(session.execute(select(cls).where(cls.is_active.is_(True))).scalars().all())
return query(db)
def get_actives(cls, db: Session):
"""在调用方 Session 中查询启用站点。"""
return list(db.execute(
select(cls).where(cls.is_active.is_(True))
).scalars().all())
@classmethod
@legacy_async_db_query
async def async_get_actives(cls, db: AsyncSession | None = None):
"""异步查询启用站点,兼容显式会话和旧插件无会话调用。"""
async def query(session: AsyncSession):
"""在给定异步会话中执行启用站点查询。"""
result = await session.execute(select(cls).where(cls.is_active.is_(True)))
return list(result.scalars().all())
return await query(db)
async def async_get_actives(cls, db: AsyncSession):
"""在调用方 AsyncSession 中查询启用站点。"""
result = await db.execute(select(cls).where(cls.is_active.is_(True)))
return list(result.scalars().all())
@classmethod
@legacy_db_query
def list_order_by_pri(cls, db: Session | None = None):
"""按优先级升序查询站点,兼容显式会话和旧插件无会话调用。"""
def query(session: Session):
"""在给定同步会话中执行优先级查询。"""
return list(session.execute(select(cls).order_by(cls.pri)).scalars().all())
return query(db)
def list_order_by_pri(cls, db: Session):
"""在调用方 Session 中按优先级升序查询站点。"""
return list(db.execute(select(cls).order_by(cls.pri)).scalars().all())
@classmethod
@legacy_async_db_query
async def async_list_order_by_pri(cls, db: AsyncSession | None = None):
"""异步按优先级升序查询站点,兼容显式会话和旧插件无会话调用。"""
async def query(session: AsyncSession):
"""在给定异步会话中执行优先级查询。"""
result = await session.execute(select(cls).order_by(cls.pri))
return list(result.scalars().all())
return await query(db)
async def async_list_order_by_pri(cls, db: AsyncSession):
"""在调用方 AsyncSession 中按优先级升序查询站点。"""
result = await db.execute(select(cls).order_by(cls.pri))
return list(result.scalars().all())
@classmethod
@legacy_db_query
def get_domains_by_ids(
cls,
db: Session | list[int] | None = None,
ids: list[int] | None = None,
db: Session,
ids: list[int],
):
"""按 ID 查询域名,兼容显式会话和旧插件无会话调用"""
if ids is None and isinstance(db, list):
ids, db = db, None
if ids is None:
raise TypeError("ids is required")
"""在调用方 Session 中按 ID 查询域名。"""
if not ids:
return []
def query(session: Session):
"""在给定同步会话中执行域名投影查询。"""
return list(session.execute(select(cls.domain).where(cls.id.in_(ids))).scalars().all())
return query(db)
return list(db.execute(
select(cls.domain).where(cls.id.in_(ids))
).scalars().all())
@classmethod
def reset(cls, db: Session):
+4 -13
View File
@@ -4,7 +4,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import Base, get_id_column
from app.db.decorators import legacy_async_db_query
class SiteIcon(Base):
@@ -27,19 +26,11 @@ class SiteIcon(Base):
return db.execute(select(cls).where(cls.domain == domain)).scalars().first()
@classmethod
@legacy_async_db_query
async def async_get_by_domain(
cls,
db: AsyncSession | None = None,
domain: str | None = None,
db: AsyncSession,
domain: str,
):
"""在调用方 AsyncSession 中查询站点图标。"""
if domain is None:
raise TypeError("domain is required")
async def query(session: AsyncSession):
"""在给定异步会话中执行站点图标查询。"""
result = await session.execute(select(cls).where(cls.domain == domain))
return result.scalar_one_or_none()
return await query(db)
result = await db.execute(select(cls).where(cls.domain == domain))
return result.scalar_one_or_none()
+5 -14
View File
@@ -6,7 +6,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import get_id_column, Base
from app.db.decorators import legacy_async_db_query
class SiteStatistic(Base):
@@ -35,22 +34,14 @@ class SiteStatistic(Base):
return db.execute(select(cls).where(cls.domain == domain)).scalars().first()
@classmethod
@legacy_async_db_query
async def async_get_by_domain(
cls,
db: AsyncSession | None = None,
domain: str | None = None,
db: AsyncSession,
domain: str,
):
"""在调用方 AsyncSession 中查询站点统计,并兼容旧无会话调用"""
if domain is None:
raise TypeError("domain is required")
async def query(session: AsyncSession):
"""在给定异步会话中执行站点统计查询。"""
result = await session.execute(select(cls).where(cls.domain == domain))
return result.scalar_one_or_none()
return await query(db)
"""在调用方 AsyncSession 中查询站点统计。"""
result = await db.execute(select(cls).where(cls.domain == domain))
return result.scalar_one_or_none()
@classmethod
def reset(cls, db: Session):
-6
View File
@@ -6,7 +6,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import Base, execute_dml, get_id_column
from app.db.decorators import legacy_async_db_query, legacy_db_query
class SiteUserData(Base):
@@ -61,7 +60,6 @@ class SiteUserData(Base):
)
@classmethod
@legacy_db_query
def get_by_domain(cls, db: Session, domain: str, workdate: Optional[str] = None, worktime: Optional[str] = None):
statement = select(cls).where(cls.domain == domain)
if workdate and worktime:
@@ -72,7 +70,6 @@ class SiteUserData(Base):
return list(db.execute(statement).scalars().all())
@classmethod
@legacy_async_db_query
async def async_get_by_domain(cls, db: AsyncSession, domain: str, workdate: Optional[str] = None, worktime: Optional[str] = None):
query = select(cls).filter(cls.domain == domain)
if workdate and worktime:
@@ -83,12 +80,10 @@ class SiteUserData(Base):
return list(result.scalars().all())
@classmethod
@legacy_db_query
def get_by_date(cls, db: Session, date: str):
return list(db.execute(select(cls).where(cls.updated_day == date)).scalars().all())
@classmethod
@legacy_db_query
def get_latest(cls, db: Session):
"""
获取各站点最新一天的数据
@@ -113,7 +108,6 @@ class SiteUserData(Base):
).scalars().all())
@classmethod
@legacy_async_db_query
async def async_get_latest(cls, db: AsyncSession):
"""
异步获取各站点最新一天的数据
+107 -210
View File
@@ -6,7 +6,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import get_id_column, Base
from app.db.decorators import legacy_async_db_query, legacy_db_query
from app.db.models._constraints import media_identity_constraint
from app.schemas.types import MUSIC_ENTITY_RECORDING, MediaSource
@@ -140,9 +139,8 @@ class Subscribe(Base):
return condition
@classmethod
@legacy_db_query
def exists(
cls, db: Session | MediaSource | None = None,
cls, db: Session,
media_source: MediaSource | str | None = None,
media_id: str | None = None,
season: Optional[int] = None,
@@ -150,27 +148,21 @@ class Subscribe(Base):
music_type: Optional[str] = None,
):
"""按媒体身份、季号与剧集组查询已有订阅。"""
if db is not None and not isinstance(db, Session):
media_source, media_id, db = db, media_source, None
condition = cls._identity_condition(
media_source, media_id, music_type
)
if condition is None:
return None
def query(session: Session):
"""在给定会话中执行订阅身份查询。"""
statement = select(cls).where(condition)
if season is not None:
statement = statement.where(cls.season == season)
return session.execute(
statement.where(cls.episode_group == episode_group)
).scalars().first()
return query(db)
statement = select(cls).where(condition)
if season is not None:
statement = statement.where(cls.season == season)
return db.execute(
statement.where(cls.episode_group == episode_group)
).scalars().first()
@classmethod
@legacy_async_db_query
async def async_exists(
cls, db: AsyncSession | MediaSource | None = None,
cls, db: AsyncSession,
media_source: MediaSource | str | None = None,
media_id: str | None = None,
season: Optional[int] = None,
@@ -178,28 +170,22 @@ class Subscribe(Base):
music_type: Optional[str] = None,
):
"""异步按媒体身份、季号与剧集组查询已有订阅。"""
if db is not None and not isinstance(db, AsyncSession):
media_source, media_id, db = db, media_source, None
condition = cls._identity_condition(
media_source, media_id, music_type
)
if condition is None:
return None
async def query(session: AsyncSession):
"""在给定异步会话中执行订阅身份查询。"""
statement = select(cls).where(condition)
if season is not None:
statement = statement.where(cls.season == season)
result = await session.execute(
statement.where(cls.episode_group == episode_group)
)
return result.scalars().first()
return await query(db)
statement = select(cls).where(condition)
if season is not None:
statement = statement.where(cls.season == season)
result = await db.execute(
statement.where(cls.episode_group == episode_group)
)
return result.scalars().first()
@classmethod
@legacy_db_query
def exists_by_username(
cls, db: Session | str | None = None,
cls, db: Session,
username: str | MediaSource | None = None,
media_source: MediaSource | str | None = None,
media_id: str | None = None,
@@ -210,8 +196,6 @@ class Subscribe(Base):
"""
按订阅 owner、媒体身份、季号与剧集组查询订阅行。
"""
if db is not None and not isinstance(db, Session):
username, media_source, media_id, db = db, username, media_source, None
if not username:
return None
condition = cls._identity_condition(
@@ -219,20 +203,16 @@ class Subscribe(Base):
)
if condition is None:
return None
def query(session: Session):
"""在给定会话中执行订阅 owner 查询。"""
statement = select(cls).where(cls.username == username, condition)
if season is not None:
statement = statement.where(cls.season == season)
return session.execute(
statement.where(cls.episode_group == episode_group)
).scalars().first()
return query(db)
statement = select(cls).where(cls.username == username, condition)
if season is not None:
statement = statement.where(cls.season == season)
return db.execute(
statement.where(cls.episode_group == episode_group)
).scalars().first()
@classmethod
@legacy_async_db_query
async def async_exists_by_username(
cls, db: AsyncSession | str | None = None,
cls, db: AsyncSession,
username: str | MediaSource | None = None,
media_source: MediaSource | str | None = None,
media_id: str | None = None, season: Optional[int] = None,
@@ -242,8 +222,6 @@ class Subscribe(Base):
"""
异步按订阅 owner、媒体身份、季号与剧集组查询订阅行。
"""
if db is not None and not isinstance(db, AsyncSession):
username, media_source, media_id, db = db, username, media_source, None
if not username:
return None
condition = cls._identity_condition(
@@ -251,112 +229,76 @@ class Subscribe(Base):
)
if condition is None:
return None
async def query(session: AsyncSession):
"""在给定异步会话中执行订阅 owner 查询。"""
statement = select(cls).where(cls.username == username, condition)
if season is not None:
statement = statement.where(cls.season == season)
result = await session.execute(
statement.where(cls.episode_group == episode_group)
)
return result.scalars().first()
return await query(db)
statement = select(cls).where(cls.username == username, condition)
if season is not None:
statement = statement.where(cls.season == season)
result = await db.execute(
statement.where(cls.episode_group == episode_group)
)
return result.scalars().first()
@classmethod
@legacy_db_query
def get_by_state(cls, db: Session | str | None = None, state: str | None = None):
"""按状态列表查询订阅,兼容显式会话和旧插件无会话调用。"""
if not isinstance(db, Session):
state, db = db if state is None else state, None
def query(session: Session):
"""在给定会话中执行状态查询。"""
statement = select(cls)
if state:
statement = statement.where(cls.state.in_(state.split(',')))
return list(session.execute(statement).scalars().all())
return query(db)
def get_by_state(cls, db: Session, state: str | None = None):
"""在调用方 Session 中按状态列表查询订阅。"""
statement = select(cls)
if state:
statement = statement.where(cls.state.in_(state.split(',')))
return list(db.execute(statement).scalars().all())
@classmethod
@legacy_async_db_query
async def async_get_by_state(
cls, db: AsyncSession | str | None = None, state: str | None = None
cls, db: AsyncSession, state: str | None = None
):
"""异步按状态列表查询订阅,兼容显式会话和旧插件无会话调用"""
if not isinstance(db, AsyncSession):
state, db = db if state is None else state, None
async def query(session: AsyncSession):
"""在给定异步会话中执行状态查询。"""
statement = select(cls)
if state:
statement = statement.where(cls.state.in_(state.split(',')))
result = await session.execute(statement)
return list(result.scalars().all())
return await query(db)
"""在调用方 AsyncSession 中按状态列表查询订阅"""
statement = select(cls)
if state:
statement = statement.where(cls.state.in_(state.split(',')))
result = await db.execute(statement)
return list(result.scalars().all())
@classmethod
@legacy_db_query
def get_by_title(
cls, db: Session | str | None = None, title: str | None = None,
cls, db: Session, title: str,
season: Optional[int] = None,
):
"""按标题查询订阅,兼容显式会话和旧插件无会话调用"""
if not isinstance(db, Session):
title, db = db if title is None else title, None
def query(session: Session):
"""在给定会话中执行标题查询。"""
statement = select(cls).where(cls.name == title)
if season is not None:
statement = statement.where(cls.season == season)
return session.execute(statement).scalars().first()
return query(db)
"""在调用方 Session 中按标题查询订阅。"""
statement = select(cls).where(cls.name == title)
if season is not None:
statement = statement.where(cls.season == season)
return db.execute(statement).scalars().first()
@classmethod
@legacy_async_db_query
async def async_get_by_title(
cls, db: AsyncSession | str | None = None, title: str | None = None,
cls, db: AsyncSession, title: str,
season: Optional[int] = None,
):
"""异步按标题查询订阅,兼容显式会话和旧插件无会话调用"""
if not isinstance(db, AsyncSession):
title, db = db if title is None else title, None
async def query(session: AsyncSession):
"""在给定异步会话中执行标题查询。"""
statement = select(cls).where(cls.name == title)
if season is not None:
statement = statement.where(cls.season == season)
result = await session.execute(statement)
return result.scalars().first()
return await query(db)
"""在调用方 AsyncSession 中按标题查询订阅"""
statement = select(cls).where(cls.name == title)
if season is not None:
statement = statement.where(cls.season == season)
result = await db.execute(statement)
return result.scalars().first()
@classmethod
@legacy_async_db_query
async def async_list_by_title(
cls, db: AsyncSession | str | None = None, title: str | None = None,
cls, db: AsyncSession, title: str,
season: Optional[int] = None,
):
"""异步按标题查询候选订阅列表,兼容显式会话和旧插件无会话调用"""
if not isinstance(db, AsyncSession):
title, db = db if title is None else title, None
async def query(session: AsyncSession):
"""在给定异步会话中执行标题列表查询。"""
statement = select(cls).where(cls.name == title)
if season is not None:
statement = statement.where(cls.season == season)
result = await session.execute(statement)
return list(result.scalars().all())
return await query(db)
"""在调用方 AsyncSession 中按标题查询候选订阅列表"""
statement = select(cls).where(cls.name == title)
if season is not None:
statement = statement.where(cls.season == season)
result = await db.execute(statement)
return list(result.scalars().all())
@classmethod
@legacy_db_query
def list_by_media_identity(
cls, db: Session | MediaSource | None = None,
cls, db: Session,
media_source: MediaSource | str | None = None,
media_id: str | None = None,
music_type: Optional[str] = None,
):
"""同步按统一媒体身份查询候选订阅列表。"""
if db is not None and not isinstance(db, Session):
media_source, media_id, db = db, media_source, None
condition = cls._identity_condition(
media_source=media_source,
media_id=media_id,
@@ -364,22 +306,16 @@ class Subscribe(Base):
)
if condition is None:
return []
def query(session: Session):
"""在给定会话中执行媒体身份列表查询。"""
return list(session.execute(select(cls).where(condition)).scalars().all())
return query(db)
return list(db.execute(select(cls).where(condition)).scalars().all())
@classmethod
@legacy_async_db_query
async def async_list_by_media_identity(
cls, db: AsyncSession | MediaSource | None = None,
cls, db: AsyncSession,
media_source: MediaSource | str | None = None,
media_id: str | None = None,
music_type: Optional[str] = None,
):
"""异步按统一媒体身份查询候选订阅列表。"""
if db is not None and not isinstance(db, AsyncSession):
media_source, media_id, db = db, media_source, None
condition = cls._identity_condition(
media_source=media_source,
media_id=media_id,
@@ -387,16 +323,12 @@ class Subscribe(Base):
)
if condition is None:
return []
async def query(session: AsyncSession):
"""在给定异步会话中执行媒体身份列表查询。"""
result = await session.execute(select(cls).where(condition))
return list(result.scalars().all())
return await query(db)
result = await db.execute(select(cls).where(condition))
return list(result.scalars().all())
@classmethod
@legacy_db_query
def get_by(
cls, db: Session | str | None = None,
cls, db: Session,
type: str | MediaSource | None = None,
media_source: MediaSource | str | None = None,
media_id: str | None = None,
@@ -406,8 +338,6 @@ class Subscribe(Base):
"""
根据条件查询订阅
"""
if db is not None and not isinstance(db, Session):
type, media_source, media_id, db = db, type, media_source, None
condition = cls._identity_condition(
media_source, media_id, music_type
)
@@ -416,15 +346,11 @@ class Subscribe(Base):
statement = select(cls).where(condition, cls.type == type)
if season is not None:
statement = statement.where(cls.season == season)
def query(session: Session):
"""在给定会话中执行类型媒体查询。"""
return session.execute(statement).scalars().first()
return query(db)
return db.execute(statement).scalars().first()
@classmethod
@legacy_async_db_query
async def async_get_by(
cls, db: AsyncSession | str | None = None,
cls, db: AsyncSession,
type: str | MediaSource | None = None,
media_source: MediaSource | str | None = None,
media_id: str | None = None,
@@ -434,8 +360,6 @@ class Subscribe(Base):
"""
根据条件查询订阅
"""
if db is not None and not isinstance(db, AsyncSession):
type, media_source, media_id, db = db, type, media_source, None
condition = cls._identity_condition(
media_source, media_id, music_type
)
@@ -444,76 +368,49 @@ class Subscribe(Base):
query = select(cls).filter(condition, cls.type == type)
if season is not None:
query = query.filter(cls.season == season)
async def execute_query(session: AsyncSession):
"""在给定异步会话中执行类型媒体查询。"""
result = await session.execute(query)
return result.scalars().first()
return await execute_query(db)
result = await db.execute(query)
return result.scalars().first()
@classmethod
@legacy_db_query
def list_by_username(cls, db: Session | str | None = None, username: str | None = None,
def list_by_username(cls, db: Session, username: str,
state: Optional[str] = None, mtype: Optional[str] = None):
"""按用户筛选订阅,兼容显式会话和旧插件无会话调用"""
if not isinstance(db, Session):
username, db = db if username is None else username, None
def query(session: Session):
"""在给定会话中执行用户筛选查询。"""
statement = select(cls).where(cls.username == username)
if state:
statement = statement.where(cls.state == state)
if mtype:
statement = statement.where(cls.type == mtype)
return list(session.execute(statement).scalars().all())
return query(db)
"""在调用方 Session 中按用户筛选订阅。"""
statement = select(cls).where(cls.username == username)
if state:
statement = statement.where(cls.state == state)
if mtype:
statement = statement.where(cls.type == mtype)
return list(db.execute(statement).scalars().all())
@classmethod
@legacy_async_db_query
async def async_list_by_username(cls, db: AsyncSession | str | None = None,
username: str | None = None, state: Optional[str] = None,
async def async_list_by_username(cls, db: AsyncSession,
username: str, state: Optional[str] = None,
mtype: Optional[str] = None):
"""异步按用户筛选订阅,兼容显式会话和旧插件无会话调用"""
if not isinstance(db, AsyncSession):
username, db = db if username is None else username, None
async def query(session: AsyncSession):
"""在给定异步会话中执行用户筛选查询。"""
statement = select(cls).where(cls.username == username)
if state:
statement = statement.where(cls.state == state)
if mtype:
statement = statement.where(cls.type == mtype)
result = await session.execute(statement)
return list(result.scalars().all())
return await query(db)
"""在调用方 AsyncSession 中按用户筛选订阅"""
statement = select(cls).where(cls.username == username)
if state:
statement = statement.where(cls.state == state)
if mtype:
statement = statement.where(cls.type == mtype)
result = await db.execute(statement)
return list(result.scalars().all())
@classmethod
@legacy_db_query
def list_by_type(cls, db: Session | str | None = None, mtype: str | None = None, days: int = 7):
"""按类型查询最近时间窗内的订阅,兼容显式会话和旧插件无会话调用。"""
if not isinstance(db, Session):
mtype, db = db if mtype is None else mtype, None
def query(session: Session):
"""在给定会话中执行时间窗订阅查询。"""
return list(session.execute(select(cls).where(
cls.type == mtype,
cls.date >= time.strftime("%Y-%m-%d %H:%M:%S",
time.localtime(time.time() - 86400 * int(days)))
)).scalars().all())
return query(db)
def list_by_type(cls, db: Session, mtype: str, days: int = 7):
"""在调用方 Session 中按类型查询最近时间窗内的订阅。"""
return list(db.execute(select(cls).where(
cls.type == mtype,
cls.date >= time.strftime("%Y-%m-%d %H:%M:%S",
time.localtime(time.time() - 86400 * int(days)))
)).scalars().all())
@classmethod
@legacy_async_db_query
async def async_list_by_type(cls, db: AsyncSession | str | None = None,
mtype: str | None = None, days: int = 7):
"""异步按类型查询最近时间窗内的订阅,兼容显式会话和旧插件无会话调用。"""
if not isinstance(db, AsyncSession):
mtype, db = db if mtype is None else mtype, None
async def query(session: AsyncSession):
"""在给定异步会话中执行时间窗订阅查询。"""
result = await session.execute(select(cls).where(
cls.type == mtype,
cls.date >= time.strftime("%Y-%m-%d %H:%M:%S",
time.localtime(time.time() - 86400 * int(days)))
))
return list(result.scalars().all())
return await query(db)
async def async_list_by_type(cls, db: AsyncSession,
mtype: str, days: int = 7):
"""在调用方 AsyncSession 中按类型查询最近时间窗内的订阅。"""
result = await db.execute(select(cls).where(
cls.type == mtype,
cls.date >= time.strftime("%Y-%m-%d %H:%M:%S",
time.localtime(time.time() - 86400 * int(days)))
))
return list(result.scalars().all())
+2 -8
View File
@@ -5,7 +5,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import Base, get_id_column
from app.db.decorators import legacy_async_db_query, legacy_db_query
from app.db.models._constraints import media_identity_constraint
from app.schemas.types import MUSIC_ENTITY_RECORDING, MediaSource
@@ -107,9 +106,8 @@ class SubscribeHistory(Base):
)
@classmethod
@legacy_db_query
def list_by_type(cls, db: Session, mtype: str, page: int = 1, count: int = 30):
"""按媒体类型分页查询订阅历史,并保留旧插件无 Session 调用"""
"""在调用方 Session 中按媒体类型分页查询订阅历史。"""
return list(db.execute(
select(cls).where(
cls.type == mtype
@@ -119,9 +117,8 @@ class SubscribeHistory(Base):
).scalars().all())
@classmethod
@legacy_async_db_query
async def async_list_by_type(cls, db: AsyncSession, mtype: str, page: int = 1, count: int = 30):
"""异步按媒体类型分页查询订阅历史,并保留旧插件无 Session 调用"""
"""在调用方 AsyncSession 中按媒体类型分页查询订阅历史"""
result = await db.execute(
select(cls).filter(
cls.type == mtype
@@ -132,7 +129,6 @@ class SubscribeHistory(Base):
return list(result.scalars().all())
@classmethod
@legacy_async_db_query
async def async_list_by_type_and_username(
cls,
db: AsyncSession,
@@ -177,7 +173,6 @@ class SubscribeHistory(Base):
return condition
@classmethod
@legacy_db_query
def exists(
cls, db: Session, media_source: MediaSource, media_id: str,
season: Optional[int] = None,
@@ -197,7 +192,6 @@ class SubscribeHistory(Base):
return db.execute(statement).scalars().first()
@classmethod
@legacy_async_db_query
async def async_exists(
cls, db: AsyncSession, media_source: MediaSource, media_id: str,
season: Optional[int] = None,
+2 -5
View File
@@ -4,7 +4,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import Base, get_id_column
from app.db.decorators import legacy_async_db_query, legacy_db_query
class SystemConfig(Base):
@@ -18,15 +17,13 @@ class SystemConfig(Base):
value: Mapped[Optional[Any]] = mapped_column(JSON)
@classmethod
@legacy_db_query
def get_by_key(cls, db: Session, key: str):
"""按配置键查询系统配置,并保留旧插件无 Session 调用"""
"""在调用方 Session 中按配置键查询系统配置。"""
return db.execute(select(cls).where(cls.key == key)).scalars().first()
@classmethod
@legacy_async_db_query
async def async_get_by_key(cls, db: AsyncSession, key: str):
"""异步按配置键查询系统配置,并保留旧插件无 Session 调用"""
"""在调用方 AsyncSession 中按配置键查询系统配置"""
result = await db.execute(select(cls).where(cls.key == key))
return result.scalar_one_or_none()
+21 -88
View File
@@ -8,10 +8,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import Base, execute_dml, get_id_column
from app.db.decorators import (
legacy_async_db_query,
legacy_db_query,
)
from app.db.models._constraints import media_identity_constraint
from app.schemas.types import MUSIC_ENTITY_ALBUM, MUSIC_ENTITY_RECORDING, MediaSource, MediaType
@@ -97,7 +93,6 @@ class TransferHistory(Base):
)
@classmethod
@legacy_db_query
def list_by_title(cls, db: Session, title: str, page: int = 1, count: int = 30,
status: Optional[bool] = None, wildcard: bool = False):
if wildcard:
@@ -124,7 +119,6 @@ class TransferHistory(Base):
return list(db.execute(statement).scalars().all())
@classmethod
@legacy_async_db_query
async def async_list_by_title(cls, db: AsyncSession, title: str, page: int = 1, count: int = 30,
status: Optional[bool] = None, wildcard: bool = False):
if wildcard:
@@ -152,7 +146,6 @@ class TransferHistory(Base):
return list(result.scalars().all())
@classmethod
@legacy_db_query
def list_by_page(cls, db: Session, page: int = 1, count: int = 30, status: Optional[bool] = None):
statement = select(cls)
if status is not None:
@@ -166,7 +159,6 @@ class TransferHistory(Base):
return list(db.execute(statement).scalars().all())
@classmethod
@legacy_async_db_query
async def async_list_by_page(cls, db: AsyncSession, page: int = 1, count: int = 30,
status: Optional[bool] = None):
if status is not None:
@@ -188,30 +180,19 @@ class TransferHistory(Base):
return list(result.scalars().all())
@classmethod
@legacy_db_query
def get_by_hash(
cls,
db: Session | str | None = None,
download_hash: str | None = None,
db: Session,
download_hash: str,
):
"""按下载哈希查询最新记录,兼容旧插件无会话调用"""
if download_hash is None and isinstance(db, str):
download_hash, db = db, None
if download_hash is None:
raise TypeError("download_hash is required")
def query(session: Session):
"""在调用方提供的同步会话中执行哈希查询。"""
return session.execute(
select(cls).where(cls.download_hash == download_hash)
).scalars().first()
return query(db)
"""在调用方 Session 中按下载哈希查询最新记录。"""
return db.execute(
select(cls).where(cls.download_hash == download_hash)
).scalars().first()
@classmethod
@legacy_db_query
def get_by_src(
cls, db: Session | str | None = None, src: str | None = None,
cls, db: Session, src: str,
storage: Optional[str] = None
) -> Optional["TransferHistory"]:
"""
@@ -222,26 +203,14 @@ class TransferHistory(Base):
:param storage: 源存储类型
:return: 命中的整理记录,未命中时返回 None
"""
if src is None and isinstance(db, str):
src, db = db, None
if src is None:
raise TypeError("src is required")
def query(session: Session):
"""在调用方提供的同步会话中执行源路径查询。"""
statement = select(cls).where(cls.src == src)
if storage:
statement = statement.where(cls.src_storage == storage)
return session.execute(
statement.order_by(cls.id.desc())
).scalars().first()
return query(db)
statement = select(cls).where(cls.src == src)
if storage:
statement = statement.where(cls.src_storage == storage)
return db.execute(statement.order_by(cls.id.desc())).scalars().first()
@classmethod
@legacy_db_query
def get_success_by_src(
cls, db: Session | str | None = None, src: str | None = None,
cls, db: Session, src: str,
storage: Optional[str] = None
) -> Optional["TransferHistory"]:
"""
@@ -254,26 +223,14 @@ class TransferHistory(Base):
:param storage: 源存储类型
:return: 命中的成功整理记录,未命中时返回 None
"""
if src is None and isinstance(db, str):
src, db = db, None
if src is None:
raise TypeError("src is required")
def query(session: Session):
"""在调用方提供的同步会话中执行成功源路径查询。"""
statement = select(cls).where(cls.src == src, cls.status.is_(True))
if storage:
statement = statement.where(cls.src_storage == storage)
return session.execute(
statement.order_by(cls.id.desc())
).scalars().first()
return query(db)
statement = select(cls).where(cls.src == src, cls.status.is_(True))
if storage:
statement = statement.where(cls.src_storage == storage)
return db.execute(statement.order_by(cls.id.desc())).scalars().first()
@classmethod
@legacy_db_query
def get_by_dest(
cls, db: Session | str | None = None, dest: str | None = None,
cls, db: Session, dest: str,
storage: Optional[str] = None
) -> Optional["TransferHistory"]:
"""
@@ -284,24 +241,12 @@ class TransferHistory(Base):
:param storage: 目标存储类型
:return: 命中的整理记录,未命中时返回 None
"""
if dest is None and isinstance(db, str):
dest, db = db, None
if dest is None:
raise TypeError("dest is required")
def query(session: Session):
"""在调用方提供的同步会话中执行目标路径查询。"""
statement = select(cls).where(cls.dest == dest)
if storage:
statement = statement.where(cls.dest_storage == storage)
return session.execute(
statement.order_by(cls.id.desc())
).scalars().first()
return query(db)
statement = select(cls).where(cls.dest == dest)
if storage:
statement = statement.where(cls.dest_storage == storage)
return db.execute(statement.order_by(cls.id.desc())).scalars().first()
@classmethod
@legacy_db_query
def list_success_by_src(
cls,
db: Session,
@@ -341,7 +286,6 @@ class TransferHistory(Base):
return list(db.execute(statement).scalars().all())
@classmethod
@legacy_db_query
def list_success_move_by_dest(
cls,
db: Session,
@@ -384,14 +328,12 @@ class TransferHistory(Base):
return list(db.execute(statement).scalars().all())
@classmethod
@legacy_db_query
def list_by_hash(cls, db: Session, download_hash: str):
return list(db.execute(
select(cls).where(cls.download_hash == download_hash)
).scalars().all())
@classmethod
@legacy_db_query
def statistic(cls, db: Session, days: int = 7):
"""
统计最近days天的下载历史数量,按日期分组返回每日数量
@@ -408,7 +350,6 @@ class TransferHistory(Base):
).all())
@classmethod
@legacy_db_query
def monthly_media_statistics(cls, db: Session):
"""
统计当月成功整理的电影、电视剧、剧集和音乐数量。
@@ -474,7 +415,6 @@ class TransferHistory(Base):
return 1
@classmethod
@legacy_async_db_query
async def async_statistic(cls, db: AsyncSession, days: int = 7):
"""
统计最近days天的下载历史数量,按日期分组返回每日数量
@@ -489,7 +429,6 @@ class TransferHistory(Base):
return result.all()
@classmethod
@legacy_db_query
def count(cls, db: Session, status: Optional[bool] = None):
statement = select(func.count(cls.id))
if status is not None:
@@ -497,7 +436,6 @@ class TransferHistory(Base):
return db.execute(statement).scalar()
@classmethod
@legacy_async_db_query
async def async_count(cls, db: AsyncSession, status: Optional[bool] = None):
if status is not None:
result = await db.execute(
@@ -510,7 +448,6 @@ class TransferHistory(Base):
return result.scalar()
@classmethod
@legacy_db_query
def count_by_title(cls, db: Session, title: str, status: Optional[bool] = None, wildcard: bool = False):
if wildcard:
text_filter = or_(
@@ -530,7 +467,6 @@ class TransferHistory(Base):
return db.execute(statement).scalar()
@classmethod
@legacy_async_db_query
async def async_count_by_title(cls, db: AsyncSession, title: str, status: Optional[bool] = None, wildcard: bool = False):
if wildcard:
text_filter = or_(
@@ -551,7 +487,6 @@ class TransferHistory(Base):
return result.scalar()
@classmethod
@legacy_db_query
def list_by(cls, db: Session, mtype: Optional[str] = None, title: Optional[str] = None, year: Optional[str] = None,
season: Optional[str] = None,
episode: Optional[str] = None,
@@ -589,7 +524,6 @@ class TransferHistory(Base):
return list(db.execute(statement).scalars().all())
@classmethod
@legacy_db_query
def get_by_media_identity(
cls, db: Session, media_source: MediaSource, media_id: str,
mtype: Optional[str] = None,
@@ -636,7 +570,6 @@ class TransferHistory(Base):
return history
@classmethod
@legacy_db_query
def list_by_date(cls, db: Session, date: str):
"""
查询某时间之后的转移历史
-2
View File
@@ -4,7 +4,6 @@ from sqlalchemy import Index, String, delete, select
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import Base, execute_dml, get_id_column
from app.db.decorators import legacy_db_query
class TransferPending(Base):
@@ -73,7 +72,6 @@ class TransferPending(Base):
)
@classmethod
@legacy_db_query
def list_all(cls, db: Session, limit: Optional[int] = 5000) -> List["TransferPending"]:
"""
列出全部待整理登记,供启动回放使用。
+17 -58
View File
@@ -4,7 +4,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import Base, get_id_column
from app.db.decorators import legacy_async_db_query, legacy_db_query
class User(Base):
@@ -35,78 +34,38 @@ class User(Base):
settings: Mapped[Optional[Any]] = mapped_column(JSON, default=dict)
@classmethod
@legacy_db_query
def get_by_name(
cls,
db: Session | str | None = None,
name: str | None = None,
db: Session,
name: str,
):
"""按用户名查询用户,兼容显式会话和旧插件无会话调用"""
if name is None and isinstance(db, str):
name, db = db, None
if name is None:
raise TypeError("name is required")
def query(session: Session):
"""在给定会话中执行用户名查询。"""
return session.execute(select(cls).where(cls.name == name)).scalars().first()
return query(db)
"""在调用方同步会话中按用户名查询用户。"""
return db.execute(select(cls).where(cls.name == name)).scalars().first()
@classmethod
@legacy_async_db_query
async def async_get_by_name(
cls,
db: AsyncSession | str | None = None,
name: str | None = None,
db: AsyncSession,
name: str,
):
"""异步按用户名查询,兼容显式会话和旧插件无会话调用"""
if name is None and isinstance(db, str):
name, db = db, None
if name is None:
raise TypeError("name is required")
async def query(session: AsyncSession):
"""在给定异步会话中执行用户名查询。"""
result = await session.execute(select(cls).filter(cls.name == name))
return result.scalars().first()
return await query(db)
"""在调用方异步会话中按用户名查询用户"""
result = await db.execute(select(cls).filter(cls.name == name))
return result.scalars().first()
@classmethod
@legacy_db_query
def get_by_id(cls, db: Session | int | None = None, user_id: int | None = None):
"""按用户 ID 查询用户,兼容显式会话和旧插件无会话调用。"""
if user_id is None and isinstance(db, int):
user_id, db = db, None
if user_id is None:
raise TypeError("user_id is required")
def query(session: Session):
"""在给定会话中执行用户 ID 查询。"""
return session.execute(select(cls).where(cls.id == user_id)).scalars().first()
return query(db)
def get_by_id(cls, db: Session, user_id: int):
"""在调用方同步会话中按用户 ID 查询用户。"""
return db.execute(select(cls).where(cls.id == user_id)).scalars().first()
@classmethod
@legacy_async_db_query
async def async_get_by_id(
cls,
db: AsyncSession | int | None = None,
user_id: int | None = None,
db: AsyncSession,
user_id: int,
):
"""异步按用户 ID 查询,兼容显式会话和旧插件无会话调用"""
if user_id is None and isinstance(db, int):
user_id, db = db, None
if user_id is None:
raise TypeError("user_id is required")
async def query(session: AsyncSession):
"""在给定异步会话中执行用户 ID 查询。"""
result = await session.execute(select(cls).filter(cls.id == user_id))
return result.scalars().first()
return await query(db)
"""在调用方异步会话中按用户 ID 查询用户"""
result = await db.execute(select(cls).filter(cls.id == user_id))
return result.scalars().first()
def delete_by_name(self, db: Session, name: str):
user = self.get_by_name(db, name)
-9
View File
@@ -7,7 +7,6 @@ from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.base import Base, get_id_column
from app.db.decorators import legacy_async_db_query, legacy_db_query
class Workflow(Base):
@@ -56,18 +55,15 @@ class Workflow(Base):
)
@classmethod
@legacy_db_query
def get_enabled_workflows(cls, db):
return list(db.execute(select(cls).where(cls.state != 'P')).scalars().all())
@classmethod
@legacy_async_db_query
async def async_get_enabled_workflows(cls, db: AsyncSession):
result = await db.execute(select(cls).where(cls.state != 'P'))
return list(result.scalars().all())
@classmethod
@legacy_db_query
def get_timer_triggered_workflows(cls, db):
"""获取定时触发的工作流"""
return list(db.execute(select(cls).where(
@@ -81,7 +77,6 @@ class Workflow(Base):
)).scalars().all())
@classmethod
@legacy_async_db_query
async def async_get_timer_triggered_workflows(cls, db: AsyncSession):
"""异步获取定时触发的工作流"""
result = await db.execute(select(cls).where(
@@ -96,7 +91,6 @@ class Workflow(Base):
return list(result.scalars().all())
@classmethod
@legacy_db_query
def get_event_triggered_workflows(cls, db):
"""获取事件触发的工作流"""
return list(db.execute(select(cls).where(
@@ -107,7 +101,6 @@ class Workflow(Base):
)).scalars().all())
@classmethod
@legacy_async_db_query
async def async_get_event_triggered_workflows(cls, db: AsyncSession):
"""异步获取事件触发的工作流"""
result = await db.execute(select(cls).where(
@@ -119,12 +112,10 @@ class Workflow(Base):
return list(result.scalars().all())
@classmethod
@legacy_db_query
def get_by_name(cls, db, name: str):
return db.execute(select(cls).where(cls.name == name)).scalars().first()
@classmethod
@legacy_async_db_query
async def async_get_by_name(cls, db: AsyncSession, name: str):
result = await db.execute(select(cls).where(cls.name == name))
return result.scalars().first()
+4
View File
@@ -321,6 +321,10 @@ class AgentChatOper(DbOper):
await self._stage_async_delete(AgentChat, chat.id)
return True
def delete_by_id(self, chat_id: int) -> None:
"""在 Oper 事务边界内按主键删除 Agent 会话。"""
self._stage_delete(AgentChat, chat_id)
async def async_stage_delete(
self,
session_id: str,
+14 -10
View File
@@ -17,10 +17,12 @@ class DownloadFailureOper(DbOper):
"""
批量按指纹查询仍在冷却期的失败记录。
"""
failures = DownloadFailure.get_active_by_fingerprints(
self._db,
fingerprints=fingerprints,
now_time=now_time,
failures = self._execute_sync_query(
lambda session: DownloadFailure.get_active_by_fingerprints(
session,
fingerprints=fingerprints,
now_time=now_time,
)
)
return {
failure.fingerprint: failure
@@ -38,12 +40,14 @@ class DownloadFailureOper(DbOper):
"""
新增或更新资源失败记录。
"""
return DownloadFailure.record_failure(
self._db,
fingerprint=fingerprint,
now_time=now_time,
next_retry_at=next_retry_at,
**kwargs,
return self._execute_sync_write(
lambda session: DownloadFailure.record_failure(
session,
fingerprint=fingerprint,
now_time=now_time,
next_retry_at=next_retry_at,
**kwargs,
)
)
def delete_expired(
+1 -1
View File
@@ -289,7 +289,7 @@ class DownloadHistoryOper(DbOper):
self._stage_delete(DownloadHistory, historyid)
def stage_delete_history(self, historyid: int) -> None:
"""暂存下载记录删除,不由模型装饰器提交事务。"""
"""暂存下载记录删除,事务由调用方统一提交"""
self._db.execute(
sqlalchemy_delete(DownloadHistory).where(
DownloadHistory.id == historyid
+35 -11
View File
@@ -19,7 +19,11 @@ class PluginDataOper(DbOper):
:param key: 数据key
:param value: 数据值
"""
plugin = PluginData.get_plugin_data_by_key(self._db, plugin_id, key)
plugin = self._execute_sync_query(
lambda session: PluginData.get_plugin_data_by_key(
session, plugin_id, key
)
)
if plugin:
self._stage_update(plugin, {
"value": value
@@ -35,8 +39,10 @@ class PluginDataOper(DbOper):
:param key: 数据键
:param value: 数据值
"""
plugin = await PluginData.async_get_plugin_data_by_key(
self._db, plugin_id, key
plugin = await self._execute_async_query(
lambda session: PluginData.async_get_plugin_data_by_key(
session, plugin_id, key
)
)
if plugin:
await self._stage_async_update(plugin, {"value": value})
@@ -52,12 +58,18 @@ class PluginDataOper(DbOper):
:param key: 数据key
"""
if key:
data = PluginData.get_plugin_data_by_key(self._db, plugin_id, key)
data = self._execute_sync_query(
lambda session: PluginData.get_plugin_data_by_key(
session, plugin_id, key
)
)
if not data:
return None
return data.value
else:
return PluginData.get_plugin_data(self._db, plugin_id)
return self._execute_sync_query(
lambda session: PluginData.get_plugin_data(session, plugin_id)
)
async def async_get_data(self, plugin_id: str, key: Optional[str] = None) -> Any:
"""
@@ -66,13 +78,17 @@ class PluginDataOper(DbOper):
:param key: 数据key
"""
if key:
data = await PluginData.async_get_plugin_data_by_key(
self._db, plugin_id, key
data = await self._execute_async_query(
lambda session: PluginData.async_get_plugin_data_by_key(
session, plugin_id, key
)
)
if not data:
return None
return data.value
return await PluginData.async_get_plugin_data(self._db, plugin_id)
return await self._execute_async_query(
lambda session: PluginData.async_get_plugin_data(session, plugin_id)
)
def del_data(self, plugin_id: str, key: Optional[str] = None) -> Any:
"""
@@ -81,7 +97,7 @@ class PluginDataOper(DbOper):
:param key: 数据key
"""
def stage(session: Session) -> None:
"""兼容删除入口映射到调用方或组合根持有的事务。"""
"""把删除入口映射到调用方或组合根持有的事务。"""
if key:
PluginData.del_plugin_data_by_key(session, plugin_id, key)
else:
@@ -109,11 +125,19 @@ class PluginDataOper(DbOper):
获取插件所有数据
:param plugin_id: 插件id
"""
return PluginData.get_plugin_data_by_plugin_id(self._db, plugin_id)
return self._execute_sync_query(
lambda session: PluginData.get_plugin_data_by_plugin_id(
session, plugin_id
)
)
async def async_get_data_all(self, plugin_id: str) -> Any:
"""
异步获取插件所有数据。
:param plugin_id: 插件id
"""
return await PluginData.async_get_plugin_data_by_plugin_id(self._db, plugin_id)
return await self._execute_async_query(
lambda session: PluginData.async_get_plugin_data_by_plugin_id(
session, plugin_id
)
)
+50 -45
View File
@@ -75,7 +75,7 @@ class SiteOper(DbOper):
site_id: int,
payload: Mapping[str, Any],
) -> bool:
"""暂存站点字段更新,不由模型装饰器提前提交。"""
"""暂存站点字段更新,事务由调用方统一提交。"""
site = await self.async_get(site_id)
if not site:
return False
@@ -338,18 +338,22 @@ class SiteOper(DbOper):
async def async_get_icon_by_domain(self, domain: str) -> Optional[SiteIcon]:
"""异步按域名获取站点图标。"""
return await SiteIcon.async_get_by_domain(self._db, domain)
return await self._execute_async_query(
lambda session: SiteIcon.async_get_by_domain(session, domain)
)
async def async_get_statistic_by_domain(
self,
domain: str,
) -> Optional[SiteStatistic]:
"""异步按域名获取站点统计。"""
return await SiteStatistic.async_get_by_domain(self._db, domain)
return await self._execute_async_query(
lambda session: SiteStatistic.async_get_by_domain(session, domain)
)
async def async_list_statistics(self) -> List[SiteStatistic]:
"""异步获取所有站点统计。"""
return await SiteStatistic.async_list(self._db)
return await self._execute_async_query(SiteStatistic.async_list)
def get_userdata_by_date(self, date: str) -> List[SiteUserData]:
"""
@@ -371,7 +375,9 @@ class SiteOper(DbOper):
"""
按域名获取站点图标
"""
return SiteIcon.get_by_domain(self._db, domain)
return self._execute_sync_query(
lambda session: SiteIcon.get_by_domain(session, domain)
)
def update_icon(self, name: str, domain: str, icon_url: str, icon_base64: str) -> bool:
"""
@@ -467,60 +473,59 @@ class SiteOper(DbOper):
"""
异步站点访问成功
"""
lst_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
sta = await SiteStatistic.async_get_by_domain(self._db, domain)
if sta:
# 使用深复制确保 note 是全新的字典对象
note = dict(sta.note) if sta.note else {}
avg_seconds = None
if seconds is not None:
note[lst_date] = seconds or 1
avg_times = len(note.keys())
if avg_times > 10:
note = dict(sorted(note.items(), key=lambda x: x[0], reverse=True)[:10])
avg_seconds = sum([v for v in note.values()]) // avg_times
await self._stage_async_update(sta, {
"success": sta.success + 1,
"seconds": avg_seconds or sta.seconds,
"lst_state": 0,
"lst_mod_date": lst_date,
"note": note
})
else:
note = {}
if seconds is not None:
note = {
lst_date: seconds or 1
}
await self._stage_async_create(SiteStatistic(
async def write(session: AsyncSession) -> None:
"""在同一异步事务中读取并更新站点成功统计。"""
lst_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
sta = await SiteStatistic.async_get_by_domain(session, domain)
if sta:
note = dict(sta.note) if sta.note else {}
avg_seconds = None
if seconds is not None:
note[lst_date] = seconds or 1
avg_times = len(note.keys())
if avg_times > 10:
note = dict(sorted(
note.items(), key=lambda item: item[0], reverse=True
)[:10])
avg_seconds = sum(note.values()) // avg_times
sta.success += 1
sta.seconds = avg_seconds or sta.seconds
sta.lst_state = 0
sta.lst_mod_date = lst_date
sta.note = note
return
note = {lst_date: seconds or 1} if seconds is not None else {}
session.add(SiteStatistic(
domain=domain,
success=1,
fail=0,
seconds=seconds or 1,
lst_state=0,
lst_mod_date=lst_date,
note=note
note=note,
))
await self._execute_async_write(write)
async def async_fail(self, domain: str):
"""
异步站点访问失败
"""
lst_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
sta = await SiteStatistic.async_get_by_domain(self._db, domain)
if sta:
await self._stage_async_update(sta, {
"fail": sta.fail + 1,
"lst_state": 1,
"lst_mod_date": lst_date
})
else:
await self._stage_async_create(SiteStatistic(
async def write(session: AsyncSession) -> None:
"""在同一异步事务中读取并更新站点失败统计。"""
lst_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
sta = await SiteStatistic.async_get_by_domain(session, domain)
if sta:
sta.fail += 1
sta.lst_state = 1
sta.lst_mod_date = lst_date
return
session.add(SiteStatistic(
domain=domain,
success=0,
fail=1,
lst_state=1,
lst_mod_date=lst_date
lst_mod_date=lst_date,
))
await self._execute_async_write(write)
+90 -163
View File
@@ -11,7 +11,7 @@ import time
from collections.abc import Awaitable, Callable
from typing import Any, Tuple, List, Optional
from sqlalchemy import delete as sqlalchemy_delete, select
from sqlalchemy import delete as sqlalchemy_delete
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
@@ -100,25 +100,6 @@ class SubscribeOper(DbOper):
订阅管理
"""
@staticmethod
def _identity_statement(identity: dict, username: Optional[str] = None):
"""构造订阅查重语句,SQL 所有权收口在 Oper。"""
condition = Subscribe._identity_condition( # pylint: disable=protected-access
identity.get("media_source"),
identity.get("media_id"),
identity.get("music_type"),
)
if condition is None or username == "":
return None
statement = select(Subscribe).where(condition)
if username:
statement = statement.where(Subscribe.username == username)
if identity.get("season") is not None:
statement = statement.where(Subscribe.season == identity["season"])
return statement.where(
Subscribe.episode_group == identity.get("episode_group")
)
def _exists(self, identity: dict, username: Optional[str]) -> Optional[Any]:
"""
按身份查重。
@@ -126,19 +107,19 @@ class SubscribeOper(DbOper):
:param username: 非空时只在该用户的订阅内查
:return: 命中的订阅行,未命中为 None
"""
if isinstance(self._db, Session):
statement = self._identity_statement(identity, username)
if statement is None:
return None
return self._db.execute(statement).scalars().first()
# 旧 SDK 允许无会话构造 Oper;保留其自动短会话行为,但规范入口不得走这里。
if username == "":
return None
if username:
return Subscribe.exists_by_username(
self._db,
username=username,
**identity,
return self._execute_sync_query(
lambda session: Subscribe.exists_by_username(
session,
username=username,
**identity,
)
)
return Subscribe.exists(self._db, **identity)
return self._execute_sync_query(
lambda session: Subscribe.exists(session, **identity)
)
async def _async_exists(self, identity: dict, username: Optional[str]) -> Optional[Any]:
"""
@@ -147,20 +128,18 @@ class SubscribeOper(DbOper):
:param username: 非空时只在该用户的订阅内查
:return: 命中的订阅行,未命中为 None
"""
if isinstance(self._db, AsyncSession):
statement = self._identity_statement(identity, username)
if statement is None:
async def query(session: AsyncSession) -> Optional[Subscribe]:
"""在调用方或组合根异步会话中执行订阅查重。"""
if username == "":
return None
result = await self._db.execute(statement)
return result.scalars().first()
# 同步路径一样只为无会话旧入口保留 Model 的自动短会话兼容。
if username:
return await Subscribe.async_exists_by_username(
self._db,
username=username,
**identity,
)
return await Subscribe.async_exists(self._db, **identity)
if username:
return await Subscribe.async_exists_by_username(
session,
username=username,
**identity,
)
return await Subscribe.async_exists(session, **identity)
return await self._execute_async_query(query)
def stage_add(
self,
@@ -297,24 +276,15 @@ class SubscribeOper(DbOper):
"""
获取订阅
"""
return self._execute_sync_query(
lambda session: session.execute(
select(Subscribe).where(Subscribe.id == sid)
).scalars().first()
)
return self._execute_sync_query(lambda session: Subscribe.get(session, sid))
async def async_get(self, sid: int) -> Optional[Subscribe]:
"""
获取订阅
"""
if self._db is not None and not isinstance(self._db, (Session, AsyncSession)):
# 保留旧测试替身与插件注入对象对 Model ABI 的兼容入口。
return await Subscribe.async_get(self._db, rid=sid)
async def query(session: AsyncSession) -> Optional[Subscribe]:
"""在调用方异步会话中执行订阅主键查询。"""
result = await session.execute(select(Subscribe).where(Subscribe.id == sid))
return result.scalars().first()
return await self._execute_async_query(query)
return await self._execute_async_query(
lambda session: Subscribe.async_get(session, sid)
)
async def async_list_by_media_identity(
self,
@@ -323,18 +293,14 @@ class SubscribeOper(DbOper):
music_type: Optional[str] = None,
) -> List[Subscribe]:
"""异步按规范媒体身份读取订阅。"""
async def query(session: AsyncSession) -> List[Subscribe]:
"""在调用方异步会话中执行媒体身份列表查询。"""
condition = Subscribe._identity_condition( # pylint: disable=protected-access
media_source, media_id, music_type
return await self._execute_async_query(
lambda session: Subscribe.async_list_by_media_identity(
session,
media_source=media_source,
media_id=media_id,
music_type=music_type,
)
if condition is None:
return []
result = await session.execute(select(Subscribe).where(condition))
return list(result.scalars().all())
if isinstance(self._db, AsyncSession):
return await query(self._db)
return await self._execute_async_query(query)
)
def list_by_media_identity(
self,
@@ -343,15 +309,14 @@ class SubscribeOper(DbOper):
music_type: Optional[str] = None,
) -> List[Subscribe]:
"""同步按规范媒体身份读取订阅。"""
def query(session: Session) -> List[Subscribe]:
"""在调用方同步会话中执行媒体身份列表查询。"""
condition = Subscribe._identity_condition( # pylint: disable=protected-access
media_source, media_id, music_type
return self._execute_sync_query(
lambda session: Subscribe.list_by_media_identity(
session,
media_source=media_source,
media_id=media_id,
music_type=music_type,
)
if condition is None:
return []
return list(session.execute(select(Subscribe).where(condition)).scalars().all())
return self._execute_sync_query(query)
)
async def get_candidate(
self,
@@ -423,18 +388,16 @@ class SubscribeOper(DbOper):
"""
根据条件查询订阅
"""
def query(session: Session) -> Optional[Subscribe]:
"""在调用方同步会话中执行类型媒体查询。"""
condition = Subscribe._identity_condition( # pylint: disable=protected-access
media_source, media_id, music_type
return self._execute_sync_query(
lambda session: Subscribe.get_by(
session,
type=type,
media_source=media_source,
media_id=media_id,
season=season,
music_type=music_type,
)
if condition is None:
return None
statement = select(Subscribe).where(condition, Subscribe.type == type)
if season is not None:
statement = statement.where(Subscribe.season == season)
return session.execute(statement).scalars().first()
return self._execute_sync_query(query)
)
async def async_get_by(
self, type: str, media_source: MediaSource, media_id: str,
@@ -444,55 +407,34 @@ class SubscribeOper(DbOper):
"""
根据条件查询订阅
"""
async def query(session: AsyncSession) -> Optional[Subscribe]:
"""在调用方异步会话中执行类型媒体查询。"""
condition = Subscribe._identity_condition( # pylint: disable=protected-access
media_source, media_id, music_type
return await self._execute_async_query(
lambda session: Subscribe.async_get_by(
session,
type=type,
media_source=media_source,
media_id=media_id,
season=season,
music_type=music_type,
)
if condition is None:
return None
statement = select(Subscribe).where(condition, Subscribe.type == type)
if season is not None:
statement = statement.where(Subscribe.season == season)
result = await session.execute(statement)
return result.scalars().first()
return await self._execute_async_query(query)
)
def list(self, state: Optional[str] = None) -> List[Subscribe]:
"""
获取订阅列表
"""
if state:
return self._execute_sync_query(
lambda session: list(session.execute(
select(Subscribe).where(Subscribe.state.in_(state.split(',')))
).scalars().all())
)
return self._execute_sync_query(
lambda session: list(session.execute(select(Subscribe)).scalars().all())
lambda session: Subscribe.get_by_state(session, state)
)
async def async_list(self, state: Optional[str] = None) -> List[Subscribe]:
"""
异步获取订阅列表
"""
if self._db is not None and not isinstance(self._db, (Session, AsyncSession)):
if state:
return await Subscribe.async_get_by_state(self._db, state)
return await Subscribe.async_list(self._db)
if state:
async def query(session: AsyncSession) -> List[Subscribe]:
"""在调用方异步会话中执行状态列表查询。"""
result = await session.execute(
select(Subscribe).where(Subscribe.state.in_(state.split(',')))
)
return list(result.scalars().all())
return await self._execute_async_query(query)
async def query_all(session: AsyncSession) -> List[Subscribe]:
"""在调用方异步会话中执行全量订阅查询。"""
result = await session.execute(select(Subscribe))
return list(result.scalars().all())
return await self._execute_async_query(query_all)
return await self._execute_async_query(
lambda session: Subscribe.async_get_by_state(session, state)
)
return await self._execute_async_query(Subscribe.async_list)
async def async_list_by_username(
self,
@@ -501,35 +443,28 @@ class SubscribeOper(DbOper):
mtype: Optional[str] = None,
) -> List[Subscribe]:
"""异步按用户获取订阅。"""
if self._db is not None and not isinstance(self._db, (Session, AsyncSession)):
return await Subscribe.async_list_by_username(
self._db, username=username, state=state, mtype=mtype
return await self._execute_async_query(
lambda session: Subscribe.async_list_by_username(
session,
username=username,
state=state,
mtype=mtype,
)
async def query(session: AsyncSession) -> List[Subscribe]:
"""在调用方异步会话中执行用户筛选查询。"""
statement = select(Subscribe).where(Subscribe.username == username)
if state:
statement = statement.where(Subscribe.state == state)
if mtype:
statement = statement.where(Subscribe.type == mtype)
result = await session.execute(statement)
return list(result.scalars().all())
return await self._execute_async_query(query)
)
async def async_list_by_title(
self,
title: str,
season: Optional[int] = None,
) -> List[Subscribe]:
"""异步按标题获取订阅,供旧查询测试和迁移调用兼容"""
async def query(session: AsyncSession) -> List[Subscribe]:
"""在调用方异步会话中执行标题列表查询。"""
statement = select(Subscribe).where(Subscribe.name == title)
if season is not None:
statement = statement.where(Subscribe.season == season)
result = await session.execute(statement)
return list(result.scalars().all())
return await self._execute_async_query(query)
"""在 Oper 会话边界内异步按标题获取订阅。"""
return await self._execute_async_query(
lambda session: Subscribe.async_list_by_title(
session,
title=title,
season=season,
)
)
def delete(self, sid: int):
"""
@@ -598,30 +533,22 @@ class SubscribeOper(DbOper):
"""
获取指定用户的订阅
"""
def query(session: Session) -> List[Subscribe]:
"""在调用方同步会话中执行用户筛选查询。"""
statement = select(Subscribe).where(Subscribe.username == username)
if state:
statement = statement.where(Subscribe.state == state)
if mtype:
statement = statement.where(Subscribe.type == mtype)
return list(session.execute(statement).scalars().all())
return self._execute_sync_query(query)
return self._execute_sync_query(
lambda session: Subscribe.list_by_username(
session,
username=username,
state=state,
mtype=mtype,
)
)
def list_by_type(self, mtype: str, days: int = 7) -> List[Subscribe]:
"""
获取指定类型的订阅
"""
def query(session: Session) -> List[Subscribe]:
"""在调用方同步会话中执行时间窗订阅查询。"""
cutoff = time.strftime(
"%Y-%m-%d %H:%M:%S",
time.localtime(time.time() - 86400 * int(days)),
)
return list(session.execute(select(Subscribe).where(
Subscribe.type == mtype, Subscribe.date >= cutoff
)).scalars().all())
return self._execute_sync_query(query)
return self._execute_sync_query(
lambda session: Subscribe.list_by_type(session, mtype, days)
)
def add_history(self, **kwargs):
"""
+8 -3
View File
@@ -2,6 +2,8 @@ import copy
import threading
from typing import Any, Optional, Union
from sqlalchemy.orm import Session
from app.db.base import DbOper
from app.db.models.systemconfig import SystemConfig
from app.schemas.types import SystemConfigKey
@@ -20,12 +22,15 @@ class SystemConfigOper(DbOper, metaclass=Singleton):
self._write_lock = threading.RLock()
self._loaded = False
def load_snapshot(self) -> None:
"""数据库加载完整配置,并一次性发布新的内存快照。"""
def load_snapshot(self, db: Optional[Session] = None) -> None:
"""显式会话或 Oper 事务边界加载配置并发布内存快照。"""
with self._write_lock:
items = SystemConfig.list(db) if db is not None else self._execute_sync_query(
SystemConfig.list
)
snapshot = {
item.key: copy.deepcopy(item.value)
for item in SystemConfig.list(self._db)
for item in items
}
with self._snapshot_lock:
self.__SYSTEMCONF = snapshot
+1 -1
View File
@@ -269,7 +269,7 @@ class TransferHistoryOper(DbOper):
self._stage_delete(TransferHistory, historyid)
def stage_delete(self, historyid: int) -> None:
"""暂存整理记录删除,不由模型装饰器提交事务。"""
"""暂存整理记录删除,事务由调用方统一提交"""
self._db.execute(
sqlalchemy_delete(TransferHistory).where(
TransferHistory.id == historyid
+10 -6
View File
@@ -27,7 +27,7 @@ class UserOper(DbOper):
"""
获取用户列表
"""
return User.list(self._db)
return self._execute_sync_query(User.list)
def add(self, **kwargs):
"""
@@ -40,15 +40,19 @@ class UserOper(DbOper):
"""
根据用户名获取用户
"""
return User.get_by_name(self._db, name)
return self._execute_sync_query(
lambda session: User.get_by_name(session, name)
)
def get_by_id(self, user_id: int) -> Optional[User]:
"""按 ID 获取用户。"""
return User.get_by_id(self._db, user_id)
return self._execute_sync_query(
lambda session: User.get_by_id(session, user_id)
)
async def async_list(self) -> List[User]:
"""异步获取用户列表。"""
return await User.async_list(self._db)
return await self._execute_async_query(User.async_list)
async def async_create(self, payload: dict) -> Optional[User]:
"""异步创建用户。"""
@@ -126,7 +130,7 @@ class UserOper(DbOper):
"""
获取用户权限
"""
user = User.get_by_name(self._db, name)
user = self.get_by_name(name)
if user:
return user.permissions or {}
return {}
@@ -135,7 +139,7 @@ class UserOper(DbOper):
"""
获取用户个性化设置,返回None表示用户不存在
"""
user = User.get_by_name(self._db, name)
user = self.get_by_name(name)
if user:
return user.settings or {}
return None
+8 -3
View File
@@ -2,6 +2,8 @@ import copy
import threading
from typing import Any, Union, Dict, Optional
from sqlalchemy.orm import Session
from app.db.base import DbOper
from app.db.models.userconfig import UserConfig
from app.schemas.types import UserConfigKey
@@ -20,11 +22,14 @@ class UserConfigOper(DbOper, metaclass=Singleton):
self._write_lock = threading.RLock()
self._loaded = False
def load_snapshot(self) -> None:
"""数据库加载完整用户配置,并一次性发布新的内存快照。"""
def load_snapshot(self, db: Optional[Session] = None) -> None:
"""显式会话或 Oper 事务边界加载用户配置并发布内存快照。"""
with self._write_lock:
snapshot: dict[str, dict[str, Any]] = {}
for item in UserConfig.list(self._db):
items = UserConfig.list(db) if db is not None else self._execute_sync_query(
UserConfig.list
)
for item in items:
if item.username and item.key:
snapshot.setdefault(item.username, {})[item.key] = copy.deepcopy(
item.value
+11
View File
@@ -1,6 +1,7 @@
from typing import List, Mapping, Tuple, Optional, Any, Protocol
from sqlalchemy import delete as sqlalchemy_delete
from sqlalchemy.orm import Session
from app.db.base import DbOper
from app.db.models.workflow import Workflow
@@ -202,6 +203,8 @@ class WorkflowOper(DbOper):
def stage_start(self, wid: int) -> bool:
"""在调用方持有的会话中暂存运行中状态。"""
if not isinstance(self._db, Session):
raise RuntimeError("工作流暂存写入需要调用方提供同步 Session")
return Workflow.start(self._db, wid)
def success(self, wid: int, result: Optional[str] = None) -> bool:
@@ -214,6 +217,8 @@ class WorkflowOper(DbOper):
def stage_success(self, wid: int, result: Optional[str] = None) -> bool:
"""在调用方持有的会话中暂存成功状态。"""
if not isinstance(self._db, Session):
raise RuntimeError("工作流暂存写入需要调用方提供同步 Session")
return Workflow.success(self._db, wid, result)
def fail(self, wid: int, result: str) -> bool:
@@ -226,6 +231,8 @@ class WorkflowOper(DbOper):
def stage_fail(self, wid: int, result: str) -> bool:
"""在调用方持有的会话中暂存失败状态。"""
if not isinstance(self._db, Session):
raise RuntimeError("工作流暂存写入需要调用方提供同步 Session")
return Workflow.fail(self._db, wid, result)
def step(
@@ -255,6 +262,8 @@ class WorkflowOper(DbOper):
execution_state: Optional[dict[str, Any]] = None,
) -> bool:
"""在调用方持有的会话中暂存动作进度。"""
if not isinstance(self._db, Session):
raise RuntimeError("工作流暂存写入需要调用方提供同步 Session")
return Workflow.update_current_action(
self._db,
wid,
@@ -277,4 +286,6 @@ class WorkflowOper(DbOper):
reset_count: bool = False,
) -> bool:
"""在调用方持有的会话中暂存执行状态重置。"""
if not isinstance(self._db, Session):
raise RuntimeError("工作流暂存写入需要调用方提供同步 Session")
return Workflow.reset(self._db, wid, reset_count=reset_count)
+6 -6
View File
@@ -1,4 +1,4 @@
"""SQLAlchemy 请求级事务适配器与 Oper 事务执行端口。"""
"""SQLAlchemy 请求级事务适配器与无会话 Oper 事务执行端口。"""
from collections.abc import Awaitable, Callable
from typing import Protocol, TypeVar
@@ -11,7 +11,7 @@ T = TypeVar("T")
class SyncTransactionRunner(Protocol):
"""为无显式 Session 的兼容写入口提供独占同步事务。"""
"""为无显式 Session 的 Oper 入口提供独占同步事务。"""
def __call__(self, operation: Callable[[Session], T]) -> T:
"""在一个独占会话中执行并提交操作。"""
@@ -19,7 +19,7 @@ class SyncTransactionRunner(Protocol):
class AsyncTransactionRunner(Protocol):
"""为无显式 Session 的兼容写入口提供独占异步事务。"""
"""为无显式 Session 的 Oper 入口提供独占异步事务。"""
def __call__(
self,
@@ -38,14 +38,14 @@ def configure_transaction_runners(
sync: SyncTransactionRunner,
async_: AsyncTransactionRunner,
) -> None:
"""由组合根登记 Oper 兼容入口使用的显式事务执行器。"""
"""由组合根登记无会话 Oper 入口使用的显式事务执行器。"""
global _sync_transaction_runner, _async_transaction_runner
_sync_transaction_runner = sync
_async_transaction_runner = async_
def run_sync_transaction(operation: Callable[[Session], T]) -> T:
"""委托组合根在独占同步事务中执行兼容写操作。"""
"""委托组合根在独占同步事务中执行 Oper 操作。"""
if _sync_transaction_runner is None:
raise RuntimeError("同步事务执行器尚未配置")
return _sync_transaction_runner(operation)
@@ -54,7 +54,7 @@ def run_sync_transaction(operation: Callable[[Session], T]) -> T:
async def run_async_transaction(
operation: Callable[[AsyncSession], Awaitable[T]],
) -> T:
"""委托组合根在独占异步事务中执行兼容写操作。"""
"""委托组合根在独占异步事务中执行 Oper 操作。"""
if _async_transaction_runner is None:
raise RuntimeError("异步事务执行器尚未配置")
return await _async_transaction_runner(operation)