Przeglądaj źródła

feat(backend): expose sample freight APIs and wire protocol saves

Register admin sample-freight routes, enrich protocol sample details with freight, and stop writing legacy transport rows on save.

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 3 tygodni temu
rodzic
commit
468a7c5e12

+ 2 - 0
backend/app/api/__init__.py

@@ -8,6 +8,7 @@ from app.api.collection_protocol import router as collection_protocol_router
 from app.api.protocol_sample_payment import router as protocol_sample_payment_router
 from app.api.protocol_refund_record import router as protocol_refund_record_router
 from app.api.presign_order import router as presign_order_router
+from app.api.sample_freight import router as sample_freight_router
 from app.api.leave import router as leave_router
 from app.api.operations import router as operations_router
 from app.api.ethnic import router as ethnic_router
@@ -50,6 +51,7 @@ __all__ = [
     "protocol_sample_payment_router",
     "protocol_refund_record_router",
     "presign_order_router",
+    "sample_freight_router",
     "leave_router",
     "operations_router",
     "ethnic_router",

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

@@ -0,0 +1,210 @@
+"""样本运费批次 API"""
+
+from __future__ import annotations
+
+from typing import Any, Dict, List, Optional
+
+from fastapi import APIRouter, Depends, Query
+from pydantic import BaseModel, ConfigDict, Field
+from sqlalchemy.orm import Session
+
+from app.core.database import get_db
+from app.core.menu_guard import require_admin_or_super_admin
+from app.core.response import error_response, success_response
+from app.crud import sample_freight as freight_svc
+from app.models.models import SysUser
+
+router = APIRouter(prefix="/web/admin", tags=["样本运费批次"])
+
+
+def _get_tenant_no(db: Session, user: dict) -> str:
+    user_id = user.get("user_id")
+    if not user_id:
+        raise PermissionError("用户身份信息缺失,无法确定租户")
+    sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
+    if not sys_user or not sys_user.tenant_no:
+        raise PermissionError("用户租户信息缺失")
+    return sys_user.tenant_no
+
+
+def _get_current_user_id(user: dict) -> str:
+    user_id = user.get("user_id")
+    if not user_id:
+        raise PermissionError("用户身份信息缺失")
+    return str(user_id)
+
+
+class SampleFreightItemBody(BaseModel):
+    model_config = ConfigDict(extra="ignore", populate_by_name=True)
+
+    sample_source: str = Field(..., alias="sampleSource")
+    sample_id: str = Field(..., alias="sampleId")
+    protocol_id: Optional[str] = Field(None, alias="protocolId")
+    sample_code: Optional[str] = Field(None, alias="sampleCode")
+
+
+class SampleFreightCreateBody(BaseModel):
+    model_config = ConfigDict(extra="ignore", populate_by_name=True)
+
+    transport_mode: str = Field(..., alias="transportMode")
+    freight_amount: float = Field(..., alias="freightAmount")
+    items: Optional[List[SampleFreightItemBody]] = None
+
+
+class SampleFreightUpdateBody(BaseModel):
+    model_config = ConfigDict(extra="ignore", populate_by_name=True)
+
+    transport_mode: Optional[str] = Field(None, alias="transportMode")
+    freight_amount: Optional[float] = Field(None, alias="freightAmount")
+
+
+class SampleFreightItemsBody(BaseModel):
+    model_config = ConfigDict(extra="ignore", populate_by_name=True)
+
+    items: List[SampleFreightItemBody] = Field(default_factory=list)
+
+
+@router.get("/sample-freights")
+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,
+    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_freights(
+            db,
+            tenant_no=tenant_no,
+            page=page,
+            page_size=pageSize,
+            keyword=keyword,
+        )
+        return success_response(
+            {"list": items, "total": total, "page": page, "pageSize": pageSize}
+        )
+    except PermissionError as e:
+        return error_response(403, str(e))
+
+
+@router.get("/sample-freights/{freight_id}")
+async def get_sample_freight(
+    freight_id: str,
+    user: dict = Depends(require_admin_or_super_admin),
+    db: Session = Depends(get_db),
+):
+    try:
+        tenant_no = _get_tenant_no(db, user)
+        data = freight_svc.get_freight_detail(db, freight_id, tenant_no)
+        return success_response(data)
+    except PermissionError as e:
+        return error_response(403, str(e))
+    except ValueError as e:
+        return error_response(404, str(e))
+
+
+@router.post("/sample-freights")
+async def create_sample_freight(
+    body: SampleFreightCreateBody,
+    user: dict = Depends(require_admin_or_super_admin),
+    db: Session = Depends(get_db),
+):
+    try:
+        tenant_no = _get_tenant_no(db, user)
+        user_id = _get_current_user_id(user)
+        items = [it.model_dump(by_alias=True) for it in (body.items or [])]
+        data = freight_svc.create_freight(
+            db,
+            tenant_no=tenant_no,
+            user_id=user_id,
+            transport_mode_raw=body.transport_mode,
+            freight_amount=body.freight_amount,
+            items=items,
+        )
+        return success_response(data, message="创建成功")
+    except PermissionError as e:
+        return error_response(403, str(e))
+    except ValueError as e:
+        return error_response(400, str(e))
+
+
+@router.put("/sample-freights/{freight_id}")
+async def update_sample_freight(
+    freight_id: str,
+    body: SampleFreightUpdateBody,
+    user: dict = Depends(require_admin_or_super_admin),
+    db: Session = Depends(get_db),
+):
+    try:
+        tenant_no = _get_tenant_no(db, user)
+        user_id = _get_current_user_id(user)
+        data = freight_svc.update_freight(
+            db,
+            freight_id,
+            tenant_no=tenant_no,
+            user_id=user_id,
+            transport_mode_raw=body.transport_mode,
+            freight_amount=body.freight_amount,
+        )
+        return success_response(data, message="更新成功")
+    except PermissionError as e:
+        return error_response(403, str(e))
+    except ValueError as e:
+        msg = str(e)
+        code = 404 if "不存在" in msg else 400
+        return error_response(code, msg)
+
+
+@router.post("/sample-freights/{freight_id}/link")
+async def link_sample_freight(
+    freight_id: str,
+    body: SampleFreightItemsBody,
+    user: dict = Depends(require_admin_or_super_admin),
+    db: Session = Depends(get_db),
+):
+    try:
+        tenant_no = _get_tenant_no(db, user)
+        user_id = _get_current_user_id(user)
+        items = [it.model_dump(by_alias=True) for it in (body.items or [])]
+        data = freight_svc.link_samples(
+            db,
+            freight_id,
+            tenant_no=tenant_no,
+            user_id=user_id,
+            items=items,
+        )
+        return success_response(data, message="关联成功")
+    except PermissionError as e:
+        return error_response(403, str(e))
+    except ValueError as e:
+        msg = str(e)
+        code = 404 if "不存在" in msg else 400
+        return error_response(code, msg)
+
+
+@router.post("/sample-freights/{freight_id}/unlink")
+async def unlink_sample_freight(
+    freight_id: str,
+    body: SampleFreightItemsBody,
+    user: dict = Depends(require_admin_or_super_admin),
+    db: Session = Depends(get_db),
+):
+    try:
+        tenant_no = _get_tenant_no(db, user)
+        user_id = _get_current_user_id(user)
+        items = [it.model_dump(by_alias=True) for it in (body.items or [])]
+        data = freight_svc.unlink_samples(
+            db,
+            freight_id,
+            tenant_no=tenant_no,
+            user_id=user_id,
+            items=items,
+        )
+        return success_response(data, message="已取消关联")
+    except PermissionError as e:
+        return error_response(403, str(e))
+    except ValueError as e:
+        msg = str(e)
+        code = 404 if "不存在" in msg else 400
+        return error_response(code, msg)

+ 129 - 7
backend/app/crud/collection_protocol.py

@@ -607,12 +607,17 @@ def _transport_to_dict(t: BizProtocolSampleTransport) -> Dict[str, Any]:
 
 
 def _storage_cert_to_dict(s: BizProtocolStorageCert) -> Dict[str, Any]:
+    from app.services.sample_info_progress import storage_cert_handled
+
     return {
         "id": s.id,
         "collectionProtocolId": s.collection_protocol_id,
         "makeStatus": s.make_status or "0",
         "certImageUrls": s.cert_image_urls or "",
         "makeDate": (s.make_date or "").strip() if s.make_date else "",
+        "handleTime": _fmt_dt(s.handle_time) if getattr(s, "handle_time", None) else "",
+        "handlerUserId": getattr(s, "handler_user_id", None) or "",
+        "handled": storage_cert_handled(s),
         "tenantNo": s.tenant_no,
         "orderNum": s.order_num if s.order_num is not None else 0,
         "remark": s.remark or "",
@@ -1348,12 +1353,21 @@ def get_protocol_detail_bundle(
     )
     unit_name_map = _load_unit_name_map(db, s_rows)
     type_name_map = _load_sample_type_name_map(db, s_rows)
+    from app.crud import sample_freight as sample_freight_svc
+
+    freight_by_sample = sample_freight_svc.freight_map_for_samples(
+        db,
+        tenant_no=tenant_no,
+        sample_source=sample_freight_svc.SAMPLE_SOURCE_CELL,
+        sample_ids=[s.id for s in s_rows],
+    )
     samples_out: List[Dict[str, Any]] = []
     for s in s_rows:
         item = _sample_to_dict(s)
         item["sampleTypeName"] = type_name_map.get(s.sample_type_id or "", "")
         item["unitName"] = unit_name_map.get(s.unit_id or "", "")
         item["transport"] = transport_by_sample.get(s.id)
+        item["freight"] = freight_by_sample.get(s.id)
         item["sampleReception"] = reception_by_sample.get(s.id)
         item["productionReception"] = (reception_by_sample.get(s.id) or {}).get(
             "productionReception"
@@ -1365,9 +1379,22 @@ def get_protocol_detail_bundle(
         samples_out.append(item)
     out["samples"] = samples_out
     out["storageCert"] = _storage_cert_to_dict(sc) if sc else None
+    if out["storageCert"]:
+        hid = (out["storageCert"].get("handlerUserId") or "").strip()
+        if hid:
+            hu = db.query(SysUser).filter(SysUser.id == hid).first()
+            out["storageCert"]["handlerUserName"] = (hu.nick_name or "") if hu else ""
+        else:
+            out["storageCert"]["handlerUserName"] = ""
     ft = get_fullmoon_todo(db, protocol_id, tenant_no)
     delivery_dt = pc.newborn_delivery_time if pc else None
     out["fullmoonTodo"] = _fullmoon_todo_detail_dict(ft, newborn_delivery_time=delivery_dt)
+    hid2 = (out["fullmoonTodo"].get("handlerUserId") or "").strip()
+    if hid2:
+        hu2 = db.query(SysUser).filter(SysUser.id == hid2).first()
+        out["fullmoonTodo"]["handlerUserName"] = (hu2.nick_name or "") if hu2 else ""
+    else:
+        out["fullmoonTodo"]["handlerUserName"] = ""
     out["attachments"] = urls_to_attachment_list(p.protocol_attachments, db, tenant_no)
     out["consentAttachmentFiles"] = urls_to_attachment_list(p.consent_form_urls, db, tenant_no)
     out["healthSurveyAttachmentFiles"] = urls_to_attachment_list(
@@ -1969,7 +1996,7 @@ def _apply_payment_detail_draft_fields(
     from app.crud.dict_data import normalize_transfer_type
 
     if d.pay_status != "dfj":
-        raise ValueError("仅待缴费状态可编辑缴费明细")
+        raise ValueError("仅已缴费状态可编辑缴费明细")
     if "transfer_type" in fields:
         d.transfer_type = normalize_transfer_type(
             db, value=str(fields.get("transfer_type") or ""), tenant_no=tenant_no
@@ -2024,7 +2051,7 @@ def _apply_submit_receipt_finance(
     user_id: Optional[str],
 ) -> None:
     if d.pay_status != "dfj":
-        raise ValueError("仅待缴费状态可提交财务确认")
+        raise ValueError("仅已缴费状态可提交财务确认")
     d.pay_status = "jfqrz"
     d.update_by = user_id
     d.update_time = datetime.now()
@@ -2797,7 +2824,19 @@ def save_samples(
     db.commit()
     for o in result:
         db.refresh(o)
-    save_sample_transports(db, collection_protocol_id, tenant_no, user_id, result, samples)
+    # 本阶段停写旧运输明细,改挂运费批次
+    from app.crud import sample_freight as sample_freight_svc
+
+    sample_freight_svc.apply_freight_on_protocol_save(
+        db,
+        tenant_no=tenant_no,
+        user_id=user_id,
+        sample_source=sample_freight_svc.SAMPLE_SOURCE_CELL,
+        protocol_id=collection_protocol_id,
+        saved_samples=result,
+        input_samples=samples,
+    )
+    db.commit()
     return result
 
 
@@ -3357,11 +3396,13 @@ def upsert_storage_cert(
 ) -> BizProtocolStorageCert:
     fields = _normalize_fields(fields)
     obj = get_storage_cert(db, collection_protocol_id, tenant_no)
-    _STR_FIELDS = ["make_status", "cert_image_urls", "make_date"]
+    _STR_FIELDS = ["make_status", "cert_image_urls", "make_date", "handler_user_id"]
     if obj:
         for k in _STR_FIELDS:
             if k in fields:
-                setattr(obj, k, (fields[k] or "").strip() or None)
+                setattr(obj, k, (fields[k] or "").strip() or None if isinstance(fields[k], str) or fields[k] is None else fields[k])
+        if "handle_time" in fields:
+            obj.handle_time = fields.get("handle_time")
         if "remark" in fields:
             obj.remark = (fields["remark"] or "").strip() or None
         obj.update_by = user_id
@@ -3385,13 +3426,49 @@ def upsert_storage_cert(
     )
     for k in _STR_FIELDS:
         if k in fields:
-            setattr(obj, k, (fields[k] or "").strip() or None)
+            val = fields[k]
+            setattr(obj, k, (val or "").strip() or None if isinstance(val, str) or val is None else val)
+    if "handle_time" in fields:
+        obj.handle_time = fields.get("handle_time")
     db.add(obj)
     db.commit()
     db.refresh(obj)
     return obj
 
 
+def upsert_storage_cert_handle(
+    db: Session,
+    collection_protocol_id: str,
+    tenant_no: str,
+    user_id: Optional[str],
+    *,
+    handle_time: Optional[datetime] = None,
+    handler_user_id: Optional[str] = None,
+    cert_image_urls: Optional[str] = None,
+    remark: Optional[str] = None,
+) -> BizProtocolStorageCert:
+    from app.services.sample_info_progress import apply_handle_payload
+
+    payload = apply_handle_payload(
+        handle_time=handle_time,
+        handler_user_id=handler_user_id,
+        attachments=cert_image_urls or "",
+        remark=remark or "",
+    )
+    return upsert_storage_cert(
+        db,
+        collection_protocol_id,
+        tenant_no,
+        user_id,
+        handle_time=payload["handle_time"],
+        handler_user_id=payload["handler_user_id"],
+        make_status=payload["make_or_todo_status"],
+        make_date=payload["legacy_date"] or None,
+        cert_image_urls=payload["attachments"] or None,
+        remark=payload["remark"] or None,
+    )
+
+
 # ---------------------------------------------------------------------------
 # 子表:满月待办
 # ---------------------------------------------------------------------------
@@ -3401,6 +3478,9 @@ def _fullmoon_todo_empty_dict() -> Dict[str, Any]:
     return {
         "todoStatus": "0",
         "handleDate": "",
+        "handleTime": "",
+        "handlerUserId": "",
+        "handled": False,
         "handleRemark": "",
         "insuranceAttachments": "",
         "insuranceCompanyId": "",
@@ -3435,11 +3515,16 @@ def _fullmoon_todo_detail_dict(
 
 
 def _fullmoon_todo_to_dict(ft: BizProtocolFullmoonTodo) -> Dict[str, Any]:
+    from app.services.sample_info_progress import insurance_handled
+
     return {
         "id": ft.id,
         "collectionProtocolId": ft.collection_protocol_id,
         "todoStatus": ft.todo_status or "0",
         "handleDate": (ft.handle_date or "").strip() if ft.handle_date else "",
+        "handleTime": _fmt_dt(ft.handle_time) if getattr(ft, "handle_time", None) else "",
+        "handlerUserId": getattr(ft, "handler_user_id", None) or "",
+        "handled": insurance_handled(ft),
         "handleRemark": ft.handle_remark or "",
         "insuranceAttachments": ft.insurance_attachments or "",
         "insuranceCompanyId": ft.insurance_company_id or "",
@@ -3486,7 +3571,7 @@ def upsert_fullmoon_todo(
     fields = _normalize_fields(fields)
     obj = get_fullmoon_todo(db, collection_protocol_id, tenant_no)
     _STR_FIELDS = [
-        "todo_status", "handle_date", "handle_remark",
+        "todo_status", "handle_date", "handle_remark", "handler_user_id",
         "insurance_attachments", "insurance_company_id",
         "insurance_company_name", "insurance_type",
         "insurance_policy_no", "insurance_effective_date",
@@ -3497,6 +3582,8 @@ def upsert_fullmoon_todo(
         for k in _STR_FIELDS:
             if k in fields:
                 setattr(obj, k, (fields[k] or "").strip() or None)
+        if "handle_time" in fields:
+            obj.handle_time = fields.get("handle_time")
         for k in _DEC_FIELDS:
             if k in fields and fields[k] is not None:
                 setattr(obj, k, Decimal(str(fields[k])))
@@ -3524,6 +3611,8 @@ def upsert_fullmoon_todo(
     for k in _STR_FIELDS:
         if k in fields:
             setattr(obj, k, (fields[k] or "").strip() or None)
+    if "handle_time" in fields:
+        obj.handle_time = fields.get("handle_time")
     for k in _DEC_FIELDS:
         if k in fields and fields[k] is not None:
             setattr(obj, k, Decimal(str(fields[k])))
@@ -3533,6 +3622,39 @@ def upsert_fullmoon_todo(
     return obj
 
 
+def upsert_fullmoon_todo_handle(
+    db: Session,
+    collection_protocol_id: str,
+    tenant_no: str,
+    user_id: Optional[str],
+    *,
+    handle_time: Optional[datetime] = None,
+    handler_user_id: Optional[str] = None,
+    insurance_attachments: Optional[str] = None,
+    handle_remark: Optional[str] = None,
+) -> BizProtocolFullmoonTodo:
+    from app.services.sample_info_progress import apply_handle_payload
+
+    payload = apply_handle_payload(
+        handle_time=handle_time,
+        handler_user_id=handler_user_id,
+        attachments=insurance_attachments or "",
+        remark=handle_remark or "",
+    )
+    return upsert_fullmoon_todo(
+        db,
+        collection_protocol_id,
+        tenant_no,
+        user_id,
+        handle_time=payload["handle_time"],
+        handler_user_id=payload["handler_user_id"],
+        todo_status=payload["make_or_todo_status"],
+        handle_date=payload["legacy_date"] or None,
+        insurance_attachments=payload["attachments"] or None,
+        handle_remark=payload["remark"] or None,
+    )
+
+
 # ---------------------------------------------------------------------------
 # 开票申请(主表 + 明细子表)
 # ---------------------------------------------------------------------------

+ 42 - 7
backend/app/crud/research_donation.py

@@ -872,10 +872,19 @@ def get_donation_detail(
     )
     samples_out: List[Dict[str, Any]] = []
     unit_name_map = _load_unit_name_map(db, s_rows)
+    from app.crud import sample_freight as sample_freight_svc
+
+    freight_by_sample = sample_freight_svc.freight_map_for_samples(
+        db,
+        tenant_no=tenant_no,
+        sample_source=sample_freight_svc.SAMPLE_SOURCE_DONATION,
+        sample_ids=sample_ids,
+    )
     for s in s_rows:
         item = _sample_to_dict(s)
         item["unitName"] = unit_name_map.get(s.unit_id or "", "")
         item["transport"] = transport_by_sample.get(s.id)
+        item["freight"] = freight_by_sample.get(s.id)
         item["sampleReception"] = reception_by_sample.get(s.id)
         item["productionReception"] = (reception_by_sample.get(s.id) or {}).get(
             "productionReception"
@@ -967,12 +976,24 @@ def create_donation(
     coll_raw = _normalize_fields(raw.get("collection") or {})
     _create_collection(db, donation_id, tenant_no, user_id, coll_raw, now)
 
-    # samples + transports
+    # samples(运费批次在 commit 后挂钩;停写旧运输依赖)
     samples_raw = raw.get("samples") or []
-    _create_samples(db, donation_id, tenant_no, user_id, samples_raw, now)
+    new_samples = _create_samples(db, donation_id, tenant_no, user_id, samples_raw, now)
 
     db.commit()
     db.refresh(obj)
+    from app.crud import sample_freight as sample_freight_svc
+
+    sample_freight_svc.apply_freight_on_protocol_save(
+        db,
+        tenant_no=tenant_no,
+        user_id=user_id,
+        sample_source=sample_freight_svc.SAMPLE_SOURCE_DONATION,
+        protocol_id=donation_id,
+        saved_samples=new_samples,
+        input_samples=samples_raw,
+    )
+    db.commit()
     return obj
 
 
@@ -1067,7 +1088,8 @@ def update_donation(
         }
         _soft_delete_samples(db, donation_id, tenant_no, user_id)
         _soft_delete_transports(db, donation_id, tenant_no, user_id)
-        new_samples = _create_samples(db, donation_id, tenant_no, user_id, raw["samples"] or [], now)
+        samples_raw = raw["samples"] or []
+        new_samples = _create_samples(db, donation_id, tenant_no, user_id, samples_raw, now)
         new_code_to_id = {
             (s.sample_code or "").strip(): s.id
             for s in new_samples
@@ -1080,9 +1102,25 @@ def update_donation(
             new_code_to_id=new_code_to_id,
             user_id=user_id,
         )
+    else:
+        samples_raw = None
+        new_samples = None
 
     db.commit()
     db.refresh(obj)
+    if new_samples is not None and samples_raw is not None:
+        from app.crud import sample_freight as sample_freight_svc
+
+        sample_freight_svc.apply_freight_on_protocol_save(
+            db,
+            tenant_no=tenant_no,
+            user_id=user_id,
+            sample_source=sample_freight_svc.SAMPLE_SOURCE_DONATION,
+            protocol_id=donation_id,
+            saved_samples=new_samples,
+            input_samples=samples_raw,
+        )
+        db.commit()
     return obj
 
 
@@ -2101,10 +2139,7 @@ def _create_samples(
         db.flush()
         result.append(obj)
 
-        # transport
-        transport_raw = raw.get("transport")
-        if isinstance(transport_raw, dict):
-            _create_transport(db, donation_id, obj.id, tenant_no, user_id, transport_raw, idx, now, sample_code=obj.sample_code)
+        # 本阶段不再写入旧运输明细;运费由 apply_freight_on_protocol_save 挂钩
 
     return result
 

+ 2 - 1
backend/app/main.py

@@ -13,7 +13,7 @@ from app.core.database import SessionLocal
 from app.core.api_request_log_middleware import ApiRequestLogMiddleware
 from app.core.api_crypto_middleware import ApiCryptoMiddleware
 from app.models import models  # noqa: F401
-from app.api import public_router, menu_router, admin_router, workflow_router, basic_info_router, collection_protocol_router, protocol_sample_payment_router, protocol_refund_record_router, presign_order_router, leave_router, operations_router, ethnic_router, sample_reception_router, sample_production_reception_router, qc_router, research_donation_router, production_router, pilot_service_router, insurance_company_router, material_router, storage_release_review_router, sample_sms_notification_router, storage_entry_sms_notification_router, qc_sms_notification_router, protocol_sample_collection_router, research_donation_sample_collection_router, pilot_application_router, pilot_checkup_router, pilot_delivery_router, pilot_infusion_confirm_router, pilot_infusion_schedule_mgmt_router, outbound_release_review_router, clinical_payment_router, env_detect_router, cleanroom_garment_router, market_router, pre_transit_sample_router, oa_approval_router, mini_auth_router, mini_menu_router
+from app.api import public_router, menu_router, admin_router, workflow_router, basic_info_router, collection_protocol_router, protocol_sample_payment_router, protocol_refund_record_router, presign_order_router, sample_freight_router, leave_router, operations_router, ethnic_router, sample_reception_router, sample_production_reception_router, qc_router, research_donation_router, production_router, pilot_service_router, insurance_company_router, material_router, storage_release_review_router, sample_sms_notification_router, storage_entry_sms_notification_router, qc_sms_notification_router, protocol_sample_collection_router, research_donation_sample_collection_router, pilot_application_router, pilot_checkup_router, pilot_delivery_router, pilot_infusion_confirm_router, pilot_infusion_schedule_mgmt_router, outbound_release_review_router, clinical_payment_router, env_detect_router, cleanroom_garment_router, market_router, pre_transit_sample_router, oa_approval_router, mini_auth_router, mini_menu_router
 
 
 @asynccontextmanager
@@ -120,6 +120,7 @@ app.include_router(collection_protocol_router, prefix=settings.API_V1_PREFIX)
 app.include_router(protocol_sample_payment_router, prefix=settings.API_V1_PREFIX)
 app.include_router(protocol_refund_record_router, prefix=settings.API_V1_PREFIX)
 app.include_router(presign_order_router, prefix=settings.API_V1_PREFIX)
+app.include_router(sample_freight_router, prefix=settings.API_V1_PREFIX)
 app.include_router(leave_router, prefix=settings.API_V1_PREFIX)
 app.include_router(operations_router, prefix=settings.API_V1_PREFIX)
 app.include_router(ethnic_router, prefix=settings.API_V1_PREFIX)