refactor(repo): 优化仓库层代码,新增多条件查询能力

1. 为count_deleted方法新增按deleted_at时间范围过滤的参数
2. 为幂等查询get方法新增operation参数以避免响应体错误重放
3. 优化定时任务更新方法的注释说明与排除字段逻辑
4. 新增活跃任务统计并更新handler聚合摘要查询
5. 为任务状态统计接口新增handler名称过滤能力
This commit is contained in:
Kris 2026-07-11 15:53:38 +08:00
parent 6ddc2a8c25
commit 202defb5f2
3 changed files with 49 additions and 33 deletions

View File

@ -155,9 +155,18 @@ class BaseRepository:
result = await self.db.execute(stmt) result = await self.db.execute(stmt)
return list(result.scalars().all()) return list(result.scalars().all())
async def count_deleted(self) -> int: async def count_deleted(
"""统计软删除记录数。""" self,
*,
start_time: datetime | None = None,
end_time: datetime | None = None,
) -> int:
"""统计软删除记录数,支持按 deleted_at 时间范围过滤。"""
stmt = select(func.count()).select_from(self.model).where(self.model.is_deleted == 1) stmt = select(func.count()).select_from(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)
result = await self.db.execute(stmt) result = await self.db.execute(stmt)
return int(result.scalar() or 0) return int(result.scalar() or 0)

View File

@ -129,19 +129,24 @@ class ScheduledTaskIdempotencyRepository(BaseRepository):
# 幂等查询 # 幂等查询
# ------------------------------------------------------------------ # ------------------------------------------------------------------
async def get(self, key: str) -> dict | None: async def get(self, key: str, *, operation: str | None = None) -> dict | None:
"""查询幂等记录的响应体。 """查询幂等记录的响应体。
Args: Args:
key: 幂等键 key: 幂等键
operation: 操作类型可选传入时额外校验记录中的 ``operation``
字段与当前操作一致避免不同 API 操作间因复用同一 key 导致
响应体被错误重放 create 的响被 update 重放
Returns: Returns:
``response_body`` 字段值dict记录不存在或响应体未就绪 ``response_body`` 字段值dict记录不存在响应体未就绪
``response_body IS NULL``时返回 ``None`` ``response_body IS NULL````operation`` 不匹配时返回 ``None``
""" """
stmt = ( stmt = (
select(self.model.response_body).where(self.model.idempotency_key == key).where(self.model.is_deleted == 0) select(self.model.response_body).where(self.model.idempotency_key == key).where(self.model.is_deleted == 0)
) )
if operation is not None:
stmt = stmt.where(self.model.operation == operation)
result = await self.db.execute(stmt) result = await self.db.execute(stmt)
return result.scalar_one_or_none() return result.scalar_one_or_none()

View File

