From f092b64a33f20abe1ac71ea97e6da9595bc05f47 Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Thu, 8 Oct 2026 09:59:29 +0800 Subject: [PATCH] feat(backend): add sample options for freight config dialog Co-authored-by: Cursor --- backend/app/api/sample_freight.py | 28 +++++ backend/app/crud/sample_freight.py | 169 ++++++++++++++++++++++++++++- 2 files changed, 196 insertions(+), 1 deletion(-) diff --git a/backend/app/api/sample_freight.py b/backend/app/api/sample_freight.py index 3422c80c..b61ae5d5 100644 --- a/backend/app/api/sample_freight.py +++ b/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, diff --git a/backend/app/crud/sample_freight.py b/backend/app/crud/sample_freight.py index 938ce4ed..a8283a86 100644 --- a/backend/app/crud/sample_freight.py +++ b/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, *,