zlwu 5 місяців тому
батько
коміт
c6c3a37fa7
100 змінених файлів з 652 додано та 184 видалено
  1. 1 1
      backend/app.pid
  2. 11 11
      backend/app/api/admin.py
  3. 11 4
      backend/app/api/collection_protocol.py
  4. 1 1
      backend/app/api/ethnic.py
  5. 25 25
      backend/app/api/insurance_company.py
  6. 1 1
      backend/app/api/leave.py
  7. 2 2
      backend/app/api/material.py
  8. 5 5
      backend/app/api/menu.py
  9. 72 4
      backend/app/api/operations.py
  10. 14 13
      backend/app/api/pilot_service.py
  11. 18 14
      backend/app/api/production.py
  12. 13 6
      backend/app/api/qc.py
  13. 2 0
      backend/app/api/research_donation.py
  14. 2 2
      backend/app/api/sample_reception.py
  15. 5 5
      backend/app/api/workflow.py
  16. 13 9
      backend/app/core/data_scope.py
  17. 4 0
      backend/app/crud/cell_product.py
  18. 36 3
      backend/app/crud/collection_protocol.py
  19. 4 0
      backend/app/crud/consumable.py
  20. 10 7
      backend/app/crud/consumable_pack.py
  21. 8 3
      backend/app/crud/dept.py
  22. 4 0
      backend/app/crud/dict_data.py
  23. 4 0
      backend/app/crud/dict_type.py
  24. 4 0
      backend/app/crud/ethnic.py
  25. 4 0
      backend/app/crud/express_company.py
  26. 4 0
      backend/app/crud/item_unit.py
  27. 4 0
      backend/app/crud/leave.py
  28. 4 0
      backend/app/crud/material.py
  29. 4 0
      backend/app/crud/material_stock_log.py
  30. 35 2
      backend/app/crud/menu.py
  31. 4 0
      backend/app/crud/output_type.py
  32. 56 14
      backend/app/crud/pilot_service.py
  33. 4 0
      backend/app/crud/post.py
  34. 10 7
      backend/app/crud/process.py
  35. 4 0
      backend/app/crud/process_category.py
  36. 5 1
      backend/app/crud/process_route.py
  37. 4 0
      backend/app/crud/production_task.py
  38. 4 0
      backend/app/crud/production_work_order.py
  39. 10 11
      backend/app/crud/qc_item_param.py
  40. 13 10
      backend/app/crud/qc_lab_test_item.py
  41. 4 0
      backend/app/crud/qc_material.py
  42. 4 0
      backend/app/crud/qc_material_usage.py
  43. 4 0
      backend/app/crud/qc_task.py
  44. 11 12
      backend/app/crud/qc_task_result.py
  45. 4 0
      backend/app/crud/qc_third_party_company.py
  46. 8 1
      backend/app/crud/qc_third_party_item.py
  47. 4 0
      backend/app/crud/qc_work_order.py
  48. 4 0
      backend/app/crud/reagent.py
  49. 8 3
      backend/app/crud/research_donation.py
  50. 5 1
      backend/app/crud/role.py
  51. 12 5
      backend/app/crud/sample_reception.py
  52. 4 0
      backend/app/crud/site.py
  53. 4 0
      backend/app/crud/sop_document.py
  54. 113 0
      backend/app/crud/storage_facility.py
  55. 4 0
      backend/app/crud/tenant.py
  56. 20 1
      backend/app/crud/workflow.py
  57. 0 0
      backend/app/graphify-out/cache/ast/019e6b1f380e4f5036fb9e7010c3c1dc6493db77c12af404fa2dc5b82d970e7c.json
  58. 0 0
      backend/app/graphify-out/cache/ast/03ab697b72e4d605afb6be4468fd96a6bb450d3063d5904f02afb31d9d888272.json
  59. 0 0
      backend/app/graphify-out/cache/ast/07590d597bc87c2fb97c06721c0bc4ccd7ed8d146da9ad8a7a2a0ae4d54b50a3.json
  60. 0 0
      backend/app/graphify-out/cache/ast/0972ed93085a7ab8ee3a3240cddf2cdb988dbab11d03afd58ee4c9e2bce09b44.json
  61. 0 0
      backend/app/graphify-out/cache/ast/0f3823afd77177cb8c0cc0b0980d831828174515cdb3f6ce7a3f1636b9ef2abb.json
  62. 0 0
      backend/app/graphify-out/cache/ast/107619226422f08de4fc3a59388f10d9fdfa86b82b6b315811bce132d623a406.json
  63. 0 0
      backend/app/graphify-out/cache/ast/143c720e5edab777be16734bc6e5682e083c681012a623429c6cf1d16ffbbd75.json
  64. 0 0
      backend/app/graphify-out/cache/ast/15e51f0d01404335b92f2e23f920ea8787df581d8d376d74906fdcd507214703.json
  65. 0 0
      backend/app/graphify-out/cache/ast/1e0eef8cba57aab6c6657e08ea5a898171612f61eb09c8123a1f5e02a9797dbd.json
  66. 0 0
      backend/app/graphify-out/cache/ast/1f4dd5ecd932fbd6b8bc2318f8c550de1b92f2080473804a5c51811dea08b08c.json
  67. 0 0
      backend/app/graphify-out/cache/ast/2536cd632f5dce910f607f32e84aa686ec1e53fe4bba026df50e0f28605b81ea.json
  68. 0 0
      backend/app/graphify-out/cache/ast/3187c71b68731ef629c821a2fce2a80ffdac618975c8b66ef69f32dc4e67dcca.json
  69. 0 0
      backend/app/graphify-out/cache/ast/37edf7fed1d65173dfaa473456d071a27b3da4ee7d8ad2bfb7365ff5b0583a1a.json
  70. 0 0
      backend/app/graphify-out/cache/ast/382514efa9edc5bd9351ce590eeea0e8430b93d7561cf6e40a154b7840a12a01.json
  71. 0 0
      backend/app/graphify-out/cache/ast/40f32ca88b736ab2e0c5b3531dbc26cc370de7d0e13573eabb5eb73a41096224.json
  72. 0 0
      backend/app/graphify-out/cache/ast/47427773f756199ad4e23cfe25c37a20ea5b823a427649bfd0b672a5e502bb63.json
  73. 0 0
      backend/app/graphify-out/cache/ast/493f1148a4ad49b64b5aae0e95426066ab05e1ba90ccdb81040f27f639a2e3fe.json
  74. 0 0
      backend/app/graphify-out/cache/ast/51196252a4dcafa36c356db3e67c4c0947433c802fc30322e9dbab4fc6464117.json
  75. 0 0
      backend/app/graphify-out/cache/ast/514be2cfb3570ded9441edf3e535da8c7832ad442cceb95dc680b53b94284335.json
  76. 0 0
      backend/app/graphify-out/cache/ast/5185820c82fca7f32b8658c6b1d838f833d87ac49ba6cf99e1be67d0bdcb3092.json
  77. 0 0
      backend/app/graphify-out/cache/ast/52d49fd058390ad996afaf2990e12cef1d9d3724b7b4b749de4b5fe1caef0f77.json
  78. 0 0
      backend/app/graphify-out/cache/ast/52fef47099f0960f42229b41cdf6bfa39b99819846285aae870fe75b707b0d2b.json
  79. 0 0
      backend/app/graphify-out/cache/ast/55e615004c5868728721d7362c7cac5ea5724b906166220fdc588e1715b8553f.json
  80. 0 0
      backend/app/graphify-out/cache/ast/5977261dfc5b160034644476706cfa9ba7b6429a9f6bc450299a821e24a4c60d.json
  81. 0 0
      backend/app/graphify-out/cache/ast/5a248299ccc8fb110be375a7458688682a0dc064497236ce9ebcd9dca843c006.json
  82. 0 0
      backend/app/graphify-out/cache/ast/62e204fc7b75802c901cc27f55e34c64bd0e3016b1e362b06dce4a933b8a535e.json
  83. 0 0
      backend/app/graphify-out/cache/ast/64857131e1421b116c96412f8c6ce5c2096472bbc9d3efb7370990258deb28b9.json
  84. 0 0
      backend/app/graphify-out/cache/ast/6685a7d6debebe55186c6a68af6660c89f89609f0a6c30c3fe83952d8c29cd19.json
  85. 0 0
      backend/app/graphify-out/cache/ast/6e4c62a4d44927dd189d87448a7c6a2c31bd6a92075dfcd1096b5c6d6d1a703d.json
  86. 0 0
      backend/app/graphify-out/cache/ast/710f4e40e20c01c775091741bc0345ab3121047e94347b749c8b4bb038116dca.json
  87. 0 0
      backend/app/graphify-out/cache/ast/741005802d368a35f408837d93d8ae373f1f0b0da69a65bb4da94d3c28dccc9e.json
  88. 0 0
      backend/app/graphify-out/cache/ast/76647286cbbca45a92fb5819c71d03668e80061839042ec0d15f951419bd0f89.json
  89. 0 0
      backend/app/graphify-out/cache/ast/77ac044900c34863392739700cd82ba7528849a8f95b85d44d298101b89e81b3.json
  90. 0 0
      backend/app/graphify-out/cache/ast/781c27d30483071275baba9359f548a39f369828ccae89e3c37e258c07fe81ca.json
  91. 0 0
      backend/app/graphify-out/cache/ast/85e2b7e57cdc1d9fb96ed1ea76707fd9c8e65ca55488f0bc28fa94033273fc38.json
  92. 0 0
      backend/app/graphify-out/cache/ast/861c2f4ed148f826d2bdb6fdfe5f97e239e523a806216d48f874f882bdc91f07.json
  93. 0 0
      backend/app/graphify-out/cache/ast/86e6833986a3dd85bb5165afd7224e99ceaaa4d54cd9684c1758d39756db985d.json
  94. 0 0
      backend/app/graphify-out/cache/ast/8b524ab71ba36aee878ba80666550e839d5bfff70252ad67ec4fa0cd5d5da6c3.json
  95. 0 0
      backend/app/graphify-out/cache/ast/8d9df2b36fdecdc7424148120e593cfdf37517102b035e34f21c3f84eed81229.json
  96. 0 0
      backend/app/graphify-out/cache/ast/919eb8a126968ecf5080795b0af9376ff583efe047db0ec39df837eb2a444b8c.json
  97. 0 0
      backend/app/graphify-out/cache/ast/a2e28129e634697709e57610fc304508793723ae995d578281e058b7dc8a2d2c.json
  98. 0 0
      backend/app/graphify-out/cache/ast/a68ac6b74b30b20a75ef6b2e4696faef6b218768e508d29c82c22094f89b4171.json
  99. 0 0
      backend/app/graphify-out/cache/ast/a9d72b23e8d55961139d76483b20d973f9ba5937718e2e8432f2fee801c81f8f.json
  100. 0 0
      backend/app/graphify-out/cache/ast/ade2407f94aa9fb433f4a8e04f28e3fdbdf89571bba2f761d6bb52a3cadf6ee2.json

+ 1 - 1
backend/app.pid

@@ -1 +1 @@
-12254
+33472

+ 11 - 11
backend/app/api/admin.py

@@ -135,7 +135,7 @@ async def list_tenants(
     _user: dict = Depends(require_admin_or_super_admin),
     db: Session = Depends(get_db),
 ):