@ -35,6 +35,7 @@ class HandlerSummary:
name: str name: str
task_count: int task_count: int
active_task_count: int
last_active_at: datetime | None last_active_at: datetime | None
@ -226,17 +227,14 @@ class ScheduledTaskRepository(BaseRepository):
return tasks, total return tasks, total
async def update(self, task_id: str, data: dict[str, Any], *, commit: bool = True) -> ScheduledTask | None: async def update(self, task_id: str, data: dict[str, Any], *, commit: bool = True) -> ScheduledTask | None:
"""按 ``task_id`` 更新任务配置字段(排除已软删除)。 """按 ``task_id`` 更新任务字段(排除已软删除)。
自动设置 ``updated_at=now`` ``data`` 中提取允许更新的字段 自动设置 ``updated_at=now`` ``data`` 中提取允许更新的字段
排除主键审计字段与运行时状态字段运行时状态字段 排除主键审计字段与运行轨迹字段运行轨迹字段
``consecutive_errors`` / ``last_run_at`` / ``next_run_at`` / ``consecutive_errors`` / ``last_run_at`` / ``last_error``仍由
``last_error`` / ``status``由专用状态机方法管理禁止通过本方法 ``record_run_result`` 专用方法管理禁止通过本方法直接更新
直接更新避免绕过状态机约束 ``status`` / ``next_run_at`` ``update_task`` 用例在已校验状态机
与调度语义后显式写入避免 lower-level 调用方绕过约束
- ``pause`` / ``resume`` active paused
- ``mark_dead_letter`` dead_letter
- ``record_run_result`` 原子回写执行结果与 next_run_at
Args: Args:
task_id: 业务任务标识 task_id: 业务任务标识
@ -254,9 +252,7 @@ class ScheduledTaskRepository(BaseRepository):
"is_deleted", "is_deleted",
"consecutive_errors", "consecutive_errors",
"last_run_at", "last_run_at",
"next_run_at",
"last_error", "last_error",
"status",
} }
update_fields = {k: v for k, v in data.items() if k not in excluded} update_fields = {k: v for k, v in data.items() if k not in excluded}
update_fields["updated_at"] = utc_now_naive() update_fields["updated_at"] = utc_now_naive()
@ -372,11 +368,13 @@ class ScheduledTaskRepository(BaseRepository):
async def list_handler_summary(self) -> list[HandlerSummary]: async def list_handler_summary(self) -> list[HandlerSummary]:
"""返回 handler 维度的聚合摘要。 """返回 handler 维度的聚合摘要。
SQL 语义``SELECT handler_name, COUNT(*) as task_count, MAX(last_run_at) SQL 语义``SELECT handler_name,
as last_active_at FROM scheduled_tasks WHERE is_deleted=0 GROUP BY COUNT(*) as task_count,
handler_name ORDER BY handler_name``聚合查询用于 Admin API 展示 SUM(CASE WHEN status='active' THEN 1 ELSE 0 END) as active_task_count,
handler 的任务数与最近执行时间扫描全表但行数有限handler × MAX(last_run_at) as last_active_at
状态组合性能可接受 FROM scheduled_tasks WHERE is_deleted=0 GROUP BY handler_name``
聚合查询用于 Admin API 展示各 handler 的任务数活跃任务数与最近执行时间
扫描全表但行数有限handler × 状态组合性能可接受
``last_active_at`` ``last_run_at``最近一次执行开始时间而非 ``last_active_at`` ``last_run_at``最近一次执行开始时间而非
``updated_at``避免配置变更等非执行操作刷新活跃时间导致语义失真 ``updated_at``避免配置变更等非执行操作刷新活跃时间导致语义失真
@ -388,6 +386,7 @@ class ScheduledTaskRepository(BaseRepository):
select( select(
ScheduledTask.handler_name, ScheduledTask.handler_name,
func.count().label("task_count"), func.count().label("task_count"),
func.sum(func.case((ScheduledTask.status == "active", 1), else_=0)).label("active_task_count"),
func.max(ScheduledTask.last_run_at).label("last_active_at"), func.max(ScheduledTask.last_run_at).label("last_active_at"),
) )
.where(self._not_deleted()) .where(self._not_deleted())
@ -399,6 +398,7 @@ class ScheduledTaskRepository(BaseRepository):
HandlerSummary( HandlerSummary(
name=row.handler_name, name=row.handler_name,
task_count=int(row.task_count), task_count=int(row.task_count),
active_task_count=int(row.active_task_count or 0),
last_active_at=row.last_active_at, last_active_at=row.last_active_at,
) )
for row in result.all() for row in result.all()
@ -409,18 +409,20 @@ class ScheduledTaskRepository(BaseRepository):
*, *,
owner_scope: str | None = None, owner_scope: str | None = None,
owner_id: str | None = None, owner_id: str | None = None,
handler_name: str | None = None,
) -> dict[str, int]: ) -> dict[str, int]:
"""按状态统计任务数,返回 ``{active: N, paused: N, dead_letter: N}``。 """按状态统计任务数,返回 ``{active: N, paused: N, dead_letter: N}``。
用于 Admin API 仪表盘展示任务状态分布SQL 语义 用于 Admin API 仪表盘展示任务状态分布SQL 语义
``SELECT status, COUNT(*) FROM scheduled_tasks WHERE is_deleted=0 ``SELECT status, COUNT(*) FROM scheduled_tasks WHERE is_deleted=0
[AND owner_scope=? AND owner_id=?] GROUP BY status``一次查询 [AND owner_scope=? AND owner_id=? AND handler_name=?] GROUP BY status``
获取所有状态分布避免上层多次调用 ``list(status=..., page_size=1)`` 一次查询获取所有状态分布避免上层多次调用 ``list(status=..., page_size=1)``
获取 total 的低效方案 获取 total 的低效方案
Args: Args:
owner_scope: 可选归属范围过滤system / business owner_scope: 可选归属范围过滤system / business
owner_id: 可选业务方 ID 过滤 owner_id: 可选业务方 ID 过滤
handler_name: 可选 handler 名称过滤
Returns: Returns:
状态 任务数的映射未出现的状态不在字典中调用方按需 ``.get`` 0 状态 任务数的映射未出现的状态不在字典中调用方按需 ``.get`` 0
@ -434,6 +436,8 @@ class ScheduledTaskRepository(BaseRepository):
stmt = stmt.where(ScheduledTask.owner_scope == owner_scope) stmt = stmt.where(ScheduledTask.owner_scope == owner_scope)
if owner_id is not None: if owner_id is not None:
stmt = stmt.where(ScheduledTask.owner_id == owner_id) stmt = stmt.where(ScheduledTask.owner_id == owner_id)
if handler_name is not None:
stmt = stmt.where(ScheduledTask.handler_name == handler_name)
result = await self.db.execute(stmt) result = await self.db.execute(stmt)
return {row.status: int(row.count) for row in result.all()} return {row.status: int(row.count) for row in result.all()}
@ -502,9 +506,7 @@ class ScheduledTaskRepository(BaseRepository):
# 状态机转换 # 状态机转换
# ------------------------------------------------------------------ # ------------------------------------------------------------------
async def pause( async def pause(self, task_id: str, *, updated_by: str | None = None, commit: bool = True) -> ScheduledTask | None:
self, task_id: str, *, updated_by: str | None = None, commit: bool = True
) -> ScheduledTask | None:
"""暂停任务(``active`` → ``paused``)。 """暂停任务(``active`` → ``paused``)。
``active`` 状态的任务可暂停``paused`` / ``dead_letter`` 状态 ``active`` 状态的任务可暂停``paused`` / ``dead_letter`` 状态