Files
zkxw/backend/app/services/lab_sample_processing.py
T
zlwuandCursor 2124ce9208 fix(lab-sample): 剔除导出自存伪数据并补齐间充质采集字段
过滤导出自存占位协议与无接收间充质行;科研捐献行从捐赠采集表取采集时间,运输温度回落到接收温度。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-12 17:38:33 +08:00

1543 lines
53 KiB
Python

"""实验室样本处理数据 — 聚合市场/生产/质检已发生业务数据与部门字段回写。
Excel 仅作为字段目录示例;列表以协议样本为主表,关联样本接收与生产接收,
补充表只承载源系统没有的列,且不得覆盖源表字段。
"""
from __future__ import annotations
import json
import uuid
from datetime import datetime
from decimal import Decimal
from typing import Any, Dict, List, Optional, Tuple
from types import SimpleNamespace
from sqlalchemy import or_, text
from sqlalchemy.orm import Session
from app.models.models import (
BizCellSampleType,
BizCollectionHospital,
BizCollectionProtocol,
BizProtocolCollectionInfo,
BizProtocolPartyCustomer,
BizProtocolSample,
BizSampleProductionReception,
BizSampleReception,
BizSampleReceptionCheckResult,
)
from app.services.lab_sample_processing_fields import (
DATASET_HSC,
DATASET_MSC,
DATASET_UBMC,
DEPT_EDIT_PERMS,
PERM_LIST,
PERM_MANAGER,
SOURCE_EXTRA,
SOURCE_PRODUCTION,
SOURCE_PROTOCOL,
SOURCE_RECEPTION,
editable_depts_for_user,
field_map,
fields_for,
resolve_dataset_by_type_name,
)
def _new_id() -> str:
return uuid.uuid4().hex.upper()
def _fmt_dt(v: Any) -> Optional[str]:
if v is None:
return None
if isinstance(v, datetime):
return v.strftime("%Y-%m-%d %H:%M:%S")
return str(v)
def _fmt_num(v: Any) -> Any:
if v is None:
return None
if isinstance(v, Decimal):
return float(v)
return v
def _parse_extra(raw: Optional[str]) -> Dict[str, Any]:
if not raw:
return {}
try:
data = json.loads(raw)
except Exception:
return {}
return data if isinstance(data, dict) else {}
def user_acl(db: Session, user: dict) -> Dict[str, Any]:
from app.core.menu_guard import user_has_menu_perm
is_manager = user_has_menu_perm(db, user, PERM_MANAGER)
dept_flags = {
dept: user_has_menu_perm(db, user, code) for dept, code in DEPT_EDIT_PERMS.items()
}
editable = editable_depts_for_user(is_manager=is_manager, dept_flags=dept_flags)
return {
"canList": user_has_menu_perm(db, user, PERM_LIST) or is_manager or any(dept_flags.values()),
"isManager": is_manager,
"editableDepts": sorted(editable),
"deptFlags": dept_flags,
}
def _type_name_filters(dataset: str):
if dataset == DATASET_HSC:
return "%造血%"
if dataset == DATASET_MSC:
return "%间充质%"
if dataset == DATASET_UBMC:
return "%单个核%"
return None
def _code_prefix_filters(dataset: str) -> List[str]:
if dataset == DATASET_HSC:
return ["B%"]
if dataset == DATASET_MSC:
return ["BC%", "UC%"]
if dataset == DATASET_UBMC:
return ["UBMC%"]
return []
def _match_keyword(row: Dict[str, Any], kw: str) -> bool:
if not kw:
return True
text = (
f"{row.get('sampleCode') or ''} "
f"{row.get('protocolSampleCode') or ''} "
f"{row.get('motherName') or ''}"
)
return kw.lower() in text.lower()
def _infectious_label(code: Optional[str]) -> Optional[str]:
c = ("" if code is None else str(code)).strip()
if not c:
return None
if c == "zc":
return "正常"
if c == "bzc":
return "不正常"
return c
def _infectious_code(label: Optional[Any]) -> Optional[str]:
if label is None:
return None
s = str(label).strip()
if not s:
return None
if s == "正常":
return "zc"
if s == "不正常":
return "bzc"
return s
def _yes_no_from_bool(flag: Optional[bool]) -> Optional[str]:
if flag is None:
return None
return "是" if flag else "否"
def _donation_collection_map(
db: Session, tenant_no: str, donation_ids: List[str]
) -> Dict[str, Any]:
from app.models.research_donation import BizResearchDonationCollection
ids = [i for i in {(x or "").strip() for x in donation_ids} if i]
if not ids:
return {}
rows = (
db.query(BizResearchDonationCollection)
.filter(
BizResearchDonationCollection.tenant_no == tenant_no,
BizResearchDonationCollection.del_flag == 0,
BizResearchDonationCollection.research_donation_id.in_(ids),
)
.all()
)
out: Dict[str, Any] = {}
for row in rows:
key = str(row.research_donation_id or "").strip()
if key and key not in out:
out[key] = row
return out
def _derive_receive_check(flags: Dict[str, bool]) -> Optional[str]:
if not flags:
return None
skip = {"知情同意书", "健康调查表", "样本信息正确,标识清晰"}
preferred = [
"针路密闭",
"容器无破损",
"外观正常",
"样本无破损",
"针卡关紧",
"两端系紧",
"温度4-25",
"采集至到库时间≤24小时",
]
parts = [name for name in preferred if flags.get(name)]
if not parts:
parts = [name for name, checked in flags.items() if checked and name not in skip]
return "、".join(parts) if parts else None
def _check_flag_map(
db: Session, tenant_no: str, reception_ids: List[str]
) -> Dict[str, Dict[str, bool]]:
ids = [i for i in reception_ids if i]
if not ids:
return {}
rows = (
db.query(BizSampleReceptionCheckResult)
.filter(
BizSampleReceptionCheckResult.tenant_no == tenant_no,
BizSampleReceptionCheckResult.del_flag == 0,
BizSampleReceptionCheckResult.reception_id.in_(ids),
)
.all()
)
out: Dict[str, Dict[str, bool]] = {}
for r in rows:
bucket = out.setdefault(r.reception_id, {})
name = (r.item_name or "").strip()
if name:
bucket[name] = bool(r.checked)
return out
def _pick_extra(
extras: Dict[str, Any], *codes: Optional[str]
) -> Dict[str, Any]:
for code in codes:
key = (code or "").strip()
if key and key in extras:
return _parse_extra(extras[key].payload_json)
return {}
# 旧台账补录字段名 → 新模块字段名(同名键保持不变)
_LEGACY_SUPPLEMENT_TO_NEW: Dict[str, str] = {
"receiveCheckText": "receiveCheck",
"infectiousFourTestLabel": "infectiousFourCard",
"preOpNucleatedCount": "preOpTnc",
"preEnrichNucleatedCount": "preEnrichTnc",
"postEnrichNucleatedCount": "postEnrichTnc",
"cd34Count1e4": "cd34Count",
"transportPackingList": "hasPackingList",
"cordBloodReceiveConfirm": "hasReceiveConfirm",
"remarkProcess": "processRemark",
"motherHbsag": "hbsag",
"motherAntiHcv": "antiHcv",
"motherAntiTp": "antiTp",
"motherAntiHiv": "antiHiv",
"motherCmvIgm": "cmvIgm",
}
_NEW_TO_LEGACY_SUPPLEMENT: Dict[str, str] = {
v: k for k, v in _LEGACY_SUPPLEMENT_TO_NEW.items()
}
# MSC 等仍使用 mother* 键名,双向都保留
for _same in (
"labMaterialCode",
"storageStatus",
"discardReason",
"hasMaternalBlood",
"remeasureMaternalBlood",
"infectiousFourExternal",
"endotoxinTest",
"bacteriaTest",
"fungusTest",
"mycoplasmaTest",
"motherHbsag",
"motherHbvDna",
"motherAntiHcv",
"motherHcvRna",
"motherAntiTp",
"motherCmvIgm",
"motherAntiHiv",
"motherHiv1Rna",
"storageDateP2",
"storageTotalTubes",
"availableCellCount1e7",
):
_LEGACY_SUPPLEMENT_TO_NEW.setdefault(_same, _same)
_NEW_TO_LEGACY_SUPPLEMENT.setdefault(_same, _same)
def _is_blank(val: Any) -> bool:
return val is None or (isinstance(val, str) and not val.strip())
def _map_legacy_supplement(payload: Optional[Dict[str, Any]]) -> Dict[str, Any]:
out: Dict[str, Any] = {}
for key, value in (payload or {}).items():
if _is_blank(value):
continue
new_key = _LEGACY_SUPPLEMENT_TO_NEW.get(str(key), str(key))
out[new_key] = value
# MSC 场景保留 mother* 原键,避免被 UBMC 别名覆盖后丢失
if str(key).startswith("mother") and new_key != str(key):
out[str(key)] = value
return out
def _map_new_to_legacy_supplement(payload: Optional[Dict[str, Any]]) -> Dict[str, Any]:
out: Dict[str, Any] = {}
for key, value in (payload or {}).items():
if _is_blank(value):
continue
legacy_key = _NEW_TO_LEGACY_SUPPLEMENT.get(str(key), str(key))
out[legacy_key] = value
return out
def _merge_extra_with_legacy(
extra: Optional[Dict[str, Any]], legacy: Optional[Dict[str, Any]]
) -> Dict[str, Any]:
"""新补充表优先;旧台账补录填空(含字段名映射)。"""
mapped = _map_legacy_supplement(legacy)
merged = dict(mapped)
for key, value in (extra or {}).items():
if not _is_blank(value):
merged[key] = value
return merged
def _load_legacy_supplements(
db: Session, tenant_no: str, codes: List[str]
) -> Dict[str, Dict[str, Any]]:
from app.models.models import BizLabSampleProcessSupplement
from app.services import lab_sample_process as legacy_ledger
uniq = [c for c in {(x or "").strip() for x in codes} if c]
if not uniq:
return {}
rows = (
db.query(BizLabSampleProcessSupplement)
.filter(
BizLabSampleProcessSupplement.tenant_no == tenant_no,
BizLabSampleProcessSupplement.del_flag == 0,
BizLabSampleProcessSupplement.lab_storage_code.in_(uniq),
)
.all()
)
out: Dict[str, Dict[str, Any]] = {}
for row in rows:
out[row.lab_storage_code] = legacy_ledger.parse_payload(row.payload)
return out
def sync_legacy_supplement_to_processing(
db: Session,
*,
tenant_no: str,
lab_storage_code: str,
sample_type_name: Optional[str] = None,
payload: Optional[Dict[str, Any]] = None,
operator_id: str = "legacy-sync",
) -> None:
"""旧台账补录保存后,同步到新模块补充表,并把传染四项写回生产接收。"""
code = (lab_storage_code or "").strip()
if not code:
return
mapped = _map_legacy_supplement(payload)
if not mapped:
return
dataset = resolve_dataset_by_type_name(sample_type_name or "", code)
if not dataset:
if code.upper().startswith("UBMC"):
dataset = DATASET_UBMC
elif code.upper().startswith(("BC", "UC")):
dataset = DATASET_MSC
else:
dataset = DATASET_HSC
upsert_extra_payload(
db,
tenant_no=tenant_no,
dataset=dataset,
sample_code=code,
payload=mapped,
operator_id=operator_id,
replace=False,
)
infectious = mapped.get("infectiousFourCard")
if infectious not in (None, ""):
reception = (
db.query(BizSampleReception)
.filter(
BizSampleReception.tenant_no == tenant_no,
BizSampleReception.del_flag == 0,
or_(
BizSampleReception.lab_storage_code == code,
BizSampleReception.sample_code == code,
),
)
.order_by(BizSampleReception.reception_time.desc())
.first()
)
if reception:
production = (
db.query(BizSampleProductionReception)
.filter(
BizSampleProductionReception.tenant_no == tenant_no,
BizSampleProductionReception.reception_id == reception.id,
BizSampleProductionReception.del_flag == 0,
)
.order_by(BizSampleProductionReception.process_time.desc())
.first()
)
if production is not None:
production.infectious_four_test_result = _infectious_code(infectious)
production.update_by = operator_id
production.update_time = datetime.now()
def _extra_field_keys(dataset: str) -> set:
return {f.key for f in fields_for(dataset) if f.source == SOURCE_EXTRA}
def _live_sample_codes(db: Session, tenant_no: str) -> set:
codes = set()
for (code,) in (
db.query(BizProtocolSample.sample_code)
.filter(
BizProtocolSample.tenant_no == tenant_no,
BizProtocolSample.del_flag == 0,
BizProtocolSample.sample_code.isnot(None),
BizProtocolSample.sample_code != "",
)
.all()
):
if code:
codes.add(str(code).strip())
for lab_code, sample_code in (
db.query(BizSampleReception.lab_storage_code, BizSampleReception.sample_code)
.filter(
BizSampleReception.tenant_no == tenant_no,
BizSampleReception.del_flag == 0,
)
.all()
):
if lab_code:
codes.add(str(lab_code).strip())
if sample_code:
codes.add(str(sample_code).strip())
return codes
def restore_retired_excel_extras(
db: Session,
*,
tenant_no: str,
operator_id: str = "restore-excel-extra",
) -> Dict[str, Any]:
"""恢复被误删的 Excel 补充字段(真菌/内毒素等),并同步旧台账。
当初按协议样本号匹配软删,但 Excel 多用实验室入库编码,导致真实样本补录被删。
仅恢复仍能关联到当前有效协议/接收样本编码的补充字段。
"""
from app.models.models import BizLabSampleProcessingExtra
live_codes = _live_sample_codes(db, tenant_no)
retired = (
db.query(BizLabSampleProcessingExtra)
.filter(
BizLabSampleProcessingExtra.tenant_no == tenant_no,
BizLabSampleProcessingExtra.del_flag == 1,
BizLabSampleProcessingExtra.update_by == "retire-excel-demo",
)
.all()
)
restored = 0
skipped = 0
field_hits = 0
for row in retired:
code = (row.sample_code or "").strip()
if not code or code not in live_codes:
skipped += 1
continue
payload = _parse_extra(row.payload_json)
allowed = _extra_field_keys(row.dataset)
extra_only = {
k: v
for k, v in payload.items()
if k in allowed and not _is_blank(v)
}
if not extra_only:
skipped += 1
continue
upsert_extra_payload(
db,
tenant_no=tenant_no,
dataset=row.dataset,
sample_code=code,
payload=extra_only,
operator_id=operator_id,
replace=False,
)
sync_processing_to_legacy_supplement(
db,
tenant_no=tenant_no,
lab_storage_code=code,
sample_type_name=None,
payload=extra_only,
operator_id=operator_id,
)
restored += 1
field_hits += len(extra_only)
db.commit()
return {
"liveCodes": len(live_codes),
"retiredCandidates": len(retired),
"restoredRows": restored,
"skippedRows": skipped,
"restoredFields": field_hits,
}
def _load_qc_tasks_map(
db: Session, tenant_no: str, codes: List[str]
) -> Dict[str, List[Dict[str, Any]]]:
from app.models.models import BizQcTask, BizQcWorkOrder, BizQcWorkOrderInspectionBatch
from app.models.qc_param import BizQcTaskResult
from app.services.sample_storage_cert_qc_prefill import _stamp, batch_kind_from_name
uniq = [c for c in {(x or "").strip() for x in codes} if c]
if not uniq:
return {}
rows = (
db.query(
BizQcWorkOrder.sample_code,
BizQcTask.id,
BizQcTask.item_name,
BizQcTask.completed_time,
BizQcTask.update_time,
BizQcWorkOrderInspectionBatch.batch_name,
BizQcTaskResult.param_name,
BizQcTaskResult.param_value,
)
.join(BizQcTask, BizQcTask.work_order_id == BizQcWorkOrder.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.in_(uniq),
BizQcWorkOrder.del_flag == 0,
BizQcTask.tenant_no == tenant_no,
BizQcTask.del_flag == 0,
BizQcTask.task_status == "ywc",
)
.all()
)
by_code: Dict[str, Dict[str, Dict[str, Any]]] = {}
for row in rows:
code = (row.sample_code or "").strip()
if not code:
continue
grouped = by_code.setdefault(code, {})
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 {code: list(tasks.values()) for code, tasks in by_code.items()}
def _qc_fields_for_dataset(dataset: str, tasks: List[Dict[str, Any]]) -> Dict[str, Any]:
from app.services import lab_sample_process as legacy_ledger
if not tasks:
return {}
out: Dict[str, Any] = {}
out.update(legacy_ledger.extract_process_tests(tasks))
mother = legacy_ledger.extract_mother_blood(tasks)
if dataset == DATASET_UBMC:
remap = {
"motherHbsag": "hbsag",
"motherAntiHcv": "antiHcv",
"motherAntiTp": "antiTp",
"motherAntiHiv": "antiHiv",
"motherCmvIgm": "cmvIgm",
}
for key, value in mother.items():
out[remap.get(key, key)] = value
else:
out.update(mother)
allowed = _extra_field_keys(dataset)
return {k: v for k, v in out.items() if k in allowed and not _is_blank(v)}
def sync_processing_to_legacy_supplement(
db: Session,
*,
tenant_no: str,
lab_storage_code: str,
sample_type_name: Optional[str] = None,
payload: Optional[Dict[str, Any]] = None,
operator_id: str = "processing-sync",
) -> None:
"""新模块保存后,回写旧台账补录,保持两边可见字段一致。"""
from app.models.models import BizLabSampleProcessSupplement
from app.services import lab_sample_process as legacy_ledger
code = (lab_storage_code or "").strip()
mapped = _map_new_to_legacy_supplement(payload)
if not code or not mapped:
return
now = datetime.now()
row = (
db.query(BizLabSampleProcessSupplement)
.filter(
BizLabSampleProcessSupplement.tenant_no == tenant_no,
BizLabSampleProcessSupplement.lab_storage_code == code,
BizLabSampleProcessSupplement.del_flag == 0,
)
.first()
)
current = legacy_ledger.parse_payload(row.payload) if row else {}
current.update(mapped)
src = "research_donation" if code.upper().startswith("UBMC") else "cell_storage"
if row is None:
db.add(
BizLabSampleProcessSupplement(
id=_new_id(),
lab_storage_code=code,
reception_source=src,
sample_type_name=(sample_type_name or None),
payload=legacy_ledger.dumps_payload(current),
tenant_no=tenant_no,
create_by=operator_id,
create_dept_id="",
create_time=now,
update_by=operator_id,
update_time=now,
del_flag=0,
)
)
else:
if sample_type_name:
row.sample_type_name = sample_type_name
row.payload = legacy_ledger.dumps_payload(current)
row.update_by = operator_id
row.update_time = now
def _match_date_range(row: Dict[str, Any], date_from: Optional[str], date_to: Optional[str]) -> bool:
if not date_from and not date_to:
return True
for key in ("receptionTime", "collectTime", "cryoTime", "storageDateP2"):
val = row.get(key)
if not val:
continue
day = str(val)[:10].replace(".", "-").replace("/", "-")
# normalize compact dates like 2025-9-8
parts = day.split("-")
if len(parts) == 3:
try:
day = f"{int(parts[0]):04d}-{int(parts[1]):02d}-{int(parts[2]):02d}"
except Exception:
pass
if date_from and day < date_from:
continue
if date_to and day > date_to:
continue
return True
return False
def list_rows(
db: Session,
*,
tenant_no: str,
dataset: str,
keyword: Optional[str] = None,
date_from: Optional[str] = None,
date_to: Optional[str] = None,
page: int = 1,
page_size: int = 20,
) -> Tuple[List[Dict[str, Any]], int]:
from app.models.models import BizLabSampleProcessingExtra
type_like = _type_name_filters(dataset)
code_likes = _code_prefix_filters(dataset)
q = (
db.query(
BizProtocolSample,
BizCellSampleType,
BizProtocolPartyCustomer,
BizProtocolCollectionInfo,
BizCollectionHospital,
)
.outerjoin(
BizCellSampleType,
(BizCellSampleType.id == BizProtocolSample.sample_type_id)
& (BizCellSampleType.del_flag == 0),
)
.outerjoin(
BizProtocolPartyCustomer,
(BizProtocolPartyCustomer.collection_protocol_id == BizProtocolSample.collection_protocol_id)
& (BizProtocolPartyCustomer.tenant_no == tenant_no)
& (BizProtocolPartyCustomer.del_flag == 0),
)
.outerjoin(
BizProtocolCollectionInfo,
(BizProtocolCollectionInfo.collection_protocol_id == BizProtocolSample.collection_protocol_id)
& (BizProtocolCollectionInfo.tenant_no == tenant_no)
& (BizProtocolCollectionInfo.del_flag == 0),
)
.outerjoin(
BizCollectionHospital,
(BizCollectionHospital.id == BizProtocolCollectionInfo.hospital_id)
& (BizCollectionHospital.del_flag == 0),
)
.filter(
BizProtocolSample.tenant_no == tenant_no,
BizProtocolSample.del_flag == 0,
BizProtocolSample.sample_code.isnot(None),
BizProtocolSample.sample_code != "",
)
)
type_or_code = []
if type_like:
type_or_code.append(BizCellSampleType.type_name.like(type_like))
for pref in code_likes:
type_or_code.append(BizProtocolSample.sample_code.like(pref))
if type_or_code:
q = q.filter(or_(*type_or_code))
rows = q.order_by(BizProtocolSample.create_time.desc()).all()
filtered = []
for sample, stype, party, coll, hospital in rows:
type_name = (stype.type_name if stype else "") or ""
code = sample.sample_code or ""
if resolve_dataset_by_type_name(type_name, code) != dataset:
continue
mother_name = ((party.mother_name if party else None) or "").strip()
# 导出联调产生的占位协议,不进入实验室处理台账
if "导出自存" in mother_name:
continue
filtered.append((sample, stype, party, coll, hospital))
extras = {
e.sample_code: e
for e in db.query(BizLabSampleProcessingExtra)
.filter(
BizLabSampleProcessingExtra.tenant_no == tenant_no,
BizLabSampleProcessingExtra.del_flag == 0,
BizLabSampleProcessingExtra.dataset == dataset,
)
.all()
}
# 仅聚合真实协议样本;补充表只挂到已有样本上,不再单独展示 Excel 演示行
codes = [s.sample_code for s, *_ in filtered if s.sample_code]
sample_ids = [s.id for s, *_ in filtered if s.id]
receptions_by_sample_id: Dict[str, BizSampleReception] = {}
receptions_by_code: Dict[str, BizSampleReception] = {}
productions: Dict[str, BizSampleProductionReception] = {}
if sample_ids or codes:
recv_filters = []
if sample_ids:
recv_filters.append(BizSampleReception.protocol_sample_id.in_(sample_ids))
if codes:
recv_filters.append(BizSampleReception.sample_code.in_(codes))
for r in (
db.query(BizSampleReception)
.filter(
BizSampleReception.tenant_no == tenant_no,
BizSampleReception.del_flag == 0,
or_(*recv_filters) if recv_filters else False,
)
.order_by(BizSampleReception.reception_time.desc())
.all()
):
if r.protocol_sample_id:
receptions_by_sample_id.setdefault(r.protocol_sample_id, r)
if r.sample_code:
receptions_by_code.setdefault(r.sample_code, r)
if receptions_by_sample_id or receptions_by_code:
rids = list(
{
*(r.id for r in receptions_by_sample_id.values()),
*(r.id for r in receptions_by_code.values()),
}
)
for p in (
db.query(BizSampleProductionReception)
.filter(
BizSampleProductionReception.tenant_no == tenant_no,
BizSampleProductionReception.del_flag == 0,
BizSampleProductionReception.reception_id.in_(rids),
)
.order_by(BizSampleProductionReception.process_time.desc())
.all()
):
productions.setdefault(p.reception_id, p)
protocol_ids = sorted(
{
s.collection_protocol_id
for s, *_ in filtered
if getattr(s, "collection_protocol_id", None)
}
)
protocols: Dict[str, BizCollectionProtocol] = {}
if protocol_ids:
for p in (
db.query(BizCollectionProtocol)
.filter(
BizCollectionProtocol.tenant_no == tenant_no,
BizCollectionProtocol.del_flag == 0,
BizCollectionProtocol.id.in_(protocol_ids),
)
.all()
):
protocols[p.id] = p
reception_ids_for_flags = list(
{
*(r.id for r in receptions_by_sample_id.values() if r and r.id),
*(r.id for r in receptions_by_code.values() if r and r.id),
}
)
check_flags_by_reception = _check_flag_map(db, tenant_no, reception_ids_for_flags)
legacy_codes: List[str] = []
for sample, *_ in filtered:
legacy_codes.append(sample.sample_code or "")
reception = receptions_by_sample_id.get(sample.id) or receptions_by_code.get(sample.sample_code or "")
if reception and reception.lab_storage_code:
legacy_codes.append(reception.lab_storage_code)
if reception and reception.sample_code:
legacy_codes.append(reception.sample_code)
legacy_supplements = _load_legacy_supplements(db, tenant_no, legacy_codes)
qc_tasks_map = _load_qc_tasks_map(db, tenant_no, legacy_codes)
def _compose_extra(*codes: Optional[str]) -> Dict[str, Any]:
picked = _pick_extra(extras, *codes)
legacy = {}
for c in codes:
key = (c or "").strip()
if key and key in legacy_supplements:
legacy = legacy_supplements[key]
break
merged = _merge_extra_with_legacy(picked, legacy)
for c in codes:
key = (c or "").strip()
if not key:
continue
for qk, qv in _qc_fields_for_dataset(dataset, qc_tasks_map.get(key) or []).items():
if qk not in merged or _is_blank(merged.get(qk)):
merged[qk] = qv
return merged
built_map: Dict[str, Dict[str, Any]] = {}
for sample, stype, party, coll, hospital in filtered:
code = sample.sample_code
reception = receptions_by_sample_id.get(sample.id) or receptions_by_code.get(code)
# 间充质台账以已接收样本为准,排除无接收的占位/草稿协议
if dataset == DATASET_MSC and reception is None:
continue
production = productions.get(reception.id) if reception else None
lab_code = (reception.lab_storage_code if reception else None) or ""
extra = _compose_extra(lab_code, code, reception.sample_code if reception else None)
protocol = protocols.get(sample.collection_protocol_id) if sample.collection_protocol_id else None
check_flags = check_flags_by_reception.get(reception.id, {}) if reception else {}
row = _build_row(
dataset=dataset,
sample=sample,
type_name=(stype.type_name if stype else None),
party=party,
coll=coll,
hospital=hospital,
reception=reception,
production=production,
extra=extra,
protocol=protocol,
check_flags=check_flags,
)
built_map[row.get("sampleCode") or code] = row
# 单个核:业务已发生数据主要在科研捐献接收(lab_storage_code=UBMC*),无细胞储存协议样本时仍需展示
if dataset == DATASET_UBMC:
ubmc_recs = (
db.query(BizSampleReception)
.filter(
BizSampleReception.tenant_no == tenant_no,
BizSampleReception.del_flag == 0,
BizSampleReception.lab_storage_code.like("UBMC%"),
)
.order_by(BizSampleReception.reception_time.desc())
.all()
)
mother_by_donation: Dict[str, str] = {}
donation_ids = sorted(
{r.research_donation_id for r in ubmc_recs if r.research_donation_id}
)
if donation_ids:
rows = db.execute(
text(
"SELECT research_donation_id, mother_name FROM biz_research_donation_party "
"WHERE del_flag=0 AND tenant_no=:tenant_no AND research_donation_id IN ("
+ ",".join(f":d{i}" for i in range(len(donation_ids)))
+ ")"
),
{
"tenant_no": tenant_no,
**{f"d{i}": did for i, did in enumerate(donation_ids)},
},
).fetchall()
for did, mother in rows:
if did and mother and did not in mother_by_donation:
mother_by_donation[str(did)] = mother
missing_rids = [
r.id
for r in ubmc_recs
if r.id and r.id not in productions
]
if missing_rids:
for p in (
db.query(BizSampleProductionReception)
.filter(
BizSampleProductionReception.tenant_no == tenant_no,
BizSampleProductionReception.del_flag == 0,
BizSampleProductionReception.reception_id.in_(missing_rids),
)
.order_by(BizSampleProductionReception.process_time.desc())
.all()
):
productions.setdefault(p.reception_id, p)
ubmc_flags = _check_flag_map(
db,
tenant_no,
[r.id for r in ubmc_recs if r.id and r.id not in check_flags_by_reception],
)
check_flags_by_reception.update(ubmc_flags)
ubmc_legacy_codes = [
(r.lab_storage_code or r.sample_code or "")
for r in ubmc_recs
]
ubmc_legacy = _load_legacy_supplements(db, tenant_no, ubmc_legacy_codes)
legacy_supplements.update(ubmc_legacy)
qc_tasks_map.update(_load_qc_tasks_map(db, tenant_no, ubmc_legacy_codes))
for reception in ubmc_recs:
code = (reception.lab_storage_code or reception.sample_code or "").strip()
if not code or code in built_map:
continue
production = productions.get(reception.id)
extra = _compose_extra(code, reception.sample_code)
mother = mother_by_donation.get(str(reception.research_donation_id or ""))
fake_sample = SimpleNamespace(
id=reception.donation_sample_id or reception.id,
sample_code=reception.sample_code or code,
collection_protocol_id=None,
)
fake_party = SimpleNamespace(mother_name=mother) if mother else None
built_map[code] = _build_row(
dataset=dataset,
sample=fake_sample,
type_name="脐血单个核细胞",
party=fake_party,
coll=None,
hospital=None,
reception=reception,
production=production,
extra=extra,
protocol=None,
check_flags=check_flags_by_reception.get(reception.id, {}),
)
# 间充质:科研捐献接收的 UC*/BC* 入库编码也需展示(无协议样本时)
if dataset == DATASET_MSC:
msc_recs = (
db.query(BizSampleReception)
.filter(
BizSampleReception.tenant_no == tenant_no,
BizSampleReception.del_flag == 0,
or_(
BizSampleReception.lab_storage_code.like("UC%"),
BizSampleReception.lab_storage_code.like("BC%"),
),
)
.order_by(BizSampleReception.reception_time.desc())
.all()
)
mother_by_donation: Dict[str, str] = {}
donation_ids = sorted(
{r.research_donation_id for r in msc_recs if r.research_donation_id}
)
if donation_ids:
rows = db.execute(
text(
"SELECT research_donation_id, mother_name FROM biz_research_donation_party "
"WHERE del_flag=0 AND tenant_no=:tenant_no AND research_donation_id IN ("
+ ",".join(f":d{i}" for i in range(len(donation_ids)))
+ ")"
),
{
"tenant_no": tenant_no,
**{f"d{i}": did for i, did in enumerate(donation_ids)},
},
).fetchall()
for did, mother in rows:
if did and mother and did not in mother_by_donation:
mother_by_donation[str(did)] = mother
missing_rids = [r.id for r in msc_recs if r.id and r.id not in productions]
if missing_rids:
for p in (
db.query(BizSampleProductionReception)
.filter(
BizSampleProductionReception.tenant_no == tenant_no,
BizSampleProductionReception.del_flag == 0,
BizSampleProductionReception.reception_id.in_(missing_rids),
)
.order_by(BizSampleProductionReception.process_time.desc())
.all()
):
productions.setdefault(p.reception_id, p)
msc_flags = _check_flag_map(
db,
tenant_no,
[r.id for r in msc_recs if r.id and r.id not in check_flags_by_reception],
)
check_flags_by_reception.update(msc_flags)
msc_legacy_codes = [
(r.lab_storage_code or r.sample_code or "") for r in msc_recs
]
legacy_supplements.update(_load_legacy_supplements(db, tenant_no, msc_legacy_codes))
qc_tasks_map.update(_load_qc_tasks_map(db, tenant_no, msc_legacy_codes))
donation_coll_map = _donation_collection_map(db, tenant_no, donation_ids)
for reception in msc_recs:
code = (reception.lab_storage_code or reception.sample_code or "").strip()
if not code or code in built_map:
continue
production = productions.get(reception.id)
extra = _compose_extra(code, reception.sample_code)
# 科研捐献:运输温度缺省时用接收到样温度
if _is_blank(extra.get("transportTempC")) and not _is_blank(
reception.receive_temperature
):
extra["transportTempC"] = reception.receive_temperature
mother = mother_by_donation.get(str(reception.research_donation_id or ""))
donation_coll = donation_coll_map.get(str(reception.research_donation_id or ""))
fake_sample = SimpleNamespace(
id=reception.donation_sample_id or reception.id,
sample_code=reception.sample_code or code,
collection_protocol_id=None,
)
fake_party = SimpleNamespace(mother_name=mother) if mother else None
built_map[code] = _build_row(
dataset=dataset,
sample=fake_sample,
type_name="脐带间充质干细胞",
party=fake_party,
coll=donation_coll,
hospital=None,
reception=reception,
production=production,
extra=extra,
protocol=None,
check_flags=check_flags_by_reception.get(reception.id, {}),
)
kw = (keyword or "").strip()
built = []
for row in built_map.values():
if not _match_keyword(row, kw):
continue
if not _match_date_range(row, date_from, date_to):
continue
built.append(row)
# 稳定排序:优先按样本编码
built.sort(key=lambda r: str(r.get("sampleCode") or ""))
total = len(built)
page = max(1, int(page or 1))
page_size = max(1, min(5000, int(page_size or 20)))
start = (page - 1) * page_size
return built[start : start + page_size], total
def _build_row(
*,
dataset: str,
sample: Optional[BizProtocolSample],
type_name: Optional[str],
party: Optional[BizProtocolPartyCustomer],
coll: Optional[BizProtocolCollectionInfo],
hospital: Optional[BizCollectionHospital],
reception: Optional[BizSampleReception],
production: Optional[BizSampleProductionReception],
extra: Dict[str, Any],
protocol: Optional[BizCollectionProtocol] = None,
check_flags: Optional[Dict[str, bool]] = None,
) -> Dict[str, Any]:
flags = check_flags or {}
protocol_code = (
(sample.sample_code if sample else None)
or extra.get("protocolSampleCode")
or ""
)
lab_code = ((reception.lab_storage_code if reception else None) or "").strip()
# 台账「细胞编码」展示实验室入库编码(B000…),协议样本号保留检索
display_code = lab_code if lab_code and len(lab_code) > 1 else (
protocol_code or extra.get("sampleCode") or ""
)
protocol_vals = {
"sample_code": display_code,
"mother_name": party.mother_name if party else None,
"sample_collect_time": coll.sample_collect_time if coll else None,
"hospital_name": hospital.hospital_name if hospital else None,
}
reception_vals = {}
if reception:
reception_vals = {
"reception_time": reception.reception_time,
"receive_temperature": reception.receive_temperature,
"handler_name": reception.handler_name,
"remark": reception.remark,
"lab_storage_code": reception.lab_storage_code,
}
production_vals = {}
if production:
production_vals = {
"original_blood_weight_g": production.original_blood_weight_g,
"hes_volume_ml": production.hes_volume_ml,
"blood_type": production.blood_type,
"infectious_four_test_result": production.infectious_four_test_result,
"enriched_blood_weight_g": production.enriched_blood_weight_g,
"cryopreservation_volume_ml": production.cryopreservation_volume_ml,
"handler_name": production.handler_name,
"remark": production.remark,
"original_blood_volume_ml": production.original_blood_volume_ml,
"nucleated_cell_count_1e8": production.nucleated_cell_count_1e8,
"inoculation_amount": production.inoculation_amount,
"cryopreservation_time": production.cryopreservation_time,
"storage_total_tubes": production.storage_total_tubes,
"available_cell_count_1e7": production.available_cell_count_1e7,
}
data: Dict[str, Any] = {
"id": (sample.id if sample else f"extra:{dataset}:{display_code}"),
"dataset": dataset,
"sampleCode": display_code,
"protocolSampleCode": protocol_code or None,
"sampleTypeName": type_name,
"protocolSampleId": sample.id if sample else None,
"collectionProtocolId": sample.collection_protocol_id if sample else None,
"receptionId": reception.id if reception else None,
"productionReceptionId": production.id if production else None,
"fromExtraOnly": sample is None,
}
for f in fields_for(dataset):
val = None
if f.source == SOURCE_PROTOCOL:
val = protocol_vals.get(f.attr)
elif f.source == SOURCE_RECEPTION:
val = reception_vals.get(f.attr)
elif f.source == SOURCE_PRODUCTION:
val = production_vals.get(f.attr)
elif f.source == SOURCE_EXTRA:
# 仅源系统没有的列走补充表;业务源表字段不被补充表覆盖
val = extra.get(f.key)
# 源表为空时,允许用新/旧补充表填空(旧台账联动)
if f.source != SOURCE_EXTRA and _is_blank(val):
fallback = extra.get(f.key)
if not _is_blank(fallback):
val = fallback
if isinstance(val, datetime):
val = _fmt_dt(val)
elif isinstance(val, Decimal):
val = _fmt_num(val)
data[f.key] = val
data["sampleCode"] = display_code
# 从协议采集信息 / 附件 / 收样检查项推导补充列(补充表有值时不覆盖)
if not data.get("hasMaternalBlood"):
if coll is not None:
data["hasMaternalBlood"] = "是" if coll.maternal_blood_collect_time else "否"
if not data.get("hasConsent"):
if protocol and (protocol.consent_form_urls or "").strip():
data["hasConsent"] = "是"
else:
flagged = _yes_no_from_bool(flags.get("知情同意书"))
if flagged is not None:
data["hasConsent"] = flagged
elif protocol is not None:
data["hasConsent"] = "否"
if not data.get("hasHealthSurvey"):
if protocol and (protocol.health_survey_form_urls or "").strip():
data["hasHealthSurvey"] = "是"
else:
flagged = _yes_no_from_bool(flags.get("健康调查表"))
if flagged is not None:
data["hasHealthSurvey"] = flagged
elif protocol is not None:
data["hasHealthSurvey"] = "否"
if not data.get("labelClear"):
data["labelClear"] = _yes_no_from_bool(flags.get("样本信息正确,标识清晰"))
if not data.get("receiveCheck"):
data["receiveCheck"] = _derive_receive_check(flags)
# 间充质运输温度:补充表为空时回落到接收到样温度
if _is_blank(data.get("transportTempC")) and reception is not None:
temp = reception.receive_temperature
if not _is_blank(temp):
data["transportTempC"] = temp
if data.get("infectiousFourCard") is not None:
data["infectiousFourCard"] = _infectious_label(data.get("infectiousFourCard"))
return data
def upsert_extra_payload(
db: Session,
*,
tenant_no: str,
dataset: str,
sample_code: str,
payload: Dict[str, Any],
operator_id: str = "import",
replace: bool = False,
) -> None:
from app.models.models import BizLabSampleProcessingExtra
code = (sample_code or "").strip()
if not code:
raise ValueError("样本编码不能为空")
now = datetime.now()
row = (
db.query(BizLabSampleProcessingExtra)
.filter(
BizLabSampleProcessingExtra.tenant_no == tenant_no,
BizLabSampleProcessingExtra.dataset == dataset,
BizLabSampleProcessingExtra.sample_code == code,
BizLabSampleProcessingExtra.del_flag == 0,
)
.first()
)
current = {} if replace else _parse_extra(row.payload_json if row else None)
for k, v in (payload or {}).items():
if v in (None, ""):
continue
current[k] = v
current["sampleCode"] = code
if row is None:
db.add(
BizLabSampleProcessingExtra(
id=_new_id(),
dataset=dataset,
sample_code=code,
payload_json=json.dumps(current, ensure_ascii=False),
tenant_no=tenant_no,
create_by=operator_id,
create_dept_id="",
create_time=now,
update_by=operator_id,
update_time=now,
del_flag=0,
)
)
else:
row.payload_json = json.dumps(current, ensure_ascii=False)
row.update_by = operator_id
row.update_time = now
def update_row(
db: Session,
*,
tenant_no: str,
dataset: str,
sample_code: str,
payload: Dict[str, Any],
editable_depts: set,
operator_id: str,
) -> Dict[str, Any]:
code = (sample_code or "").strip()
if not code:
raise ValueError("样本编码不能为空")
fmap = field_map(dataset)
if not fmap:
raise ValueError("未知数据集")
denied = []
accepted: Dict[str, Any] = {}
for key, value in (payload or {}).items():
f = fmap.get(key)
if not f:
continue
if f.dept not in editable_depts:
denied.append(key)
continue
accepted[key] = value
if denied:
raise PermissionError(f"无权编辑字段: {', '.join(denied)}")
if not accepted:
raise ValueError("没有可保存的字段")
reception = (
db.query(BizSampleReception)
.filter(
BizSampleReception.tenant_no == tenant_no,
BizSampleReception.del_flag == 0,
or_(
BizSampleReception.lab_storage_code == code,
BizSampleReception.sample_code == code,
),
)
.order_by(BizSampleReception.reception_time.desc())
.first()
)
sample = (
db.query(BizProtocolSample)
.filter(
BizProtocolSample.tenant_no == tenant_no,
BizProtocolSample.sample_code == code,
BizProtocolSample.del_flag == 0,
)
.first()
)
if sample is None and reception is not None and reception.protocol_sample_id:
sample = (
db.query(BizProtocolSample)
.filter(
BizProtocolSample.tenant_no == tenant_no,
BizProtocolSample.id == reception.protocol_sample_id,
BizProtocolSample.del_flag == 0,
)
.first()
)
if reception is None and sample is not None:
reception = (
db.query(BizSampleReception)
.filter(
BizSampleReception.tenant_no == tenant_no,
BizSampleReception.del_flag == 0,
or_(
BizSampleReception.protocol_sample_id == sample.id,
BizSampleReception.sample_code == sample.sample_code,
),
)
.order_by(BizSampleReception.reception_time.desc())
.first()
)
production = None
if reception:
production = (
db.query(BizSampleProductionReception)
.filter(
BizSampleProductionReception.tenant_no == tenant_no,
BizSampleProductionReception.reception_id == reception.id,
BizSampleProductionReception.del_flag == 0,
)
.order_by(BizSampleProductionReception.process_time.desc())
.first()
)
party = None
coll = None
if sample:
party = (
db.query(BizProtocolPartyCustomer)
.filter(
BizProtocolPartyCustomer.collection_protocol_id == sample.collection_protocol_id,
BizProtocolPartyCustomer.tenant_no == tenant_no,
BizProtocolPartyCustomer.del_flag == 0,
)
.first()
)
coll = (
db.query(BizProtocolCollectionInfo)
.filter(
BizProtocolCollectionInfo.collection_protocol_id == sample.collection_protocol_id,
BizProtocolCollectionInfo.tenant_no == tenant_no,
BizProtocolCollectionInfo.del_flag == 0,
)
.first()
)
now = datetime.now()
extra_payload: Dict[str, Any] = {}
def _set_dt(obj, attr, value):
if value in (None, ""):
setattr(obj, attr, None)
return
if isinstance(value, datetime):
setattr(obj, attr, value)
return
text = str(value).strip().replace("/", "-").replace(".", "-")
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%d"):
try:
setattr(obj, attr, datetime.strptime(text, fmt))
return
except ValueError:
continue
setattr(obj, attr, value)
for key, value in accepted.items():
f = fmap[key]
wrote = False
if f.source == SOURCE_PROTOCOL and sample is not None:
if f.attr == "mother_name" and party is not None:
party.mother_name = None if value in (None, "") else str(value)
party.update_by = operator_id
party.update_time = now
wrote = True
elif f.attr == "sample_collect_time" and coll is not None:
_set_dt(coll, "sample_collect_time", value)
coll.update_by = operator_id
coll.update_time = now
wrote = True
elif f.source == SOURCE_RECEPTION and reception is not None:
if f.attr == "reception_time":
_set_dt(reception, f.attr, value)
elif f.attr in {"receive_temperature", "handler_name", "remark", "lab_storage_code"}:
setattr(reception, f.attr, None if value in (None, "") else value)
reception.update_by = operator_id
reception.update_time = now
wrote = True
elif f.source == SOURCE_PRODUCTION and production is not None:
numeric_attrs = {
"original_blood_weight_g",
"hes_volume_ml",
"enriched_blood_weight_g",
"cryopreservation_volume_ml",
"original_blood_volume_ml",
"nucleated_cell_count_1e8",
"available_cell_count_1e7",
}
if f.attr == "cryopreservation_time":
_set_dt(production, f.attr, value)
elif f.attr == "storage_total_tubes":
production.storage_total_tubes = None if value in (None, "") else int(value)
elif f.attr in numeric_attrs:
production_val = None if value in (None, "") else Decimal(str(value))
setattr(production, f.attr, production_val)
elif f.attr == "infectious_four_test_result":
production.infectious_four_test_result = (
None if value in (None, "") else _infectious_code(value)
)
elif f.attr in {
"blood_type",
"handler_name",
"remark",
"inoculation_amount",
}:
setattr(production, f.attr, None if value in (None, "") else value)
production.update_by = operator_id
production.update_time = now
wrote = True
# 补充表始终保留一份,保证导入台账与无权源表场景可编辑
extra_payload[key] = value
_ = wrote
upsert_extra_payload(
db,
tenant_no=tenant_no,
dataset=dataset,
sample_code=code,
payload=extra_payload,
operator_id=operator_id,
replace=False,
)
legacy_code = (
(reception.lab_storage_code if reception else None)
or (sample.sample_code if sample else None)
or code
)
sync_processing_to_legacy_supplement(
db,
tenant_no=tenant_no,
lab_storage_code=str(legacy_code),
sample_type_name=None,
payload=extra_payload,
operator_id=operator_id,
)
db.commit()
rows, _ = list_rows(db, tenant_no=tenant_no, dataset=dataset, keyword=code, page=1, page_size=20)
for row in rows:
if row.get("sampleCode") == code or row.get("protocolSampleCode") == code:
return row
raise LookupError("保存成功但读取失败")
def export_rows(
db: Session,
*,
tenant_no: str,
dataset: str,
keyword: Optional[str] = None,
date_from: Optional[str] = None,
date_to: Optional[str] = None,
) -> Tuple[List[str], List[List[Any]]]:
rows, _ = list_rows(
db,
tenant_no=tenant_no,
dataset=dataset,
keyword=keyword,
date_from=date_from,
date_to=date_to,
page=1,
page_size=5000,
)
headers = [f.label for f in fields_for(dataset)]
keys = [f.key for f in fields_for(dataset)]
body = [[row.get(k) for k in keys] for row in rows]
return headers, body