feat(backend): enrich sample freight list with items and filters

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-10-08 09:58:45 +08:00
1 parent 04eb1e81bd
commit 971af93132
3 files changed
+309 -8

No files matched your search

+12
View File
@@ -69,6 +69,11 @@ async def list_sample_freights(
page: int = Query(1, ge=1),
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
keyword: Optional[str] = None,
transportMode: Optional[str] = None,
sampleSource: Optional[str] = None,
createTimeFrom: Optional[str] = None,
createTimeTo: Optional[str] = None,
includeItems: bool = Query(False),
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
@@ -80,12 +85,19 @@ async def list_sample_freights(
page=page,
page_size=pageSize,
keyword=keyword,
transport_mode=transportMode,
sample_source=sampleSource,
create_time_from=createTimeFrom,
create_time_to=createTimeTo,
include_items=includeItems,
)
return success_response(
{"list": items, "total": total, "page": page, "pageSize": pageSize}
)
except PermissionError as e:
return error_response(403, str(e))
except ValueError as e:
return error_response(400, str(e))
@router.get("/sample-freights/{freight_id}")
+253 -8
View File
@@ -2,15 +2,23 @@
from __future__ import annotations
from datetime import datetime
from datetime import datetime, timedelta
from decimal import Decimal
from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import and_, func, or_
from sqlalchemy import and_, exists, func, or_
from sqlalchemy.orm import Session
from app.core.utils import generate_uuid
from app.models.models import BizSampleFreight, BizSampleFreightItem, SysDictData
from app.models.models import (
BizCellSampleType,
BizCollectionProtocol,
BizProtocolSample,
BizSampleFreight,
BizSampleFreightItem,
SysDictData,
)
from app.models.research_donation import BizResearchDonation, BizResearchDonationSample
from app.services.sample_transport_mode import DICT_TYPE, resolve_transport_mode_value
SAMPLE_SOURCE_CELL = "cell_storage"
@@ -127,6 +135,128 @@ def _item_to_dict(item: BizSampleFreightItem) -> Dict[str, Any]:
}
def _item_to_dict_enriched(
item: BizSampleFreightItem,
*,
protocol_no: str = "",
sample_type_name: str = "",
) -> Dict[str, Any]:
data = _item_to_dict(item)
data["protocolNo"] = protocol_no or ""
data["sampleTypeName"] = sample_type_name or ""
return data
def _normalize_date_bound(value: str, *, end_of_day: bool = False) -> datetime:
raw = (value or "").strip()
if not raw:
raise ValueError("时间参数无效")
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%d"):
try:
dt = datetime.strptime(raw, fmt)
if fmt == "%Y-%m-%d" and end_of_day:
dt = dt + timedelta(days=1) - timedelta(seconds=1)
return dt
except ValueError:
continue
try:
return datetime.fromisoformat(raw.replace("Z", "+00:00")).replace(tzinfo=None)
except ValueError as exc:
raise ValueError("时间参数格式无效") from exc
def _enrich_items_maps(
db: Session,
items: List[BizSampleFreightItem],
tenant_no: str,
) -> Tuple[Dict[str, str], Dict[str, str]]:
"""批量解析 protocolNo / sampleTypeName,key = sample_source|sample_id。"""
protocol_no_map: Dict[str, str] = {}
type_name_map: Dict[str, str] = {}
if not items:
return protocol_no_map, type_name_map
cell_ids = [
i.sample_id
for i in items
if i.sample_source == SAMPLE_SOURCE_CELL and i.sample_id
]
donation_ids = [
i.sample_id
for i in items
if i.sample_source == SAMPLE_SOURCE_DONATION and i.sample_id
]
if cell_ids:
rows = (
db.query(
BizProtocolSample.id,
BizCollectionProtocol.protocol_no,
BizCellSampleType.type_name,
)
.outerjoin(
BizCollectionProtocol,
and_(
BizCollectionProtocol.id == BizProtocolSample.collection_protocol_id,
BizCollectionProtocol.del_flag == 0,
),
)
.outerjoin(
BizCellSampleType,
and_(
BizCellSampleType.id == BizProtocolSample.sample_type_id,
BizCellSampleType.del_flag == 0,
),
)
.filter(
BizProtocolSample.id.in_(cell_ids),
BizProtocolSample.tenant_no == tenant_no,
BizProtocolSample.del_flag == 0,
)
.all()
)
for sid, pno, tname in rows:
key = f"{SAMPLE_SOURCE_CELL}|{sid}"
protocol_no_map[key] = pno or ""
type_name_map[key] = tname or ""
if donation_ids:
rows = (
db.query(
BizResearchDonationSample.id,
BizResearchDonation.donation_no,
BizCellSampleType.type_name,
)
.outerjoin(
BizResearchDonation,
and_(
BizResearchDonation.id
== BizResearchDonationSample.research_donation_id,
BizResearchDonation.del_flag == 0,
),
)
.outerjoin(
BizCellSampleType,
and_(
BizCellSampleType.id == BizResearchDonationSample.sample_type_id,
BizCellSampleType.del_flag == 0,
),
)
.filter(
BizResearchDonationSample.id.in_(donation_ids),
BizResearchDonationSample.tenant_no == tenant_no,
BizResearchDonationSample.del_flag == 0,
)
.all()
)
for sid, dno, tname in rows:
key = f"{SAMPLE_SOURCE_DONATION}|{sid}"
protocol_no_map[key] = dno or ""
type_name_map[key] = tname or ""
return protocol_no_map, type_name_map
def get_freight(db: Session, freight_id: str, tenant_no: str) -> Optional[BizSampleFreight]:
return (
db.query(BizSampleFreight)
@@ -146,6 +276,11 @@ def list_freights(
page: int = 1,
page_size: int = 10,
keyword: Optional[str] = None,
transport_mode: Optional[str] = None,
sample_source: Optional[str] = None,
create_time_from: Optional[str] = None,
create_time_to: Optional[str] = None,
include_items: bool = False,
) -> Tuple[List[Dict[str, Any]], int]:
page = max(1, int(page or 1))
page_size = max(1, min(100, int(page_size or 10)))
@@ -153,6 +288,36 @@ def list_freights(
BizSampleFreight.tenant_no == tenant_no,
BizSampleFreight.del_flag == 0,
)
mode = (transport_mode or "").strip()
if mode:
q = q.filter(BizSampleFreight.transport_mode == mode)
src = (sample_source or "").strip()
if src:
if src not in (SAMPLE_SOURCE_CELL, SAMPLE_SOURCE_DONATION):
raise ValueError("sampleSource 无效")
item_exists = exists().where(
and_(
BizSampleFreightItem.freight_id == BizSampleFreight.id,
BizSampleFreightItem.tenant_no == tenant_no,
BizSampleFreightItem.del_flag == 0,
BizSampleFreightItem.sample_source == src,
)
)
q = q.filter(item_exists)
if create_time_from and str(create_time_from).strip():
q = q.filter(
BizSampleFreight.create_time
>= _normalize_date_bound(str(create_time_from).strip())
)
if create_time_to and str(create_time_to).strip():
q = q.filter(
BizSampleFreight.create_time
<= _normalize_date_bound(str(create_time_to).strip(), end_of_day=True)
)
kw = (keyword or "").strip()
if kw:
like = f"%{kw}%"
@@ -170,7 +335,47 @@ def list_freights(
conds = [BizSampleFreight.transport_mode.like(like)]
if label_values:
conds.append(BizSampleFreight.transport_mode.in_(label_values))
sample_code_exists = exists().where(
and_(
BizSampleFreightItem.freight_id == BizSampleFreight.id,
BizSampleFreightItem.tenant_no == tenant_no,
BizSampleFreightItem.del_flag == 0,
BizSampleFreightItem.sample_code.like(like),
)
)
conds.append(sample_code_exists)
cell_proto_exists = exists().where(
and_(
BizSampleFreightItem.freight_id == BizSampleFreight.id,
BizSampleFreightItem.tenant_no == tenant_no,
BizSampleFreightItem.del_flag == 0,
BizSampleFreightItem.sample_source == SAMPLE_SOURCE_CELL,
BizSampleFreightItem.protocol_id == BizCollectionProtocol.id,
BizCollectionProtocol.tenant_no == tenant_no,
BizCollectionProtocol.del_flag == 0,
BizCollectionProtocol.protocol_no.like(like),
)
)
conds.append(cell_proto_exists)
donation_proto_exists = exists().where(
and_(
BizSampleFreightItem.freight_id == BizSampleFreight.id,
BizSampleFreightItem.tenant_no == tenant_no,
BizSampleFreightItem.del_flag == 0,
BizSampleFreightItem.sample_source == SAMPLE_SOURCE_DONATION,
BizSampleFreightItem.protocol_id == BizResearchDonation.id,
BizResearchDonation.tenant_no == tenant_no,
BizResearchDonation.del_flag == 0,
BizResearchDonation.donation_no.like(like),
)
)
conds.append(donation_proto_exists)
q = q.filter(or_(*conds))
total = q.count()
rows = (
q.order_by(BizSampleFreight.create_time.desc(), BizSampleFreight.id.desc())
@@ -179,14 +384,54 @@ def list_freights(
.all()
)
labels = _label_map(db, tenant_no, [r.transport_mode for r in rows])
out = [
_row_to_dict(
items_by_freight: Dict[str, List[BizSampleFreightItem]] = {}
if include_items and rows:
freight_ids = [r.id for r in rows]
all_items = (
db.query(BizSampleFreightItem)
.filter(
BizSampleFreightItem.freight_id.in_(freight_ids),
BizSampleFreightItem.tenant_no == tenant_no,
BizSampleFreightItem.del_flag == 0,
)
.order_by(
BizSampleFreightItem.create_time.asc(),
BizSampleFreightItem.id.asc(),
)
.all()
)
for it in all_items:
items_by_freight.setdefault(it.freight_id, []).append(it)
protocol_no_map, type_name_map = _enrich_items_maps(db, all_items, tenant_no)
else:
protocol_no_map, type_name_map = {}, {}
out: List[Dict[str, Any]] = []
for r in rows:
linked = (
len(items_by_freight.get(r.id, []))
if include_items
else _linked_count(db, r.id, tenant_no)
)
data = _row_to_dict(
r,
linked_sample_count=_linked_count(db, r.id, tenant_no),
linked_sample_count=linked,
transport_mode_label=labels.get(r.transport_mode or "", r.transport_mode or ""),
)
for r in rows
]
if include_items:
enriched = []
for it in items_by_freight.get(r.id, []):
key = f"{it.sample_source}|{it.sample_id}"
enriched.append(
_item_to_dict_enriched(
it,
protocol_no=protocol_no_map.get(key, ""),
sample_type_name=type_name_map.get(key, ""),
)
)
data["items"] = enriched
out.append(data)
return out, total
+44
View File
@@ -61,5 +61,49 @@ class TestFreightAmountValidation(unittest.TestCase):
_parse_amount("")
class TestFreightListHelpers(unittest.TestCase):
def test_item_dict_has_enrichment_keys(self):
from app.crud.sample_freight import _item_to_dict_enriched
class I:
id = "i1"
freight_id = "f1"
sample_source = "cell_storage"
sample_id = "s1"
sample_code = "CODE1"
protocol_id = "p1"
d = _item_to_dict_enriched(
I(),
protocol_no="CP001",
sample_type_name="脐带血",
)
self.assertEqual(d["protocolNo"], "CP001")
self.assertEqual(d["sampleTypeName"], "脐带血")
self.assertEqual(d["sampleSource"], "cell_storage")
class TestFreightListHelpers(unittest.TestCase):
def test_item_dict_has_enrichment_keys(self):
from app.crud.sample_freight import _item_to_dict_enriched
class I:
id = "i1"
freight_id = "f1"
sample_source = "cell_storage"
sample_id = "s1"
sample_code = "CODE1"
protocol_id = "p1"
d = _item_to_dict_enriched(
I(),
protocol_no="CP001",
sample_type_name="脐带血",
)
self.assertEqual(d["protocolNo"], "CP001")
self.assertEqual(d["sampleTypeName"], "脐带血")
self.assertEqual(d["sampleSource"], "cell_storage")
if __name__ == "__main__":
unittest.main()