certflow.services.print_history_service 源代码

# src/certflow/services/print_history_service.py
"""历史回填兜底服务(docs/PRINT_BLUEPRINT.md §11.2 / §11.8 阶段1·2)。

阶段1(本文件当前范围):仅实现 :meth:`HistoryBackfillService.candidates`,
读 ``PrintLog`` 聚合「同型号上次实际打印值」,供统一护栏的 ④ 差异确认闸
(``reconfirm_on_diff``)使用。复用 PrintLog(51 字段/每产品一条)作历史源,零新表。

阶段2 待扩展:``backfill``(仅补缺、易变字段不兜底)+ ``record_after_generate``
(打印成功后写入 PrintLog,§10.8 不可事后改写)。

注意:``check_standard``(检验标准)不在 PrintLog 的 51 字段中,故差异闸对该字段
暂取不到历史值、自动跳过差异确认(仍走 required/reconfirm 逻辑)。后续如需覆盖,
可在阶段2 扩展其他历史源。
"""

from __future__ import annotations

from datetime import datetime
from typing import Any

from sqlalchemy.orm import Session

from certflow.models.print_log import PrintLog
from certflow.utils.database import DatabaseManager
from certflow.utils.logger import logger

# PrintLog 列 -> Certificate 字段名(用于 diff 历史比对;check_standard 不在 PrintLog,略过)
PRINTLOG_TO_CERT_FIELD: dict[str, str] = {
    "ShiYongWenDu": "temperature",
    "ShiYongJieZhi": "medium",
    "PN": "pn",
    "DN": "dn",
}

# 历史自动补缺(阶段2·A backfill)允许的字段白名单:仅易变、可衍生字段,
# 名称/型号/规格/日期/编号等「易变且不兜底」字段绝不自动回填(§11.8 阶段2)。
BACKFILL_FIELDS: tuple[str, ...] = ("temperature", "medium", "pn", "dn", "check_standard")


[文档] def backfill( data: dict[str, Any], history_last: dict[str, str] | None, fields: tuple[str, ...] | None = None, ) -> dict[str, Any]: """历史自动补缺(护栏 stage ①):用同型号上次实际值预填 ``data`` 中**缺失**的字段。 仅对 ``fields`` 白名单内、且当前为空的字段生效;用户已填值、历史缺失、或不在 白名单的字段一律不动。返回更新后的 ``data``(不污染入参)。 Args: data: 当前合格证字段字典。 history_last: 同型号上次实际值(``cert_field -> value``),缺省跳过。 fields: 允许补缺的字段白名单,缺省用 :data:`BACKFILL_FIELDS`。 """ if not history_last: return data fields = fields or BACKFILL_FIELDS out = dict(data) for f in fields: if f not in history_last: continue if (out.get(f) or "").strip(): continue # 用户已填,不覆盖 out[f] = history_last[f] return out
[文档] class HistoryBackfillService: """历史回填兜底服务:读 PrintLog 聚合历史 / 构造并写入 PrintLog(§11.8 阶段1·2)。""" def __init__(self, session: Session) -> None: self.session = session
[文档] def candidates(self, model: str | None, spec: str | None = None) -> dict[str, str] | None: """按产品型号取最近一条成功打印记录的实际值(作历史候选)。 Args: model: 产品型号(``product_model``)。 spec: 规格(预留;§11.2 归并键 ``key: [product_model, product_spec]``, 阶段2 起按 型号+规格 进一步归并,当前仅按型号)。 Returns: 字典 ``{cert_field: value}``(如 ``{"temperature": "≤200℃", ...}``); 无历史记录返回 ``None``。 """ if not model: return None model = model.strip() if not model: return None try: log = ( self.session.query(PrintLog) .filter(PrintLog.ChanPinXingHao == model) .filter(PrintLog.status == "success") .order_by(PrintLog.id.desc()) .first() ) except Exception as exc: # noqa: BLE001 logger.debug(f"[HistoryBackfill] 查询 PrintLog 失败({model}): {exc}") return None if log is None: return None res: dict[str, str] = {} for col, cert_field in PRINTLOG_TO_CERT_FIELD.items(): val = getattr(log, col, None) if val: res[cert_field] = str(val) return res or None
# ---- 阶段2·B:PrintLog 统一写点(消除 cert_log_service / certificate_print_service 两处构造) ----
[文档] def assemble_print_log(self, *, certificate_id: int, fields: dict[str, Any]) -> PrintLog: """单一 PrintLog 构造器:由调用方解析好的字段字典组装 ``PrintLog``(不提交)。 各打印路径(GUI / CLI-batch)保留各自的 SN/KKS/温度/PN 派生逻辑, 仅在此处统一落库构造,杜绝运行期重复写与字段不一致。 Args: certificate_id: 关联合格证批次 ID。 fields: 已解析的 PrintLog 字段字典(键为 PrintLog 列名)。 """ return PrintLog(certificate_id=certificate_id, **fields)
[文档] def record_after_generate( self, *, certificate_id: int, fields: dict[str, Any], status: str = "success", error_message: str | None = None, content_summary: str | None = None, now: datetime | None = None, ) -> PrintLog | None: """打印成功后写入一条 PrintLog(统一写点,逐条提交)。 取代旧 ``CertificatePrintService.record_print_log`` 的内联构造+提交; CLI/batch 路径经此单点写库。GUI 路径因批量单提交语义走 :meth:`assemble_print_log` + 外部统一提交,不调用本方法。 §10.8:PrintLog 不可事后改写,故本方法仅新增、不更新既有记录。 Returns: 成功返回 ``PrintLog``,异常返回 ``None``。 """ try: now = now or datetime.now() f = dict(fields) f.setdefault("hgz_PrintTime", now) f["status"] = status if error_message is not None: f["error_message"] = error_message if content_summary is not None: f["content_summary"] = content_summary log = self.assemble_print_log(certificate_id=certificate_id, fields=f) self.session.add(log) DatabaseManager.commit_with_retry(self.session) return log except Exception as e: # noqa: BLE001 logger.error(f"[HistoryBackfill] 写入 PrintLog 失败(cert={certificate_id}): {e}") self.session.rollback() return None