Browse Source

feat(backend): add sample options for freight config dialog

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 3 weeks ago
parent
commit
f092b64a33
2 changed files with 196 additions and 1 deletions
  1. 28 0
      backend/app/api/sample_freight.py
  2. 168 1
      backend/app/crud/sample_freight.py

+ 28 - 0
backend/app/api/sample_freight.py

@@ -100,6 +100,34 @@ async def list_sample_freights(
         return error_response(400, str(e))
 
 
+@router.get("/sample-freights/sample-options")
+async def list_sample_freight_options(
+    page: int = Query(1, ge=1),
+    pageSize: int = Query(20, ge=1, le=100, alias="pageSize"),
+    keyword: Optional[str] = None,
+    sampleSource: Optional[str] = None,
+    user: dict = Depends(require_admin_or_super_admin),
+    db: Session = Depends(get_db),
+):
+    try:
+        tenant_no = _get_tenant_no(db, user)
+        items, total = freight_svc.list_sample_options(
+            db,
+            tenant_no=tenant_no,
+            page=page,
+            page_size=pageSize,
+            keyword=keyword,
+            sample_source=sampleSource,
+        )
+        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}")
 async def get_sample_freight(
     freight_id: str,

+ 168 - 1
backend/app/crud/sample_freight.py

@@ -455,10 +455,177 @@ def get_freight_detail(db: Session, freight_id: str, tenant_no: str) -> Dict[str
         linked_sample_count=len(items),
         transport_mode_label=labels.get(obj.transport_mode or "", obj.transport_mode or ""),
     )
-    data["items"] = [_item_to_dict(i) for i in items]
+    protocol_no_map, type_name_map = _enrich_items_maps(db, items, tenant_no)
+    enriched = []
+    for i in items:
+        key = f"{i.sample_source}|{i.sample_id}"
+        enriched.append(
+            _item_to_dict_enriched(
+                i,
+                protocol_no=protocol_no_map.get(key, ""),
+                sample_type_name=type_name_map.get(key, ""),
+            )
+        )
+    data["items"] = enriched
     return data
 
 
+def list_sample_options(
+    db: Session,
+    *,
+    tenant_no: str,
+    page: int = 1,
+    page_size: int = 20,
+    keyword: Optional[str] = None,
+    sample_source: Optional[str] = None,
+) -> Tuple[List[Dict[str, Any]], int]:
+    """可选样本列表(配置运费弹窗),支持细胞存储 / 科研捐赠。"""
+    page = max(1, int(page or 1))
+    page_size = max(1, min(100, int(page_size or 20)))
+    src = (sample_source or "").strip() or None
+    if src and src not in (SAMPLE_SOURCE_CELL, SAMPLE_SOURCE_DONATION):
+        raise ValueError("sampleSource 无效")
+    kw = (keyword or "").strip()
+    like = f"%{kw}%" if kw else None
+
+    rows: List[Dict[str, Any]] = []
+
+    if src in (None, SAMPLE_SOURCE_CELL):
+        cq = (
+            db.query(
+                BizProtocolSample.id,
+                BizProtocolSample.sample_code,
+                BizProtocolSample.collection_protocol_id,
+                BizProtocolSample.create_time,
+                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.tenant_no == tenant_no,
+                BizProtocolSample.del_flag == 0,
+            )
+        )
+        if like:
+            cq = cq.filter(
+                or_(
+                    BizProtocolSample.sample_code.like(like),
+                    BizCollectionProtocol.protocol_no.like(like),
+                )
+            )
+        for sid, scode, pid, ctime, pno, tname in cq.all():
+            rows.append(
+                {
+                    "sampleSource": SAMPLE_SOURCE_CELL,
+                    "sampleId": sid,
+                    "sampleCode": scode or "",
+                    "protocolId": pid or "",
+                    "protocolNo": pno or "",
+                    "sampleTypeName": tname or "",
+                    "createTime": ctime,
+                    "freightId": "",
+                }
+            )
+
+    if src in (None, SAMPLE_SOURCE_DONATION):
+        dq = (
+            db.query(
+                BizResearchDonationSample.id,
+                BizResearchDonationSample.sample_code,
+                BizResearchDonationSample.research_donation_id,
+                BizResearchDonationSample.create_time,
+                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.tenant_no == tenant_no,
+                BizResearchDonationSample.del_flag == 0,
+            )
+        )
+        if like:
+            dq = dq.filter(
+                or_(
+                    BizResearchDonationSample.sample_code.like(like),
+                    BizResearchDonation.donation_no.like(like),
+                )
+            )
+        for sid, scode, pid, ctime, dno, tname in dq.all():
+            rows.append(
+                {
+                    "sampleSource": SAMPLE_SOURCE_DONATION,
+                    "sampleId": sid,
+                    "sampleCode": scode or "",
+                    "protocolId": pid or "",
+                    "protocolNo": dno or "",
+                    "sampleTypeName": tname or "",
+                    "createTime": ctime,
+                    "freightId": "",
+                }
+            )
+
+    # 左连当前运费关联
+    if rows:
+        sample_ids = [r["sampleId"] for r in rows]
+        link_rows = (
+            db.query(
+                BizSampleFreightItem.sample_source,
+                BizSampleFreightItem.sample_id,
+                BizSampleFreightItem.freight_id,
+            )
+            .filter(
+                BizSampleFreightItem.tenant_no == tenant_no,
+                BizSampleFreightItem.del_flag == 0,
+                BizSampleFreightItem.sample_id.in_(sample_ids),
+            )
+            .all()
+        )
+        freight_map = {
+            f"{src_}|{sid}": fid for src_, sid, fid in link_rows if src_ and sid and fid
+        }
+        for r in rows:
+            r["freightId"] = freight_map.get(f"{r['sampleSource']}|{r['sampleId']}", "") or ""
+
+    rows.sort(
+        key=lambda r: (r["createTime"] or datetime.min, r["sampleId"]),
+        reverse=True,
+    )
+    total = len(rows)
+    start = (page - 1) * page_size
+    page_rows = rows[start : start + page_size]
+    for r in page_rows:
+        r["createTime"] = _fmt_dt(r.get("createTime"))
+    return page_rows, total
+
+
 def _upsert_item(
     db: Session,
     *,