-    items, total = tenant_svc.list_tenants(db, page=page, page_size=pageSize, keyword=keyword)
+    items, total = tenant_svc.list_tenants(db, page=page, page_size=pageSize, keyword=keyword, user_info=_user)
     total_pages = tenant_svc.total_pages(total, pageSize)
     return success_response(
         {
@@ -440,7 +440,7 @@ async def list_dict_types(
 ):
     tenant_no = _get_tenant_no(db, user)
     items, total = dict_type_svc.list_dict_types(
-        db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no
+        db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user
     )
     tp = dict_type_svc.total_pages(total, pageSize)
     return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
@@ -541,7 +541,7 @@ async def list_dict_data(
 ):
     tenant_no = _get_tenant_no(db, user)
     items, total = dict_data_svc.list_dict_data(
-        db, page=page, page_size=pageSize, dict_type_code=dictType, keyword=keyword, tenant_no=tenant_no
+        db, page=page, page_size=pageSize, dict_type_code=dictType, keyword=keyword, tenant_no=tenant_no, user_info=user
     )
     tp = dict_data_svc.total_pages(total, pageSize)
     return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
@@ -654,7 +654,7 @@ async def list_depts(
     db: Session = Depends(get_db),
 ):
     tenant_no = _get_tenant_no(db, user)
-    tree = dept_svc.list_depts_tree(db, keyword=keyword, tenant_no=tenant_no)
+    tree = dept_svc.list_depts_tree(db, keyword=keyword, tenant_no=tenant_no, user_info=user)
     return success_response(tree)
 
 
@@ -767,7 +767,7 @@ async def list_sites(
     db: Session = Depends(get_db),
 ):
     tenant_no = _get_tenant_no(db, user)
-    items, total = site_svc.list_sites(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no)
+    items, total = site_svc.list_sites(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
     tp = site_svc.total_pages(total, pageSize)
     return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
 
@@ -876,7 +876,7 @@ async def list_posts(
     db: Session = Depends(get_db),
 ):
     tenant_no = _get_tenant_no(db, user)
-    items, total = post_svc.list_posts(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no)
+    items, total = post_svc.list_posts(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
     tp = post_svc.total_pages(total, pageSize)
     return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
 
@@ -1013,7 +1013,7 @@ async def list_item_units(
     db: Session = Depends(get_db),
 ):
     tenant_no = _get_tenant_no(db, user)
-    items, total = item_unit_svc.list_item_units(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no)
+    items, total = item_unit_svc.list_item_units(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
     tp = item_unit_svc.total_pages(total, pageSize)
     return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
 
@@ -1108,7 +1108,7 @@ async def list_express_companies(
     db: Session = Depends(get_db),
 ):
     tenant_no = _get_tenant_no(db, user)
-    items, total = express_company_svc.list_express_companies(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no)
+    items, total = express_company_svc.list_express_companies(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
     tp = express_company_svc.total_pages(total, pageSize)
     return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
 
@@ -1215,7 +1215,7 @@ async def list_menus(
     db: Session = Depends(get_db),
 ):
     tenant_no = _get_tenant_no(db, user)
-    tree = menu_svc.list_menus_tree(db, keyword=keyword, tenant_no=tenant_no)
+    tree = menu_svc.list_menus_tree(db, keyword=keyword, tenant_no=tenant_no, user_info=user)
     return success_response(tree)
 
 
@@ -1300,7 +1300,7 @@ class RoleCreateBody(BaseModel):
     model_config = ConfigDict(populate_by_name=True)
 
     role_name: str = Field(..., alias="roleName")
-    data_scope: Optional[str] = Field("1", alias="dataScope")
+    data_scope: Optional[str] = Field("4", alias="dataScope")
     status: Optional[str] = Field("1", alias="status")
     order_num: Optional[int] = Field(None, alias="orderNum")
     remark: Optional[str] = None
@@ -1336,7 +1336,7 @@ async def list_role_management(
     db: Session = Depends(get_db),
 ):
     tenant_no = _get_tenant_no(db, user)
-    items, total = role_svc.list_roles(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no)
+    items, total = role_svc.list_roles(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
     tp = role_svc.total_pages(total, pageSize)
     return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
 

+ 11 - 4
backend/app/api/collection_protocol.py

@@ -131,6 +131,7 @@ async def list_collection_protocols(
         protocol_status=protocolStatus,
         tenant_no=tenant_no,
         scope_collector_user_id=current_user_id,
+        user_info=user,
     )
     tp = cp_svc.total_pages(total, pageSize)
     return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
@@ -377,6 +378,7 @@ async def list_collection_protocol_audits(
         keyword=keyword,
         audit_status=auditStatus,
         tenant_no=tenant_no,
+        user_info=user,
     )
     tp = cp_svc.total_pages(total, pageSize)
     return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
@@ -437,6 +439,7 @@ async def list_payment_details(
         keyword=keyword or None,
         transfer_type=transferType or None,
         tenant_no=tenant_no,
+        user_info=user,
     )
     tp = cp_svc.total_pages(total, pageSize)
     return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
@@ -553,7 +556,7 @@ async def list_receipt_confirm(
     db: Session = Depends(get_db),
 ):
     tenant_no = _get_tenant_no(db, user)
-    items, total = cp_svc.list_receipt_confirm_details(db, page=page, page_size=pageSize, tenant_no=tenant_no)
+    items, total = cp_svc.list_receipt_confirm_details(db, page=page, page_size=pageSize, tenant_no=tenant_no, user_info=user)
     tp = cp_svc.total_pages(total, pageSize)
     return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
 
@@ -567,7 +570,7 @@ async def list_receipt_confirmed(
 ):
     tenant_no = _get_tenant_no(db, user)
     items, total = cp_svc.list_receipt_confirmed_details(
-        db, page=page, page_size=pageSize, tenant_no=tenant_no
+        db, page=page, page_size=pageSize, tenant_no=tenant_no, user_info=user
     )
     tp = cp_svc.total_pages(total, pageSize)
     return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
@@ -624,7 +627,7 @@ async def list_refund_confirm(
     db: Session = Depends(get_db),
 ):
     tenant_no = _get_tenant_no(db, user)
-    items, total = cp_svc.list_refund_confirm_details(db, page=page, page_size=pageSize, tenant_no=tenant_no)
+    items, total = cp_svc.list_refund_confirm_details(db, page=page, page_size=pageSize, tenant_no=tenant_no, user_info=user)
     tp = cp_svc.total_pages(total, pageSize)
     return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
 
@@ -638,7 +641,7 @@ async def list_refund_confirmed(
 ):
     tenant_no = _get_tenant_no(db, user)
     items, total = cp_svc.list_refund_confirmed_details(
-        db, page=page, page_size=pageSize, tenant_no=tenant_no
+        db, page=page, page_size=pageSize, tenant_no=tenant_no, user_info=user
     )
     tp = cp_svc.total_pages(total, pageSize)
     return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
@@ -688,6 +691,7 @@ async def list_storage_certs(
         make_status=makeStatus,
         page=page,
         page_size=pageSize,
+        user_info=user,
     )
     page_safe = max(1, page)
     page_size_safe = min(max(1, pageSize), 100)
@@ -731,6 +735,9 @@ async def list_fullmoon_todos(
             )
         )
 
+    from app.core.data_scope import apply_data_scope
+    q = apply_data_scope(q, BizCollectionProtocol, user, db, tenant_no)
+
     rows = q.order_by(BizCollectionProtocol.create_time.desc()).all()
 
     protocol_ids = [r.id for r, _ in rows]

+ 1 - 1
backend/app/api/ethnic.py

@@ -20,5 +20,5 @@ async def list_ethnics_for_select(
     db: Session = Depends(get_db),
 ):
     """返回民族码表选项(按排序号升序)。"""
-    rows = ethnic_svc.list_ethnics(db, keyword=keyword, limit=limit)
+    rows = ethnic_svc.list_ethnics(db, keyword=keyword, limit=limit, user_info=_user)
     return success_response({"list": rows})

+ 25 - 25
backend/app/api/insurance_company.py

@@ -121,7 +121,7 @@ def _row_to_dict_project_price(row: BizInsuranceProjectPrice) -> dict:
 
 
 def _list_insurance_companies(
-    db: Session, *, page: int, page_size: int, keyword: Optional[str], tenant_no: str
+    db: Session, *, page: int, page_size: int, keyword: Optional[str], tenant_no: str, user_info=None
 ):
     q = db.query(BizInsuranceCompany).filter(
         BizInsuranceCompany.tenant_no == tenant_no,
@@ -129,6 +129,9 @@ def _list_insurance_companies(
     )
     if keyword:
         q = q.filter(BizInsuranceCompany.company_name.like(f"%{keyword}%"))
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q = apply_data_scope(q, BizInsuranceCompany, user_info, db, tenant_no)
     total = q.count()
     rows = (
         q.order_by(BizInsuranceCompany.create_time.desc())
@@ -243,7 +246,7 @@ def _soft_delete_insurance_company(db: Session, row_id: str, *, tenant_no: str):
 
 
 def _list_insurance_company_invoices(
-    db: Session, insurance_company_id: str, *, tenant_no: str
+    db: Session, insurance_company_id: str, *, tenant_no: str, user_info=None
 ):
     # Verify parent exists
     parent = (
@@ -257,16 +260,15 @@ def _list_insurance_company_invoices(
     )
     if not parent:
         raise ValueError("保险公司不存在")
-    rows = (
-        db.query(BizInsuranceCompanyInvoice)
-        .filter(
-            BizInsuranceCompanyInvoice.insurance_company_id == insurance_company_id,
-            BizInsuranceCompanyInvoice.tenant_no == tenant_no,
-            BizInsuranceCompanyInvoice.del_flag == 0,
-        )
-        .order_by(BizInsuranceCompanyInvoice.create_time.desc())
-        .all()
+    q = db.query(BizInsuranceCompanyInvoice).filter(
+        BizInsuranceCompanyInvoice.insurance_company_id == insurance_company_id,
+        BizInsuranceCompanyInvoice.tenant_no == tenant_no,
+        BizInsuranceCompanyInvoice.del_flag == 0,
     )
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q = apply_data_scope(q, BizInsuranceCompanyInvoice, user_info, db, tenant_no)
+    rows = q.order_by(BizInsuranceCompanyInvoice.create_time.desc()).all()
     return [_row_to_dict_invoice(r) for r in rows]
 
 
@@ -381,9 +383,8 @@ def _soft_delete_insurance_company_invoice(db: Session, row_id: str, *, tenant_n
 
 
 def _list_insurance_project_prices(
-    db: Session, insurance_company_id: str, *, tenant_no: str
+    db: Session, insurance_company_id: str, *, tenant_no: str, user_info=None
 ):
-    # Verify parent exists
     parent = (
         db.query(BizInsuranceCompany)
         .filter(
@@ -395,16 +396,15 @@ def _list_insurance_project_prices(
     )
     if not parent:
         raise ValueError("保险公司不存在")
-    rows = (
-        db.query(BizInsuranceProjectPrice)
-        .filter(
-            BizInsuranceProjectPrice.insurance_company_id == insurance_company_id,
-            BizInsuranceProjectPrice.tenant_no == tenant_no,
-            BizInsuranceProjectPrice.del_flag == 0,
-        )
-        .order_by(BizInsuranceProjectPrice.create_time.desc())
-        .all()
+    q = db.query(BizInsuranceProjectPrice).filter(
+        BizInsuranceProjectPrice.insurance_company_id == insurance_company_id,
+        BizInsuranceProjectPrice.tenant_no == tenant_no,
+        BizInsuranceProjectPrice.del_flag == 0,
     )
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q = apply_data_scope(q, BizInsuranceProjectPrice, user_info, db, tenant_no)
+    rows = q.order_by(BizInsuranceProjectPrice.create_time.desc()).all()
     return [_row_to_dict_project_price(r) for r in rows]
 
 
@@ -537,7 +537,7 @@ async def list_insurance_companies(
 ):
     tenant_no = _get_tenant_no(db, user)
     items, total = _list_insurance_companies(
-        db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no
+        db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user
     )
     tp = _total_pages(total, pageSize)
     return success_response(
@@ -651,7 +651,7 @@ async def list_insurance_company_invoices(
 ):
     try:
         tenant_no = _get_tenant_no(db, user)
-        items = _list_insurance_company_invoices(db, company_id, tenant_no=tenant_no)
+        items = _list_insurance_company_invoices(db, company_id, tenant_no=tenant_no, user_info=user)
         return success_response(items)
     except ValueError as e:
         return error_response(400, str(e))
@@ -761,7 +761,7 @@ async def list_insurance_project_prices(
 ):
     try:
         tenant_no = _get_tenant_no(db, user)
-        items = _list_insurance_project_prices(db, company_id, tenant_no=tenant_no)
+        items = _list_insurance_project_prices(db, company_id, tenant_no=tenant_no, user_info=user)
         return success_response(items)
     except ValueError as e:
         return error_response(400, str(e))

+ 1 - 1
backend/app/api/leave.py

@@ -48,7 +48,7 @@ async def list_leaves(
     user_id = _user.get("user_id") or _user.get("id", "")
     items, total = leave_crud.list_leaves(
         db, page=page, page_size=pageSize, keyword=keyword, status=status,
-        tenant_no=tenant_no, create_by=user_id,
+        tenant_no=tenant_no, create_by=user_id, user_info=_user,
     )
     for item in items:
         item["leaveTypeLabel"] = _LEAVE_TYPE_MAP.get(item.get("leaveType", ""), item.get("leaveType", ""))

+ 2 - 2
backend/app/api/material.py

@@ -75,7 +75,7 @@ async def list_materials(
         tenant_no = _get_tenant_no(db, user)
         items, total = mat_svc.list_materials(
             db, tenant_no, page=page, page_size=pageSize,
-            keyword=keyword, category=category, status=status,
+            keyword=keyword, category=category, status=status, user_info=user,
         )
         return success_response(_page_payload(items, total, page, pageSize))
     except PermissionError as e:
@@ -217,7 +217,7 @@ async def list_stock_logs(
         items, total = msl_svc.list_stock_logs(
             db, tenant_no, page=page, page_size=pageSize,
             material_id=materialId, change_type=changeType,
-            start_time=startTime, end_time=endTime, keyword=keyword,
+            start_time=startTime, end_time=endTime, keyword=keyword, user_info=user,
         )
         return success_response(_page_payload(items, total, page, pageSize))
     except PermissionError as e:

+ 5 - 5
backend/app/api/menu.py

@@ -116,35 +116,35 @@ STATIC_MENU_TREE = [
                 "name": "BizCustomer",
                 "path": "customer",
                 "component": "/basic-info/customer/index",
-                "meta": {"title": "客户管理", "keepAlive": True},
+                "meta": {"title": "客户管理", "keepAlive": True, "menuCode": "biz:customer:list"},
             },
             {
                 "id": "40000000000000000000000000000003",
                 "name": "BizCellSampleType",
                 "path": "cell-sample-type",
                 "component": "/basic-info/cell-sample-type/index",
-                "meta": {"title": "细胞样本类型管理", "keepAlive": True},
+                "meta": {"title": "细胞样本类型管理", "keepAlive": True, "menuCode": "biz:cellSampleType:list"},
             },
             {
                 "id": "40000000000000000000000000000004",
                 "name": "BizCollectionRegion",
                 "path": "collection-region",
                 "component": "/basic-info/collection-region/index",
-                "meta": {"title": "采集地区及医院管理", "keepAlive": True},
+                "meta": {"title": "采集地区及医院管理", "keepAlive": True, "menuCode": "biz:collectionRegion:list"},
             },
             {
                 "id": "40000000000000000000000000000005",
                 "name": "BizCollectionProtocolNo",
                 "path": "collection-protocol-no",
                 "component": "/basic-info/collection-protocol-no/index",
-                "meta": {"title": "采集协议号管理", "keepAlive": True},
+                "meta": {"title": "采集协议号管理", "keepAlive": True, "menuCode": "biz:collectionProtocolNo:list"},
             },
             {
                 "id": "40000000000000000000000000000006",
                 "name": "BizSampleCode",
                 "path": "sample-code",
                 "component": "/basic-info/sample-code/index",
-                "meta": {"title": "样本编码管理", "keepAlive": True},
+                "meta": {"title": "样本编码管理", "keepAlive": True, "menuCode": "biz:sampleCode:list"},
             },
         ],
     },

+ 72 - 4
backend/app/api/operations.py

@@ -4,6 +4,7 @@ from typing import Any, Optional
 
 from fastapi import APIRouter, Depends, Query
 from pydantic import BaseModel, ConfigDict, Field
+from sqlalchemy import or_
 from sqlalchemy.orm import Session
 
 from app.core.data_scope import apply_data_scope
@@ -70,8 +71,10 @@ async def list_clean_rooms(
         BizCleanRoom.del_flag == 0,
         BizCleanRoom.tenant_no == tenant_no,
     )
-    if keyword:
-        query = query.filter(BizCleanRoom.room_name.like(f"%{keyword}%"))
+    kw = (keyword or "").strip()
+    if kw:
+        like = f"%{kw}%"
+        query = query.filter(or_(BizCleanRoom.room_code.like(like), BizCleanRoom.room_name.like(like)))
     query = apply_data_scope(query, BizCleanRoom, user, db, tenant_no)
     total = query.count()
     items = query.order_by(BizCleanRoom.create_time.desc()).offset((page - 1) * pageSize).limit(pageSize).all()
@@ -425,6 +428,19 @@ class StorageSlotUpdateBody(BaseModel):
     remark: Optional[str] = None
 
 
+class StorageSlotClearBody(BaseModel):
+    model_config = ConfigDict(populate_by_name=True)
+    facility_node_id: str = Field(..., alias="facilityNodeId")
+
+
+class StorageSlotInsertRowBody(BaseModel):
+    model_config = ConfigDict(populate_by_name=True)
+    facility_node_id: str = Field(..., alias="facilityNodeId")
+    after_row_num: int = Field(..., ge=0, alias="afterRowNum")
+    cols: int = Field(..., ge=1)
+    code_prefix: Optional[str] = Field("", alias="codePrefix")
+
+
 @router.get("/storage-facility-slots")
 async def list_storage_slots(
     facilityNodeId: str = Query(..., alias="facilityNodeId"),
@@ -513,6 +529,50 @@ async def update_storage_slot(
         return error_response(400, str(e))
 
 
+@router.post("/storage-facility-slots/insert-row")
+async def insert_storage_slot_row(
+    body: StorageSlotInsertRowBody,
+    user: dict = Depends(require_admin_or_super_admin),
+    db: Session = Depends(get_db),
+):
+    try:
+        tenant_no = _get_tenant_no(db, user)
+        result = storage_facility_svc.insert_row_and_shift_slots(
+            db,
+            facility_node_id=body.facility_node_id,
+            after_row_num=body.after_row_num,
+            cols=body.cols,
+            code_prefix=body.code_prefix or "",
+            tenant_no=tenant_no,
+            user_id=user.get("user_id"),
+        )
+        return success_response(result, message="新增行成功")
+    except ValueError as e:
+        return error_response(400, str(e))
+
+
+@router.post("/storage-facility-slots/clear")
+async def clear_storage_slots(
+    body: StorageSlotClearBody,
+    user: dict = Depends(require_admin_or_super_admin),
+    db: Session = Depends(get_db),
+):
+    try:
+        tenant_no = _get_tenant_no(db, user)
+        cleared_count = storage_facility_svc.clear_slots_by_facility_node(
+            db,
+            facility_node_id=body.facility_node_id,
+            tenant_no=tenant_no,
+            user_id=user.get("user_id"),
+        )
+        return success_response(
+            {"clearedCount": cleared_count},
+            message=f"已清空 {cleared_count} 个格子",
+        )
+    except ValueError as e:
+        return error_response(400, str(e))
+
+
 @router.delete("/storage-facility-slots/{slot_id}")
 async def delete_storage_slot(
     slot_id: str,
@@ -570,8 +630,16 @@ async def list_exhibition_appointments(
         BizExhibitionAppointment.del_flag == 0,
         BizExhibitionAppointment.tenant_no == tenant_no,
     )
-    if keyword:
-        query = query.filter(BizExhibitionAppointment.visit_org.like(f"%{keyword}%"))
+    kw = (keyword or "").strip()
+    if kw:
+        like = f"%{kw}%"
+        query = query.filter(
+            or_(
+                BizExhibitionAppointment.appointment_no.like(like),
+                BizExhibitionAppointment.contact_name.like(like),
+                BizExhibitionAppointment.visit_org.like(like),
+            )
+        )
     query = apply_data_scope(query, BizExhibitionAppointment, user, db, tenant_no)
     total = query.count()
     items = query.order_by(BizExhibitionAppointment.create_time.desc()).offset((page - 1) * pageSize).limit(pageSize).all()

+ 14 - 13
backend/app/api/pilot_service.py

@@ -57,7 +57,7 @@ async def list_cell_product_types(
 ):
     tenant_no = _get_tenant_no(db, user)
     items, total = ps_svc.list_cell_product_types(
-        db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no
+        db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user
     )
     tp = ps_svc.total_pages(total, pageSize)
     return success_response(
@@ -159,7 +159,7 @@ async def list_cell_product_specs(
 ):
     tenant_no = _get_tenant_no(db, user)
     items, total = ps_svc.list_cell_product_specs(
-        db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no
+        db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user
     )
     tp = ps_svc.total_pages(total, pageSize)
     return success_response(
@@ -280,6 +280,7 @@ async def list_cell_product_prices(
         keyword=keyword,
         tenant_no=tenant_no,
         cell_product_type_id=cell_product_type_id,
+        user_info=user,
     )
     tp = ps_svc.total_pages(total, pageSize)
     return success_response(
@@ -395,7 +396,7 @@ async def list_project_systems(
 ):
     tenant_no = _get_tenant_no(db, user)
     items, total = ps_svc.list_project_systems(
-        db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no
+        db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user
     )
     tp = ps_svc.total_pages(total, pageSize)
     return success_response(
@@ -509,7 +510,7 @@ async def list_project_system_items(
 ):
     try:
         tenant_no = _get_tenant_no(db, user)
-        items = ps_svc.list_project_system_items(db, system_id, tenant_no=tenant_no)
+        items = ps_svc.list_project_system_items(db, system_id, tenant_no=tenant_no, user_info=user)
         return success_response(items)
     except ValueError as e:
         return error_response(400, str(e))
@@ -635,7 +636,7 @@ async def list_channels(
 ):
     tenant_no = _get_tenant_no(db, user)
     items, total = ps_svc.list_channels(
-        db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no
+        db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user
     )
     tp = ps_svc.total_pages(total, pageSize)
     return success_response(
@@ -757,7 +758,7 @@ async def list_channel_invoices(
 ):
     try:
         tenant_no = _get_tenant_no(db, user)
-        items = ps_svc.list_channel_invoices(db, channel_id, tenant_no=tenant_no)
+        items = ps_svc.list_channel_invoices(db, channel_id, tenant_no=tenant_no, user_info=user)
         return success_response(items)
     except ValueError as e:
         return error_response(400, str(e))
@@ -865,7 +866,7 @@ async def list_channel_mail_addresses(
 ):
     try:
         tenant_no = _get_tenant_no(db, user)
-        items = ps_svc.list_channel_mail_addresses(db, channel_id, tenant_no=tenant_no)
+        items = ps_svc.list_channel_mail_addresses(db, channel_id, tenant_no=tenant_no, user_info=user)
         return success_response(items)
     except ValueError as e:
         return error_response(400, str(e))
@@ -973,7 +974,7 @@ async def list_channel_agreements(
 ):
     try:
         tenant_no = _get_tenant_no(db, user)
-        items = ps_svc.list_channel_agreements(db, channel_id, tenant_no=tenant_no)
+        items = ps_svc.list_channel_agreements(db, channel_id, tenant_no=tenant_no, user_info=user)
         return success_response(items)
     except ValueError as e:
         return error_response(400, str(e))
@@ -1127,7 +1128,7 @@ async def list_channel_agreement_prices(
 ):
     try:
         tenant_no = _get_tenant_no(db, user)
-        items = ps_svc.list_channel_agreement_prices(db, agreement_id, tenant_no=tenant_no)
+        items = ps_svc.list_channel_agreement_prices(db, agreement_id, tenant_no=tenant_no, user_info=user)
         return success_response(items)
     except ValueError as e:
         return error_response(400, str(e))
@@ -1245,7 +1246,7 @@ async def list_checkup_institutions(
 ):
     tenant_no = _get_tenant_no(db, user)
     items, total = ps_svc.list_checkup_institutions(
-        db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no
+        db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user
     )
     tp = ps_svc.total_pages(total, pageSize)
     return success_response(
@@ -1357,7 +1358,7 @@ async def list_checkup_institution_invoices(
 ):
     try:
         tenant_no = _get_tenant_no(db, user)
-        items = ps_svc.list_checkup_institution_invoices(db, institution_id, tenant_no=tenant_no)
+        items = ps_svc.list_checkup_institution_invoices(db, institution_id, tenant_no=tenant_no, user_info=user)
         return success_response(items)
     except ValueError as e:
         return error_response(400, str(e))
@@ -1465,7 +1466,7 @@ async def list_checkup_institution_projects(
 ):
     try:
         tenant_no = _get_tenant_no(db, user)
-        items = ps_svc.list_checkup_institution_projects(db, institution_id, tenant_no=tenant_no)
+        items = ps_svc.list_checkup_institution_projects(db, institution_id, tenant_no=tenant_no, user_info=user)
         return success_response(items)
     except ValueError as e:
         return error_response(400, str(e))
@@ -1606,7 +1607,7 @@ async def list_checkup_institution_packages(
 ):
     try:
         tenant_no = _get_tenant_no(db, user)
-        items = ps_svc.list_checkup_institution_packages(db, institution_id, tenant_no=tenant_no)
+        items = ps_svc.list_checkup_institution_packages(db, institution_id, tenant_no=tenant_no, user_info=user)
         return success_response(items)
     except ValueError as e:
         return error_response(400, str(e))

+ 18 - 14
backend/app/api/production.py

@@ -95,7 +95,7 @@ async def list_consumables(
     db: Session = Depends(get_db),
 ):
     tenant_no = _get_tenant_no(db, user)
-    items, total = consumable_svc.list_consumables(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no)
+    items, total = consumable_svc.list_consumables(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
     tp = consumable_svc.total_pages(total, pageSize)
     return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
 
@@ -106,7 +106,7 @@ async def list_all_consumables(
     db: Session = Depends(get_db),
 ):
     tenant_no = _get_tenant_no(db, user)
-    items, _ = consumable_svc.list_consumables(db, page=1, page_size=9999, tenant_no=tenant_no)
+    items, _ = consumable_svc.list_consumables(db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user)
     return success_response(items)
 
 
@@ -214,7 +214,7 @@ async def list_reagents(
     db: Session = Depends(get_db),
 ):
     tenant_no = _get_tenant_no(db, user)
-    items, total = reagent_svc.list_reagents(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no)
+    items, total = reagent_svc.list_reagents(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
     tp = reagent_svc.total_pages(total, pageSize)
     return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
 
@@ -225,7 +225,7 @@ async def list_all_reagents(
     db: Session = Depends(get_db),
 ):
     tenant_no = _get_tenant_no(db, user)
-    items, _ = reagent_svc.list_reagents(db, page=1, page_size=9999, tenant_no=tenant_no)
+    items, _ = reagent_svc.list_reagents(db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user)
     return success_response(items)
 
 
@@ -329,7 +329,7 @@ async def list_sop_documents(
     db: Session = Depends(get_db),
 ):
     tenant_no = _get_tenant_no(db, user)
-    items, total = sop_document_svc.list_sop_documents(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no)
+    items, total = sop_document_svc.list_sop_documents(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
     tp = sop_document_svc.total_pages(total, pageSize)
     return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
 
@@ -340,7 +340,7 @@ async def list_all_sop_documents(
     db: Session = Depends(get_db),
 ):
     tenant_no = _get_tenant_no(db, user)
-    items, _ = sop_document_svc.list_sop_documents(db, page=1, page_size=9999, tenant_no=tenant_no)
+    items, _ = sop_document_svc.list_sop_documents(db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user)
     return success_response(items)
 
 
@@ -443,7 +443,7 @@ async def list_process_categories(
     db: Session = Depends(get_db),
 ):
     tenant_no = _get_tenant_no(db, user)
-    items, total = process_category_svc.list_process_categories(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no)
+    items, total = process_category_svc.list_process_categories(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
     tp = process_category_svc.total_pages(total, pageSize)
     return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
 
@@ -454,7 +454,7 @@ async def list_all_process_categories(
     db: Session = Depends(get_db),
 ):
     tenant_no = _get_tenant_no(db, user)
-    items, _ = process_category_svc.list_process_categories(db, page=1, page_size=9999, tenant_no=tenant_no)
+    items, _ = process_category_svc.list_process_categories(db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user)
     return success_response(items)
 
 
@@ -546,7 +546,7 @@ async def list_output_types(
     db: Session = Depends(get_db),
 ):
     tenant_no = _get_tenant_no(db, user)
-    items, total = output_type_svc.list_output_types(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no)
+    items, total = output_type_svc.list_output_types(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
     tp = output_type_svc.total_pages(total, pageSize)
     return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
 
@@ -557,7 +557,7 @@ async def list_all_output_types(
     db: Session = Depends(get_db),
 ):
     tenant_no = _get_tenant_no(db, user)
-    items, _ = output_type_svc.list_output_types(db, page=1, page_size=9999, tenant_no=tenant_no)
+    items, _ = output_type_svc.list_output_types(db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user)
     return success_response(items)
 
 
@@ -667,7 +667,7 @@ async def list_consumable_packs(
     db: Session = Depends(get_db),
 ):
     tenant_no = _get_tenant_no(db, user)
-    items, total = consumable_pack_svc.list_consumable_packs(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no)
+    items, total = consumable_pack_svc.list_consumable_packs(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
     tp = consumable_pack_svc.total_pages(total, pageSize)
     return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
 
@@ -678,7 +678,7 @@ async def list_all_consumable_packs(
     db: Session = Depends(get_db),
 ):
     tenant_no = _get_tenant_no(db, user)
-    items = consumable_pack_svc.list_all_packs(db, tenant_no)
+    items = consumable_pack_svc.list_all_packs(db, tenant_no, user_info=user)
     return success_response(items)
 
 
@@ -848,7 +848,7 @@ async def list_processes(
     db: Session = Depends(get_db),
 ):
     tenant_no = _get_tenant_no(db, user)
-    items, total = process_svc.list_processes(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no)
+    items, total = process_svc.list_processes(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
     tp = process_svc.total_pages(total, pageSize)
     return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
 
@@ -859,7 +859,7 @@ async def list_all_processes(
     db: Session = Depends(get_db),
 ):
     tenant_no = _get_tenant_no(db, user)
-    items = process_svc.list_all_processes(db, tenant_no)
+    items = process_svc.list_all_processes(db, tenant_no, user_info=user)
     return success_response(items)
 
 
@@ -993,6 +993,7 @@ async def list_process_routes(
         db, page=page, page_size=pageSize,
         route_name=routeName, route_code=routeCode,
         product_type_id=productTypeId, tenant_no=tenant_no,
+        user_info=user,
     )
     tp = route_svc.total_pages(total, pageSize)
     return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
@@ -1252,6 +1253,7 @@ async def list_production_work_orders(
     )
     result = production_work_order_svc.list_production_work_orders(
         db, tenant_no=tenant_no, query=query, page=page, page_size=pageSize,
+        user_info=user,
     )
     items = result.get("items", [])
     total = result.get("total", 0)
@@ -1327,6 +1329,7 @@ async def list_production_tasks(
     items, total = production_task_svc.list_production_tasks(
         db, tenant_no=tenant_no, page=page, page_size=pageSize,
         work_order_id=workOrderId, task_no=taskNo, task_status=taskStatus,
+        user_info=user,
     )
     tp = production_task_svc.total_pages(total, pageSize)
     return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
@@ -1560,6 +1563,7 @@ async def list_cell_products(
         db, tenant_no=tenant_no, page=page, page_size=pageSize,
         product_code=productCode, product_name=productName,
         product_type_id=productTypeId, product_status=productStatus,
+        user_info=user,
     )
     items = result.get("items", [])
     total = result.get("total", 0)

+ 13 - 6
backend/app/api/qc.py

@@ -72,7 +72,8 @@ async def list_qc_third_party_companies(
     try:
         tenant_no = _get_tenant_no(db, user)
         items, total = tpc_svc.list_companies(
-            db, page=page, page_size=pageSize, keyword=keyword, status=status, tenant_no=tenant_no
+            db, page=page, page_size=pageSize, keyword=keyword, status=status, tenant_no=tenant_no,
+            user_info=user
         )
         return success_response(_page_payload(items, total, page, pageSize))
     except PermissionError as e:
@@ -230,6 +231,7 @@ async def list_qc_third_party_items(
             status=status,
             company_id=companyId,
             tenant_no=tenant_no,
+            user_info=user,
         )
         return success_response(_page_payload(items, total, page, pageSize))
     except PermissionError as e:
@@ -246,7 +248,7 @@ async def list_qc_third_party_items_active(
 ):
     try:
         tenant_no = _get_tenant_no(db, user)
-        return success_response(tpi_svc.list_active_items(db, tenant_no, company_id=companyId))
+        return success_response(tpi_svc.list_active_items(db, tenant_no, company_id=companyId, user_info=user))
     except PermissionError as e:
         return error_response(str(e), code=403)
     except Exception as e:
@@ -390,7 +392,8 @@ async def list_qc_lab_test_items(
     try:
         tenant_no = _get_tenant_no(db, user)
         items, total = lab_svc.list_lab_items(
-            db, page=page, page_size=pageSize, keyword=keyword, status=status, tenant_no=tenant_no
+            db, page=page, page_size=pageSize, keyword=keyword, status=status, tenant_no=tenant_no,
+            user_info=user
         )
         return success_response(_page_payload(items, total, page, pageSize))
     except PermissionError as e:
@@ -406,7 +409,7 @@ async def list_qc_lab_test_items_active(
 ):
     try:
         tenant_no = _get_tenant_no(db, user)
-        return success_response(lab_svc.list_active_lab_items(db, tenant_no))
+        return success_response(lab_svc.list_active_lab_items(db, tenant_no, user_info=user))
     except PermissionError as e:
         return error_response(str(e), code=403)
     except Exception as e:
@@ -501,7 +504,7 @@ async def list_item_params(
 ):
     try:
         tenant_no = _get_tenant_no(db, user)
-        return success_response(param_svc.list_params_by_item(db, itemSource, itemId, tenant_no))
+        return success_response(param_svc.list_params_by_item(db, itemSource, itemId, tenant_no, user_info=user))
     except PermissionError as e:
         return error_response(str(e), code=403)
     except Exception as e:
@@ -558,6 +561,7 @@ async def list_qc_work_orders(
             keyword=keyword,
             work_order_status=workOrderStatus,
             tenant_no=tenant_no,
+            user_info=user,
         )
         return success_response(_page_payload(items, total, page, pageSize))
     except PermissionError as e:
@@ -645,6 +649,7 @@ async def list_qc_tasks(
             work_order_id=workOrderId,
             qc_biz_type=qcBizType,
             tenant_no=tenant_no,
+            user_info=user,
         )
         return success_response(_page_payload(items, total, page, pageSize))
     except PermissionError as e:
@@ -717,7 +722,7 @@ async def get_task_results(
 ):
     try:
         tenant_no = _get_tenant_no(db, user)
-        return success_response(result_svc.list_results_by_task(db, task_id, tenant_no))
+        return success_response(result_svc.list_results_by_task(db, task_id, tenant_no, user_info=user))
     except PermissionError as e:
         return error_response(str(e), code=403)
     except Exception as e:
@@ -849,6 +854,7 @@ async def list_qc_materials(
         tenant_no = _get_tenant_no(db, user)
         items, total = mat_svc.list_materials(
             db, page=page, page_size=pageSize, keyword=keyword, status=status, tenant_no=tenant_no,
+            user_info=user,
         )
         return success_response(_page_payload(items, total, page, pageSize))
     except PermissionError as e:
@@ -959,6 +965,7 @@ async def list_qc_material_usages(
             db, tenant_no, page=page, page_size=pageSize,
             material_id=materialId, change_type=changeType,
             start_time=startTime, end_time=endTime,
+            user_info=user,
         )
         return success_response(_page_payload(items, total, page, pageSize))
     except PermissionError as e:

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

@@ -130,6 +130,7 @@ async def list_research_donations(
         keyword=keyword,
         donation_status=donationStatus,
         collector_user_id=collector_user_id,
+        user_info=user,
     )
     return success_response(result)
 
@@ -156,6 +157,7 @@ async def list_research_donation_audits(
         page_size=pageSize,
         keyword=keyword,
         audit_status=auditStatus,
+        user_info=user,
     )
     return success_response(result)
 

+ 2 - 2
backend/app/api/sample_reception.py

@@ -208,7 +208,7 @@ async def list_pending_samples(
         tenant_no = _get_tenant_no(db, user)
         items, total = sr_svc.list_pending_protocol_samples(
             db, page=page, page_size=pageSize, tenant_no=tenant_no, keyword=keyword,
-            source_type=sourceType,
+            source_type=sourceType, user_info=user,
         )
         return success_response(_page_payload(items, total, page, pageSize))
     except PermissionError as e:
@@ -230,7 +230,7 @@ async def list_received_samples(
         tenant_no = _get_tenant_no(db, user)
         items, total = sr_svc.list_received_protocol_samples(
             db, page=page, page_size=pageSize, tenant_no=tenant_no, keyword=keyword,
-            source_type=sourceType,
+            source_type=sourceType, user_info=user,
         )
         return success_response(_page_payload(items, total, page, pageSize))
     except PermissionError as e:

+ 5 - 5
backend/app/api/workflow.py

@@ -158,7 +158,7 @@ async def list_process_defs(
 ):
     tenant_no = _user.get("tenant_no", "zkxw")
     items, total = wf_crud.list_process_defs(
-        db, page=page, page_size=pageSize, keyword=keyword, status=status, tenant_no=tenant_no
+        db, page=page, page_size=pageSize, keyword=keyword, status=status, tenant_no=tenant_no, user_info=_user
     )
     tp = wf_crud.total_pages(total, pageSize)
     return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
@@ -261,7 +261,7 @@ async def list_versions(
     db: Session = Depends(get_db),
 ):
     tenant_no = _user.get("tenant_no", "zkxw")
-    items = wf_crud.list_versions(db, def_id, tenant_no=tenant_no)
+    items = wf_crud.list_versions(db, def_id, tenant_no=tenant_no, user_info=_user)
     return success_response(items)
 
 
@@ -347,7 +347,7 @@ async def list_my_instances(
     tenant_no = user.get("tenant_no", "zkxw")
     items, total = wf_crud.list_my_instances(
         db, user_id, page=page, page_size=pageSize,
-        keyword=keyword, status=status, tenant_no=tenant_no,
+        keyword=keyword, status=status, tenant_no=tenant_no, user_info=user,
     )
     _enrich_task_list(db, items)
     tp = wf_crud.total_pages(total, pageSize)
@@ -428,7 +428,7 @@ async def list_pending_tasks(
     user_id = user.get("user_id") or user.get("id")
     tenant_no = user.get("tenant_no", "zkxw")
     items, total = wf_crud.list_pending_tasks(
-        db, user_id, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no
+        db, user_id, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user
     )
     _enrich_task_list(db, items)
     tp = wf_crud.total_pages(total, pageSize)
@@ -446,7 +446,7 @@ async def list_completed_tasks(
     user_id = user.get("user_id") or user.get("id")
     tenant_no = user.get("tenant_no", "zkxw")
     items, total = wf_crud.list_completed_tasks(
-        db, user_id, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no
+        db, user_id, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user
     )
     _enrich_task_list(db, items)
     tp = wf_crud.total_pages(total, pageSize)

+ 13 - 9
backend/app/core/data_scope.py

@@ -122,34 +122,38 @@ def apply_data_scope(query, model, user_info: dict, db: Session, tenant_no: str
     Returns:
         应用数据权限过滤后的 query
     """
-    # 1. super_admin 直接放行
+    # 1. 无用户信息 → 不过滤
+    if not user_info:
+        return query
+
+    # 2. super_admin 直接放行
     if user_info.get("role") == "super_admin":
         return query
 
-    # 2. 获取 user_id
+    # 3. 获取 user_id
     user_id = user_info.get("user_id")
     if not user_id:
         return query
 
-    # 3. 确定 tenant_no
+    # 4. 确定 tenant_no
     effective_tenant = tenant_no or settings.DEFAULT_TENANT_NO
 
-    # 4. 检查 model 是否有可过滤字段
+    # 5. 检查 model 是否有可过滤字段
     has_dept = hasattr(model, "create_dept_id")
     has_user = hasattr(model, "create_by")
     if not has_dept and not has_user:
         return query
 
-    # 5. 获取用户所有角色的数据权限
+    # 6. 获取用户所有角色的数据权限
     role_scopes = _resolve_user_role_scopes(db, user_id, effective_tenant)
     if not role_scopes:
-        return query
+        return query.filter(False)
 
-    # 6. 任一角色拥有全部数据权限 → 放行(union 语义)
+    # 7. 任一角色拥有全部数据权限 → 放行(union 语义)
     if any(rs["data_scope"] == "1" for rs in role_scopes):
         return query
 
-    # 7. 收集所有角色的 dept_ids 和 user_ids(union 合并)
+    # 8. 收集所有角色的 dept_ids 和 user_ids(union 合并)
     dept_ids: Set[str] = set()
     user_ids: Set[str] = set()
 
@@ -184,7 +188,7 @@ def apply_data_scope(query, model, user_info: dict, db: Session, tenant_no: str
                 elif detail.scope_type == "user":
                     user_ids.add(val)
 
-    # 8. 构建 OR 条件并应用
+    # 9. 构建 OR 条件并应用
     conditions = []
     if dept_ids and has_dept:
         conditions.append(model.create_dept_id.in_(dept_ids))

+ 4 - 0
backend/app/crud/cell_product.py

@@ -48,6 +48,7 @@ def list_cell_products(
     product_name: Optional[str] = None,
     product_type_id: Optional[str] = None,
     product_status: Optional[str] = None,
+    user_info=None,
 ) -> Dict[str, Any]:
     def _filtered():
         q0 = db.query(BizCellProduct).filter(
@@ -64,6 +65,9 @@ def list_cell_products(
             q0 = q0.filter(BizCellProduct.product_type_id == product_type_id)
         if product_status:
             q0 = q0.filter(BizCellProduct.product_status == product_status)
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, BizCellProduct, user_info, db, tenant_no)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(BizCellProduct.id)).scalar() or 0

+ 36 - 3
backend/app/crud/collection_protocol.py

@@ -600,8 +600,8 @@ def list_storage_certs(
     make_status: Optional[str] = None,
     page: int = 1,
     page_size: int = 10,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
-    """储存证管理列表:生效中协议 + 当事人/样本/储存证信息(不要求样本已入库)。"""
     q = db.query(BizCollectionProtocol).filter(
         BizCollectionProtocol.del_flag == 0,
         BizCollectionProtocol.tenant_no == tenant_no,
@@ -624,6 +624,9 @@ def list_storage_certs(
                 BizCollectionProtocol.id.in_(party_ids),
             )
         )
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q = apply_data_scope(q, BizCollectionProtocol, user_info, db, tenant_no)
 
     rows_all = q.order_by(BizCollectionProtocol.create_time.desc()).all()
     protocol_ids = [r.id for r in rows_all]
@@ -715,6 +718,7 @@ def list_collection_protocols(
     protocol_status: Optional[str] = None,
     tenant_no: Optional[str] = None,
     scope_collector_user_id: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     def _filtered():
         q0 = db.query(BizCollectionProtocol).filter(BizCollectionProtocol.del_flag == 0)
@@ -735,6 +739,9 @@ def list_collection_protocols(
                     BizCollectionProtocol.remark.like(like),
                 )
             )
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, BizCollectionProtocol, user_info, db, tenant_no)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(BizCollectionProtocol.id)).scalar() or 0
@@ -988,6 +995,7 @@ def list_audits(
     keyword: Optional[str] = None,
     audit_status: Optional[str] = None,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     q_base = (
         db.query(BizProtocolAudit, BizCollectionProtocol.protocol_no)
@@ -1010,6 +1018,9 @@ def list_audits(
         q_base = q_base.filter(
             or_(BizCollectionProtocol.protocol_no.like(like), BizProtocolAudit.audit_remark.like(like))
         )
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q_base = apply_data_scope(q_base, BizProtocolAudit, user_info, db, tenant_no)
 
     total = q_base.order_by(None).with_entities(func.count(BizProtocolAudit.id)).scalar() or 0
     page = max(1, page)
@@ -1132,10 +1143,14 @@ def list_receipt_confirm_details(
     page: int = 1,
     page_size: int = 10,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     if not tenant_no:
         return [], 0
     q_base = _detail_query_with_protocol(db, tenant_no, "jfqrz")
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q_base = apply_data_scope(q_base, BizProtocolPaymentDetail, user_info, db, tenant_no)
     total = q_base.order_by(None).with_entities(func.count(BizProtocolPaymentDetail.id)).scalar() or 0
     page = max(1, page)
     page_size = min(max(1, page_size), 100)
@@ -1154,11 +1169,14 @@ def list_receipt_confirmed_details(
     page: int = 1,
     page_size: int = 10,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
-    """财务已确认收款(yjf),按确认时间倒序。"""
     if not tenant_no:
         return [], 0
     q_base = _detail_query_with_protocol(db, tenant_no, "yjf")
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q_base = apply_data_scope(q_base, BizProtocolPaymentDetail, user_info, db, tenant_no)
     total = q_base.order_by(None).with_entities(func.count(BizProtocolPaymentDetail.id)).scalar() or 0
     page = max(1, page)
     page_size = min(max(1, page_size), 100)
@@ -1225,10 +1243,14 @@ def list_refund_confirm_details(
     page: int = 1,
     page_size: int = 10,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     if not tenant_no:
         return [], 0
     q_base = _detail_query_with_protocol(db, tenant_no, "tfqrz")
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q_base = apply_data_scope(q_base, BizProtocolPaymentDetail, user_info, db, tenant_no)
     total = q_base.order_by(None).with_entities(func.count(BizProtocolPaymentDetail.id)).scalar() or 0
     page = max(1, page)
     page_size = min(max(1, page_size), 100)
@@ -1255,11 +1277,14 @@ def list_refund_confirmed_details(
     page: int = 1,
     page_size: int = 10,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
-    """财务已确认退费(ytf),按确认时间倒序。"""
     if not tenant_no:
         return [], 0
     q_base = _detail_query_with_protocol(db, tenant_no, "ytf")
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q_base = apply_data_scope(q_base, BizProtocolPaymentDetail, user_info, db, tenant_no)
     total = q_base.order_by(None).with_entities(func.count(BizProtocolPaymentDetail.id)).scalar() or 0
     page = max(1, page)
     page_size = min(max(1, page_size), 100)
@@ -1486,6 +1511,7 @@ def list_payment_details_page(
     keyword: Optional[str] = None,
     transfer_type: Optional[str] = None,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     if not tenant_no:
         return [], 0
@@ -1507,6 +1533,9 @@ def list_payment_details_page(
         q = q.filter(BizCollectionProtocol.protocol_no.like(like))
     if transfer_type and transfer_type.strip():
         q = q.filter(BizProtocolPaymentDetail.transfer_type == transfer_type.strip())
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q = apply_data_scope(q, BizProtocolPaymentDetail, user_info, db, tenant_no)
     total = q.order_by(None).with_entities(func.count(BizProtocolPaymentDetail.id)).scalar() or 0
     page = max(1, page)
     page_size = min(max(1, page_size), 100)
@@ -1559,6 +1588,7 @@ def list_installment_warning(
     page_size: int = 10,
     warning_type: str = "all",
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     if not tenant_no:
         return [], 0
@@ -1566,6 +1596,9 @@ def list_installment_warning(
     q0 = _detail_query_with_protocol(db, tenant_no, "dfj").filter(
         BizProtocolPaymentDetail.installment_no.isnot(None),
     )
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q0 = apply_data_scope(q0, BizProtocolPaymentDetail, user_info, db, tenant_no)
 
     # 拉取当前租户下候选(分期号非空);日期过滤在内存中完成以保证与 classify 一致
     rows_all = q0.order_by(BizProtocolPaymentDetail.installment_due_date.asc()).all()

+ 4 - 0
backend/app/crud/consumable.py

@@ -36,6 +36,7 @@ def list_consumables(
     page_size: int = 10,
     keyword: Optional[str] = None,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     def _filtered():
         q0 = db.query(BizConsumable).filter(BizConsumable.del_flag == 0)
@@ -50,6 +51,9 @@ def list_consumables(
                     BizConsumable.consumable_name.like(like),
                 )
             )
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, BizConsumable, user_info, db, tenant_no)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(BizConsumable.id)).scalar() or 0

+ 10 - 7
backend/app/crud/consumable_pack.py

@@ -48,6 +48,7 @@ def list_consumable_packs(
     page_size: int = 10,
     keyword: Optional[str] = None,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     def _filtered():
         q0 = db.query(BizConsumablePack).filter(BizConsumablePack.del_flag == 0)
@@ -62,6 +63,9 @@ def list_consumable_packs(
                     BizConsumablePack.pack_name.like(like),
                 )
             )
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, BizConsumablePack, user_info, db, tenant_no)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(BizConsumablePack.id)).scalar() or 0
@@ -77,13 +81,12 @@ def list_consumable_packs(
     return [_row_to_dict(x) for x in rows], int(total)
 
 
-def list_all_packs(db: Session, tenant_no: str) -> List[Dict[str, Any]]:
-    rows = (
-        db.query(BizConsumablePack)
-        .filter(and_(BizConsumablePack.tenant_no == tenant_no, BizConsumablePack.del_flag == 0))
-        .order_by(BizConsumablePack.create_time.desc())
-        .all()
-    )
+def list_all_packs(db: Session, tenant_no: str, user_info=None) -> List[Dict[str, Any]]:
+    q0 = db.query(BizConsumablePack).filter(and_(BizConsumablePack.tenant_no == tenant_no, BizConsumablePack.del_flag == 0))
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q0 = apply_data_scope(q0, BizConsumablePack, user_info, db, tenant_no)
+    rows = q0.order_by(BizConsumablePack.create_time.desc()).all()
     return [_row_to_dict(x) for x in rows]
 
 

+ 8 - 3
backend/app/crud/dept.py

@@ -56,6 +56,7 @@ def list_depts(
     *,
     keyword: Optional[str] = None,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> List[Dict[str, Any]]:
     q = db.query(SysDept).filter(SysDept.del_flag == 0)
     if tenant_no:
@@ -64,6 +65,9 @@ def list_depts(
     if kw:
         like = f"%{kw}%"
         q = q.filter(SysDept.dept_name.like(like))
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q = apply_data_scope(q, SysDept, user_info, db, tenant_no)
     rows = q.order_by(SysDept.create_time.desc()).all()
     return [_row_to_dict(x) for x in rows]
 
@@ -73,17 +77,18 @@ def list_depts_tree(
     *,
     keyword: Optional[str] = None,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> List[Dict[str, Any]]:
     kw = (keyword or "").strip()
     if not kw:
-        flat = list_depts(db, keyword=None, tenant_no=tenant_no)
+        flat = list_depts(db, keyword=None, tenant_no=tenant_no, user_info=user_info)
         return _build_tree(flat, "0")
 
-    matched = list_depts(db, keyword=keyword, tenant_no=tenant_no)
+    matched = list_depts(db, keyword=keyword, tenant_no=tenant_no, user_info=user_info)
     if not matched:
         return []
 
-    all_flat = list_depts(db, keyword=None, tenant_no=tenant_no)
+    all_flat = list_depts(db, keyword=None, tenant_no=tenant_no, user_info=user_info)
     seed_ids = {m["id"] for m in matched if m.get("id")}
     expanded = _collect_with_ancestors(all_flat, seed_ids)
     return _build_tree(expanded, "0")

+ 4 - 0
backend/app/crud/dict_data.py

@@ -34,6 +34,7 @@ def list_dict_data(
     dict_type_code: Optional[str] = None,
     keyword: Optional[str] = None,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     def _filtered():
         q0 = db.query(SysDictData).filter(SysDictData.del_flag == 0)
@@ -45,6 +46,9 @@ def list_dict_data(
         if kw:
             like = f"%{kw}%"
             q0 = q0.filter(SysDictData.dict_label.like(like))
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, SysDictData, user_info, db, tenant_no)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(SysDictData.id)).scalar() or 0

+ 4 - 0
backend/app/crud/dict_type.py

@@ -32,6 +32,7 @@ def list_dict_types(
     page_size: int = 10,
     keyword: Optional[str] = None,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     def _filtered():
         q0 = db.query(SysDictType).filter(SysDictType.del_flag == 0)
@@ -41,6 +42,9 @@ def list_dict_types(
         if kw:
             like = f"%{kw}%"
             q0 = q0.filter(or_(SysDictType.dict_name.like(like), SysDictType.dict_type.like(like)))
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, SysDictType, user_info, db, tenant_no)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(SysDictType.id)).scalar() or 0

+ 4 - 0
backend/app/crud/ethnic.py

@@ -20,11 +20,15 @@ def list_ethnics(
     *,
     keyword: Optional[str] = None,
     limit: int = 200,
+    user_info=None,
 ) -> List[Dict[str, Any]]:
     q = db.query(SysEthnic).filter(SysEthnic.del_flag == 0)
     kw = (keyword or "").strip()
     if kw:
         like = f"%{kw}%"
         q = q.filter(SysEthnic.ethnic_name.like(like))
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q = apply_data_scope(q, SysEthnic, user_info, db)
     rows = q.order_by(SysEthnic.order_num.asc(), SysEthnic.ethnic_code.asc()).limit(max(1, min(limit, 500))).all()
     return [_row_to_dict(x) for x in rows]

+ 4 - 0
backend/app/crud/express_company.py

@@ -32,6 +32,7 @@ def list_express_companies(
     page_size: int = 10,
     keyword: Optional[str] = None,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     def _filtered():
         q0 = db.query(BizExpressCompany).filter(BizExpressCompany.del_flag == 0)
@@ -41,6 +42,9 @@ def list_express_companies(
         if kw:
             like = f"%{kw}%"
             q0 = q0.filter(or_(BizExpressCompany.express_code.like(like), BizExpressCompany.express_name.like(like)))
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, BizExpressCompany, user_info, db, tenant_no)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(BizExpressCompany.id)).scalar() or 0

+ 4 - 0
backend/app/crud/item_unit.py

@@ -32,6 +32,7 @@ def list_item_units(
     page_size: int = 10,
     keyword: Optional[str] = None,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     def _filtered():
         q0 = db.query(BizItemUnit).filter(BizItemUnit.del_flag == 0)
@@ -41,6 +42,9 @@ def list_item_units(
         if kw:
             like = f"%{kw}%"
             q0 = q0.filter(or_(BizItemUnit.unit_code.like(like), BizItemUnit.unit_name.like(like)))
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, BizItemUnit, user_info, db, tenant_no)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(BizItemUnit.id)).scalar() or 0

+ 4 - 0
backend/app/crud/leave.py

@@ -44,6 +44,7 @@ def list_leaves(
     status: Optional[str] = None,
     tenant_no: Optional[str] = None,
     create_by: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     q = db.query(BizLeaveRequest).filter(BizLeaveRequest.del_flag == 0)
     if tenant_no:
@@ -56,6 +57,9 @@ def list_leaves(
     if kw:
         like = f"%{kw}%"
         q = q.filter(or_(BizLeaveRequest.reason.like(like), BizLeaveRequest.leave_type.like(like)))
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q = apply_data_scope(q, BizLeaveRequest, user_info, db, tenant_no)
     total = q.count()
     rows = (
         q.order_by(BizLeaveRequest.create_time.desc())

+ 4 - 0
backend/app/crud/material.py

@@ -65,6 +65,7 @@ def list_materials(
     keyword: Optional[str] = None,
     category: Optional[str] = None,
     status: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     def _filtered():
         q0 = db.query(BizMaterial).filter(
@@ -86,6 +87,9 @@ def list_materials(
         st = (status or "").strip()
         if st in ("0", "1"):
             q0 = q0.filter(BizMaterial.status == st)
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, BizMaterial, user_info, db, tenant_no)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(BizMaterial.id)).scalar() or 0

+ 4 - 0
backend/app/crud/material_stock_log.py

@@ -50,6 +50,7 @@ def list_stock_logs(
     start_time: Optional[str] = None,
     end_time: Optional[str] = None,
     keyword: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     def _filtered():
         q0 = db.query(BizMaterialStockLog).filter(
@@ -73,6 +74,9 @@ def list_stock_logs(
                     BizMaterialStockLog.material_name.ilike(like),
                 )
             )
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, BizMaterialStockLog, user_info, db, tenant_no)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(BizMaterialStockLog.id)).scalar() or 0

+ 35 - 2
backend/app/crud/menu.py

@@ -43,6 +43,7 @@ def list_menus(
     *,
     keyword: Optional[str] = None,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> List[Dict[str, Any]]:
     q = db.query(SysMenu).filter(SysMenu.del_flag == 0)
     if tenant_no:
@@ -51,6 +52,9 @@ def list_menus(
     if kw:
         like = f"%{kw}%"
         q = q.filter(SysMenu.menu_name.like(like))
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q = apply_data_scope(q, SysMenu, user_info, db, tenant_no)
     rows = q.order_by(SysMenu.create_time.desc()).all()
     return [_row_to_dict(x) for x in rows]
 
@@ -60,9 +64,38 @@ def list_menus_tree(
     *,
     keyword: Optional[str] = None,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> List[Dict[str, Any]]:
-    flat = list_menus(db, keyword=keyword, tenant_no=tenant_no)
-    return _build_tree(flat, "0")
+    kw = (keyword or "").strip()
+    # 关键词检索时:仅返回命中节点会导致其父节点缺失,从而无法组装为以 parentId=0 开始的树。
+    # 这里改为:命中节点 + 其所有祖先节点,再组树。
+    if not kw:
+        flat = list_menus(db, keyword=None, tenant_no=tenant_no, user_info=user_info)
+        return _build_tree(flat, "0")
+
+    all_items = list_menus(db, keyword=None, tenant_no=tenant_no, user_info=user_info)
+    id_map: Dict[str, Dict[str, Any]] = {x.get("id"): x for x in all_items if x.get("id")}
+
+    like_items = [
+        x for x in all_items if kw and kw in (x.get("menuName") or "")
+    ]
+
+    picked: Dict[str, Dict[str, Any]] = {}
+
+    def pick_with_ancestors(item_id: str) -> None:
+        cur = id_map.get(item_id)
+        while cur and cur.get("id") and cur.get("id") not in picked:
+            picked[cur["id"]] = cur
+            parent_id = (cur.get("parentId") or "0").strip()
+            if not parent_id or parent_id == "0":
+                break
+            cur = id_map.get(parent_id)
+
+    for it in like_items:
+        if it.get("id"):
+            pick_with_ancestors(it["id"])
+
+    return _build_tree(list(picked.values()), "0")
 
 
 def get_menu(db: Session, menu_id: str) -> Optional[SysMenu]:

+ 4 - 0
backend/app/crud/output_type.py

@@ -32,6 +32,7 @@ def list_output_types(
     page_size: int = 10,
     keyword: Optional[str] = None,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     def _filtered():
         q0 = db.query(BizOutputType).filter(BizOutputType.del_flag == 0)
@@ -46,6 +47,9 @@ def list_output_types(
                     BizOutputType.type_name.like(like),
                 )
             )
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, BizOutputType, user_info, db, tenant_no)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(BizOutputType.id)).scalar() or 0

+ 56 - 14
backend/app/crud/pilot_service.py

@@ -46,7 +46,7 @@ def _row_to_dict_cell_product_type(row) -> Dict[str, Any]:
     }
 
 
-def list_cell_product_types(db, *, page=1, page_size=10, keyword=None, tenant_no=None):
+def list_cell_product_types(db, *, page=1, page_size=10, keyword=None, tenant_no=None, user_info=None):
     def _filtered():
         q0 = db.query(BizCellProductType).filter(BizCellProductType.del_flag == 0)
         if tenant_no:
@@ -60,6 +60,9 @@ def list_cell_product_types(db, *, page=1, page_size=10, keyword=None, tenant_no
                     BizCellProductType.product_type_name.like(like),
                 )
             )
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, BizCellProductType, user_info, db, tenant_no)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(BizCellProductType.id)).scalar() or 0
@@ -158,7 +161,7 @@ def _row_to_dict_cell_product_spec(row) -> Dict[str, Any]:
     }
 
 
-def list_cell_product_specs(db, *, page=1, page_size=10, keyword=None, tenant_no=None):
+def list_cell_product_specs(db, *, page=1, page_size=10, keyword=None, tenant_no=None, user_info=None):
     def _filtered():
         q0 = db.query(BizCellProductSpec).filter(BizCellProductSpec.del_flag == 0)
         if tenant_no:
@@ -172,6 +175,9 @@ def list_cell_product_specs(db, *, page=1, page_size=10, keyword=None, tenant_no
                     BizCellProductSpec.spec_name.like(like),
                 )
             )
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, BizCellProductSpec, user_info, db, tenant_no)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(BizCellProductSpec.id)).scalar() or 0
@@ -276,7 +282,7 @@ def _row_to_dict_cell_product_price(row) -> Dict[str, Any]:
     }
 
 
-def list_cell_product_prices(db, *, page=1, page_size=10, keyword=None, tenant_no=None, cell_product_type_id=None):
+def list_cell_product_prices(db, *, page=1, page_size=10, keyword=None, tenant_no=None, cell_product_type_id=None, user_info=None):
     def _filtered():
         q0 = db.query(BizCellProductPrice).filter(BizCellProductPrice.del_flag == 0)
         if tenant_no:
@@ -293,6 +299,9 @@ def list_cell_product_prices(db, *, page=1, page_size=10, keyword=None, tenant_n
                     BizCellProductPrice.cell_type_name.like(like),
                 )
             )
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, BizCellProductPrice, user_info, db, tenant_no)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(BizCellProductPrice.id)).scalar() or 0
@@ -441,7 +450,7 @@ def _row_to_dict_project_system(row) -> Dict[str, Any]:
     }
 
 
-def list_project_systems(db, *, page=1, page_size=10, keyword=None, tenant_no=None):
+def list_project_systems(db, *, page=1, page_size=10, keyword=None, tenant_no=None, user_info=None):
     def _filtered():
         q0 = db.query(BizCellProductProjectSystem).filter(BizCellProductProjectSystem.del_flag == 0)
         if tenant_no:
@@ -455,6 +464,9 @@ def list_project_systems(db, *, page=1, page_size=10, keyword=None, tenant_no=No
                     BizCellProductProjectSystem.system_name.like(like),
                 )
             )
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, BizCellProductProjectSystem, user_info, db, tenant_no)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(BizCellProductProjectSystem.id)).scalar() or 0
@@ -566,7 +578,7 @@ def _row_to_dict_project_system_item(row) -> Dict[str, Any]:
     }
 
 
-def list_project_system_items(db, project_system_id, tenant_no=None):
+def list_project_system_items(db, project_system_id, tenant_no=None, user_info=None):
     q = db.query(BizCellProductProjectSystemItem).filter(
         and_(
             BizCellProductProjectSystemItem.project_system_id == project_system_id,
@@ -575,6 +587,9 @@ def list_project_system_items(db, project_system_id, tenant_no=None):
     )
     if tenant_no:
         q = q.filter(BizCellProductProjectSystemItem.tenant_no == tenant_no)
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q = apply_data_scope(q, BizCellProductProjectSystemItem, user_info, db, tenant_no)
     rows = q.order_by(BizCellProductProjectSystemItem.create_time.desc()).all()
     return [_row_to_dict_project_system_item(x) for x in rows]
 
@@ -723,7 +738,7 @@ def _row_to_dict_channel(
     }
 
 
-def list_channels(db, *, page=1, page_size=10, keyword=None, tenant_no=None):
+def list_channels(db, *, page=1, page_size=10, keyword=None, tenant_no=None, user_info=None):
     def _filtered():
         q0 = db.query(BizChannel).filter(BizChannel.del_flag == 0)
         if tenant_no:
@@ -738,6 +753,9 @@ def list_channels(db, *, page=1, page_size=10, keyword=None, tenant_no=None):
                     BizChannel.contact_name.like(like),
                 )
             )
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, BizChannel, user_info, db, tenant_no)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(BizChannel.id)).scalar() or 0
@@ -870,7 +888,7 @@ def _row_to_dict_channel_invoice(row) -> Dict[str, Any]:
     }
 
 
-def list_channel_invoices(db, channel_id, tenant_no=None):
+def list_channel_invoices(db, channel_id, tenant_no=None, user_info=None):
     q = db.query(BizChannelInvoice).filter(
         and_(
             BizChannelInvoice.channel_id == channel_id,
@@ -879,6 +897,9 @@ def list_channel_invoices(db, channel_id, tenant_no=None):
     )
     if tenant_no:
         q = q.filter(BizChannelInvoice.tenant_no == tenant_no)
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q = apply_data_scope(q, BizChannelInvoice, user_info, db, tenant_no)
     rows = q.order_by(BizChannelInvoice.create_time.desc()).all()
     return [_row_to_dict_channel_invoice(x) for x in rows]
 
@@ -1003,7 +1024,7 @@ def _row_to_dict_channel_mail_address(row) -> Dict[str, Any]:
     }
 
 
-def list_channel_mail_addresses(db, channel_id, tenant_no=None):
+def list_channel_mail_addresses(db, channel_id, tenant_no=None, user_info=None):
     q = db.query(BizChannelMailAddress).filter(
         and_(
             BizChannelMailAddress.channel_id == channel_id,
@@ -1012,6 +1033,9 @@ def list_channel_mail_addresses(db, channel_id, tenant_no=None):
     )
     if tenant_no:
         q = q.filter(BizChannelMailAddress.tenant_no == tenant_no)
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q = apply_data_scope(q, BizChannelMailAddress, user_info, db, tenant_no)
     rows = q.order_by(BizChannelMailAddress.create_time.desc()).all()
     return [_row_to_dict_channel_mail_address(x) for x in rows]
 
@@ -1131,7 +1155,7 @@ def _row_to_dict_channel_agreement(row) -> Dict[str, Any]:
     }
 
 
-def list_channel_agreements(db, channel_id, tenant_no=None):
+def list_channel_agreements(db, channel_id, tenant_no=None, user_info=None):
     q = db.query(BizChannelAgreement).filter(
         and_(
             BizChannelAgreement.channel_id == channel_id,
@@ -1140,6 +1164,9 @@ def list_channel_agreements(db, channel_id, tenant_no=None):
     )
     if tenant_no:
         q = q.filter(BizChannelAgreement.tenant_no == tenant_no)
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q = apply_data_scope(q, BizChannelAgreement, user_info, db, tenant_no)
     rows = q.order_by(BizChannelAgreement.create_time.desc()).all()
     return [_row_to_dict_channel_agreement(x) for x in rows]
 
@@ -1366,7 +1393,7 @@ def _channel_agreement_price_exists(
     return False
 
 
-def list_channel_agreement_prices(db, channel_agreement_id, tenant_no=None):
+def list_channel_agreement_prices(db, channel_agreement_id, tenant_no=None, user_info=None):
     q = db.query(BizChannelAgreementProductPrice).filter(
         and_(
             BizChannelAgreementProductPrice.channel_agreement_id == channel_agreement_id,
@@ -1375,6 +1402,9 @@ def list_channel_agreement_prices(db, channel_agreement_id, tenant_no=None):
     )
     if tenant_no:
         q = q.filter(BizChannelAgreementProductPrice.tenant_no == tenant_no)
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q = apply_data_scope(q, BizChannelAgreementProductPrice, user_info, db, tenant_no)
     rows = q.order_by(BizChannelAgreementProductPrice.create_time.desc()).all()
     return [_row_to_dict_channel_agreement_price(x) for x in rows]
 
@@ -1533,7 +1563,7 @@ def _row_to_dict_checkup_institution(row) -> Dict[str, Any]:
     }
 
 
-def list_checkup_institutions(db, *, page=1, page_size=10, keyword=None, tenant_no=None):
+def list_checkup_institutions(db, *, page=1, page_size=10, keyword=None, tenant_no=None, user_info=None):
     def _filtered():
         q0 = db.query(BizCheckupInstitution).filter(BizCheckupInstitution.del_flag == 0)
         if tenant_no:
@@ -1547,6 +1577,9 @@ def list_checkup_institutions(db, *, page=1, page_size=10, keyword=None, tenant_
                     BizCheckupInstitution.contact_name.like(like),
                 )
             )
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, BizCheckupInstitution, user_info, db, tenant_no)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(BizCheckupInstitution.id)).scalar() or 0
@@ -1671,7 +1704,7 @@ def _row_to_dict_checkup_institution_invoice(row) -> Dict[str, Any]:
     }
 
 
-def list_checkup_institution_invoices(db, checkup_institution_id, tenant_no=None):
+def list_checkup_institution_invoices(db, checkup_institution_id, tenant_no=None, user_info=None):
     q = db.query(BizCheckupInstitutionInvoice).filter(
         and_(
             BizCheckupInstitutionInvoice.checkup_institution_id == checkup_institution_id,
@@ -1680,6 +1713,9 @@ def list_checkup_institution_invoices(db, checkup_institution_id, tenant_no=None
     )
     if tenant_no:
         q = q.filter(BizCheckupInstitutionInvoice.tenant_no == tenant_no)
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q = apply_data_scope(q, BizCheckupInstitutionInvoice, user_info, db, tenant_no)
     rows = q.order_by(BizCheckupInstitutionInvoice.create_time.desc()).all()
     return [_row_to_dict_checkup_institution_invoice(x) for x in rows]
 
@@ -1804,7 +1840,7 @@ def _row_to_dict_checkup_institution_project(row) -> Dict[str, Any]:
     }
 
 
-def list_checkup_institution_projects(db, checkup_institution_id, tenant_no=None):
+def list_checkup_institution_projects(db, checkup_institution_id, tenant_no=None, user_info=None):
     q = db.query(BizCheckupInstitutionProject).filter(
         and_(
             BizCheckupInstitutionProject.checkup_institution_id == checkup_institution_id,
@@ -1813,6 +1849,9 @@ def list_checkup_institution_projects(db, checkup_institution_id, tenant_no=None
     )
     if tenant_no:
         q = q.filter(BizCheckupInstitutionProject.tenant_no == tenant_no)
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q = apply_data_scope(q, BizCheckupInstitutionProject, user_info, db, tenant_no)
     rows = q.order_by(BizCheckupInstitutionProject.create_time.desc()).all()
     return [_row_to_dict_checkup_institution_project(x) for x in rows]
 
@@ -2084,7 +2123,7 @@ def _insert_package_items(
         db.add(obj)
 
 
-def list_checkup_institution_packages(db, checkup_institution_id, tenant_no=None):
+def list_checkup_institution_packages(db, checkup_institution_id, tenant_no=None, user_info=None):
     q = db.query(BizCheckupInstitutionPackage).filter(
         and_(
             BizCheckupInstitutionPackage.checkup_institution_id == checkup_institution_id,
@@ -2093,6 +2132,9 @@ def list_checkup_institution_packages(db, checkup_institution_id, tenant_no=None
     )
     if tenant_no:
         q = q.filter(BizCheckupInstitutionPackage.tenant_no == tenant_no)
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q = apply_data_scope(q, BizCheckupInstitutionPackage, user_info, db, tenant_no)
     rows = q.order_by(BizCheckupInstitutionPackage.create_time.desc()).all()
     out = []
     for row in rows:

+ 4 - 0
backend/app/crud/post.py

@@ -33,6 +33,7 @@ def list_posts(
     page_size: int = 10,
     keyword: Optional[str] = None,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     def _filtered():
         q0 = db.query(SysPost).filter(SysPost.del_flag == 0)
@@ -42,6 +43,9 @@ def list_posts(
         if kw:
             like = f"%{kw}%"
             q0 = q0.filter(or_(SysPost.post_name.like(like), SysPost.post_code.like(like)))
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, SysPost, user_info, db, tenant_no)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(SysPost.id)).scalar() or 0

+ 10 - 7
backend/app/crud/process.py

@@ -92,6 +92,7 @@ def list_processes(
     page_size: int = 10,
     keyword: Optional[str] = None,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     def _filtered():
         q0 = db.query(BizProcess).filter(BizProcess.del_flag == 0)
@@ -106,6 +107,9 @@ def list_processes(
                     BizProcess.process_name.like(like),
                 )
             )
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, BizProcess, user_info, db, tenant_no)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(BizProcess.id)).scalar() or 0
@@ -121,13 +125,12 @@ def list_processes(
     return [_row_to_dict(x) for x in rows], int(total)
 
 
-def list_all_processes(db: Session, tenant_no: str) -> List[Dict[str, Any]]:
-    rows = (
-        db.query(BizProcess)
-        .filter(and_(BizProcess.tenant_no == tenant_no, BizProcess.del_flag == 0))
-        .order_by(BizProcess.create_time.desc())
-        .all()
-    )
+def list_all_processes(db: Session, tenant_no: str, user_info=None) -> List[Dict[str, Any]]:
+    q0 = db.query(BizProcess).filter(and_(BizProcess.tenant_no == tenant_no, BizProcess.del_flag == 0))
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q0 = apply_data_scope(q0, BizProcess, user_info, db, tenant_no)
+    rows = q0.order_by(BizProcess.create_time.desc()).all()
     return [_row_to_dict(x) for x in rows]
 
 

+ 4 - 0
backend/app/crud/process_category.py

@@ -32,6 +32,7 @@ def list_process_categories(
     page_size: int = 10,
     keyword: Optional[str] = None,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     def _filtered():
         q0 = db.query(BizProcessCategory).filter(BizProcessCategory.del_flag == 0)
@@ -46,6 +47,9 @@ def list_process_categories(
                     BizProcessCategory.category_name.like(like),
                 )
             )
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, BizProcessCategory, user_info, db, tenant_no)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(BizProcessCategory.id)).scalar() or 0

+ 5 - 1
backend/app/crud/process_route.py

@@ -118,7 +118,8 @@ def _step_data_collection_row_to_dict(d: BizProcessRouteStepDataCollection) -> D
 def list_routes(db: Session, *, page: int = 1, page_size: int = 10,
                 route_name: Optional[str] = None, route_code: Optional[str] = None,
                 product_type_id: Optional[str] = None,
-                tenant_no: Optional[str] = None) -> Tuple[List[Dict], int]:
+                tenant_no: Optional[str] = None,
+                user_info=None) -> Tuple[List[Dict], int]:
     def _filtered():
         q = db.query(BizProcessRoute).filter(BizProcessRoute.del_flag == 0)
         if tenant_no:
@@ -129,6 +130,9 @@ def list_routes(db: Session, *, page: int = 1, page_size: int = 10,
             q = q.filter(BizProcessRoute.route_code == route_code.strip())
         if product_type_id and product_type_id.strip():
             q = q.filter(BizProcessRoute.product_type_id == product_type_id.strip())
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q = apply_data_scope(q, BizProcessRoute, user_info, db, tenant_no)
         return q
 
     total = _filtered().order_by(None).with_entities(func.count(BizProcessRoute.id)).scalar() or 0

+ 4 - 0
backend/app/crud/production_task.py

@@ -320,6 +320,7 @@ def list_production_tasks(
     work_order_id: Optional[str] = None,
     task_no: Optional[str] = None,
     task_status: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     def _filtered():
         q = db.query(BizProductionTask).filter(
@@ -332,6 +333,9 @@ def list_production_tasks(
             q = q.filter(BizProductionTask.task_no.like(f"%{task_no.strip()}%"))
         if task_status and task_status.strip():
             q = q.filter(BizProductionTask.task_status == task_status.strip())
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q = apply_data_scope(q, BizProductionTask, user_info, db, tenant_no)
         return q
 
     total = _filtered().order_by(None).with_entities(func.count(BizProductionTask.id)).scalar() or 0

+ 4 - 0
backend/app/crud/production_work_order.py

@@ -193,6 +193,7 @@ def list_production_work_orders(
     query: Optional[ProductionWorkOrderListQuery] = None,
     page: int = 1,
     page_size: int = 20,
+    user_info=None,
 ) -> Dict[str, Any]:
     def _filtered():
         q0 = db.query(BizProductionWorkOrder).filter(BizProductionWorkOrder.del_flag == 0)
@@ -210,6 +211,9 @@ def list_production_work_orders(
         if query.sample_code and query.sample_code.strip():
             like = f"%{query.sample_code.strip()}%"
             q0 = q0.filter(BizProductionWorkOrder.sample_code.like(like))
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, BizProductionWorkOrder, user_info, db, tenant_no)
         return q0
 
     effective_page = query.page if query and query.page else page

+ 10 - 11
backend/app/crud/qc_item_param.py

@@ -22,19 +22,18 @@ def _row_to_dict(r: BizQcItemParam) -> Dict[str, Any]:
 
 
 def list_params_by_item(
-    db: Session, item_source: str, item_id: str, tenant_no: str
+    db: Session, item_source: str, item_id: str, tenant_no: str, *, user_info=None
 ) -> List[Dict[str, Any]]:
-    rows = (
-        db.query(BizQcItemParam)
-        .filter(
-            BizQcItemParam.item_source == item_source,
-            BizQcItemParam.item_id == item_id,
-            BizQcItemParam.tenant_no == tenant_no,
-            BizQcItemParam.del_flag == 0,
-        )
-        .order_by(BizQcItemParam.order_num.asc())
-        .all()
+    q = db.query(BizQcItemParam).filter(
+        BizQcItemParam.item_source == item_source,
+        BizQcItemParam.item_id == item_id,
+        BizQcItemParam.tenant_no == tenant_no,
+        BizQcItemParam.del_flag == 0,
     )
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q = apply_data_scope(q, BizQcItemParam, user_info, db, tenant_no)
+    rows = q.order_by(BizQcItemParam.order_num.asc()).all()
     return [_row_to_dict(r) for r in rows]
 
 

+ 13 - 10
backend/app/crud/qc_lab_test_item.py

@@ -49,6 +49,7 @@ def list_lab_items(
     keyword: Optional[str] = None,
     status: Optional[str] = None,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     def _filtered():
         q0 = db.query(BizQcLabTestItem).filter(BizQcLabTestItem.del_flag == 0)
@@ -63,6 +64,9 @@ def list_lab_items(
         st = (status or "").strip()
         if st in ("0", "1"):
             q0 = q0.filter(BizQcLabTestItem.status == st)
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, BizQcLabTestItem, user_info, db, tenant_no)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(BizQcLabTestItem.id)).scalar() or 0
@@ -78,17 +82,16 @@ def list_lab_items(
     return [_row_to_dict(x) for x in rows], int(total)
 
 
-def list_active_lab_items(db: Session, tenant_no: str) -> List[Dict[str, Any]]:
-    rows = (
-        db.query(BizQcLabTestItem)
-        .filter(
-            BizQcLabTestItem.tenant_no == tenant_no,
-            BizQcLabTestItem.del_flag == 0,
-            BizQcLabTestItem.status == "1",
-        )
-        .order_by(BizQcLabTestItem.order_num.asc(), BizQcLabTestItem.create_time.desc())
-        .all()
+def list_active_lab_items(db: Session, tenant_no: str, *, user_info=None) -> List[Dict[str, Any]]:
+    q = db.query(BizQcLabTestItem).filter(
+        BizQcLabTestItem.tenant_no == tenant_no,
+        BizQcLabTestItem.del_flag == 0,
+        BizQcLabTestItem.status == "1",
     )
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q = apply_data_scope(q, BizQcLabTestItem, user_info, db, tenant_no)
+    rows = q.order_by(BizQcLabTestItem.order_num.asc(), BizQcLabTestItem.create_time.desc()).all()
     return [_row_to_dict(x) for x in rows]
 
 

+ 4 - 0
backend/app/crud/qc_material.py

@@ -77,6 +77,7 @@ def list_materials(
     keyword: Optional[str] = None,
     status: Optional[str] = None,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     def _filtered():
         q0 = db.query(BizQcMaterial).filter(BizQcMaterial.del_flag == 0)
@@ -94,6 +95,9 @@ def list_materials(
         st = (status or "").strip()
         if st in ("0", "1"):
             q0 = q0.filter(BizQcMaterial.status == st)
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, BizQcMaterial, user_info, db, tenant_no)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(BizQcMaterial.id)).scalar() or 0

+ 4 - 0
backend/app/crud/qc_material_usage.py

@@ -42,6 +42,7 @@ def list_usages(
     change_type: Optional[str] = None,
     start_time: Optional[str] = None,
     end_time: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     def _filtered():
         q0 = db.query(BizQcMaterialUsage).filter(
@@ -56,6 +57,9 @@ def list_usages(
             q0 = q0.filter(BizQcMaterialUsage.create_time >= start_time)
         if end_time:
             q0 = q0.filter(BizQcMaterialUsage.create_time <= end_time + " 23:59:59")
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, BizQcMaterialUsage, user_info, db, tenant_no)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(BizQcMaterialUsage.id)).scalar() or 0

+ 4 - 0
backend/app/crud/qc_task.py

@@ -231,6 +231,7 @@ def list_tasks(
     work_order_id: Optional[str] = None,
     qc_biz_type: Optional[str] = None,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     def _filtered():
         q0 = (
@@ -269,6 +270,9 @@ def list_tasks(
         bt = (qc_biz_type or "").strip()
         if bt:
             q0 = q0.filter(BizQcWorkOrder.qc_biz_type == bt)
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, BizQcTask, user_info, db, tenant_no)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(BizQcTask.id)).scalar() or 0

+ 11 - 12
backend/app/crud/qc_task_result.py

@@ -25,18 +25,17 @@ def _row_to_dict(r: BizQcTaskResult) -> Dict[str, Any]:
 
 
 def _list_saved_results_by_task(
-    db: Session, task_id: str, tenant_no: str
+    db: Session, task_id: str, tenant_no: str, *, user_info=None
 ) -> List[Dict[str, Any]]:
-    rows = (
-        db.query(BizQcTaskResult)
-        .filter(
-            BizQcTaskResult.task_id == task_id,
-            BizQcTaskResult.tenant_no == tenant_no,
-            BizQcTaskResult.del_flag == 0,
-        )
-        .order_by(BizQcTaskResult.create_time.asc())
-        .all()
+    q = db.query(BizQcTaskResult).filter(
+        BizQcTaskResult.task_id == task_id,
+        BizQcTaskResult.tenant_no == tenant_no,
+        BizQcTaskResult.del_flag == 0,
     )
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q = apply_data_scope(q, BizQcTaskResult, user_info, db, tenant_no)
+    rows = q.order_by(BizQcTaskResult.create_time.asc()).all()
     return [_row_to_dict(r) for r in rows]
 
 
@@ -92,10 +91,10 @@ def _merge_saved_into_template(
 
 
 def list_results_by_task(
-    db: Session, task_id: str, tenant_no: str
+    db: Session, task_id: str, tenant_no: str, *, user_info=None
 ) -> List[Dict[str, Any]]:
     """返回任务结果填写表单:项目参数模板 + 已保存值(无模板时仅返回已保存记录)。"""
-    saved = _list_saved_results_by_task(db, task_id, tenant_no)
+    saved = _list_saved_results_by_task(db, task_id, tenant_no, user_info=user_info)
     task = get_task(db, task_id, tenant_no=tenant_no)
     if not task:
         return saved

+ 4 - 0
backend/app/crud/qc_third_party_company.py

@@ -55,6 +55,7 @@ def list_companies(
     keyword: Optional[str] = None,
     status: Optional[str] = None,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     def _filtered():
         q0 = db.query(BizQcThirdPartyCompany).filter(BizQcThirdPartyCompany.del_flag == 0)
@@ -72,6 +73,9 @@ def list_companies(
         st = (status or "").strip()
         if st in ("0", "1"):
             q0 = q0.filter(BizQcThirdPartyCompany.status == st)
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, BizQcThirdPartyCompany, user_info, db, tenant_no)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(BizQcThirdPartyCompany.id)).scalar() or 0

+ 8 - 1
backend/app/crud/qc_third_party_item.py

@@ -76,6 +76,7 @@ def list_items(
     status: Optional[str] = None,
     company_id: Optional[str] = None,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     def _filtered():
         q0 = db.query(BizQcThirdPartyItem).filter(BizQcThirdPartyItem.del_flag == 0)
@@ -95,6 +96,9 @@ def list_items(
         st = (status or "").strip()
         if st in ("0", "1"):
             q0 = q0.filter(BizQcThirdPartyItem.status == st)
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, BizQcThirdPartyItem, user_info, db, tenant_no)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(BizQcThirdPartyItem.id)).scalar() or 0
@@ -113,7 +117,7 @@ def list_items(
 
 
 def list_active_items(
-    db: Session, tenant_no: str, company_id: Optional[str] = None
+    db: Session, tenant_no: str, company_id: Optional[str] = None, *, user_info=None
 ) -> List[Dict[str, Any]]:
     q = db.query(BizQcThirdPartyItem).filter(
         BizQcThirdPartyItem.tenant_no == tenant_no,
@@ -122,6 +126,9 @@ def list_active_items(
     )
     if company_id:
         q = q.filter(BizQcThirdPartyItem.company_id == company_id)
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q = apply_data_scope(q, BizQcThirdPartyItem, user_info, db, tenant_no)
     rows = q.order_by(
         BizQcThirdPartyItem.order_num.asc(), BizQcThirdPartyItem.create_time.desc()
     ).all()

+ 4 - 0
backend/app/crud/qc_work_order.py

@@ -343,6 +343,7 @@ def list_work_orders(
     keyword: Optional[str] = None,
     work_order_status: Optional[str] = None,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     def _filtered():
         q0 = db.query(BizQcWorkOrder).filter(BizQcWorkOrder.del_flag == 0)
@@ -361,6 +362,9 @@ def list_work_orders(
         st = (work_order_status or "").strip()
         if st:
             q0 = q0.filter(BizQcWorkOrder.work_order_status == st)
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, BizQcWorkOrder, user_info, db, tenant_no)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(BizQcWorkOrder.id)).scalar() or 0

+ 4 - 0
backend/app/crud/reagent.py

@@ -36,6 +36,7 @@ def list_reagents(
     page_size: int = 10,
     keyword: Optional[str] = None,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     def _filtered():
         q0 = db.query(BizReagent).filter(BizReagent.del_flag == 0)
@@ -50,6 +51,9 @@ def list_reagents(
                     BizReagent.reagent_name.like(like),
                 )
             )
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, BizReagent, user_info, db, tenant_no)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(BizReagent.id)).scalar() or 0

+ 8 - 3
backend/app/crud/research_donation.py

@@ -376,8 +376,8 @@ def list_donations(
     keyword: Optional[str] = None,
     donation_status: Optional[str] = None,
     collector_user_id: Optional[str] = None,
+    user_info=None,
 ) -> Dict[str, Any]:
-    """分页查询科研捐赠列表,含母亲姓名/电话(join party 表)。"""
     def _base():
         q = (
             db.query(BizResearchDonation)
@@ -392,7 +392,6 @@ def list_donations(
         kw = (keyword or "").strip()
         if kw:
             like = f"%{kw}%"
-            # 子查询:party 表中 mother_name 匹配的 donation_id 集合
             party_match = (
                 db.query(BizResearchDonationParty.research_donation_id)
                 .filter(
@@ -407,6 +406,9 @@ def list_donations(
                     BizResearchDonation.id.in_(party_match),
                 )
             )
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q = apply_data_scope(q, BizResearchDonation, user_info, db, tenant_no)
         return q
 
     total = (
@@ -855,8 +857,8 @@ def list_audits(
     page_size: int = 10,
     keyword: Optional[str] = None,
     audit_status: Optional[str] = None,
+    user_info=None,
 ) -> Dict[str, Any]:
-    """分页查询审核记录。"""
     q_base = (
         db.query(BizResearchDonationAudit, BizResearchDonation.donation_no)
         .join(
@@ -881,6 +883,9 @@ def list_audits(
                 BizResearchDonationAudit.audit_remark.like(like),
             )
         )
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q_base = apply_data_scope(q_base, BizResearchDonationAudit, user_info, db, tenant_no)
 
     total = (
         q_base.order_by(None)

+ 5 - 1
backend/app/crud/role.py

@@ -49,6 +49,7 @@ def list_roles(
     page_size: int = 10,
     keyword: Optional[str] = None,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     def _filtered():
         q0 = db.query(SysRole).filter(SysRole.del_flag == 0)
@@ -58,6 +59,9 @@ def list_roles(
         if kw:
             like = f"%{kw}%"
             q0 = q0.filter(SysRole.role_name.like(like))
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, SysRole, user_info, db, tenant_no)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(SysRole.id)).scalar() or 0
@@ -99,7 +103,7 @@ def create_role(
     db: Session,
     *,
     role_name: str,
-    data_scope: str = "1",
+    data_scope: str = "4",
     status: str = "1",
     tenant_no: str = "zkxw",
     order_num: Optional[int] = None,

+ 12 - 5
backend/app/crud/sample_reception.py

@@ -379,15 +379,18 @@ def list_pending_protocol_samples(
     tenant_no: Optional[str] = None,
     keyword: Optional[str] = None,
     source_type: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     if not tenant_no:
         return [], 0
 
-    # 收集两种来源的样本
     all_items: List[Dict[str, Any]] = []
 
     if not source_type or source_type == "cell_storage":
         q_base = _sample_pending_query(db, tenant_no, keyword)
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q_base = apply_data_scope(q_base, BizProtocolSample, user_info, db, tenant_no)
         cell_total = q_base.order_by(None).with_entities(func.count(BizProtocolSample.id)).scalar() or 0
         cell_rows = (
             q_base.order_by(
@@ -403,6 +406,9 @@ def list_pending_protocol_samples(
 
     if not source_type or source_type == "research_donation":
         q_donation = _donation_sample_pending_query(db, tenant_no, keyword)
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q_donation = apply_data_scope(q_donation, BizResearchDonationSample, user_info, db, tenant_no)
         donation_total = q_donation.order_by(None).with_entities(func.count(BizResearchDonationSample.id)).scalar() or 0
         donation_rows = (
             q_donation.order_by(
@@ -435,11 +441,11 @@ def list_received_protocol_samples(
     tenant_no: Optional[str] = None,
     keyword: Optional[str] = None,
     source_type: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     if not tenant_no:
         return [], 0
 
-    # 查询所有接收记录,按 source_type 过滤
     q_rec = db.query(BizSampleReception).filter(
         BizSampleReception.tenant_no == tenant_no,
         BizSampleReception.del_flag == 0,
@@ -447,10 +453,8 @@ def list_received_protocol_samples(
     if source_type:
         q_rec = q_rec.filter(BizSampleReception.reception_source == source_type)
 
-    # 关键词搜索
     if keyword and keyword.strip():
         kw = f"%{keyword.strip()}%"
-        # 细胞存储样本关键词
         cell_sample_ids_kw = (
             _sample_detail_rows_query(db)
             .with_entities(BizProtocolSample.id)
@@ -467,7 +471,6 @@ def list_received_protocol_samples(
             )
             .subquery()
         )
-        # 捐赠样本关键词
         donation_sample_ids_kw = (
             _donation_sample_detail_rows_query(db)
             .with_entities(_donation_sample_id_col(BizResearchDonationSample.id))
@@ -492,6 +495,10 @@ def list_received_protocol_samples(
             )
         )
 
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q_rec = apply_data_scope(q_rec, BizSampleReception, user_info, db, tenant_no)
+
     total = q_rec.count()
     page = max(1, page)
     page_size = min(max(1, page_size), 100)

+ 4 - 0
backend/app/crud/site.py

@@ -38,6 +38,7 @@ def list_sites(
     page_size: int = 10,
     keyword: Optional[str] = None,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     def _filtered():
         q0 = db.query(SysSite).filter(SysSite.del_flag == 0)
@@ -47,6 +48,9 @@ def list_sites(
         if kw:
             like = f"%{kw}%"
             q0 = q0.filter(or_(SysSite.site_name.like(like), SysSite.site_code.like(like)))
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, SysSite, user_info, db, tenant_no)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(SysSite.id)).scalar() or 0

+ 4 - 0
backend/app/crud/sop_document.py

@@ -41,6 +41,7 @@ def list_sop_documents(
     page_size: int = 10,
     keyword: Optional[str] = None,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     def _filtered():
         q0 = db.query(BizSopDocument).filter(BizSopDocument.del_flag == 0)
@@ -55,6 +56,9 @@ def list_sop_documents(
                     BizSopDocument.sop_name.like(like),
                 )
             )
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, BizSopDocument, user_info, db, tenant_no)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(BizSopDocument.id)).scalar() or 0

+ 113 - 0
backend/app/crud/storage_facility.py

@@ -2,6 +2,7 @@
 from __future__ import annotations
 
 from datetime import datetime
+import re
 from typing import Any, Dict, List, Optional
 
 from sqlalchemy import and_, or_
@@ -302,6 +303,38 @@ def soft_delete_slot(db: Session, slot_id: str, tenant_no: Optional[str] = None)
     db.commit()
 
 
+def clear_slots_by_facility_node(
+    db: Session,
+    *,
+    facility_node_id: str,
+    tenant_no: str,
+    user_id: Optional[str] = None,
+) -> int:
+    fid = (facility_node_id or "").strip()
+    if not fid:
+        raise ValueError("所属设施节点不能为空")
+    node = db.query(BizStorageFacility).filter(
+        and_(
+            BizStorageFacility.id == fid,
+            BizStorageFacility.del_flag == 0,
+            BizStorageFacility.tenant_no == tenant_no,
+        )
+    ).first()
+    if not node:
+        raise ValueError("所属设施节点不存在")
+    now = datetime.now()
+    updated = db.query(BizStorageFacilitySlot).filter(
+        BizStorageFacilitySlot.facility_node_id == fid,
+        BizStorageFacilitySlot.del_flag == 0,
+        BizStorageFacilitySlot.tenant_no == tenant_no,
+    ).update(
+        {"del_flag": 1, "update_time": now, "update_by": user_id},
+        synchronize_session=False
+    )
+    db.commit()
+    return int(updated or 0)
+
+
 def batch_create_slots(
     db: Session,
     *,
@@ -354,3 +387,83 @@ def batch_create_slots(
     for s in slots:
         db.refresh(s)
     return [_slot_to_dict(s) for s in slots]
+
+
+def insert_row_and_shift_slots(
+    db: Session,
+    *,
+    facility_node_id: str,
+    after_row_num: int,
+    cols: int,
+    code_prefix: str = "",
+    tenant_no: str = "zkxw",
+    user_id: Optional[str] = None,
+) -> Dict[str, int]:
+    fid = (facility_node_id or "").strip()
+    if not fid:
+        raise ValueError("所属设施节点不能为空")
+    if after_row_num < 0:
+        raise ValueError("插入行号参数不合法")
+    if cols < 1:
+        raise ValueError("列数必须大于 0")
+
+    node = db.query(BizStorageFacility).filter(
+        and_(
+            BizStorageFacility.id == fid,
+            BizStorageFacility.del_flag == 0,
+            BizStorageFacility.tenant_no == tenant_no,
+        )
+    ).first()
+    if not node:
+        raise ValueError("所属设施节点不存在")
+    if not node.is_leaf:
+        raise ValueError("只能为叶子节点新增格子行")
+
+    row_code_pattern = re.compile(r"^(.*?)(\d+)-(\d+)$")
+    slots = db.query(BizStorageFacilitySlot).filter(
+        and_(
+            BizStorageFacilitySlot.facility_node_id == fid,
+            BizStorageFacilitySlot.del_flag == 0,
+            BizStorageFacilitySlot.tenant_no == tenant_no,
+        )
+    ).all()
+
+    now = datetime.now()
+    shifted_count = 0
+    for slot in slots:
+        match = row_code_pattern.match((slot.slot_code or "").strip())
+        if not match:
+            continue
+        prefix, row_no_raw, col_no_raw = match.groups()
+        row_no = int(row_no_raw)
+        if row_no > after_row_num:
+            slot.slot_code = f"{prefix}{row_no + 1}-{int(col_no_raw)}"
+            slot.update_by = user_id
+            slot.update_time = now
+            shifted_count += 1
+
+    insert_row_num = after_row_num + 1
+    prefix = (code_prefix or "").strip()
+    created_slots: List[BizStorageFacilitySlot] = []
+    for col in range(1, cols + 1):
+        created_slots.append(
+            BizStorageFacilitySlot(
+                id=generate_uuid(),
+                facility_node_id=fid,
+                slot_code=f"{prefix}{insert_row_num}-{col}",
+                slot_status="空",
+                tenant_no=tenant_no,
+                order_num=0,
+                remark=None,
+                create_by=user_id,
+                create_dept_id="",
+                create_time=now,
+                update_by=user_id,
+                update_time=now,
+                del_flag=0,
+            )
+        )
+    if created_slots:
+        db.add_all(created_slots)
+    db.commit()
+    return {"shiftedCount": shifted_count, "createdCount": len(created_slots)}

+ 4 - 0
backend/app/crud/tenant.py

@@ -33,6 +33,7 @@ def list_tenants(
     page: int = 1,
     page_size: int = 10,
     keyword: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     def _filtered():
         q0 = db.query(SysTenant).filter(SysTenant.del_flag == 0)
@@ -40,6 +41,9 @@ def list_tenants(
         if kw:
             like = f"%{kw}%"
             q0 = q0.filter(or_(SysTenant.tenant_no.like(like), SysTenant.tenant_name.like(like)))
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, SysTenant, user_info, db)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(SysTenant.id)).scalar() or 0

+ 20 - 1
backend/app/crud/workflow.py

@@ -48,6 +48,7 @@ def list_process_defs(
     keyword: Optional[str] = None,
     status: Optional[str] = None,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     def _filtered():
         q0 = db.query(WfProcessDef).filter(WfProcessDef.del_flag == 0)
@@ -59,6 +60,9 @@ def list_process_defs(
         if kw:
             like = f"%{kw}%"
             q0 = q0.filter(or_(WfProcessDef.process_name.like(like), WfProcessDef.process_code.like(like)))
+        if user_info:
+            from app.core.data_scope import apply_data_scope
+            q0 = apply_data_scope(q0, WfProcessDef, user_info, db, tenant_no)
         return q0
 
     total = _filtered().order_by(None).with_entities(func.count(WfProcessDef.id)).scalar() or 0
@@ -320,13 +324,16 @@ def _version_to_dict(v: WfProcessVersion) -> Dict[str, Any]:
 
 
 def list_versions(
-    db: Session, process_def_id: str, tenant_no: Optional[str] = None
+    db: Session, process_def_id: str, tenant_no: Optional[str] = None, user_info=None
 ) -> List[Dict[str, Any]]:
     q = db.query(WfProcessVersion).filter(
         and_(WfProcessVersion.process_def_id == process_def_id, WfProcessVersion.del_flag == 0)
     )
     if tenant_no:
         q = q.filter(WfProcessVersion.tenant_no == tenant_no)
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q = apply_data_scope(q, WfProcessVersion, user_info, db, tenant_no)
     rows = q.order_by(WfProcessVersion.version_no.desc()).all()
     return [_version_to_dict(v) for v in rows]
 
@@ -419,6 +426,7 @@ def list_my_instances(
     keyword: Optional[str] = None,
     status: Optional[str] = None,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     from sqlalchemy import func as sa_func
 
@@ -469,6 +477,9 @@ def list_my_instances(
         q0 = q0.filter(
             or_(WfProcessDef.process_name.like(like), WfProcessInstance.business_id.like(like))
         )
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q0 = apply_data_scope(q0, WfProcessInstance, user_info, db, tenant_no)
 
     total = q0.order_by(None).with_entities(func.count(WfProcessInstance.id)).scalar() or 0
     page = max(1, page)
@@ -644,6 +655,7 @@ def list_pending_tasks(
     page_size: int = 10,
     keyword: Optional[str] = None,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     q0 = (
         db.query(WfTask, WfProcessInstance, WfProcessVersion, WfProcessDef)
@@ -669,6 +681,9 @@ def list_pending_tasks(
         q0 = q0.filter(
             or_(WfProcessDef.process_name.like(like), WfProcessDef.process_code.like(like))
         )
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q0 = apply_data_scope(q0, WfTask, user_info, db, tenant_no)
 
     total = q0.order_by(None).with_entities(func.count(WfTask.id)).scalar() or 0
     page = max(1, page)
@@ -703,6 +718,7 @@ def list_completed_tasks(
     page_size: int = 10,
     keyword: Optional[str] = None,
     tenant_no: Optional[str] = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     q0 = (
         db.query(WfTask, WfProcessInstance, WfProcessVersion, WfProcessDef)
@@ -728,6 +744,9 @@ def list_completed_tasks(
         q0 = q0.filter(
             or_(WfProcessDef.process_name.like(like), WfProcessDef.process_code.like(like))
         )
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+        q0 = apply_data_scope(q0, WfTask, user_info, db, tenant_no)
 
     total = q0.order_by(None).with_entities(func.count(WfTask.id)).scalar() or 0
     page = max(1, page)

Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/019e6b1f380e4f5036fb9e7010c3c1dc6493db77c12af404fa2dc5b82d970e7c.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/03ab697b72e4d605afb6be4468fd96a6bb450d3063d5904f02afb31d9d888272.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/07590d597bc87c2fb97c06721c0bc4ccd7ed8d146da9ad8a7a2a0ae4d54b50a3.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/0972ed93085a7ab8ee3a3240cddf2cdb988dbab11d03afd58ee4c9e2bce09b44.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/0f3823afd77177cb8c0cc0b0980d831828174515cdb3f6ce7a3f1636b9ef2abb.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/107619226422f08de4fc3a59388f10d9fdfa86b82b6b315811bce132d623a406.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/143c720e5edab777be16734bc6e5682e083c681012a623429c6cf1d16ffbbd75.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/15e51f0d01404335b92f2e23f920ea8787df581d8d376d74906fdcd507214703.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/1e0eef8cba57aab6c6657e08ea5a898171612f61eb09c8123a1f5e02a9797dbd.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/1f4dd5ecd932fbd6b8bc2318f8c550de1b92f2080473804a5c51811dea08b08c.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/2536cd632f5dce910f607f32e84aa686ec1e53fe4bba026df50e0f28605b81ea.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/3187c71b68731ef629c821a2fce2a80ffdac618975c8b66ef69f32dc4e67dcca.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/37edf7fed1d65173dfaa473456d071a27b3da4ee7d8ad2bfb7365ff5b0583a1a.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/382514efa9edc5bd9351ce590eeea0e8430b93d7561cf6e40a154b7840a12a01.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/40f32ca88b736ab2e0c5b3531dbc26cc370de7d0e13573eabb5eb73a41096224.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/47427773f756199ad4e23cfe25c37a20ea5b823a427649bfd0b672a5e502bb63.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/493f1148a4ad49b64b5aae0e95426066ab05e1ba90ccdb81040f27f639a2e3fe.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/51196252a4dcafa36c356db3e67c4c0947433c802fc30322e9dbab4fc6464117.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/514be2cfb3570ded9441edf3e535da8c7832ad442cceb95dc680b53b94284335.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/5185820c82fca7f32b8658c6b1d838f833d87ac49ba6cf99e1be67d0bdcb3092.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/52d49fd058390ad996afaf2990e12cef1d9d3724b7b4b749de4b5fe1caef0f77.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/52fef47099f0960f42229b41cdf6bfa39b99819846285aae870fe75b707b0d2b.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/55e615004c5868728721d7362c7cac5ea5724b906166220fdc588e1715b8553f.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/5977261dfc5b160034644476706cfa9ba7b6429a9f6bc450299a821e24a4c60d.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/5a248299ccc8fb110be375a7458688682a0dc064497236ce9ebcd9dca843c006.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/62e204fc7b75802c901cc27f55e34c64bd0e3016b1e362b06dce4a933b8a535e.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/64857131e1421b116c96412f8c6ce5c2096472bbc9d3efb7370990258deb28b9.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/6685a7d6debebe55186c6a68af6660c89f89609f0a6c30c3fe83952d8c29cd19.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/6e4c62a4d44927dd189d87448a7c6a2c31bd6a92075dfcd1096b5c6d6d1a703d.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/710f4e40e20c01c775091741bc0345ab3121047e94347b749c8b4bb038116dca.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/741005802d368a35f408837d93d8ae373f1f0b0da69a65bb4da94d3c28dccc9e.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/76647286cbbca45a92fb5819c71d03668e80061839042ec0d15f951419bd0f89.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/77ac044900c34863392739700cd82ba7528849a8f95b85d44d298101b89e81b3.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/781c27d30483071275baba9359f548a39f369828ccae89e3c37e258c07fe81ca.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/85e2b7e57cdc1d9fb96ed1ea76707fd9c8e65ca55488f0bc28fa94033273fc38.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/861c2f4ed148f826d2bdb6fdfe5f97e239e523a806216d48f874f882bdc91f07.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/86e6833986a3dd85bb5165afd7224e99ceaaa4d54cd9684c1758d39756db985d.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/8b524ab71ba36aee878ba80666550e839d5bfff70252ad67ec4fa0cd5d5da6c3.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/8d9df2b36fdecdc7424148120e593cfdf37517102b035e34f21c3f84eed81229.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/919eb8a126968ecf5080795b0af9376ff583efe047db0ec39df837eb2a444b8c.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/a2e28129e634697709e57610fc304508793723ae995d578281e058b7dc8a2d2c.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/a68ac6b74b30b20a75ef6b2e4696faef6b218768e508d29c82c22094f89b4171.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/a9d72b23e8d55961139d76483b20d973f9ba5937718e2e8432f2fee801c81f8f.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/ade2407f94aa9fb433f4a8e04f28e3fdbdf89571bba2f761d6bb52a3cadf6ee2.json


Деякі файли не було показано, через те що забагато файлів було змінено