"""同步管理只读查询;只公开受控元数据,不公开源数据和原异常。""" from app.core.exceptions import NotFoundError from app.core.response import PageResult from app.core.tplus_db import resolve_tplus_settings from app.repositories import tplus_repository as repository from app.schemas.tplus import ( TPlusBatchDetailOut, TPlusBatchOut, TPlusErrorOut, TPlusOverviewOut, ) from app.services.tplus import progress, task_service from app.services.tplus.automation import overview as automation_overview from app.services.tplus.errors import MESSAGES from app.services.tplus.mapping import DETAIL_MAPPINGS, TPLUS_MAPPINGS from app.services.tplus.relationship_audit import public_report ERROR_MESSAGES = { "READ_ERROR": "源数据读取失败,请检查只读连接和字段映射", "STAGE_ERROR": "原始数据保存失败,请核对字段类型后重试", "INTERRUPTED": "执行进程中断,可重新读取,已保存数据会自动去重", "EXECUTION_ERROR": "任务执行失败,请检查只读连接和字段映射", "TYPE_UNRESOLVED": "关联核验所需的单据类型缺失或不唯一", } _ENTITY_TYPES = {item.entity_type for item in (*TPLUS_MAPPINGS, *DETAIL_MAPPINGS)} ERROR_MESSAGES.update(MESSAGES) def batch_out(batch, active_parents: set[str], report=None) -> TPlusBatchOut: """只使用元数据字段组装输出,历史批次范围保持未知。""" entities = batch.entity_types_json scope_valid = ( isinstance(entities, list) and bool(entities) and all(isinstance(item, str) and item in _ENTITY_TYPES for item in entities) ) return TPlusBatchOut( batch_no=batch.batch_no, task_type="AUDIT" if batch.sync_mode == "AUDIT" else "READ", status=batch.status, entity_types=entities if scope_valid else None, source_record_count=batch.source_record_count, staged_record_count=batch.staged_record_count, skipped_record_count=batch.skipped_record_count, failed_record_count=batch.failed_record_count, parent_batch_no=batch.parent_batch_no, create_time=batch.create_time, started_at=batch.started_at, finished_at=batch.finished_at, can_retry=batch.status in ("FAILED", "PARTIAL") and (batch.sync_mode == "AUDIT" or (batch.sync_mode == "FULL" and scope_valid)) and batch.batch_no not in active_parents, error_summary="任务有异常,请查看异常明细" if batch.status in ("FAILED", "PARTIAL") else None, trigger_type=getattr(batch, "trigger_type", "MANUAL") or "MANUAL", automatic_attempt=getattr(batch, "automatic_attempt", 0) or 0, progress=progress.public(batch, report), ) def get_batch(session, number: str) -> TPlusBatchDetailOut: """按需加载报告,未找到时统一返回 404。""" batch = repository.get_batch(session, number, with_result=True) if batch is None: raise NotFoundError("同步批次不存在") out = batch_out(batch, repository.active_retry_parents(session, [number])) return TPlusBatchDetailOut( **out.model_dump(), audit_result=public_report(batch.audit_result_json) if batch.sync_mode == "AUDIT" else None ) def list_batches(session, query): """分页批次并批量查询重试状态。""" rows, total = repository.list_batches(session, query) active = repository.active_retry_parents(session, [row.batch_no for row in rows]) reports = repository.progress_reports(session, [row.batch_no for row in rows]) return PageResult[TPlusBatchOut]( records=[batch_out(row, active, reports.get(row.batch_no)) for row in rows], total=total, page_num=query.page_num, page_size=query.page_size, ) def _safe_id(value) -> str | None: if isinstance(value, str) and value.isascii() and value.isdigit() and len(value) <= 10: return value if int(value) <= 2147483647 else None return None def list_errors(session, number, query): """错误按编码生成固定说明,避免驱动异常泄漏连接参数。""" if repository.get_batch(session, number) is None: raise NotFoundError("同步批次不存在") rows, total = repository.list_errors(session, number, query) records = [ TPlusErrorOut( id=row.id, entity_type=row.entity_type if row.entity_type in _ENTITY_TYPES else "system", source_id=_safe_id(row.source_id), error_code=row.error_code if row.error_code in ERROR_MESSAGES else "UNKNOWN", error_message=ERROR_MESSAGES.get(row.error_code, "任务未完成,请核对配置后重试"), retry_count=row.retry_count, create_time=row.create_time, ) for row in rows ] return PageResult[TPlusErrorOut]( records=records, total=total, page_num=query.page_num, page_size=query.page_size ) def overview(session) -> TPlusOverviewOut: """只校验配置,不在页面查询期间建立 T+ 连接。""" settings = task_service.get_settings() try: resolve_tplus_settings(settings) configured, message = True, "只读配置已就绪,实际连接结果以任务执行为准" except RuntimeError: configured = False message = ( "源读取未开启,仍可查看已有采集记录和资料" if not settings.t_plus_enabled else "T+ 只读配置未就绪,请联系管理员配置" ) latest = repository.latest_audit(session) current = repository.current_batch(session) return TPlusOverviewOut( is_configured=configured, configuration_message=message, status_counts=repository.status_counts(session), total_staged_records=repository.total_staged_records(session), current_batch=batch_out(current, set()) if current else None, latest_audit=public_report(latest.audit_result_json) if latest else None, automation=automation_overview(session, settings=task_service.get_settings()), )