refactor: govern background tasks and query ownership

This commit is contained in:
jxxghp
2026-08-23 13:24:04 +08:00
parent 43c173a0e7
commit f1e542bef0
37 changed files with 1570 additions and 510 deletions
+15
View File
@@ -173,6 +173,12 @@ class DbOper:
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)
async def _execute_async_write(
self,
operation: Callable[[AsyncSession], Awaitable[T]],
@@ -184,6 +190,15 @@ class DbOper:
return await run_async_transaction(operation)
return await operation(self._db)
async def _execute_async_query(
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:
+73 -48
View File
@@ -5,7 +5,7 @@ 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 async_db_query, db_query
from app.db.decorators import run_legacy_async_query, run_legacy_sync_query
class Message(Base):
@@ -49,21 +49,32 @@ class Message(Base):
return self.to_dict()
@classmethod
@db_query
def list_by_page(cls, db: Session, page: int = 1, count: int = 30) -> List["Message"]:
def list_by_page(
cls,
db: Session | None = None,
page: int = 1,
count: int = 30,
) -> List["Message"]:
"""
分页获取消息记录。
分页获取消息记录,兼容显式会话和旧插件无会话调用
"""
return list(db.execute(
select(cls)
.order_by(cls.reg_time.desc(), cls.id.desc())
.offset((page - 1) * count)
.limit(count)
).scalars().all())
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) if isinstance(db, Session) else run_legacy_sync_query(query)
@classmethod
@db_query
def exists_by_source(cls, db: Session, source: str) -> bool:
def exists_by_source(
cls,
db: Session | str | None = None,
source: str | None = None,
) -> bool:
"""
判断指定来源标识的消息记录是否存在。
@@ -71,31 +82,42 @@ class Message(Base):
:param source: 消息来源唯一标识
:return: 是否存在匹配记录
"""
return db.execute(
select(cls.id).where(cls.source == source).limit(1)
).scalars().first() is not None
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) if isinstance(db, Session) else run_legacy_sync_query(query)
@classmethod
@async_db_query
async def async_list_by_page(
cls, db: AsyncSession, page: int = 1, count: int = 30
cls, db: AsyncSession | None = None, page: int = 1, count: int = 30
) -> List["Message"]:
"""
异步分页获取消息记录。
"""
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())
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) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
@classmethod
@async_db_query
async def async_list_sent_by_page(
cls,
db: AsyncSession,
db: AsyncSession | None = None,
page: int = 1,
count: int = 30,
all_clear_before: Optional[str] = None,
@@ -105,32 +127,35 @@ class Message(Base):
"""
分页获取系统发送的通知消息。
"""
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,
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,
)
)
)
if media_clear_before:
statement = statement.where(
or_(
cls.image.is_(None),
cls.image == "",
cls.reg_time > media_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())
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())
return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
@classmethod
def delete_before(
+95 -29
View File
@@ -6,7 +6,7 @@ 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 db_query, async_db_query
from app.db.decorators import run_legacy_async_query, run_legacy_sync_query
class Site(Base):
@@ -58,48 +58,114 @@ class Site(Base):
downloader: Mapped[Optional[str]] = mapped_column(String)
@classmethod
@db_query
def get_by_domain(cls, db: Session, domain: str):
return db.execute(select(cls).where(cls.domain == domain)).scalars().first()
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) if isinstance(db, Session) else run_legacy_sync_query(query)
@classmethod
@async_db_query
async def async_get_by_domain(cls, db: AsyncSession, domain: str):
result = await db.execute(select(cls).where(cls.domain == domain))
return result.scalar_one_or_none()
async def async_get_by_domain(
cls,
db: AsyncSession | 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")
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) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
@classmethod
@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.scalar_one_or_none()
async def async_get_by_name(
cls,
db: AsyncSession | str | None = None,
name: str | None = None,
):
"""异步按站点名称查询,兼容显式会话和旧插件无会话调用。"""
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) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
@classmethod
@db_query
def get_actives(cls, db: Session):
return list(db.execute(select(cls).where(cls.is_active.is_(True))).scalars().all())
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) if isinstance(db, Session) else run_legacy_sync_query(query)
@classmethod
@async_db_query
async def async_get_actives(cls, db: AsyncSession):
result = await db.execute(select(cls).where(cls.is_active.is_(True)))
return list(result.scalars().all())
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) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
@classmethod
@db_query
def list_order_by_pri(cls, db: Session):
return list(db.execute(select(cls).order_by(cls.pri)).scalars().all())
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) if isinstance(db, Session) else run_legacy_sync_query(query)
@classmethod
@async_db_query
async def async_list_order_by_pri(cls, db: AsyncSession):
result = await db.execute(select(cls).order_by(cls.pri))
return list(result.scalars().all())
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) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
@classmethod
@db_query
def get_domains_by_ids(cls, db: Session, ids: list):
return list(db.execute(select(cls.domain).where(cls.id.in_(ids))).scalars().all())
def get_domains_by_ids(
cls,
db: Session | list[int] | None = None,
ids: list[int] | None = None,
):
"""按 ID 查询域名,兼容显式会话和旧插件无会话调用。"""
if ids is None and isinstance(db, list):
ids, db = db, None
if ids is None:
raise TypeError("ids is required")
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) if isinstance(db, Session) else run_legacy_sync_query(query)
@classmethod
def reset(cls, db: Session):
+221 -140
View File
@@ -6,7 +6,7 @@ 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 async_db_query, db_query
from app.db.decorators import run_legacy_async_query, run_legacy_sync_query
from app.db.models._constraints import media_identity_constraint
from app.schemas.types import MUSIC_ENTITY_RECORDING, MediaSource
@@ -140,50 +140,66 @@ class Subscribe(Base):
return condition
@classmethod
@db_query
def exists(
cls, db: Session, media_source: MediaSource, media_id: str,
cls, db: Session | MediaSource | None = None,
media_source: MediaSource | str | None = None,
media_id: str | None = None,
season: Optional[int] = None,
episode_group: Optional[str] = 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_id, music_type
)
if condition is None:
return None
statement = select(cls).where(condition)
if season is not None:
statement = statement.where(cls.season == season)
statement = statement.where(cls.episode_group == episode_group)
return db.execute(statement).scalars().first()
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) if isinstance(db, Session) else run_legacy_sync_query(query)
@classmethod
@async_db_query
async def async_exists(
cls, db: AsyncSession, media_source: MediaSource, media_id: str,
cls, db: AsyncSession | MediaSource | None = None,
media_source: MediaSource | str | None = None,
media_id: str | None = None,
season: Optional[int] = None,
episode_group: Optional[str] = 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_id, music_type
)
if condition is None:
return None
query = select(cls).filter(condition)
if season is not None:
query = query.filter(cls.season == season)
query = query.filter(cls.episode_group == episode_group)
result = await db.execute(query)
return result.scalars().first()
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) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
@classmethod
@db_query
def exists_by_username(
cls, db: Session, username: str, media_source: MediaSource, media_id: str,
cls, db: Session | str | None = None,
username: str | MediaSource | None = None,
media_source: MediaSource | str | None = None,
media_id: str | None = None,
season: Optional[int] = None,
episode_group: Optional[str] = None,
music_type: Optional[str] = None,
@@ -191,6 +207,8 @@ 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(
@@ -198,23 +216,30 @@ class Subscribe(Base):
)
if condition is None:
return None
statement = select(cls).where(cls.username == username, condition)
if season is not None:
statement = statement.where(cls.season == season)
statement = statement.where(cls.episode_group == episode_group)
return db.execute(statement).scalars().first()
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) if isinstance(db, Session) else run_legacy_sync_query(query)
@classmethod
@async_db_query
async def async_exists_by_username(
cls, db: AsyncSession, username: str, media_source: MediaSource,
media_id: str, season: Optional[int] = None,
cls, db: AsyncSession | str | None = None,
username: str | MediaSource | None = None,
media_source: MediaSource | str | None = None,
media_id: str | None = None, season: Optional[int] = None,
episode_group: Optional[str] = None,
music_type: Optional[str] = None,
):
"""
异步按订阅 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(
@@ -222,80 +247,106 @@ class Subscribe(Base):
)
if condition is None:
return None
query = select(cls).filter(cls.username == username, condition)
if season is not None:
query = query.filter(cls.season == season)
query = query.filter(cls.episode_group == episode_group)
result = await db.execute(query)
return result.scalars().first()
@classmethod
@db_query
def get_by_state(cls, db: Session, state: str):
# 如果 state 为空或 None,返回所有订阅
statement = select(cls)
if state:
# 如果传入的状态不为空,拆分成多个状态
statement = statement.where(cls.state.in_(state.split(',')))
return list(db.execute(statement).scalars().all())
@classmethod
@async_db_query
async def async_get_by_state(cls, db: AsyncSession, state: str):
# 如果 state 为空或 None,返回所有订阅
if not state:
result = await db.execute(select(cls))
else:
# 如果传入的状态不为空,拆分成多个状态
result = await db.execute(
select(cls).filter(cls.state.in_(state.split(',')))
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 list(result.scalars().all())
return result.scalars().first()
return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
@classmethod
@db_query
def get_by_title(cls, db: Session, title: str, season: Optional[int] = None):
statement = select(cls).where(cls.name == title)
if season is not None:
statement = statement.where(cls.season == season)
return db.execute(statement).scalars().first()
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) if isinstance(db, Session) else run_legacy_sync_query(query)
@classmethod
@async_db_query
async def async_get_by_title(cls, db: AsyncSession, title: str, season: Optional[int] = None):
if season is not None:
result = await db.execute(
select(cls).filter(cls.name == title, cls.season == season)
)
else:
result = await db.execute(
select(cls).filter(cls.name == title)
)
return result.scalars().first()
async def async_get_by_state(
cls, db: AsyncSession | str | None = None, 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) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
@classmethod
@async_db_query
async def async_list_by_title(cls, db: AsyncSession, title: str, season: Optional[int] = None):
"""
异步按标题查询候选订阅列表。
"""
if season is not None:
result = await db.execute(
select(cls).filter(cls.name == title, cls.season == season)
)
else:
result = await db.execute(
select(cls).filter(cls.name == title)
)
return list(result.scalars().all())
def get_by_title(
cls, db: Session | str | None = None, title: str | None = None,
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) if isinstance(db, Session) else run_legacy_sync_query(query)
@classmethod
async def async_get_by_title(
cls, db: AsyncSession | str | None = None, title: str | None = None,
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) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
@classmethod
async def async_list_by_title(
cls, db: AsyncSession | str | None = None, title: str | None = None,
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) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
@classmethod
@db_query
def list_by_media_identity(
cls, db: Session, media_source: MediaSource, media_id: str,
cls, db: Session | MediaSource | None = None,
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,
@@ -303,15 +354,21 @@ class Subscribe(Base):
)
if condition is None:
return []
return list(db.execute(select(cls).where(condition)).scalars().all())
def query(session: Session):
"""在给定会话中执行媒体身份列表查询。"""
return list(session.execute(select(cls).where(condition)).scalars().all())
return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
@classmethod
@async_db_query
async def async_list_by_media_identity(
cls, db: AsyncSession, media_source: MediaSource, media_id: str,
cls, db: AsyncSession | MediaSource | None = None,
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,
@@ -319,19 +376,26 @@ class Subscribe(Base):
)
if condition is None:
return []
result = await db.execute(select(cls).filter(condition))
return list(result.scalars().all())
async def query(session: AsyncSession):
"""在给定异步会话中执行媒体身份列表查询。"""
result = await session.execute(select(cls).where(condition))
return list(result.scalars().all())
return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
@classmethod
@db_query
def get_by(
cls, db: Session, type: str, media_source: MediaSource, media_id: str,
cls, db: Session | str | None = None,
type: str | MediaSource | None = None,
media_source: MediaSource | str | None = None,
media_id: str | None = None,
season: Optional[str] = None,
music_type: Optional[str] = None,
):
"""
根据条件查询订阅
"""
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
)
@@ -340,18 +404,25 @@ class Subscribe(Base):
statement = select(cls).where(condition, cls.type == type)
if season is not None:
statement = statement.where(cls.season == season)
return db.execute(statement).scalars().first()
def query(session: Session):
"""在给定会话中执行类型媒体查询。"""
return session.execute(statement).scalars().first()
return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
@classmethod
@async_db_query
async def async_get_by(
cls, db: AsyncSession, type: str, media_source: MediaSource, media_id: str,
cls, db: AsyncSession | str | None = None,
type: str | MediaSource | None = None,
media_source: MediaSource | str | None = None,
media_id: str | None = None,
season: Optional[str] = None,
music_type: Optional[str] = None,
):
"""
根据条件查询订阅
"""
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
)
@@ -360,62 +431,72 @@ class Subscribe(Base):
query = select(cls).filter(condition, cls.type == type)
if season is not None:
query = query.filter(cls.season == season)
result = await db.execute(query)
return result.scalars().first()
async def execute_query(session: AsyncSession):
"""在给定异步会话中执行类型媒体查询。"""
result = await session.execute(query)
return result.scalars().first()
return await execute_query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(execute_query)
@classmethod
@db_query
def list_by_username(cls, db: Session, username: str, state: Optional[str] = None, mtype: Optional[str] = None):
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())
def list_by_username(cls, db: Session | str | None = None, username: str | None = None,
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) if isinstance(db, Session) else run_legacy_sync_query(query)
@classmethod
@async_db_query
async def async_list_by_username(cls, db: AsyncSession, username: str, state: Optional[str] = None,
async def async_list_by_username(cls, db: AsyncSession | str | None = None,
username: str | None = None, state: Optional[str] = None,
mtype: Optional[str] = None):
if mtype:
"""异步按用户筛选订阅,兼容显式会话和旧插件无会话调用。"""
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:
result = await db.execute(
select(cls).filter(cls.state == state, cls.username == username, cls.type == mtype)
)
else:
result = await db.execute(
select(cls).filter(cls.username == username, cls.type == mtype)
)
else:
if state:
result = await db.execute(
select(cls).filter(cls.state == state, cls.username == username)
)
else:
result = await db.execute(
select(cls).filter(cls.username == username)
)
return list(result.scalars().all())
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) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
@classmethod
@db_query
def list_by_type(cls, db: Session, mtype: str, days: int):
return list(db.execute(
select(cls).where(
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())
)).scalars().all())
return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
@classmethod
@async_db_query
async def async_list_by_type(cls, db: AsyncSession, mtype: str, days: int):
result = await db.execute(
select(cls).filter(
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 list(result.scalars().all())
return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
+38 -13
View File
@@ -4,7 +4,10 @@ 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 async_db_query, run_legacy_sync_query
from app.db.decorators import (
run_legacy_async_query,
run_legacy_sync_query,
)
class User(Base):
@@ -55,12 +58,23 @@ class User(Base):
return run_legacy_sync_query(query)
@classmethod
@async_db_query
async def async_get_by_name(cls, db: AsyncSession, name: str):
result = await db.execute(
select(cls).filter(cls.name == name)
)
return result.scalars().first()
async def async_get_by_name(
cls,
db: AsyncSession | str | None = None,
name: str | None = None,
):
"""异步按用户名查询,兼容显式会话和旧插件无会话调用。"""
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) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
@classmethod
def get_by_id(cls, db: Session | int | None = None, user_id: int | None = None):
@@ -79,12 +93,23 @@ class User(Base):
return run_legacy_sync_query(query)
@classmethod
@async_db_query
async def async_get_by_id(cls, db: AsyncSession, user_id: int):
result = await db.execute(
select(cls).filter(cls.id == user_id)
)
return result.scalars().first()
async def async_get_by_id(
cls,
db: AsyncSession | 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")
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) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
def delete_by_name(self, db: Session, name: str):
user = self.get_by_name(db, name)
+50 -11
View File
@@ -3,6 +3,7 @@ from typing import Optional, Union
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from sqlalchemy import and_, or_, select
from app.db.base import DbOper
from app.db.models.message import Message
@@ -105,7 +106,14 @@ class MessageOper(DbOper):
"""
分页获取消息记录。
"""
return Message.list_by_page(self._db, page, count)
return self._execute_sync_query(
lambda session: list(session.execute(
select(Message)
.order_by(Message.reg_time.desc(), Message.id.desc())
.offset((page - 1) * count)
.limit(count)
).scalars().all())
)
def exists_by_source(self, source: str) -> bool:
"""
@@ -114,7 +122,11 @@ class MessageOper(DbOper):
:param source: 消息来源唯一标识
:return: 是否存在匹配记录
"""
return Message.exists_by_source(self._db, source)
return self._execute_sync_query(
lambda session: session.execute(
select(Message.id).where(Message.source == source).limit(1)
).scalars().first() is not None
)
async def async_list_by_page(
self, page: int = 1, count: int = 30
@@ -122,7 +134,17 @@ class MessageOper(DbOper):
"""
分页获取消息记录。
"""
return await Message.async_list_by_page(self._db, page, count)
async def query(session: AsyncSession) -> list[Message]:
"""在调用方异步会话中执行消息分页查询。"""
result = await session.execute(
select(Message)
.order_by(Message.reg_time.desc(), Message.id.desc())
.offset((page - 1) * count)
.limit(count)
)
return list(result.scalars().all())
return await self._execute_async_query(query)
async def async_list_sent_by_page(
self,
@@ -135,11 +157,28 @@ class MessageOper(DbOper):
"""
分页获取系统发送的通知消息。
"""
return await Message.async_list_sent_by_page(
self._db,
page,
count,
all_clear_before=all_clear_before,
system_clear_before=system_clear_before,
media_clear_before=media_clear_before,
)
async def query(session: AsyncSession) -> list[Message]:
"""在调用方异步会话中执行通知消息分页查询。"""
statement = select(Message).where(Message.action == 1)
if all_clear_before:
statement = statement.where(Message.reg_time > all_clear_before)
if system_clear_before:
statement = statement.where(or_(
and_(Message.image.isnot(None), Message.image != ""),
Message.reg_time > system_clear_before,
))
if media_clear_before:
statement = statement.where(or_(
Message.image.is_(None),
Message.image == "",
Message.reg_time > media_clear_before,
))
result = await session.execute(
statement
.order_by(Message.reg_time.desc(), Message.id.desc())
.offset((page - 1) * count)
.limit(count)
)
return list(result.scalars().all())
return await self._execute_async_query(query)
+86 -18
View File
@@ -1,7 +1,8 @@
from datetime import datetime
from typing import Any, List, Mapping, Tuple, Optional
from sqlalchemy import delete as sqlalchemy_delete
from sqlalchemy import delete as sqlalchemy_delete, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from app.db.base import DbOper
@@ -11,6 +12,18 @@ from app.db.models.sitestatistic import SiteStatistic
from app.db.models.siteuserdata import SiteUserData
async def _async_first(session: AsyncSession, statement: Any) -> Optional[Site]:
"""执行异步站点查询并返回首条记录。"""
result = await session.execute(statement)
return result.scalars().first()
async def _async_all(session: AsyncSession, statement: Any) -> list[Site]:
"""执行异步站点查询并返回稳定列表。"""
result = await session.execute(statement)
return list(result.scalars().all())
class SiteOper(DbOper):
"""
站点管理
@@ -21,7 +34,7 @@ class SiteOper(DbOper):
新增站点
"""
site = Site(**kwargs)
if not site.get_by_domain(self._db, kwargs.get("domain")):
if not self.get_by_domain(kwargs.get("domain")):
self._stage_create(site)
return True, "新增站点成功"
return False, "站点已存在"
@@ -30,13 +43,22 @@ class SiteOper(DbOper):
"""
查询单个站点
"""
return Site.get(self._db, sid)
return self._execute_sync_query(
lambda session: session.execute(
select(Site).where(Site.id == sid)
).scalars().first()
)
async def async_get(self, sid: int) -> Optional[Site]:
"""
异步查询单个站点
"""
return await Site.async_get(self._db, sid)
return await self._execute_async_query(
lambda session: _async_first(
session,
select(Site).where(Site.id == sid),
)
)
async def get_by_id(self, site_id: int) -> Optional[Site]:
"""读取站点写用例需要的目标站点。"""
@@ -80,35 +102,59 @@ class SiteOper(DbOper):
"""
获取站点列表
"""
return Site.list(self._db)
return self._execute_sync_query(
lambda session: list(session.execute(select(Site)).scalars().all())
)
async def async_list(self) -> List[Site]:
"""
异步获取站点列表
"""
return await Site.async_list(self._db)
return await self._execute_async_query(
lambda session: _async_all(session, select(Site))
)
async def async_list_order_by_pri(self) -> List[Site]:
"""异步按优先级获取站点,供站点查询应用服务使用。"""
return await Site.async_list_order_by_pri(self._db)
return await self._execute_async_query(
lambda session: _async_all(
session,
select(Site).order_by(Site.pri),
)
)
def list_order_by_pri(self) -> List[Site]:
"""
获取站点列表
"""
return Site.list_order_by_pri(self._db)
return self._execute_sync_query(
lambda session: list(
session.execute(select(Site).order_by(Site.pri)).scalars().all()
)
)
def list_active(self) -> List[Site]:
"""
按状态获取站点列表
"""
return Site.get_actives(self._db)
return self._execute_sync_query(
lambda session: list(
session.execute(
select(Site).where(Site.is_active.is_(True))
).scalars().all()
)
)
async def async_list_active(self) -> List[Site]:
"""
异步按状态获取站点列表
"""
return await Site.async_get_actives(self._db)
return await self._execute_async_query(
lambda session: _async_all(
session,
select(Site).where(Site.is_active.is_(True)),
)
)
def delete(self, sid: int):
"""
@@ -128,7 +174,7 @@ class SiteOper(DbOper):
"""
更新站点
"""
site = Site.get(self._db, sid)
site = self.get(sid)
if not site:
return None
self._stage_update(site, payload)
@@ -147,37 +193,59 @@ class SiteOper(DbOper):
"""
按域名获取站点
"""
return Site.get_by_domain(self._db, domain)
return self._execute_sync_query(
lambda session: session.execute(
select(Site).where(Site.domain == domain)
).scalars().first()
)
async def async_get_by_domain(self, domain: str) -> Optional[Site]:
"""
异步按域名获取站点
"""
return await Site.async_get_by_domain(self._db, domain)
return await self._execute_async_query(
lambda session: _async_first(
session,
select(Site).where(Site.domain == domain),
)
)
async def async_get_by_name(self, name: str) -> Optional[Site]:
"""
异步按名称获取站点
"""
return await Site.async_get_by_name(self._db, name)
return await self._execute_async_query(
lambda session: _async_first(
session,
select(Site).where(Site.name == name),
)
)
def get_domains_by_ids(self, ids: List[int]) -> List[Optional[str]]:
"""
按ID获取站点域名
"""
return Site.get_domains_by_ids(self._db, ids)
if not ids:
return []
return self._execute_sync_query(
lambda session: list(
session.execute(
select(Site.domain).where(Site.id.in_(ids))
).scalars().all()
)
)
def exists(self, domain: str) -> bool:
"""
判断站点是否存在
"""
return Site.get_by_domain(self._db, domain) is not None
return self.get_by_domain(domain) is not None
def update_cookie(self, domain: str, cookies: str) -> Tuple[bool, str]:
"""
更新站点Cookie
"""
site = Site.get_by_domain(self._db, domain)
site = self.get_by_domain(domain)
if not site:
return False, "站点不存在"
self._stage_update(site, {
@@ -189,7 +257,7 @@ class SiteOper(DbOper):
"""
更新站点rss
"""
site = Site.get_by_domain(self._db, domain)
site = self.get_by_domain(domain)
if not site:
return False, "站点不存在"
self._stage_update(site, {
+127 -47
View File
@@ -297,13 +297,24 @@ class SubscribeOper(DbOper):
"""
获取订阅
"""
return Subscribe.get(self._db, rid=sid)
return self._execute_sync_query(
lambda session: session.execute(
select(Subscribe).where(Subscribe.id == sid)
).scalars().first()
)
async def async_get(self, sid: int) -> Optional[Subscribe]:
"""
获取订阅
"""
return await Subscribe.async_get(self._db, rid=sid)
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)
async def async_list_by_media_identity(
self,
@@ -312,12 +323,18 @@ class SubscribeOper(DbOper):
music_type: Optional[str] = None,
) -> List[Subscribe]:
"""异步按规范媒体身份读取订阅。"""
return await Subscribe.async_list_by_media_identity(
self._db,
media_source=media_source,
media_id=media_id,
music_type=music_type,
)
async def query(session: AsyncSession) -> List[Subscribe]:
"""在调用方异步会话中执行媒体身份列表查询。"""
condition = Subscribe._identity_condition( # pylint: disable=protected-access
media_source, media_id, 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,
@@ -326,12 +343,15 @@ class SubscribeOper(DbOper):
music_type: Optional[str] = None,
) -> List[Subscribe]:
"""同步按规范媒体身份读取订阅。"""
return Subscribe.list_by_media_identity(
self._db,
media_source=media_source,
media_id=media_id,
music_type=music_type,
)
def query(session: Session) -> List[Subscribe]:
"""在调用方同步会话中执行媒体身份列表查询。"""
condition = Subscribe._identity_condition( # pylint: disable=protected-access
media_source, media_id, 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,
@@ -360,11 +380,8 @@ class SubscribeOper(DbOper):
music_type: Optional[str],
) -> List[SubscribeDeletionCandidate]:
"""按媒体身份读取去重后的订阅删除快照。"""
subscribes = await Subscribe.async_list_by_media_identity(
self._db,
media_source=media_source,
media_id=media_id,
music_type=music_type,
subscribes = await self.async_list_by_media_identity(
media_source, media_id, music_type
)
candidates = []
seen_ids = set()
@@ -395,11 +412,7 @@ class SubscribeOper(DbOper):
async def list_search_ids(self, username: str, state: str) -> List[int]:
"""返回用户指定状态的订阅编号,不向应用用例暴露 ORM 列表。"""
subscribes = await Subscribe.async_list_by_username(
self._db,
username,
state=state,
)
subscribes = await self.async_list_by_username(username, state=state)
return [subscribe.id for subscribe in subscribes if subscribe.id]
def get_by(
@@ -410,9 +423,18 @@ class SubscribeOper(DbOper):
"""
根据条件查询订阅
"""
return Subscribe.get_by(
self._db, type, media_source, media_id, season, music_type,
)
def query(session: Session) -> Optional[Subscribe]:
"""在调用方同步会话中执行类型媒体查询。"""
condition = Subscribe._identity_condition( # pylint: disable=protected-access
media_source, media_id, 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,
@@ -422,25 +444,55 @@ class SubscribeOper(DbOper):
"""
根据条件查询订阅
"""
return await Subscribe.async_get_by(
self._db, type, media_source, media_id, season, music_type,
)
async def query(session: AsyncSession) -> Optional[Subscribe]:
"""在调用方异步会话中执行类型媒体查询。"""
condition = Subscribe._identity_condition( # pylint: disable=protected-access
media_source, media_id, 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 Subscribe.get_by_state(self._db, state)
return Subscribe.list(self._db)
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())
)
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:
return await Subscribe.async_get_by_state(self._db, state)
return await Subscribe.async_list(self._db)
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)
async def async_list_by_username(
self,
@@ -449,12 +501,20 @@ class SubscribeOper(DbOper):
mtype: Optional[str] = None,
) -> List[Subscribe]:
"""异步按用户获取订阅。"""
return await Subscribe.async_list_by_username(
self._db,
username=username,
state=state,
mtype=mtype,
)
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
)
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,
@@ -462,11 +522,14 @@ class SubscribeOper(DbOper):
season: Optional[int] = None,
) -> List[Subscribe]:
"""异步按标题获取订阅,供旧查询测试和迁移调用兼容。"""
return await Subscribe.async_list_by_title(
self._db,
title=title,
season=season,
)
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)
def delete(self, sid: int):
"""
@@ -535,13 +598,30 @@ class SubscribeOper(DbOper):
"""
获取指定用户的订阅
"""
return Subscribe.list_by_username(self._db, username=username, state=state, mtype=mtype)
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)
def list_by_type(self, mtype: str, days: int = 7) -> List[Subscribe]:
"""
获取指定类型的订阅
"""
return Subscribe.list_by_type(self._db, mtype=mtype, days=days)
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)
def add_history(self, **kwargs):
"""
+13 -2
View File
@@ -12,6 +12,7 @@ runtime 兼容映射指向 SDK 薄门面;canonical 数据访问模块仍只依
from typing import List, Optional
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.db.base import DbOper
from app.db.models.user import User
@@ -103,13 +104,23 @@ class UserOper(DbOper):
"""
异步根据用户名获取用户。
"""
return await User.async_get_by_name(self._db, name)
async def query(session: AsyncSession) -> Optional[User]:
"""在调用方异步会话中执行用户名查询。"""
result = await session.execute(select(User).where(User.name == name))
return result.scalars().first()
return await self._execute_async_query(query)
async def async_get_by_id(self, user_id: int) -> Optional[User]:
"""
异步根据用户 ID 获取用户。
"""
return await User.async_get_by_id(self._db, user_id)
async def query(session: AsyncSession) -> Optional[User]:
"""在调用方异步会话中执行用户 ID 查询。"""
result = await session.execute(select(User).where(User.id == user_id))
return result.scalars().first()
return await self._execute_async_query(query)
def get_permissions(self, name: str) -> dict:
"""