"""批次读取进度元数据;只暴露类别和数量,不携带业务原始载荷。""" import copy from datetime import datetime from app.services.tplus.mapping import DETAIL_MAPPINGS, TPLUS_MAPPINGS ENTITIES = frozenset(mapping.entity_type for mapping in (*TPLUS_MAPPINGS, *DETAIL_MAPPINGS)) def write(batch, *, phase=None, totals=None, active=None, read=None, done=None, failed=None): report = copy.deepcopy(batch.audit_result_json or {}) value = report.setdefault("progress", {"phase": "COUNTING", "entities": {}}) if phase: value["phase"] = phase if totals is not None: value["total_records"] = sum(totals.values()) for entity, count in totals.items(): value["entities"][entity] = {"total": count, "processed": 0, "status": "WAITING"} if active is not None: value["active_entity"] = active value["entities"].setdefault(active, {"total": None, "processed": 0})["status"] = "READING" if read is not None and value.get("active_entity"): value["entities"][value["active_entity"]]["processed"] = read if done: value["entities"][done]["status"] = "COMPLETED" if failed: value["entities"].setdefault(failed, {"total": None, "processed": 0})["status"] = "FAILED" value["updated_at"] = datetime.now().isoformat() batch.audit_result_json = report def public(batch, report=None): """旧任务总量未知时不给百分比;成功必须以批次终态为准。""" report = report or batch.__dict__.get("audit_result_json") or {} value = report.get("progress") or {} if not isinstance(value, dict): value = {} total = value.get("total_records") total = total if isinstance(total, int) and not isinstance(total, bool) and total >= 0 else None processed = batch.source_record_count status = batch.status percentage = None if status == "SUCCEEDED": total, percentage = processed, 100.0 elif total and processed >= 0: percentage = min(99.9, round(processed * 100 / total, 1)) entities = [] observed = report.get("observed_entities", []) for entity in batch.entity_types_json or []: if not isinstance(entity, str) or entity not in ENTITIES: continue details = value.get("entities", {}).get(entity, {}) count = details.get("total") entities.append({"entity_type": entity, "total_records": count, "processed_records": details.get("processed"), "status": details.get("status", "COMPLETED" if entity in observed else "WAITING")}) updated = value.get("updated_at") try: updated = datetime.fromisoformat(updated) if updated else None except (ValueError, TypeError): updated = None elapsed = max(0, int(((batch.finished_at or datetime.now()) - batch.started_at).total_seconds())) if batch.started_at else 0 phase = "COMPLETED" if status == "SUCCEEDED" else "FAILED" if status in ("FAILED", "PARTIAL") else value.get("phase", status) return {"phase": phase, "total_records": total, "processed_records": processed, "percentage": percentage, "active_entity": value.get("active_entity") if value.get("active_entity") in ENTITIES else None, "updated_at": updated, "elapsed_seconds": elapsed, "entities": entities}