在质检管理下增加按来源×样本类型展示的台账页,聚合接收/生产/质检参数, 补录按实验室入库编码关联,支持 Excel 导出与中文时间格式。 Co-authored-by: Cursor <cursoragent@cursor.com>
375 lines
11 KiB
Python
375 lines
11 KiB
Python
"""实验室样本处理台账:字段合并、质检参数匹配、中文时间格式。"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import datetime
|
|
from typing import Any, Dict, Iterable, List, Optional, Set
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.models.models import (
|
|
BizQcTask,
|
|
BizQcWorkOrder,
|
|
BizQcWorkOrderInspectionBatch,
|
|
)
|
|
from app.models.qc_param import BizQcTaskResult
|
|
from app.services.sample_storage_cert_qc_prefill import (
|
|
_MOTHER_SEROLOGY_RULES,
|
|
_process_qual_from_tasks,
|
|
_serology_from_tasks,
|
|
_stamp,
|
|
batch_kind_from_name,
|
|
normalize_negative_positive,
|
|
qual_yin_yang,
|
|
)
|
|
|
|
SAMPLE_TYPE_PERINATAL_HSC = "围产期造血干细胞"
|
|
SAMPLE_TYPE_UC_MSC = "脐带间充质干细胞"
|
|
|
|
TAB_KEYS = (
|
|
"cell_hsc",
|
|
"donation_hsc",
|
|
"cell_msc",
|
|
"donation_msc",
|
|
)
|
|
|
|
TAB_META = {
|
|
"cell_hsc": {
|
|
"source": "cell_storage",
|
|
"sampleTypeName": SAMPLE_TYPE_PERINATAL_HSC,
|
|
"label": "细胞存储·围产期造血干细胞",
|
|
},
|
|
"donation_hsc": {
|
|
"source": "research_donation",
|
|
"sampleTypeName": SAMPLE_TYPE_PERINATAL_HSC,
|
|
"label": "科研捐赠·围产期造血干细胞",
|
|
},
|
|
"cell_msc": {
|
|
"source": "cell_storage",
|
|
"sampleTypeName": SAMPLE_TYPE_UC_MSC,
|
|
"label": "细胞存储·脐带间充质干细胞",
|
|
},
|
|
"donation_msc": {
|
|
"source": "research_donation",
|
|
"sampleTypeName": SAMPLE_TYPE_UC_MSC,
|
|
"label": "科研捐赠·脐带间充质干细胞",
|
|
},
|
|
}
|
|
|
|
# 各 Tab 可补录字段(系统空或口径需覆盖时写入)
|
|
SUPPLEMENT_KEYS: Dict[str, List[str]] = {
|
|
"cell_hsc": [
|
|
"receiveCheckText",
|
|
"hasMaternalBlood",
|
|
"remeasureMaternalBlood",
|
|
"transportPackingList",
|
|
"cordBloodReceiveConfirm",
|
|
"infectiousFourExternal",
|
|
"preEnrichNucleatedCount",
|
|
"postEnrichNucleatedCount",
|
|
"cd34Count1e4",
|
|
"remarkProcess",
|
|
],
|
|
"donation_hsc": [
|
|
"labMaterialCode",
|
|
"storageStatus",
|
|
"discardReason",
|
|
"receiveCheckText",
|
|
"preOpNucleatedCount",
|
|
"infectiousFourTestLabel",
|
|
"motherHbsag",
|
|
"motherAntiHcv",
|
|
"motherAntiTp",
|
|
"motherAntiHiv",
|
|
"motherCmvIgm",
|
|
"remarkProcess",
|
|
],
|
|
"cell_msc": [
|
|
"storageStatus",
|
|
"endotoxinTest",
|
|
"bacteriaTest",
|
|
"fungusTest",
|
|
"mycoplasmaTest",
|
|
"motherHbsag",
|
|
"motherHbvDna",
|
|
"motherAntiHcv",
|
|
"motherHcvRna",
|
|
"motherAntiTp",
|
|
"motherCmvIgm",
|
|
"motherAntiHiv",
|
|
"motherHiv1Rna",
|
|
"storageDateP2",
|
|
"remark",
|
|
],
|
|
"donation_msc": [
|
|
"storageStatus",
|
|
"storageTotalTubes",
|
|
"availableCellCount1e7",
|
|
"endotoxinTest",
|
|
"bacteriaTest",
|
|
"fungusTest",
|
|
"mycoplasmaTest",
|
|
"motherHbsag",
|
|
"motherHbvDna",
|
|
"motherAntiHcv",
|
|
"motherHcvRna",
|
|
"motherAntiTp",
|
|
"motherCmvIgm",
|
|
"motherAntiHiv",
|
|
"motherHiv1Rna",
|
|
"storageDateP2",
|
|
"remark",
|
|
],
|
|
}
|
|
|
|
# 补录优先覆盖系统推导值的字段
|
|
SUPPLEMENT_OVERRIDE_KEYS: Dict[str, Set[str]] = {
|
|
"cell_hsc": {"receiveCheckText"},
|
|
"donation_hsc": {
|
|
"receiveCheckText",
|
|
"storageStatus",
|
|
"infectiousFourTestLabel",
|
|
"motherHbsag",
|
|
"motherAntiHcv",
|
|
"motherAntiTp",
|
|
"motherAntiHiv",
|
|
"motherCmvIgm",
|
|
},
|
|
"cell_msc": {
|
|
"storageStatus",
|
|
"endotoxinTest",
|
|
"bacteriaTest",
|
|
"fungusTest",
|
|
"mycoplasmaTest",
|
|
"motherHbsag",
|
|
"motherHbvDna",
|
|
"motherAntiHcv",
|
|
"motherHcvRna",
|
|
"motherAntiTp",
|
|
"motherCmvIgm",
|
|
"motherAntiHiv",
|
|
"motherHiv1Rna",
|
|
"storageDateP2",
|
|
},
|
|
"donation_msc": {
|
|
"storageStatus",
|
|
"storageTotalTubes",
|
|
"availableCellCount1e7",
|
|
"endotoxinTest",
|
|
"bacteriaTest",
|
|
"fungusTest",
|
|
"mycoplasmaTest",
|
|
"motherHbsag",
|
|
"motherHbvDna",
|
|
"motherAntiHcv",
|
|
"motherHcvRna",
|
|
"motherAntiTp",
|
|
"motherCmvIgm",
|
|
"motherAntiHiv",
|
|
"motherHiv1Rna",
|
|
"storageDateP2",
|
|
},
|
|
}
|
|
|
|
|
|
def format_cn_datetime(value: Any) -> str:
|
|
"""统一展示:2026年11月12日 00时00分00秒"""
|
|
if value is None or value == "":
|
|
return ""
|
|
if isinstance(value, datetime):
|
|
dt = value
|
|
else:
|
|
text = str(value).strip().replace("T", " ")
|
|
if not text:
|
|
return ""
|
|
dt = None
|
|
for candidate, fmt in (
|
|
(text[:19], "%Y-%m-%d %H:%M:%S"),
|
|
(text[:16], "%Y-%m-%d %H:%M"),
|
|
(text[:10], "%Y-%m-%d"),
|
|
):
|
|
try:
|
|
dt = datetime.strptime(candidate, fmt)
|
|
break
|
|
except ValueError:
|
|
continue
|
|
if dt is None:
|
|
return text
|
|
return (
|
|
f"{dt.year:04d}年{dt.month:02d}月{dt.day:02d}日 "
|
|
f"{dt.hour:02d}时{dt.minute:02d}分{dt.second:02d}秒"
|
|
)
|
|
|
|
|
|
def parse_payload(raw: Any) -> Dict[str, Any]:
|
|
if isinstance(raw, dict):
|
|
return dict(raw)
|
|
if not raw:
|
|
return {}
|
|
try:
|
|
data = json.loads(raw)
|
|
except (TypeError, ValueError, json.JSONDecodeError):
|
|
return {}
|
|
return data if isinstance(data, dict) else {}
|
|
|
|
|
|
def dumps_payload(payload: Dict[str, Any]) -> str:
|
|
return json.dumps(payload or {}, ensure_ascii=False, separators=(",", ":"))
|
|
|
|
|
|
def yes_no_from_urls(urls: Optional[str]) -> str:
|
|
return "是" if (urls or "").strip() else "否"
|
|
|
|
|
|
def yes_no_from_bool(flag: Optional[bool]) -> str:
|
|
if flag is None:
|
|
return ""
|
|
return "是" if flag else "否"
|
|
|
|
|
|
def merge_row(
|
|
*,
|
|
tab_key: str,
|
|
system_values: Dict[str, Any],
|
|
supplement: Optional[Dict[str, Any]],
|
|
) -> Dict[str, Any]:
|
|
"""系统值优先;override 键补录优先;空用补录填。"""
|
|
out = dict(system_values or {})
|
|
sup = supplement or {}
|
|
overrides = SUPPLEMENT_OVERRIDE_KEYS.get(tab_key, set())
|
|
for key, val in sup.items():
|
|
text = "" if val is None else str(val).strip()
|
|
if not text:
|
|
continue
|
|
cur = out.get(key)
|
|
cur_empty = cur is None or (isinstance(cur, str) and not cur.strip())
|
|
if key in overrides or cur_empty:
|
|
out[key] = text
|
|
return out
|
|
|
|
|
|
def load_qc_tasks_for_sample(db: Session, tenant_no: str, sample_code: str) -> List[Dict[str, Any]]:
|
|
code = (sample_code or "").strip()
|
|
if not code:
|
|
return []
|
|
rows = (
|
|
db.query(
|
|
BizQcTask.id,
|
|
BizQcTask.item_name,
|
|
BizQcTask.completed_time,
|
|
BizQcTask.update_time,
|
|
BizQcWorkOrderInspectionBatch.batch_name,
|
|
BizQcTaskResult.param_name,
|
|
BizQcTaskResult.param_value,
|
|
)
|
|
.join(BizQcWorkOrder, BizQcWorkOrder.id == BizQcTask.work_order_id)
|
|
.outerjoin(
|
|
BizQcWorkOrderInspectionBatch,
|
|
BizQcWorkOrderInspectionBatch.id == BizQcTask.batch_id,
|
|
)
|
|
.outerjoin(
|
|
BizQcTaskResult,
|
|
(BizQcTaskResult.task_id == BizQcTask.id) & (BizQcTaskResult.del_flag == 0),
|
|
)
|
|
.filter(
|
|
BizQcWorkOrder.tenant_no == tenant_no,
|
|
BizQcWorkOrder.sample_code == code,
|
|
BizQcWorkOrder.del_flag == 0,
|
|
BizQcTask.tenant_no == tenant_no,
|
|
BizQcTask.del_flag == 0,
|
|
BizQcTask.task_status == "ywc",
|
|
)
|
|
.all()
|
|
)
|
|
grouped: Dict[str, Dict[str, Any]] = {}
|
|
for row in rows:
|
|
task = grouped.setdefault(
|
|
row.id,
|
|
{
|
|
"item_name": row.item_name or "",
|
|
"batch_kind": batch_kind_from_name(row.batch_name),
|
|
"stamp": _stamp(row.completed_time, row.update_time),
|
|
"params": [],
|
|
"values": [],
|
|
},
|
|
)
|
|
if (row.param_name or "").strip() or (row.param_value or "").strip():
|
|
task["params"].append((row.param_name or "", row.param_value or ""))
|
|
if (row.param_value or "").strip():
|
|
task["values"].append(row.param_value or "")
|
|
return list(grouped.values())
|
|
|
|
|
|
def extract_mother_blood(tasks: List[Dict[str, Any]], *, include_nucleic: bool = True) -> Dict[str, str]:
|
|
rules = _MOTHER_SEROLOGY_RULES
|
|
if not include_nucleic:
|
|
rules = tuple(
|
|
r
|
|
for r in rules
|
|
if r[0] not in {"mother_hbv_dna", "mother_hcv_rna", "mother_hiv1_rna"}
|
|
)
|
|
raw = _serology_from_tasks(tasks, rules)
|
|
mapping = {
|
|
"mother_hbsag": "motherHbsag",
|
|
"mother_hbv_dna": "motherHbvDna",
|
|
"mother_anti_hcv": "motherAntiHcv",
|
|
"mother_hcv_rna": "motherHcvRna",
|
|
"mother_anti_tp": "motherAntiTp",
|
|
"mother_cmv_igm": "motherCmvIgm",
|
|
"mother_anti_hiv": "motherAntiHiv",
|
|
"mother_hiv1_rna": "motherHiv1Rna",
|
|
}
|
|
out: Dict[str, str] = {}
|
|
for src, dst in mapping.items():
|
|
val = normalize_negative_positive(raw.get(src) or "") or (raw.get(src) or "")
|
|
if val:
|
|
# 台账 Excel 多用「阴」
|
|
if val in ("阴性", "阴"):
|
|
out[dst] = "阴"
|
|
elif val in ("阳性", "阳"):
|
|
out[dst] = "阳"
|
|
else:
|
|
out[dst] = val
|
|
return out
|
|
|
|
|
|
def extract_process_tests(tasks: List[Dict[str, Any]]) -> Dict[str, str]:
|
|
raw = _process_qual_from_tasks(tasks)
|
|
bacteria_values: List[str] = []
|
|
for task in tasks:
|
|
name = task.get("item_name") or ""
|
|
params = task.get("params") or []
|
|
hit = "细菌" in name or any("细菌" in (pn or "") for pn, _ in params)
|
|
if not hit:
|
|
continue
|
|
for _, pv in params:
|
|
bacteria_values.append(pv)
|
|
bacteria_values.extend(task.get("values") or [])
|
|
bacteria = qual_yin_yang(bacteria_values)
|
|
out = {
|
|
"endotoxinTest": _to_yin(raw.get("endotoxin_test")),
|
|
"fungusTest": _to_yin(raw.get("fungus_test")),
|
|
"mycoplasmaTest": _to_yin(raw.get("mycoplasma_test")),
|
|
}
|
|
if bacteria:
|
|
out["bacteriaTest"] = _to_yin(bacteria)
|
|
return {k: v for k, v in out.items() if v}
|
|
|
|
|
|
def _to_yin(value: Optional[str]) -> str:
|
|
text = (value or "").strip()
|
|
if text in ("阴性", "阴"):
|
|
return "阴"
|
|
if text in ("阳性", "阳"):
|
|
return "阳"
|
|
return text
|
|
|
|
|
|
def infectious_label(code: Optional[str]) -> str:
|
|
c = (code or "").strip()
|
|
if c == "zc":
|
|
return "正常"
|
|
if c == "bzc":
|
|
return "不正常"
|
|
return c
|