feat(backend): enrich sample freight list with items and filters
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
04eb1e81bd
commit
971af93132
3 files changed
+309
-8
No files matched your search
@@ -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}")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user