150 lines
5.1 KiB
Python
150 lines
5.1 KiB
Python
"""T+ 管理库访问;只 flush,事务由 service 管理。"""
|
||
|
||
from sqlalchemy import func, select, update
|
||
from sqlalchemy.orm import Session, defer
|
||
|
||
from app.models import TPlusSyncBatch, TPlusSyncError
|
||
|
||
|
||
def get_batch(
|
||
session: Session, batch_no: str, *, for_update: bool = False, with_result: bool = False
|
||
):
|
||
"""按批次号查询;普通查询不加载核验大字段。"""
|
||
statement = select(TPlusSyncBatch).where(TPlusSyncBatch.batch_no == batch_no)
|
||
if not with_result:
|
||
statement = statement.options(defer(TPlusSyncBatch.audit_result_json))
|
||
if for_update:
|
||
statement = statement.with_for_update().execution_options(populate_existing=True)
|
||
return session.scalar(statement)
|
||
|
||
|
||
def get_request(session: Session, key: str, *, for_update: bool = False):
|
||
"""锁读用于唯一约束竞争后的当前读,避免旧事务快照看不到新任务。"""
|
||
statement = select(TPlusSyncBatch).where(TPlusSyncBatch.request_key == key)
|
||
if for_update:
|
||
statement = statement.with_for_update().execution_options(populate_existing=True)
|
||
return session.scalar(statement)
|
||
|
||
|
||
def active_retry(session: Session, parent_no: str):
|
||
"""调用方先锁父批次,再用当前读判断是否已有未完成重试。"""
|
||
return session.scalar(
|
||
select(TPlusSyncBatch)
|
||
.where(
|
||
TPlusSyncBatch.parent_batch_no == parent_no,
|
||
TPlusSyncBatch.status.in_(("QUEUED", "RUNNING")),
|
||
)
|
||
.with_for_update()
|
||
)
|
||
|
||
|
||
def increment_retry_errors(session: Session, parent_no: str) -> None:
|
||
"""创建新重试时对原异常累计重试次数。"""
|
||
session.execute(
|
||
update(TPlusSyncError)
|
||
.where(TPlusSyncError.batch_no == parent_no)
|
||
.values(retry_count=TPlusSyncError.retry_count + 1)
|
||
)
|
||
session.flush()
|
||
|
||
|
||
def next_queued(session: Session):
|
||
"""调用方持有账套执行锁后领取最早排队任务。"""
|
||
return session.scalar(
|
||
select(TPlusSyncBatch)
|
||
.where(TPlusSyncBatch.status == "QUEUED")
|
||
.order_by(TPlusSyncBatch.id)
|
||
.limit(1)
|
||
.with_for_update()
|
||
)
|
||
|
||
|
||
def running_batches(session: Session) -> list[TPlusSyncBatch]:
|
||
"""只在持有执行锁时使用,不能依据时间戳判断任务已经停止。"""
|
||
return list(
|
||
session.scalars(
|
||
select(TPlusSyncBatch).where(TPlusSyncBatch.status == "RUNNING").with_for_update()
|
||
).all()
|
||
)
|
||
|
||
|
||
def list_batches(session: Session, query):
|
||
"""分页只读元数据,核验报告和原异常不随列表加载。"""
|
||
filters = []
|
||
if query.status:
|
||
filters.append(TPlusSyncBatch.status == query.status)
|
||
if query.task_type:
|
||
filters.append(
|
||
TPlusSyncBatch.sync_mode == ("AUDIT" if query.task_type == "AUDIT" else "FULL")
|
||
)
|
||
total = session.scalar(select(func.count()).select_from(TPlusSyncBatch).where(*filters))
|
||
rows = session.scalars(
|
||
select(TPlusSyncBatch)
|
||
.where(*filters)
|
||
.options(defer(TPlusSyncBatch.audit_result_json), defer(TPlusSyncBatch.error_message))
|
||
.order_by(TPlusSyncBatch.id.desc())
|
||
.offset(query.offset)
|
||
.limit(query.page_size)
|
||
).all()
|
||
return rows, total or 0
|
||
|
||
|
||
def active_retry_parents(session: Session, numbers: list[str]) -> set[str]:
|
||
"""整页一次获取未完成的重试,避免逐行查询。"""
|
||
if not numbers:
|
||
return set()
|
||
return set(
|
||
session.scalars(
|
||
select(TPlusSyncBatch.parent_batch_no).where(
|
||
TPlusSyncBatch.parent_batch_no.in_(numbers),
|
||
TPlusSyncBatch.status.in_(("QUEUED", "RUNNING")),
|
||
)
|
||
).all()
|
||
)
|
||
|
||
|
||
def list_errors(session: Session, number: str, query):
|
||
"""只读取当前批次的异常定位元数据。"""
|
||
filters = [TPlusSyncError.batch_no == number]
|
||
if query.entity_type:
|
||
filters.append(TPlusSyncError.entity_type == query.entity_type)
|
||
total = session.scalar(select(func.count()).select_from(TPlusSyncError).where(*filters))
|
||
rows = session.scalars(
|
||
select(TPlusSyncError)
|
||
.where(*filters)
|
||
.options(defer(TPlusSyncError.error_message))
|
||
.order_by(TPlusSyncError.id.desc())
|
||
.offset(query.offset)
|
||
.limit(query.page_size)
|
||
).all()
|
||
return rows, total or 0
|
||
|
||
|
||
def status_counts(session: Session) -> dict[str, int]:
|
||
"""汇总任务表,不扫描原始快照。"""
|
||
return dict(
|
||
session.execute(
|
||
select(TPlusSyncBatch.status, func.count()).group_by(TPlusSyncBatch.status)
|
||
).all()
|
||
)
|
||
|
||
|
||
def latest_audit(session: Session):
|
||
"""最近完成的核验;返回批次供查询层过滤报告。"""
|
||
return session.scalar(
|
||
select(TPlusSyncBatch)
|
||
.where(
|
||
TPlusSyncBatch.sync_mode == "AUDIT",
|
||
TPlusSyncBatch.finished_at.is_not(None),
|
||
)
|
||
.order_by(TPlusSyncBatch.id.desc())
|
||
.limit(1)
|
||
)
|
||
|
||
|
||
def total_staged_records(session: Session) -> int:
|
||
"""从批次元数据汇总累计新增数,避免扫描原始快照。"""
|
||
return (
|
||
session.scalar(select(func.coalesce(func.sum(TPlusSyncBatch.staged_record_count), 0))) or 0
|
||
)
|