ForcePilot/backend/package/yuxi/repositories/scheduled/base.py
Kris 23f7a8a863 refactor(scheduled-repo): 优化代码格式与函数签名,补充审计字段
1.  格式化SQLAlchemy查询语句,将多行合并为单行提升可读性
2.  调整部分函数参数换行格式,统一代码风格
3.  为幂等键抢占、响应回写、任务状态变更等方法新增updated_by/created_by审计字段
4.  修复僵尸日志清理逻辑,补全日聚合统计写入路径并返回handler列表
5.  新增一次性任务执行成功后的收尾方法,避免调度循环
2026-07-11 05:40:36 +08:00

193 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""定时任务调度域仓库基类,提供共享的分页、转义与软删除工具。
本模块是调度域(``scheduled``)专属的 ``BaseRepository`` 独立副本,与
``external_systems.base.BaseRepository`` 在设计上保持一致但**独立维护**
不 import ``yuxi.repositories.external_systems`` 包的任何符号,避免跨限界
上下文耦合。后续调度域仓储的演化由本子包自行控制。
所有 ``scheduled`` 下的仓库均继承 ``BaseRepository``,复用:
- ``_escape_like_pattern`` —— LIKE 关键词转义
- ``_resolve_pagination`` —— 分页参数解析page/page_size 优先,其次 limit/offset
- ``_not_deleted`` —— 软删除过滤条件 ``is_deleted == 0``
- ``delete_by_id`` —— 按主键执行软删除(``is_deleted=1, deleted_at=now``
- ``get_deleted_by_id`` —— 按主键查询软删除记录(恢复前精确查询,避免 list_deleted 误匹配)
- ``restore_by_id`` —— 按主键恢复软删除记录(``is_deleted=0, deleted_at=None``
- ``list_deleted`` —— 查询软删除记录列表(按 ``deleted_at`` 降序,支持时间范围与分页)
- ``count_deleted`` —— 统计软删除记录数
- ``hard_delete_by_id`` —— 按主键物理删除记录(仅删除 ``is_deleted=1`` 的记录)
- ``hard_delete_before`` —— 物理删除 ``deleted_at`` 早于截止时间的软删除记录
"""
from __future__ import annotations
from datetime import datetime
from sqlalchemy import delete as sa_delete
from sqlalchemy import func, select
from sqlalchemy import update as sa_update
from sqlalchemy.ext.asyncio import AsyncSession
from yuxi.utils.datetime_utils import utc_now_naive
class BaseRepository:
"""定时任务调度域仓库基类。
子类需设置 ``model`` 类属性为对应的 SQLAlchemy 模型,即可复用软删除
过滤、分页解析与 LIKE 转义等通用逻辑。
"""
model: type = None # 子类覆盖为具体模型
def __init__(self, db: AsyncSession) -> None:
self.db = db
# ------------------------------------------------------------------
# 通用工具
# ------------------------------------------------------------------
@staticmethod
def _escape_like_pattern(value: str) -> str:
"""转义 SQL LIKE 特殊字符,避免关键词注入错误匹配。"""
return value.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
@staticmethod
def _resolve_pagination(
page: int | None,
page_size: int | None,
limit: int | None,
offset: int | None,
) -> tuple[int | None, int]:
"""解析分页参数,优先使用 page/page_size其次 limit/offset。"""
if page is not None and page_size is not None:
return page_size, max(page - 1, 0) * page_size
if limit is not None:
return limit, offset or 0
return None, 0
def _not_deleted(self):
"""返回当前模型的软删除过滤条件is_deleted == 0"""
return self.model.is_deleted == 0
async def delete_by_id(
self,
record_id: int,
*,
updated_by: str | None = None,
commit: bool = True,
) -> int:
"""按主键执行软删除(``is_deleted=1, deleted_at=now``)。返回受影响行数。
仅标记 ``is_deleted=0`` 的记录,避免重复软删除。当 ``commit=False``
时仅执行 ``flush()`` 使变更进入会话但不提交,便于上层在事务中组合
多个写操作。
"""
now = utc_now_naive()
stmt = (
sa_update(self.model)
.where(self.model.id == record_id)
.where(self.model.is_deleted == 0)
.values(is_deleted=1, deleted_at=now, updated_at=now, updated_by=updated_by)
)
result = await self.db.execute(stmt)
if commit:
await self.db.commit()
else:
await self.db.flush()
return result.rowcount
# ------------------------------------------------------------------
# 回收站(软删除记录查询 / 恢复 / 物理删除)
# ------------------------------------------------------------------
async def get_deleted_by_id(self, record_id: int):
"""按主键查询软删除记录is_deleted=1。返回 ORM 实例或 None。
用于恢复前的记录查询与唯一约束预检,避免 list_deleted(limit=1) 的误匹配。
"""
stmt = select(self.model).where(self.model.id == record_id).where(self.model.is_deleted == 1)
result = await self.db.execute(stmt)
return result.scalars().first()
async def restore_by_id(
self,
record_id: int,
*,
updated_by: str | None = None,
commit: bool = True,
) -> int:
"""按主键恢复软删除记录is_deleted=0, deleted_at=None。返回受影响行数。
仅恢复 is_deleted=1 的记录,避免误恢复未删除资源。
"""
now = utc_now_naive()
stmt = (
sa_update(self.model)
.where(self.model.id == record_id)
.where(self.model.is_deleted == 1)
.values(is_deleted=0, deleted_at=None, updated_at=now, updated_by=updated_by)
)
result = await self.db.execute(stmt)
if commit:
await self.db.commit()
else:
await self.db.flush()
return result.rowcount
async def list_deleted(
self,
*,
limit: int | None = None,
offset: int = 0,
start_time: datetime | None = None,
end_time: datetime | None = None,
) -> list:
"""查询软删除记录is_deleted=1按 deleted_at 降序排序。"""
stmt = select(self.model).where(self.model.is_deleted == 1)
if start_time is not None:
stmt = stmt.where(self.model.deleted_at >= start_time)
if end_time is not None:
stmt = stmt.where(self.model.deleted_at <= end_time)
stmt = stmt.order_by(self.model.deleted_at.desc())
if limit is not None:
stmt = stmt.limit(limit).offset(offset)
result = await self.db.execute(stmt)
return list(result.scalars().all())
async def count_deleted(self) -> int:
"""统计软删除记录数。"""
stmt = select(func.count()).select_from(self.model).where(self.model.is_deleted == 1)
result = await self.db.execute(stmt)
return int(result.scalar() or 0)
async def hard_delete_by_id(
self,
record_id: int,
*,
commit: bool = True,
) -> int:
"""按主键物理删除记录(仅删除 is_deleted=1 的记录)。返回受影响行数。"""
stmt = sa_delete(self.model).where(self.model.id == record_id).where(self.model.is_deleted == 1)
result = await self.db.execute(stmt)
if commit:
await self.db.commit()
else:
await self.db.flush()
return result.rowcount
async def hard_delete_before(
self,
cutoff_at: datetime,
*,
commit: bool = True,
) -> int:
"""物理删除 deleted_at 早于 cutoff_at 的软删除记录。返回受影响行数。"""
stmt = sa_delete(self.model).where(self.model.is_deleted == 1).where(self.model.deleted_at < cutoff_at)
result = await self.db.execute(stmt)
if commit:
await self.db.commit()
else:
await self.db.flush()
return result.rowcount