zlwu 5 mesiacov pred
rodič
commit
d387ba4017
34 zmenil súbory, kde vykonal 2243 pridanie a 1510 odobranie
  1. 1 1
      backend/app.pid
  2. 43 9
      backend/app/api/production.py
  3. 170 3
      backend/app/crud/cell_product.py
  4. 15 1
      backend/app/crud/production_work_order.py
  5. 0 0
      backend/app/graphify-out/cache/ast/0d5821f79e094d8f6ed55b7df14d76b3a5077bb27865f482c167aea4f3d07d5f.json
  6. 0 0
      backend/app/graphify-out/cache/ast/1cf6d724598eba27d5b26fa7329285f8ab02bd73a598bf45e28d593faa641060.json
  7. 0 0
      backend/app/graphify-out/cache/ast/268b15ab5a8bcd771c9aa8e7b978ce5396a74e8f875b4cb7409fa30a006d229f.json
  8. 0 0
      backend/app/graphify-out/cache/ast/54826378969b6b392c2f7ee30ffa4c4a506f660422d2b7fa87f8cc3cfc862cd4.json
  9. 0 0
      backend/app/graphify-out/cache/ast/63291cf51fc23432df0f9b9dcbaff048ff7c13b389fed3d39c795881400480d5.json
  10. 0 0
      backend/app/graphify-out/cache/ast/8582c6a6d8f4c97990bee390574bd03c8573650bd3f30829090f2fe667861cd5.json
  11. 0 0
      backend/app/graphify-out/cache/ast/8f1f3f9405c02971e811f1a11815afa9586c667b10e1bd0fe02f1fc5cc07da1a.json
  12. 0 0
      backend/app/graphify-out/cache/ast/ad6163c3f525bad21cd7e7d106902482b90bd0e57222d8449513e29325ed02ae.json
  13. 0 0
      backend/app/graphify-out/cache/ast/f5eb8bc04f46baa87186005019d7965b20311e96f7fc58c8d7496645d98cd174.json
  14. 38 2
      backend/app/schemas/cell_product.py
  15. 473 685
      backend/graphify-out/.graphify_analysis.json
  16. 523 68
      backend/graphify-out/.graphify_extract.json
  17. 112 102
      backend/graphify-out/GRAPH_REPORT.md
  18. 1 1
      backend/graphify-out/graph.html
  19. 112 112
      backend/graphify-out/graph.json
  20. 4 4
      backend/graphify-out/manifest.json
  21. 414 359
      web-admin/graphify-out/.graphify_analysis.json
  22. 31 6
      web-admin/graphify-out/.graphify_extract.json
  23. 65 133
      web-admin/graphify-out/GRAPH_REPORT.md
  24. 1 1
      web-admin/graphify-out/graph.html
  25. 5 5
      web-admin/graphify-out/manifest.json
  26. 5 2
      web-admin/src/api/cell-product.ts
  27. 17 2
      web-admin/src/api/production-work-order.ts
  28. 1 0
      web-admin/src/graphify-out/cache/ast/14ebe1091f507aeb5673689b60aaf94d39d2ccdbab76e56c0347a1eaf5d6ce0b.json
  29. 0 0
      web-admin/src/graphify-out/cache/ast/2afef016be933363785341d29a9843c505b71b7c0e269583d58d86d12118b6e6.json
  30. 1 0
      web-admin/src/graphify-out/cache/ast/49c1ae5fdafcc18bc3ac6d875445549d24b2de5460772ee6e0d40bd0e82aa280.json
  31. 1 0
      web-admin/src/graphify-out/cache/ast/4f5b18636b0af27d04ccbc5253e89c716b5c301c4f0f2dc922737f2e241c4fd5.json
  32. 0 0
      web-admin/src/graphify-out/cache/ast/7801c3bdd26a9c3e0643a1ee9c5f31c9bc0a58d51ddc913ecd1dd93cf9fb9028.json
  33. 194 10
      web-admin/src/views/production/cell-product/index.vue
  34. 16 4
      web-admin/src/views/production/production-work-order/index.vue

+ 1 - 1
backend/app.pid

@@ -1 +1 @@
-39408
+56422

+ 43 - 9
backend/app/api/production.py

@@ -1271,7 +1271,7 @@ async def get_production_work_order(
     wo = production_work_order_svc.get_production_work_order(db, wo_id, tenant_no)
     if not wo:
         return error_response(404, "工单不存在")
-    return success_response(production_work_order_svc._wo_to_dict(wo))
+    return success_response(production_work_order_svc._wo_to_response_dict(db, wo))
 
 
 @router.put("/production-work-orders/{wo_id}/complete")
@@ -1286,7 +1286,7 @@ async def complete_production_work_order(
         wo = production_work_order_svc.complete_production_work_order(
             db, wo_id, tenant_no, user.get("user_id"), body,
         )
-        return success_response(production_work_order_svc._wo_to_dict(wo), message="操作成功")
+        return success_response(production_work_order_svc._wo_to_response_dict(db, wo), message="操作成功")
     except ValueError as e:
         return error_response(400, str(e))
 
@@ -1549,12 +1549,16 @@ async def set_production_task_outputs(
 
 @router.get("/cell-products/next-code")
 async def get_next_cell_product_code(
+    count: int = Query(1, ge=1, le=50),
     user: dict = Depends(require_admin_or_super_admin),
     db: Session = Depends(get_db),
 ):
     tenant_no = _get_tenant_no(db, user)
-    code = cell_product_svc.get_next_code(db, tenant_no)
-    return success_response({"code": code})
+    if count <= 1:
+        code = cell_product_svc.get_next_code(db, tenant_no)
+        return success_response({"code": code})
+    codes = cell_product_svc.reserve_next_codes(db, tenant_no, count)
+    return success_response({"codes": codes, "code": codes[0] if codes else ""})
 
 
 @router.get("/cell-products")
@@ -1606,12 +1610,42 @@ async def create_cell_product(
 ):
     try:
         tenant_no = _get_tenant_no(db, user)
-        obj = cell_product_svc.create_cell_product(
-            db, tenant_no=tenant_no, user_id=user.get("user_id"),
-            create_dept_id="", data=body,
-        )
+        items = body.items or []
+        qty = int(body.quantity) if body.quantity is not None else 1
+        qty = max(1, min(qty, 50))
+        if items:
+            created = cell_product_svc.create_cell_products_batch(
+                db,
+                tenant_no=tenant_no,
+                user_id=user.get("user_id"),
+                create_dept_id="",
+                data=body,
+                items=items,
+            )
+        elif qty > 1:
+            created = cell_product_svc.create_cell_products_by_quantity(
+                db,
+                tenant_no=tenant_no,
+                user_id=user.get("user_id"),
+                create_dept_id="",
+                data=body,
+                count=qty,
+            )
+        else:
+            obj = cell_product_svc.create_cell_product(
+                db, tenant_no=tenant_no, user_id=user.get("user_id"),
+                create_dept_id="", data=body,
+            )
+            return success_response(
+                cell_product_svc.product_to_response(db, tenant_no, obj), message="创建成功"
+            )
+        resp_list = [
+            cell_product_svc.product_to_response(db, tenant_no, obj) for obj in created
+        ]
+        n = len(created)
         return success_response(
-            cell_product_svc.product_to_response(db, tenant_no, obj), message="创建成功"
+            {"list": resp_list, "count": n},
+            message=f"成功创建 {n} 份细胞产品",
         )
     except ValueError as e:
         return error_response(400, str(e))

+ 170 - 3
backend/app/crud/cell_product.py

@@ -2,6 +2,7 @@
 from __future__ import annotations
 
 import math
+import re
 from datetime import datetime
 from typing import Any, Dict, List, Optional, Tuple
 
@@ -344,9 +345,7 @@ def delete_cell_product(
     db.commit()
 
 
-def get_next_code(db: Session, tenant_no: str) -> str:
-    today_str = datetime.now().strftime("%Y%m%d")
-    prefix = f"CP{today_str}"
+def _next_code_start_seq(db: Session, tenant_no: str, prefix: str) -> int:
     last = (
         db.query(BizCellProduct)
         .filter(
@@ -363,9 +362,177 @@ def get_next_code(db: Session, tenant_no: str) -> str:
             seq = int(last.product_code[-6:]) + 1
         except (ValueError, IndexError):
             seq = 1
+    return seq
+
+
+def get_next_code(db: Session, tenant_no: str) -> str:
+    today_str = datetime.now().strftime("%Y%m%d")
+    prefix = f"CP{today_str}"
+    seq = _next_code_start_seq(db, tenant_no, prefix)
     return f"{prefix}{seq:06d}"
 
 
+_PRODUCT_CODE_SUFFIX_RE = re.compile(r"^(.+)-(\d{2})$")
+
+
+def normalize_product_code_base(code: str) -> str:
+    """去掉末尾 -01/-02 等序号,得到多份入库时的基础编码。"""
+    c = (code or "").strip()
+    if not c:
+        return ""
+    m = _PRODUCT_CODE_SUFFIX_RE.match(c)
+    if m:
+        return m.group(1).strip()
+    return c
+
+
+def build_split_product_codes(base: str, count: int) -> List[str]:
+    """按基础编码生成 基础-01、基础-02 …"""
+    if count < 1:
+        return []
+    return [f"{base}-{i:02d}" for i in range(1, count + 1)]
+
+
+def reserve_next_codes(db: Session, tenant_no: str, count: int) -> List[str]:
+    if count < 1:
+        return []
+    count = min(count, 50)
+    today_str = datetime.now().strftime("%Y%m%d")
+    prefix = f"CP{today_str}"
+    start = _next_code_start_seq(db, tenant_no, prefix)
+    return [f"{prefix}{start + i:06d}" for i in range(count)]
+
+
+def _validate_batch_items(
+    db: Session,
+    tenant_no: str,
+    items: List[Any],
+    *,
+    require_storage_slot: bool = True,
+) -> Tuple[List[str], List[Optional[str]]]:
+    if not items:
+        raise ValueError("至少需要一份产品明细")
+    if len(items) > 50:
+        raise ValueError("单次入库份数不能超过 50")
+
+    codes: List[str] = []
+    slot_ids: List[Optional[str]] = []
+    for it in items:
+        code = (getattr(it, "product_code", None) or "").strip()
+        if not code:
+            raise ValueError("产品编码不能为空")
+        if code in codes:
+            raise ValueError(f"产品编码 {code} 重复")
+        codes.append(code)
+
+        sid = (getattr(it, "storage_slot_id", None) or "").strip() or None
+        if require_storage_slot and not sid:
+            raise ValueError("每份产品须选择储物格位置")
+        slot_ids.append(sid)
+
+    existing_codes = (
+        db.query(BizCellProduct.product_code)
+        .filter(
+            BizCellProduct.tenant_no == tenant_no,
+            BizCellProduct.product_code.in_(codes),
+            BizCellProduct.del_flag == 0,
+        )
+        .all()
+    )
+    if existing_codes:
+        dup = existing_codes[0][0]
+        raise ValueError(f"产品编码 {dup} 已存在")
+
+    return codes, slot_ids
+
+
+def create_cell_products_by_quantity(
+    db: Session,
+    *,
+    tenant_no: str,
+    user_id: Optional[str] = None,
+    create_dept_id: Optional[str] = None,
+    data: Any = None,
+    count: int,
+) -> List[BizCellProduct]:
+    """按入库份数拆分:基础编码-01、基础编码-02 …"""
+    count = max(1, min(int(count), 50))
+    base = normalize_product_code_base(getattr(data, "product_code", None) or "")
+    if not base:
+        raise ValueError("产品编码不能为空")
+    codes = build_split_product_codes(base, count)
+    slot_id = (getattr(data, "storage_slot_id", None) or "").strip() or None
+    from types import SimpleNamespace
+
+    items = [
+        SimpleNamespace(product_code=c, storage_slot_id=slot_id) for c in codes
+    ]
+    return create_cell_products_batch(
+        db,
+        tenant_no=tenant_no,
+        user_id=user_id,
+        create_dept_id=create_dept_id,
+        data=data,
+        items=items,
+        require_storage_slot=False,
+    )
+
+
+def create_cell_products_batch(
+    db: Session,
+    *,
+    tenant_no: str,
+    user_id: Optional[str] = None,
+    create_dept_id: Optional[str] = None,
+    data: Any = None,
+    items: List[Any],
+    require_storage_slot: bool = True,
+) -> List[BizCellProduct]:
+    codes, slot_ids = _validate_batch_items(
+        db,
+        tenant_no,
+        items,
+        require_storage_slot=require_storage_slot,
+    )
+    now = datetime.now()
+    created: List[BizCellProduct] = []
+    for code, slot_id in zip(codes, slot_ids):
+        obj = BizCellProduct(
+            id=generate_uuid(),
+            product_code=code,
+            product_name=(data.product_name or "").strip(),
+            product_type_id=(data.product_type_id or "").strip() or None,
+            spec_id=(data.spec_id or "").strip() or None,
+            batch_no=(data.batch_no or "").strip() or None,
+            storage_slot_id=slot_id,
+            passage=((data.passage or "").strip() or None) if getattr(data, "passage", None) else None,
+            freeze_date=data.freeze_date if hasattr(data, "freeze_date") else None,
+            expected_cell_count=data.expected_cell_count if hasattr(data, "expected_cell_count") else None,
+            viability=data.viability if hasattr(data, "viability") else None,
+            product_status=data.product_status or "frozen",
+            source_type=data.source_type or "manual",
+            quantity=1,
+            unit=(data.unit or "").strip() or None,
+            expiry_date=data.expiry_date,
+            product_images=(data.product_images or "").strip() or None,
+            tenant_no=tenant_no,
+            order_num=0,
+            remark=(data.remark or "").strip() or None,
+            create_by=user_id,
+            create_dept_id=create_dept_id or "",
+            create_time=now,
+            update_by=user_id,
+            update_time=now,
+            del_flag=0,
+        )
+        db.add(obj)
+        created.append(obj)
+    db.commit()
+    for obj in created:
+        db.refresh(obj)
+    return created
+
+
 def total_pages(total: int, page_size: int) -> int:
     if total <= 0:
         return 0

+ 15 - 1
backend/app/crud/production_work_order.py

@@ -69,12 +69,26 @@ def _wo_to_dict(wo: BizProductionWorkOrder) -> Dict[str, Any]:
         ),
         "workOrderStatus": wo.work_order_status,
         "workOrderStatusLabel": _WORK_ORDER_STATUS_LABELS.get(wo.work_order_status, wo.work_order_status),
+        "receptionTime": "",
+        "handlerName": "",
+        "sampleTypeName": "",
         "remark": wo.remark or "",
         "createTime": _fmt_dt(wo.create_time),
         "updateTime": _fmt_dt(wo.update_time),
     }
 
 
+def _enrich_wo_items(db: Session, items: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
+    from app.crud import qc_work_order as qc_wo_svc
+
+    return qc_wo_svc._enrich_work_order_list_items(db, items)
+
+
+def _wo_to_response_dict(db: Session, wo: BizProductionWorkOrder) -> Dict[str, Any]:
+    """工单响应(含接收记录关联的样本类型、接收人、接收时间)。"""
+    return _enrich_wo_items(db, [_wo_to_dict(wo)])[0]
+
+
 def create_production_work_order_from_reception(
     db: Session,
     reception: BizSampleReception,
@@ -229,7 +243,7 @@ def list_production_work_orders(
         .limit(effective_size)
         .all()
     )
-    items = [_wo_to_dict(x) for x in rows]
+    items = _enrich_wo_items(db, [_wo_to_dict(x) for x in rows])
     return {"items": items, "total": int(total)}
 
 

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
backend/app/graphify-out/cache/ast/0d5821f79e094d8f6ed55b7df14d76b3a5077bb27865f482c167aea4f3d07d5f.json


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
backend/app/graphify-out/cache/ast/1cf6d724598eba27d5b26fa7329285f8ab02bd73a598bf45e28d593faa641060.json


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
backend/app/graphify-out/cache/ast/268b15ab5a8bcd771c9aa8e7b978ce5396a74e8f875b4cb7409fa30a006d229f.json


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
backend/app/graphify-out/cache/ast/54826378969b6b392c2f7ee30ffa4c4a506f660422d2b7fa87f8cc3cfc862cd4.json


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
backend/app/graphify-out/cache/ast/63291cf51fc23432df0f9b9dcbaff048ff7c13b389fed3d39c795881400480d5.json


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
backend/app/graphify-out/cache/ast/8582c6a6d8f4c97990bee390574bd03c8573650bd3f30829090f2fe667861cd5.json


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
backend/app/graphify-out/cache/ast/8f1f3f9405c02971e811f1a11815afa9586c667b10e1bd0fe02f1fc5cc07da1a.json


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
backend/app/graphify-out/cache/ast/ad6163c3f525bad21cd7e7d106902482b90bd0e57222d8449513e29325ed02ae.json


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
backend/app/graphify-out/cache/ast/f5eb8bc04f46baa87186005019d7965b20311e96f7fc58c8d7496645d98cd174.json


+ 38 - 2
backend/app/schemas/cell_product.py

@@ -1,13 +1,38 @@
 from datetime import datetime
-from typing import Optional
+from typing import Any, List, Optional
 
 from pydantic import BaseModel, ConfigDict, Field, field_validator
 
+from app.core.utils import parse_datetime
+
+
+def _coerce_optional_datetime(value: Any) -> Optional[datetime]:
+    """兼容前端 date-picker 的 YYYY-MM-DD 与完整日期时间字符串。"""
+    if value is None or value == "":
+        return None
+    if isinstance(value, datetime):
+        return value
+    if isinstance(value, str):
+        parsed = parse_datetime(value.strip())
+        if parsed is not None:
+            return parsed
+        raise ValueError("无效的日期格式")
+    return value
+
+
+class CellProductBatchItem(BaseModel):
+    """批量入库时每一份产品的编码与储物格。"""
 
-class CellProductCreate(BaseModel):
     model_config = ConfigDict(populate_by_name=True)
 
     product_code: str = Field(..., alias="productCode")
+    storage_slot_id: Optional[str] = Field(None, alias="storageSlotId")
+
+
+class CellProductCreate(BaseModel):
+    model_config = ConfigDict(populate_by_name=True)
+
+    product_code: Optional[str] = Field(None, alias="productCode")
     product_name: str = Field(..., alias="productName")
     product_type_id: Optional[str] = Field(None, alias="productTypeId")
     spec_id: Optional[str] = Field(None, alias="specId")
@@ -24,6 +49,12 @@ class CellProductCreate(BaseModel):
     expiry_date: Optional[datetime] = Field(None, alias="expiryDate")
     product_images: Optional[str] = Field(None, alias="productImages")
     remark: Optional[str] = None
+    items: Optional[List[CellProductBatchItem]] = Field(None, alias="items")
+
+    @field_validator("freeze_date", "expiry_date", mode="before")
+    @classmethod
+    def coerce_date_fields(cls, v):
+        return _coerce_optional_datetime(v)
 
     @field_validator("viability")
     @classmethod
@@ -62,6 +93,11 @@ class CellProductUpdate(BaseModel):
     product_images: Optional[str] = Field(None, alias="productImages")
     remark: Optional[str] = None
 
+    @field_validator("freeze_date", "expiry_date", mode="before")
+    @classmethod
+    def coerce_date_fields(cls, v):
+        return _coerce_optional_datetime(v)
+
     @field_validator("viability")
     @classmethod
     def validate_viability(cls, v):

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 473 - 685
backend/graphify-out/.graphify_analysis.json


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 523 - 68
backend/graphify-out/.graphify_extract.json


+ 112 - 102
backend/graphify-out/GRAPH_REPORT.md

@@ -1,12 +1,12 @@
 # Graph Report - app  (2026-05-21)
 
 ## Corpus Check
-- 135 files · ~118,210 words
+- 135 files · ~119,275 words
 - Verdict: corpus is large enough that graph structure adds value.
 
 ## Summary
-- 2380 nodes · 9268 edges · 52 communities detected
-- Extraction: 42% EXTRACTED · 58% INFERRED · 0% AMBIGUOUS · INFERRED: 5366 edges (avg confidence: 0.64)
+- 2400 nodes · 9339 edges · 54 communities detected
+- Extraction: 42% EXTRACTED · 58% INFERRED · 0% AMBIGUOUS · INFERRED: 5402 edges (avg confidence: 0.64)
 - Token cost: 0 input · 0 output
 
 ## Community Hubs (Navigation)
@@ -56,12 +56,14 @@
 - [[_COMMUNITY_Community 43|Community 43]]
 - [[_COMMUNITY_Community 44|Community 44]]
 - [[_COMMUNITY_Community 45|Community 45]]
-- [[_COMMUNITY_Community 48|Community 48]]
-- [[_COMMUNITY_Community 49|Community 49]]
+- [[_COMMUNITY_Community 46|Community 46]]
+- [[_COMMUNITY_Community 47|Community 47]]
 - [[_COMMUNITY_Community 50|Community 50]]
 - [[_COMMUNITY_Community 51|Community 51]]
 - [[_COMMUNITY_Community 52|Community 52]]
 - [[_COMMUNITY_Community 53|Community 53]]
+- [[_COMMUNITY_Community 54|Community 54]]
+- [[_COMMUNITY_Community 55|Community 55]]
 
 ## God Nodes (most connected - your core abstractions)
 1. `success_response()` - 377 edges
@@ -73,7 +75,7 @@
 7. `BizQcMaterial` - 193 edges
 8. `BizMaterial` - 190 edges
 9. `BizQcItemParam` - 188 edges
-10. `generate_uuid()` - 129 edges
+10. `generate_uuid()` - 130 edges
 
 ## Surprising Connections (you probably didn't know these)
 - `统一记录 API 请求/响应出入参的中间件` --uses--> `SysApiRequestLog`  [INFERRED]
@@ -91,247 +93,255 @@
 
 ### Community 0 - "Community 0"
 Cohesion: 0.02
-Nodes (426): create_dept(), create_dict_data(), create_dict_type(), create_express_company(), create_item_unit(), create_menu(), create_post(), create_role_management() (+418 more)
+Nodes (423): attachment_download_url(), create_dept(), create_dict_data(), create_dict_type(), create_express_company(), create_item_unit(), create_menu(), create_post() (+415 more)
 
 ### Community 1 - "Community 1"
-Cohesion: 0.01
-Nodes (393): attachment_download_url(), AttachmentDownloadUrlBody, DeptCreateBody, DeptUpdateBody, DictDataCreateBody, DictDataUpdateBody, DictTypeCreateBody, DictTypeUpdateBody (+385 more)
+Cohesion: 0.02
+Nodes (342): BatchStockInBody, BatchStockInItem, BatchStockOutBody, BatchStockOutItem, MaterialBody, MaterialUpdateBody, StockInBody, StockOutBody (+334 more)
 
 ### Community 2 - "Community 2"
 Cohesion: 0.06
-Nodes (213): InsuranceCompanyCreateBody, InsuranceCompanyInvoiceCreateBody, InsuranceCompanyInvoiceUpdateBody, InsuranceCompanyUpdateBody, InsuranceProjectPriceCreateBody, InsuranceProjectPriceUpdateBody, 保险公司及险种价格管理 API(保险公司 + 开票信息 + 险种价格), ProcessDefCreateBody (+205 more)
+Nodes (219): InsuranceCompanyCreateBody, InsuranceCompanyInvoiceCreateBody, InsuranceCompanyInvoiceUpdateBody, InsuranceCompanyUpdateBody, InsuranceProjectPriceCreateBody, InsuranceProjectPriceUpdateBody, 保险公司及险种价格管理 API(保险公司 + 开票信息 + 险种价格), AccessLogCreateBody (+211 more)
 
 ### Community 3 - "Community 3"
-Cohesion: 0.02
-Nodes (153): generate_uuid(), create_consumable_pack(), get_consumable_pack(), get_pack_detail(), _item_row_to_dict(), list_all_packs(), list_consumable_packs(), pack_code_exists() (+145 more)
+Cohesion: 0.06
+Nodes (184): AttachmentDownloadUrlBody, DeptCreateBody, DeptUpdateBody, DictDataCreateBody, DictDataUpdateBody, DictTypeCreateBody, DictTypeUpdateBody, ExpressCompanyCreateBody (+176 more)
 
 ### Community 4 - "Community 4"
-Cohesion: 0.03
-Nodes (120): _persist_protocol_sub_resources(), list_ethnics_for_select(), _list_insurance_companies(), _total_pages(), list_access_logs(), apply_data_scope(), _get_child_dept_ids(), get_user_dept_id() (+112 more)
+Cohesion: 0.02
+Nodes (139): list_ethnics_for_select(), apply_data_scope(), _get_child_dept_ids(), get_user_dept_id(), _resolve_user_role_scopes(), delete(), get_multi_with_filters(), _detail_query_with_protocol() (+131 more)
 
 ### Community 5 - "Community 5"
-Cohesion: 0.08
-Nodes (126): AuditCreateBody, BatchDetailIdsBody, BatchRefundFinanceSubmitBody, FinanceConfirmBody, FullmoonTodoInsuranceBody, ProtocolCreateBody, ProtocolUpdateBody, 细胞存储 — 采集协议单 / 审核 / 财务确认 / 分期预警 API (+118 more)
+Cohesion: 0.03
+Nodes (120): generate_uuid(), create_consumable_pack(), get_consumable_pack(), get_pack_detail(), _item_row_to_dict(), list_all_packs(), list_consumable_packs(), pack_code_exists() (+112 more)
 
 ### Community 6 - "Community 6"
 Cohesion: 0.04
-Nodes (94): get_role_data_scope(), get_role_menus(), _format_attachment_file_size(), get_protocol_nos_used_in_protocols(), urls_to_attachment_list(), create_dict_type(), dict_type_code_exists(), get_dict_type() (+86 more)
+Nodes (61): _avatar_url_field(), _build_user_info_payload(), _jwt_user_payload(), logout(), 公共接口(Web 登录、刷新 Token、登出;用户模型对齐 docs/sql/zkxw.sql 基线), 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。, 与 web-admin `userInfo` 字段约定一致。, Web 端登录用 RSA 公钥(PKCS#1 v1.5,与 JSEncrypt 一致) (+53 more)
 
 ### Community 7 - "Community 7"
-Cohesion: 0.03
-Nodes (54): get_designer_data(), BaseStorage, convert_heic_to_jpeg(), get_storage(), LocalStorage, MinIOStorage, OSSStorage, process_image_content() (+46 more)
+Cohesion: 0.04
+Nodes (33): BusinessException, InvalidStatusTransitionException, log_delivery_operation(), log_operation(), log_order_operation(), log_settlement_operation(), log_user_operation(), OrderLockedException (+25 more)
 
 ### Community 8 - "Community 8"
-Cohesion: 0.04
-Nodes (77): _avatar_url_field(), _build_user_info_payload(), _jwt_user_payload(), logout(), 公共接口(Web 登录、刷新 Token、登出;用户模型对齐 docs/sql/zkxw.sql 基线), 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。, 与 web-admin `userInfo` 字段约定一致。, Web 端登录用 RSA 公钥(PKCS#1 v1.5,与 JSEncrypt 一致) (+69 more)
+Cohesion: 0.07
+Nodes (69): _format_attachment_file_size(), urls_to_attachment_list(), apply_task_result(), complete_task(), _conclusion_label(), create_task_for_line(), _dec(), _enrich_tasks_reception_fields() (+61 more)
 
 ### Community 9 - "Community 9"
 Cohesion: 0.07
-Nodes (67): AccessLogCreateBody, AccessLogUpdateBody, CleanRoomCreateBody, CleanRoomUpdateBody, ExhibitionAppointmentCreateBody, ExhibitionAppointmentUpdateBody, list_storage_slots(), 运营支撑 API(洁净室、进出记录、仓储设施、储位、展厅预约) (+59 more)
+Nodes (69): _persist_protocol_sub_resources(), _add_months_yyyymmdd(), align_fq_payment_details_payload(), _apply_submit_receipt_finance(), _apply_submit_refund_finance(), attachments_to_urls(), _audit_to_dict(), batch_submit_receipt_finance() (+61 more)
 
 ### Community 10 - "Community 10"
-Cohesion: 0.29
-Nodes (53): ConsumableCreateBody, ConsumablePackCreateBody, ConsumablePackUpdateBody, ConsumableUpdateBody, OutputTypeCreateBody, OutputTypeUpdateBody, PackItemInput, ProcessCategoryCreateBody (+45 more)
+Cohesion: 0.05
+Nodes (58): get_protocol_nos_used_in_protocols(), create_dict_type(), dict_type_code_exists(), get_dict_type(), list_dict_types(), _row_to_dict(), soft_delete_dict_type(), update_dict_type() (+50 more)
 
 ### Community 11 - "Community 11"
-Cohesion: 0.07
-Nodes (33): BaseHTTPMiddleware, ApiCryptoError, _b64url_decode(), _b64url_encode(), decrypt_json(), derive_key(), encrypt_json(), gen_nonce_hex() (+25 more)
+Cohesion: 0.08
+Nodes (45): _business_type_label(), _enrich_task_list(), get_business_all_tasks(), _get_business_form_fields(), get_designer_data(), get_instance_detail(), list_completed_tasks(), list_my_instances() (+37 more)
 
 ### Community 12 - "Community 12"
-Cohesion: 0.12
-Nodes (11): get_wechat_pay_client(), get_wechat_pay_v2(), 微信支付工具模块  支持微信支付 V3 API 文档参考: https://pay.weixin.qq.com/docs/v3/apis/index.ht, 统一下单接口 (V3)          Args:             out_trade_no: 商户订单号             total, 申请退款          Args:             out_trade_no: 商户订单号             total_amount, 小程序调起支付的二次签名参数(与公众号 JSAPI 规则一致), 统一下单 (V2 API)          Args:             out_trade_no: 商户订单号             tot, 生成签名 V3 (SHA256 with RSA) (+3 more)
+Cohesion: 0.14
+Nodes (34): create_collection_protocol(), assert_biz_no_available(), _audit_to_dict(), batch_delete_donations(), _collection_to_dict(), _create_collection(), create_donation(), _create_party() (+26 more)
 
 ### Community 13 - "Community 13"
-Cohesion: 0.17
-Nodes (12): BusinessException, InvalidStatusTransitionException, log_delivery_operation(), log_operation(), log_order_operation(), log_settlement_operation(), log_user_operation(), OrderLockedException (+4 more)
+Cohesion: 0.12
+Nodes (11): get_wechat_pay_client(), get_wechat_pay_v2(), 微信支付工具模块  支持微信支付 V3 API 文档参考: https://pay.weixin.qq.com/docs/v3/apis/index.ht, 统一下单接口 (V3)          Args:             out_trade_no: 商户订单号             total, 申请退款          Args:             out_trade_no: 商户订单号             total_amount, 小程序调起支付的二次签名参数(与公众号 JSAPI 规则一致), 统一下单 (V2 API)          Args:             out_trade_no: 商户订单号             tot, 生成签名 V3 (SHA256 with RSA) (+3 more)
 
 ### Community 14 - "Community 14"
-Cohesion: 0.21
-Nodes (19): _consumable_row_to_dict(), create_process(), _create_sub_consumable(), _create_sub_data_collection(), _create_sub_output(), _create_sub_reagent(), _data_collection_row_to_dict(), get_process() (+11 more)
+Cohesion: 0.1
+Nodes (26): BaseHTTPMiddleware, parse_admin_request_json(), 运营 Web(/web/admin)JSON 请求体解析。  与 ApiCryptoMiddleware 配合:中间件未改写 body 时,在路由内解密 `, 读取 JSON body;dict 时走 resolve_request_payload(明文 / 密文 / data 包装)。      Returns:, ApiCryptoError, _b64url_decode(), _b64url_encode(), decrypt_json() (+18 more)
 
 ### Community 15 - "Community 15"
-Cohesion: 0.18
-Nodes (18): company_code_exists(), create_company(), get_company(), list_companies(), list_company_options(), _row_to_dict(), soft_delete_company(), update_company() (+10 more)
+Cohesion: 0.21
+Nodes (22): _create_insurance_company(), _create_insurance_company_invoice(), _create_insurance_project_price(), delete_insurance_company(), delete_insurance_company_invoice(), delete_insurance_project_price(), _fmt_dt(), _generate_uuid() (+14 more)
 
 ### Community 16 - "Community 16"
-Cohesion: 0.25
-Nodes (13): code_exists(), create_material(), _dec(), _fmt_dt(), get_material(), list_materials(), _norm_status(), options_list() (+5 more)
+Cohesion: 0.18
+Nodes (18): company_code_exists(), create_company(), get_company(), list_companies(), list_company_options(), _row_to_dict(), soft_delete_company(), update_company() (+10 more)
 
 ### Community 17 - "Community 17"
-Cohesion: 0.23
-Nodes (12): create_collection_region(), create_hospital(), get_collection_region(), get_hospital(), _hospital_names_by_region_ids(), _hospital_to_dict(), list_collection_regions(), list_hospitals() (+4 more)
+Cohesion: 0.18
+Nodes (17): get_next_cell_product_code(), _build_facility_path_label(), build_split_product_codes(), create_cell_product(), create_cell_products_batch(), create_cell_products_by_quantity(), delete_cell_product(), _enrich_product_dicts() (+9 more)
 
 ### Community 18 - "Community 18"
-Cohesion: 0.23
-Nodes (12): Config, ProductBase, ProductCategoryBase, ProductCategoryCreate, ProductCategoryResponse, ProductCreate, ProductResponse, ProductUnitBase (+4 more)
+Cohesion: 0.21
+Nodes (13): _access_log_to_dict(), create_access_log(), create_clean_room(), get_access_log(), get_clean_room(), list_access_logs(), list_clean_rooms(), room_code_exists() (+5 more)
 
 ### Community 19 - "Community 19"
-Cohesion: 0.29
-Nodes (11): batch_issue_sample_codes(), batch_revoke_sample_codes(), create_sample_code(), get_sample_code(), list_sample_codes(), _parse_issued_time(), _resolve_issued_user_display_name(), _row_to_dict() (+3 more)
+Cohesion: 0.21
+Nodes (13): create_collection_region(), create_hospital(), get_collection_region(), get_hospital(), _hospital_names_by_region_ids(), _hospital_to_dict(), list_collection_regions(), list_hospitals() (+5 more)
 
 ### Community 20 - "Community 20"
-Cohesion: 0.31
-Nodes (12): _code_exists(), create_material(), _dec(), _fmt_dt(), get_material(), list_materials(), options_list(), _row_to_dict() (+4 more)
+Cohesion: 0.25
+Nodes (13): batch_issue_protocol_nos(), batch_revoke_protocol_nos(), create_protocol_no(), get_protocol_no(), list_protocol_nos(), _parse_issued_time(), protocol_no_exists(), 采集协议号池 CRUD(biz_collection_protocol_no) (+5 more)
 
 ### Community 21 - "Community 21"
-Cohesion: 0.31
-Nodes (11): batch_issue_protocol_nos(), batch_revoke_protocol_nos(), create_protocol_no(), get_protocol_no(), list_protocol_nos(), _parse_issued_time(), protocol_no_exists(), _resolve_issued_user_display_name() (+3 more)
+Cohesion: 0.26
+Nodes (12): batch_issue_sample_codes(), batch_revoke_sample_codes(), create_sample_code(), get_sample_code(), list_sample_codes(), _parse_issued_time(), 样本编码池 CRUD(biz_sample_code), _resolve_issued_user_display_name() (+4 more)
 
 ### Community 22 - "Community 22"
 Cohesion: 0.24
-Nodes (12): list_depts(), _build_tree(), _cascade_soft_delete(), _collect_with_ancestors(), create_dept(), get_dept(), list_depts(), list_depts_tree() (+4 more)
+Nodes (9): 微信消息通知服务  支持微信小程序模板消息推送  使用方式:     from app.core.wechat_message import Wech, 发送模板消息                  Args:             openid: 用户openid             templ, send_delivery_notice(), send_delivery_warning(), send_order_status(), send_payment_success(), send_refund_result(), send_urge_notification() (+1 more)
 
 ### Community 23 - "Community 23"
-Cohesion: 0.26
-Nodes (10): list_menus(), _build_tree(), _cascade_soft_delete(), create_menu(), get_menu(), list_menus(), list_menus_tree(), _row_to_dict() (+2 more)
+Cohesion: 0.23
+Nodes (12): Config, ProductBase, ProductCategoryBase, ProductCategoryCreate, ProductCategoryResponse, ProductCreate, ProductResponse, ProductUnitBase (+4 more)
 
 ### Community 24 - "Community 24"
-Cohesion: 0.24
-Nodes (9): dict_data_options(), create_dict_data(), get_dict_data(), list_dict_data(), list_dict_data_options(), 字典数据 CRUD(sys_dict_data), _row_to_dict(), soft_delete_dict_data() (+1 more)
+Cohesion: 0.27
+Nodes (11): _build_tree(), _cascade_soft_delete(), _collect_with_ancestors(), create_dept(), get_dept(), list_depts(), list_depts_tree(), 保留匹配节点及其全部上级,供关键词检索后仍能正确组树。 (+3 more)
 
 ### Community 25 - "Community 25"
-Cohesion: 0.31
-Nodes (8): consumable_code_exists(), create_consumable(), get_consumable(), list_consumables(), 耗材 CRUD(biz_consumable), _row_to_dict(), soft_delete_consumable(), update_consumable()
+Cohesion: 0.29
+Nodes (9): _build_tree(), _cascade_soft_delete(), create_menu(), get_menu(), list_menus(), list_menus_tree(), _row_to_dict(), soft_delete_menu() (+1 more)
 
 ### Community 26 - "Community 26"
-Cohesion: 0.31
-Nodes (8): create_sop_document(), get_sop_document(), list_sop_documents(), SOP文档 CRUD(biz_sop_document), _row_to_dict(), soft_delete_sop_document(), sop_code_exists(), update_sop_document()
+Cohesion: 0.27
+Nodes (9): _ensure_customer_for_mother(), create_customer(), get_customer(), id_card_taken(), list_customers(), 客户 CRUD(biz_customer), _row_to_dict(), soft_delete_customer() (+1 more)
 
 ### Community 27 - "Community 27"
-Cohesion: 0.31
-Nodes (8): create_express_company(), express_code_exists(), get_express_company(), list_express_companies(), 快递公司 CRUD(biz_express_company), _row_to_dict(), soft_delete_express_company(), update_express_company()
+Cohesion: 0.36
+Nodes (9): complete_production_work_order(), create_production_work_order_from_reception(), _enrich_wo_items(), _fmt_dt(), _generate_work_order_no(), get_production_work_order(), list_production_work_orders(), _wo_to_dict() (+1 more)
 
 ### Community 28 - "Community 28"
-Cohesion: 0.27
-Nodes (8): create_cell_product(), delete_cell_product(), get_cell_product(), get_next_code(), list_cell_products(), 细胞产品 CRUD(biz_cell_product), _row_to_dict(), update_cell_product()
+Cohesion: 0.31
+Nodes (8): consumable_code_exists(), create_consumable(), get_consumable(), list_consumables(), 耗材 CRUD(biz_consumable), _row_to_dict(), soft_delete_consumable(), update_consumable()
 
 ### Community 29 - "Community 29"
-Cohesion: 0.38
-Nodes (9): create_check_item(), get_check_item(), item_code_exists(), list_active_check_items(), list_check_items(), _order_query(), _row_to_dict(), soft_delete_check_item() (+1 more)
+Cohesion: 0.31
+Nodes (8): create_express_company(), express_code_exists(), get_express_company(), list_express_companies(), 快递公司 CRUD(biz_express_company), _row_to_dict(), soft_delete_express_company(), update_express_company()
 
 ### Community 30 - "Community 30"
-Cohesion: 0.31
-Nodes (8): category_code_exists(), create_process_category(), get_process_category(), list_process_categories(), 工艺工序分类 CRUD(biz_process_category), _row_to_dict(), soft_delete_process_category(), update_process_category()
+Cohesion: 0.38
+Nodes (9): create_check_item(), get_check_item(), item_code_exists(), list_active_check_items(), list_check_items(), _order_query(), _row_to_dict(), soft_delete_check_item() (+1 more)
 
 ### Community 31 - "Community 31"
-Cohesion: 0.31
-Nodes (8): create_item_unit(), get_item_unit(), list_item_units(), 物品单位 CRUD(biz_item_unit), _row_to_dict(), soft_delete_item_unit(), unit_code_exists(), update_item_unit()
+Cohesion: 0.27
+Nodes (8): create_dict_data(), get_dict_data(), list_dict_data(), list_dict_data_options(), 字典数据 CRUD(sys_dict_data), _row_to_dict(), soft_delete_dict_data(), update_dict_data()
 
 ### Community 32 - "Community 32"
 Cohesion: 0.31
-Nodes (8): create_customer(), get_customer(), id_card_taken(), list_customers(), 客户 CRUD(biz_customer), _row_to_dict(), soft_delete_customer(), update_customer()
+Nodes (8): category_code_exists(), create_process_category(), get_process_category(), list_process_categories(), 工艺工序分类 CRUD(biz_process_category), _row_to_dict(), soft_delete_process_category(), update_process_category()
 
 ### Community 33 - "Community 33"
 Cohesion: 0.31
-Nodes (8): create_cell_sample_type(), get_cell_sample_type(), list_cell_sample_types(), 细胞样本类型 CRUD(biz_cell_sample_type), _row_to_dict(), soft_delete_cell_sample_type(), type_code_exists(), update_cell_sample_type()
+Nodes (8): create_item_unit(), get_item_unit(), list_item_units(), 物品单位 CRUD(biz_item_unit), _row_to_dict(), soft_delete_item_unit(), unit_code_exists(), update_item_unit()
 
 ### Community 34 - "Community 34"
 Cohesion: 0.31
-Nodes (8): create_output_type(), get_output_type(), list_output_types(), 工艺产出物类型 CRUD(biz_output_type), _row_to_dict(), soft_delete_output_type(), type_code_exists(), update_output_type()
+Nodes (8): create_cell_sample_type(), get_cell_sample_type(), list_cell_sample_types(), 细胞样本类型 CRUD(biz_cell_sample_type), _row_to_dict(), soft_delete_cell_sample_type(), type_code_exists(), update_cell_sample_type()
 
 ### Community 35 - "Community 35"
+Cohesion: 0.31
+Nodes (8): create_output_type(), get_output_type(), list_output_types(), 工艺产出物类型 CRUD(biz_output_type), _row_to_dict(), soft_delete_output_type(), type_code_exists(), update_output_type()
+
+### Community 36 - "Community 36"
 Cohesion: 0.36
 Nodes (7): create_post(), get_post(), list_posts(), post_code_exists(), _row_to_dict(), soft_delete_post(), update_post()
 
-### Community 36 - "Community 36"
+### Community 37 - "Community 37"
 Cohesion: 0.36
 Nodes (7): create_tenant(), get_tenant(), list_tenants(), _row_to_dict(), soft_delete_tenant(), tenant_no_exists(), update_tenant()
 
-### Community 37 - "Community 37"
+### Community 38 - "Community 38"
 Cohesion: 0.36
 Nodes (7): create_reagent(), get_reagent(), list_reagents(), reagent_code_exists(), _row_to_dict(), soft_delete_reagent(), update_reagent()
 
-### Community 38 - "Community 38"
+### Community 39 - "Community 39"
 Cohesion: 0.36
 Nodes (7): create_site(), get_site(), list_sites(), _row_to_dict(), site_code_exists(), soft_delete_site(), update_site()
 
-### Community 39 - "Community 39"
+### Community 40 - "Community 40"
+Cohesion: 0.25
+Nodes (7): get_user_menu_codes(), 菜单 code 权限守卫(V0.3:通过 sys_user_role → sys_role_menu → sys_menu.perms), 运营端角色守卫:admin / super_admin 可访问。, 读取用户授权的菜单权限标识集合(与前端 meta.menuCode / perms 对齐)。, 接口菜单权限守卫。super_admin 直接放行。, require_admin_or_super_admin(), require_menu_code()
+
+### Community 41 - "Community 41"
 Cohesion: 0.29
 Nodes (2): BaseModel, SoftDeleteModel
 
-### Community 40 - "Community 40"
+### Community 42 - "Community 42"
 Cohesion: 0.38
 Nodes (6): derive_legacy_status_from_three(), derive_three_status_from_legacy(), 阶段A兼容:从旧 status 推导三字段。     约束:旧 status 本身混合了支付/配送/订单生命周期,因此只能尽力推断;     若业务侧已有更精确, 阶段A兼容:从三字段反推旧 status(用于旧前端/旧筛选参数兼容)。, ThreeStatus, _to_decimal()
 
-### Community 41 - "Community 41"
+### Community 43 - "Community 43"
 Cohesion: 0.5
 Nodes (3): BaseSettings, Config, Settings
 
-### Community 42 - "Community 42"
+### Community 44 - "Community 44"
 Cohesion: 0.5
 Nodes (2): _auto_fill_create_dept_id(), ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID
 
-### Community 43 - "Community 43"
+### Community 45 - "Community 45"
 Cohesion: 0.67
 Nodes (2): get_menu_list(), 运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,供后端菜单模式联调)
 
-### Community 44 - "Community 44"
+### Community 46 - "Community 46"
 Cohesion: 1.0
 Nodes (1): 请求级别的用户上下文变量  通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传
 
-### Community 45 - "Community 45"
+### Community 47 - "Community 47"
 Cohesion: 1.0
 Nodes (1): API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)
 
-### Community 48 - "Community 48"
+### Community 50 - "Community 50"
 Cohesion: 1.0
 Nodes (1): 发送订单状态通知                  Args:             openid: 用户openid             ord
 
-### Community 49 - "Community 49"
+### Community 51 - "Community 51"
 Cohesion: 1.0
 Nodes (1): 发送支付成功通知                  Args:             openid: 用户openid             ord
 
-### Community 50 - "Community 50"
+### Community 52 - "Community 52"
 Cohesion: 1.0
 Nodes (1): 发送配送通知                  Args:             openid: 配送员openid             task
 
-### Community 51 - "Community 51"
+### Community 53 - "Community 53"
 Cohesion: 1.0
 Nodes (1): 发送配送时效预警                  Args:             openid: 配送员openid             ta
 
-### Community 52 - "Community 52"
+### Community 54 - "Community 54"
 Cohesion: 1.0
 Nodes (1): 发送退款结果通知                  Args:             openid: 用户openid             ord
 
-### Community 53 - "Community 53"
+### Community 55 - "Community 55"
 Cohesion: 1.0
 Nodes (1): 发送催单通知给配送员                  Args:             deliver_openid: 配送员openid
 
 ## Knowledge Gaps
-- **89 isolated node(s):** `刷新会话 TTL(refreshToken 续期;已登录用户每次 API 校验通过后滑动续期)`, `写入 refreshToken(用于 one-time 使用/吊销)`, `根据 token 获取当前用户(包含 Redis 会话校验;会话滑动续期)`, `登录凭据 RSA 解密(与前端 JSEncrypt PKCS#1 v1.5 兼容)。`, `Base64 密文 -> JSON 对象(须含 username、password、ts 毫秒时间戳)。` (+84 more)
+- **90 isolated node(s):** `刷新会话 TTL(refreshToken 续期;已登录用户每次 API 校验通过后滑动续期)`, `写入 refreshToken(用于 one-time 使用/吊销)`, `根据 token 获取当前用户(包含 Redis 会话校验;会话滑动续期)`, `登录凭据 RSA 解密(与前端 JSEncrypt PKCS#1 v1.5 兼容)。`, `Base64 密文 -> JSON 对象(须含 username、password、ts 毫秒时间戳)。` (+85 more)
   These have ≤1 connection - possible missing edges or undocumented components.
-- **Thin community `Community 39`** (8 nodes): `BaseModel`, `.to_dict()`, `get_active()`, `base.py`, `SoftDeleteModel`, `.restore()`, `.soft_delete()`, `__tablename__()`
+- **Thin community `Community 41`** (8 nodes): `BaseModel`, `.to_dict()`, `get_active()`, `base.py`, `SoftDeleteModel`, `.restore()`, `.soft_delete()`, `__tablename__()`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 42`** (4 nodes): `_auto_fill_create_dept_id()`, `get_db()`, `database.py`, `ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID`
+- **Thin community `Community 44`** (4 nodes): `_auto_fill_create_dept_id()`, `get_db()`, `database.py`, `ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 43`** (3 nodes): `get_menu_list()`, `menu.py`, `运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,供后端菜单模式联调)`
+- **Thin community `Community 45`** (3 nodes): `get_menu_list()`, `menu.py`, `运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,供后端菜单模式联调)`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 44`** (2 nodes): `context.py`, `请求级别的用户上下文变量  通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传`
+- **Thin community `Community 46`** (2 nodes): `context.py`, `请求级别的用户上下文变量  通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 45`** (2 nodes): `__init__.py`, `API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)`
+- **Thin community `Community 47`** (2 nodes): `__init__.py`, `API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 48`** (1 nodes): `发送订单状态通知                  Args:             openid: 用户openid             ord`
+- **Thin community `Community 50`** (1 nodes): `发送订单状态通知                  Args:             openid: 用户openid             ord`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 49`** (1 nodes): `发送支付成功通知                  Args:             openid: 用户openid             ord`
+- **Thin community `Community 51`** (1 nodes): `发送支付成功通知                  Args:             openid: 用户openid             ord`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 50`** (1 nodes): `发送配送通知                  Args:             openid: 配送员openid             task`
+- **Thin community `Community 52`** (1 nodes): `发送配送通知                  Args:             openid: 配送员openid             task`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 51`** (1 nodes): `发送配送时效预警                  Args:             openid: 配送员openid             ta`
+- **Thin community `Community 53`** (1 nodes): `发送配送时效预警                  Args:             openid: 配送员openid             ta`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 52`** (1 nodes): `发送退款结果通知                  Args:             openid: 用户openid             ord`
+- **Thin community `Community 54`** (1 nodes): `发送退款结果通知                  Args:             openid: 用户openid             ord`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 53`** (1 nodes): `发送催单通知给配送员                  Args:             deliver_openid: 配送员openid`
+- **Thin community `Community 55`** (1 nodes): `发送催单通知给配送员                  Args:             deliver_openid: 配送员openid`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
 
 ## Suggested Questions
 _Questions this graph is uniquely positioned to answer:_
 
-- **Why does `SysUser` connect `Community 1` to `Community 2`, `Community 5`, `Community 8`, `Community 9`, `Community 10`?**
-  _High betweenness centrality (0.098) - this node is a cross-community bridge._
-- **Why does `generate_uuid()` connect `Community 3` to `Community 0`, `Community 1`, `Community 2`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 11`, `Community 14`, `Community 15`, `Community 16`, `Community 17`, `Community 19`, `Community 20`, `Community 21`, `Community 22`, `Community 23`, `Community 24`, `Community 25`, `Community 26`, `Community 27`, `Community 28`, `Community 29`, `Community 30`, `Community 31`, `Community 32`, `Community 33`, `Community 34`, `Community 35`, `Community 36`, `Community 37`, `Community 38`?**
-  _High betweenness centrality (0.097) - this node is a cross-community bridge._
-- **Why does `success_response()` connect `Community 0` to `Community 1`, `Community 4`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 43`, `Community 22`, `Community 23`, `Community 24`?**
-  _High betweenness centrality (0.065) - this node is a cross-community bridge._
+- **Why does `generate_uuid()` connect `Community 5` to `Community 0`, `Community 2`, `Community 3`, `Community 4`, `Community 6`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 16`, `Community 17`, `Community 18`, `Community 19`, `Community 20`, `Community 21`, `Community 24`, `Community 25`, `Community 26`, `Community 27`, `Community 28`, `Community 29`, `Community 30`, `Community 31`, `Community 32`, `Community 33`, `Community 34`, `Community 35`, `Community 36`, `Community 37`, `Community 38`, `Community 39`?**
+  _High betweenness centrality (0.108) - this node is a cross-community bridge._
+- **Why does `SysUser` connect `Community 3` to `Community 0`, `Community 1`, `Community 2`, `Community 6`, `Community 10`, `Community 11`, `Community 20`, `Community 21`?**
+  _High betweenness centrality (0.103) - this node is a cross-community bridge._
+- **Why does `success_response()` connect `Community 0` to `Community 1`, `Community 3`, `Community 4`, `Community 6`, `Community 11`, `Community 45`, `Community 15`, `Community 17`?**
+  _High betweenness centrality (0.064) - this node is a cross-community bridge._
 - **Are the 376 inferred relationships involving `success_response()` (e.g. with `web_login_rsa_public_key()` and `web_login()`) actually correct?**
   _`success_response()` has 376 INFERRED edges - model-reasoned connections that need verification._
 - **Are the 348 inferred relationships involving `str` (e.g. with `extract_login_credentials()` and `._make_request()`) actually correct?**

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1 - 1
backend/graphify-out/graph.html


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 112 - 112
backend/graphify-out/graph.json


+ 4 - 4
backend/graphify-out/manifest.json

@@ -48,7 +48,7 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/order.py": 1778578258.0960286,
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/__init__.py": 1778578258.0864499,
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/order_adjust.py": 1778578258.0963318,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/cell_product.py": 1779336629.616835,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/cell_product.py": 1779339582.3612273,
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/menu.py": 1778578258.0957444,
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/attachment.py": 1778763456.6244864,
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/category_parameter.py": 1778578258.0944862,
@@ -66,7 +66,7 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/post.py": 1779265648.38155,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/sample_reception_check_result.py": 1779255909.8554008,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/user.py": 1778775532.8870275,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_work_order.py": 1779265675.1974137,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_work_order.py": 1779340717.0384307,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/material.py": 1779283386.3879013,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/dict_type.py": 1779265621.5212462,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_third_party_company.py": 1779265648.2051165,
@@ -84,7 +84,7 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/express_company.py": 1779265659.3453207,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_lab_test_item.py": 1779265673.3888667,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/role.py": 1779265616.3701816,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/cell_product.py": 1779336727.023542,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/cell_product.py": 1779340488.4383276,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/menu.py": 1779265729.0381103,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/collection_region.py": 1779277867.6408322,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/sample_reception_check_item.py": 1779007909.2933013,
@@ -128,7 +128,7 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/workflow.py": 1779266232.4568832,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/sample_reception.py": 1779304425.1104686,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/insurance_company.py": 1779281097.279193,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/api/production.py": 1779336678.7838151,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/api/production.py": 1779340693.8205905,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/leave.py": 1779266168.8693042,
   "/Users/wuzilong/work/00code/zkxw/backend/app/services/workflow_callback.py": 1778727966.7277644,
   "/Users/wuzilong/work/00code/zkxw/backend/app/services/workflow_engine.py": 1778634664.6293883,

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 414 - 359
web-admin/graphify-out/.graphify_analysis.json


+ 31 - 6
web-admin/graphify-out/.graphify_extract.json

@@ -8267,6 +8267,13 @@
       "source_file": "views/production/cell-product/index.vue",
       "source_location": "L1"
     },
+    {
+      "id": "cell_product_index_if",
+      "label": "if()",
+      "file_type": "code",
+      "source_file": "views/production/cell-product/index.vue",
+      "source_location": "L828"
+    },
     {
       "id": "views_production_process_route_index_vue",
       "label": "index.vue",
@@ -8279,21 +8286,21 @@
       "label": "if()",
       "file_type": "code",
       "source_file": "views/production/process-route/index.vue",
-      "source_location": "L1024"
+      "source_location": "L1049"
     },
     {
       "id": "process_route_index_get",
       "label": "get()",
       "file_type": "code",
       "source_file": "views/production/process-route/index.vue",
-      "source_location": "L1089"
+      "source_location": "L1203"
     },
     {
       "id": "process_route_index_set",
       "label": "set()",
       "file_type": "code",
       "source_file": "views/production/process-route/index.vue",
-      "source_location": "L1099"
+      "source_location": "L1213"
     },
     {
       "id": "views_production_sop_index_vue",
@@ -23773,13 +23780,31 @@
       "source_location": "L504",
       "weight": 1.0
     },
+    {
+      "source": "views_production_cell_product_index_vue",
+      "target": "cell_product_index_if",
+      "relation": "contains",
+      "confidence": "EXTRACTED",
+      "source_file": "views/production/cell-product/index.vue",
+      "source_location": "L828",
+      "weight": 1.0
+    },
     {
       "source": "views_production_process_route_index_vue",
       "target": "process_route_index_if",
       "relation": "contains",
       "confidence": "EXTRACTED",
       "source_file": "views/production/process-route/index.vue",
-      "source_location": "L1024",
+      "source_location": "L1049",
+      "weight": 1.0
+    },
+    {
+      "source": "views_production_process_route_index_vue",
+      "target": "process_route_index_if",
+      "relation": "contains",
+      "confidence": "EXTRACTED",
+      "source_file": "views/production/process-route/index.vue",
+      "source_location": "L1138",
       "weight": 1.0
     },
     {
@@ -23788,7 +23813,7 @@
       "relation": "contains",
       "confidence": "EXTRACTED",
       "source_file": "views/production/process-route/index.vue",
-      "source_location": "L1089",
+      "source_location": "L1203",
       "weight": 1.0
     },
     {
@@ -23797,7 +23822,7 @@
       "relation": "contains",
       "confidence": "EXTRACTED",
       "source_file": "views/production/process-route/index.vue",
-      "source_location": "L1099",
+      "source_location": "L1213",
       "weight": 1.0
     },
     {

+ 65 - 133
web-admin/graphify-out/GRAPH_REPORT.md

@@ -1,10 +1,10 @@
 # Graph Report - src  (2026-05-21)
 
 ## Corpus Check
-- Large corpus: 577 files · ~296,820 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
+- Large corpus: 577 files · ~298,349 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
 
 ## Summary
-- 1403 nodes · 1754 edges · 45 communities detected
+- 1404 nodes · 1755 edges · 35 communities detected
 - Extraction: 78% EXTRACTED · 22% INFERRED · 0% AMBIGUOUS · INFERRED: 384 edges (avg confidence: 0.8)
 - Token cost: 0 input · 0 output
 
@@ -33,27 +33,17 @@
 - [[_COMMUNITY_Community 21|Community 21]]
 - [[_COMMUNITY_Community 22|Community 22]]
 - [[_COMMUNITY_Community 23|Community 23]]
-- [[_COMMUNITY_Community 24|Community 24]]
+- [[_COMMUNITY_Community 25|Community 25]]
 - [[_COMMUNITY_Community 26|Community 26]]
-- [[_COMMUNITY_Community 27|Community 27]]
 - [[_COMMUNITY_Community 28|Community 28]]
 - [[_COMMUNITY_Community 29|Community 29]]
+- [[_COMMUNITY_Community 30|Community 30]]
 - [[_COMMUNITY_Community 31|Community 31]]
 - [[_COMMUNITY_Community 32|Community 32]]
 - [[_COMMUNITY_Community 33|Community 33]]
 - [[_COMMUNITY_Community 34|Community 34]]
 - [[_COMMUNITY_Community 35|Community 35]]
-- [[_COMMUNITY_Community 36|Community 36]]
 - [[_COMMUNITY_Community 37|Community 37]]
-- [[_COMMUNITY_Community 38|Community 38]]
-- [[_COMMUNITY_Community 39|Community 39]]
-- [[_COMMUNITY_Community 40|Community 40]]
-- [[_COMMUNITY_Community 41|Community 41]]
-- [[_COMMUNITY_Community 42|Community 42]]
-- [[_COMMUNITY_Community 43|Community 43]]
-- [[_COMMUNITY_Community 45|Community 45]]
-- [[_COMMUNITY_Community 53|Community 53]]
-- [[_COMMUNITY_Community 54|Community 54]]
 
 ## God Nodes (most connected - your core abstractions)
 1. `get()` - 153 edges
@@ -68,103 +58,103 @@
 10. `StorageConfig` - 10 edges
 
 ## Surprising Connections (you probably didn't know these)
-- `handleBatchDelete()` --calls--> `withFullscreenLoading()`  [INFERRED]
-  views/system/site/index.vue → utils/fullscreen-loading.ts
-- `handleBatchDelete()` --calls--> `withFullscreenLoading()`  [INFERRED]
-  views/basic-info/sample-code/index.vue → utils/fullscreen-loading.ts
-- `handleBatchRevoke()` --calls--> `withFullscreenLoading()`  [INFERRED]
-  views/basic-info/sample-code/index.vue → utils/fullscreen-loading.ts
 - `handleBatchDelete()` --calls--> `withFullscreenLoading()`  [INFERRED]
   views/basic-info/customer/index.vue → utils/fullscreen-loading.ts
+- `handleBatchDelete()` --calls--> `withFullscreenLoading()`  [INFERRED]
+  views/basic-info/collection-region/index.vue → utils/fullscreen-loading.ts
 - `handleUnauthorizedResponse()` --calls--> `fetchRefreshToken()`  [INFERRED]
   utils/http/index.ts → api/auth.ts
+- `fetchProcessDefList()` --calls--> `get()`  [INFERRED]
+  api/workflow.ts → views/production/process-route/index.vue
+- `fetchVersionList()` --calls--> `get()`  [INFERRED]
+  api/workflow.ts → views/production/process-route/index.vue
 
 ## Communities
 
 ### Community 0 - "Community 0"
 Cohesion: 0.01
-Nodes (77): fetchActiveRegions(), fetchCategoryList(), fetchCategoryParameters(), fetchCellProductPriceList(), fetchCellProductSpecList(), fetchCellProductTypeList(), fetchCellSampleTypeList(), fetchChannelAgreementPrices() (+69 more)
+Nodes (113): fetchActiveRegions(), fetchCategoryList(), fetchCategoryParameters(), fetchCellProductPriceList(), fetchCellProductSpecList(), fetchCellProductTypeList(), fetchCellSampleTypeList(), fetchChannelAgreementPrices() (+105 more)
 
 ### Community 1 - "Community 1"
-Cohesion: 0.03
-Nodes (53): fetchBatchGenerateProtocolNos(), fetchBatchIssueProtocolNos(), fetchCollectionProtocolNoList(), fetchCollectionRegionList(), fetchCreateCollectionProtocolNo(), fetchCreateCollectionRegion(), fetchCreateHospital(), fetchDeleteCollectionProtocolNo() (+45 more)
+Cohesion: 0.02
+Nodes (6): fetchDeleteProjectSystemItem(), fetchGetRoleList(), fetchNotDeletedRegions(), fetchRegionList(), loadRoleList(), if()
 
 ### Community 2 - "Community 2"
-Cohesion: 0.04
-Nodes (31): fetchGetUserInfo(), clickTab(), handleTouchEnd(), setTransition(), IframeRouteManager, RouteRegistry, useCommon(), setupAfterEachGuard() (+23 more)
+Cohesion: 0.03
+Nodes (56): fetchBatchGenerateProtocolNos(), fetchBatchGenerateSampleCodes(), fetchBatchIssueProtocolNos(), fetchBatchIssueSampleCodes(), fetchCollectionProtocolNoList(), fetchCreateCollectionProtocolNo(), fetchCreateSampleCode(), fetchDeleteCollectionProtocolNo() (+48 more)
 
 ### Community 3 - "Community 3"
 Cohesion: 0.04
-Nodes (33): fetchDeptTree(), fetchPostBoundUserIds(), reload(), collectIds(), formatUserLabel(), loadDeptData(), onPersonSelectChange(), RoutePermissionValidator (+25 more)
+Nodes (31): fetchGetUserInfo(), clickTab(), handleTouchEnd(), setTransition(), IframeRouteManager, RouteRegistry, useCommon(), setupAfterEachGuard() (+23 more)
 
 ### Community 4 - "Community 4"
-Cohesion: 0.04
-Nodes (18): fetchAllConsumablePacks(), fetchAllConsumables(), fetchAllOutputTypes(), fetchAllProcessCategories(), fetchAllProcesses(), fetchAllReagents(), fetchAllSopDocuments(), fetchConsumableList() (+10 more)
+Cohesion: 0.05
+Nodes (32): fetchDeptTree(), fetchPostBoundUserIds(), FireworkSystem, reload(), formatUserLabel(), loadDeptData(), onPersonSelectChange(), subscribeTokenRefresh() (+24 more)
 
 ### Community 5 - "Community 5"
 Cohesion: 0.05
-Nodes (40): fetchAccessLogList(), fetchCleanRoomList(), fetchCreateAccessLog(), fetchCreateCleanRoom(), fetchCreateSite(), fetchDeleteAccessLog(), fetchDeleteCleanRoom(), fetchDeleteSite() (+32 more)
+Nodes (41): fetchAccessLogList(), fetchCleanRoomList(), fetchCreateAccessLog(), fetchCreateCleanRoom(), fetchCreateSite(), fetchDeleteAccessLog(), fetchDeleteCleanRoom(), fetchDeleteSite() (+33 more)
 
 ### Community 6 - "Community 6"
 Cohesion: 0.09
 Nodes (5): handleReset(), handleSearch(), WebSocketClient, TableCache, emit()
 
 ### Community 7 - "Community 7"
-Cohesion: 0.09
-Nodes (31): toLogin(), base64ToBytes(), bytesToBase64(), decryptJson(), deriveKey(), encryptJson(), fromBase64Url(), hexToBytes() (+23 more)
+Cohesion: 0.07
+Nodes (11): useSettingsHandlers(), useSettingsPanel(), useSettingsState(), ComponentLoader, RoutePermissionValidator, RouteTransformer, useCeremony(), useTheme() (+3 more)
 
 ### Community 8 - "Community 8"
 Cohesion: 0.09
-Nodes (8): getDefaultLanguage(), checkStorageCompatibility(), StorageConfig, getSystemStorage(), getSysVersion(), StorageKeyManager, StorageCompatibilityManager, validateStorageData()
+Nodes (31): toLogin(), base64ToBytes(), bytesToBase64(), decryptJson(), deriveKey(), encryptJson(), fromBase64Url(), hexToBytes() (+23 more)
 
 ### Community 9 - "Community 9"
-Cohesion: 0.07
-Nodes (13): fetchAttachmentDownloadUrl(), fetchOrderAdjusts(), fetchOrderDetail(), fetchOrderItems(), fetchOrderList(), fetchPriceVersionDetail(), fetchSystemConfig(), handleReceptionAttachmentDownload() (+5 more)
+Cohesion: 0.09
+Nodes (8): getDefaultLanguage(), checkStorageCompatibility(), StorageConfig, getSystemStorage(), getSysVersion(), StorageKeyManager, StorageCompatibilityManager, validateStorageData()
 
 ### Community 10 - "Community 10"
-Cohesion: 0.09
-Nodes (9): fetchGetAllUpgradeLogs(), fetchGetChinaMapData(), fetchGetComments(), fetchGetCurrentVersion(), fetchGetLatestUpgradeLog(), fetchGetSystemConfig(), fetchGetUpgradeLogs(), systemUpgrade() (+1 more)
+Cohesion: 0.08
+Nodes (17): fetchCreateMenu(), fetchDeleteMenu(), fetchMenuTree(), fetchUpdateMenu(), collectIds(), postorder(), preorder(), applyTreeExpand() (+9 more)
 
 ### Community 11 - "Community 11"
 Cohesion: 0.1
-Nodes (16): fetchCreateMenu(), fetchDeleteMenu(), fetchMenuTree(), fetchUpdateMenu(), postorder(), preorder(), applyTreeExpand(), handleAdd() (+8 more)
+Nodes (8): fetchGetMenuList(), fetchGetMenuCodes(), fetchLogin(), fetchRefreshToken(), MenuProcessor, useAppMode(), useAuth(), buildWebEncryptedLoginBody()
 
 ### Community 12 - "Community 12"
 Cohesion: 0.1
-Nodes (13): fetchEthnicList(), addSample(), applyDonationBundleToForm(), confirmBatchTransportApply(), createEmptyTransport(), ensureDonationProtocolNoInOptions(), ensureEthnicOptionInList(), handleOpenForAdd() (+5 more)
+Nodes (19): fetchCollectionRegionList(), fetchCreateCollectionRegion(), fetchCreateHospital(), fetchDeleteCollectionRegion(), fetchDeleteHospital(), fetchHospitalList(), fetchUpdateCollectionRegion(), fetchUpdateHospital() (+11 more)
 
 ### Community 13 - "Community 13"
 Cohesion: 0.1
-Nodes (8): fetchGetMenuList(), fetchGetMenuCodes(), fetchLogin(), fetchRefreshToken(), MenuProcessor, useAppMode(), useAuth(), buildWebEncryptedLoginBody()
+Nodes (13): fetchEthnicList(), addSample(), applyDonationBundleToForm(), confirmBatchTransportApply(), createEmptyTransport(), ensureDonationProtocolNoInOptions(), ensureEthnicOptionInList(), handleOpenForAdd() (+5 more)
 
 ### Community 14 - "Community 14"
 Cohesion: 0.11
-Nodes (21): fetchBatchGenerateSampleCodes(), fetchBatchIssueSampleCodes(), fetchCreateSampleCode(), fetchDeleteSampleCode(), fetchSampleCodeList(), fetchUpdateSampleCode(), fetchUserOptions(), handleAdd() (+13 more)
+Nodes (9): auditorDisplay(), dash(), expressName(), formatYm(), formatYmd(), sampleTypeName(), siteName(), unitName() (+1 more)
 
 ### Community 15 - "Community 15"
-Cohesion: 0.11
-Nodes (9): auditorDisplay(), dash(), expressName(), formatYm(), formatYmd(), sampleTypeName(), siteName(), unitName() (+1 more)
+Cohesion: 0.12
+Nodes (15): addCopyButton(), highlightCode(), insertLineNumbers(), isBlockProcessed(), markBlockAsProcessed(), processBlock(), setupHighlightDirective(), setupRippleDirective() (+7 more)
 
 ### Community 16 - "Community 16"
 Cohesion: 0.12
-Nodes (15): addCopyButton(), highlightCode(), insertLineNumbers(), isBlockProcessed(), markBlockAsProcessed(), processBlock(), setupHighlightDirective(), setupRippleDirective() (+7 more)
+Nodes (19): fetchCreateUser(), fetchDeleteUser(), fetchResetPassword(), fetchRoleOptions(), fetchUpdateUser(), fetchUserDetail(), formatUserDeptDisplay(), handleAdd() (+11 more)
 
 ### Community 17 - "Community 17"
-Cohesion: 0.14
-Nodes (17): fetchCreateUser(), fetchDeleteUser(), fetchResetPassword(), fetchUpdateUser(), fetchUserDetail(), formatUserDeptDisplay(), handleAdd(), handleDelete() (+9 more)
+Cohesion: 0.12
+Nodes (7): fetchAttachmentDownloadUrl(), handleReceptionAttachmentDownload(), attachmentFullUrl(), handleAttachmentDownload(), handleAttachmentPreview(), downloadAttachmentFile(), triggerAnchorDownload()
 
 ### Community 18 - "Community 18"
+Cohesion: 0.19
+Nodes (3): fetchGetLatestUpgradeLog(), systemUpgrade(), VersionManager
+
+### Community 19 - "Community 19"
 Cohesion: 0.12
 Nodes (12): fetchApproveTask(), fetchBusinessAllTasks(), fetchCompletedTasks(), fetchDesignerData(), fetchInstanceDetail(), fetchMyInstances(), fetchPendingTasks(), fetchProcessDefList() (+4 more)
 
-### Community 19 - "Community 19"
+### Community 20 - "Community 20"
 Cohesion: 0.16
 Nodes (10): useTable(), useTableImpl(), getColumnChecks(), useTableColumns(), createErrorHandler(), createSmartDebounce(), defaultResponseAdapter(), extractPagination() (+2 more)
 
-### Community 20 - "Community 20"
-Cohesion: 0.2
-Nodes (2): ComponentLoader, RouteTransformer
-
 ### Community 21 - "Community 21"
 Cohesion: 0.27
 Nodes (14): useChart(), useChartComponent(), useChartOps(), colourBlend(), getCssVar(), getDarkColor(), getLightColor(), handleElementThemeColor() (+6 more)
@@ -175,137 +165,79 @@ Nodes (12): fetchCreateCustomer(), fetchCustomerList(), fetchDeleteCustomer(), f
 
 ### Community 23 - "Community 23"
 Cohesion: 0.14
-Nodes (8): useSettingsHandlers(), useSettingsPanel(), useSettingsState(), useCeremony(), useTheme(), themeAnimation(), toggleTheme(), toggleTransition()
-
-### Community 24 - "Community 24"
-Cohesion: 0.14
 Nodes (2): fetchProductionTaskDetail(), fetchProductionTaskList()
 
-### Community 26 - "Community 26"
+### Community 25 - "Community 25"
 Cohesion: 0.17
 Nodes (3): fetchResearchDonationAuditList(), fetchResearchDonationDetail(), fetchResearchDonationList()
 
-### Community 27 - "Community 27"
-Cohesion: 0.18
-Nodes (3): fetchMaterialList(), fetchMaterialOptions(), fetchMaterialStockLogList()
-
-### Community 28 - "Community 28"
+### Community 26 - "Community 26"
 Cohesion: 0.2
 Nodes (2): fetchQcMaterialList(), fetchQcMaterialOptions()
 
-### Community 29 - "Community 29"
-Cohesion: 0.28
-Nodes (1): FireworkSystem
-
-### Community 31 - "Community 31"
+### Community 28 - "Community 28"
 Cohesion: 0.39
 Nodes (6): isIgnorableScriptError(), normalizeErrorMessage(), registerPromiseErrorHandler(), registerResourceErrorHandler(), scriptErrorHandler(), setupErrorHandle()
 
-### Community 32 - "Community 32"
+### Community 29 - "Community 29"
 Cohesion: 0.36
 Nodes (1): TableHeightCalculator
 
-### Community 33 - "Community 33"
+### Community 30 - "Community 30"
 Cohesion: 0.33
 Nodes (2): logApiCryptoBootInfo(), maskSecret()
 
-### Community 34 - "Community 34"
-Cohesion: 0.29
-Nodes (2): fetchLeaveDetail(), fetchLeaveList()
-
-### Community 35 - "Community 35"
+### Community 31 - "Community 31"
 Cohesion: 0.47
 Nodes (3): isPaymentDueWithinWarnWindow(), parsePaymentDueYmd(), startOfLocalDay()
 
-### Community 36 - "Community 36"
-Cohesion: 0.33
-Nodes (2): fetchSampleReceptionCheckItemList(), fetchSampleReceptionCheckItemsActive()
-
-### Community 37 - "Community 37"
-Cohesion: 0.33
-Nodes (2): fetchQcThirdPartyCompanyList(), fetchQcThirdPartyCompanyOptions()
-
-### Community 38 - "Community 38"
-Cohesion: 0.33
-Nodes (2): fetchQcLabTestItemList(), fetchQcLabTestItemsActive()
-
-### Community 39 - "Community 39"
-Cohesion: 0.33
-Nodes (2): fetchQcTaskDetail(), fetchQcTaskList()
-
-### Community 40 - "Community 40"
+### Community 32 - "Community 32"
 Cohesion: 0.53
 Nodes (4): isEtaTodayOrEarlier(), parseEtaLocalDate(), sampleReceptionRowClassName(), startOfLocalDay()
 
-### Community 41 - "Community 41"
+### Community 33 - "Community 33"
 Cohesion: 0.5
 Nodes (2): focusInput(), handleKeydown()
 
-### Community 42 - "Community 42"
+### Community 34 - "Community 34"
 Cohesion: 0.67
 Nodes (2): if(), logSidebarMenu()
 
-### Community 43 - "Community 43"
+### Community 35 - "Community 35"
 Cohesion: 1.0
 Nodes (2): getSettingDefaults(), resetToDefaults()
 
-### Community 45 - "Community 45"
+### Community 37 - "Community 37"
 Cohesion: 1.0
 Nodes (2): defaultAvatarPathByRole(), resolveUserAvatarUrl()
 
-### Community 53 - "Community 53"
-Cohesion: 0.67
-Nodes (1): fetchQcItemParams()
-
-### Community 54 - "Community 54"
-Cohesion: 0.67
-Nodes (2): fetchDeleteProjectSystemItem(), if()
-
 ## Knowledge Gaps
-- **Thin community `Community 20`** (17 nodes): `ComponentLoader`, `.constructor()`, `.createEmptyComponent()`, `.createErrorComponent()`, `.load()`, `.loadIframe()`, `.loadLayout()`, `.load()`, `RouteTransformer`, `.extractFirstSegment()`, `.handleFirstLevelRoute()`, `.handleIframeRoute()`, `.handleNormalRoute()`, `.isFirstLevelRoute()`, `.transform()`, `ComponentLoader.ts`, `RouteTransformer.ts`
-  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 24`** (14 nodes): `fetchAddTaskStep()`, `fetchCreateProductionTask()`, `fetchDeleteTaskStep()`, `fetchProductionTaskDetail()`, `fetchProductionTaskList()`, `fetchReplaceStepConsumables()`, `fetchReplaceStepDataCollection()`, `fetchReplaceStepReagents()`, `fetchSetTaskInputs()`, `fetchSetTaskOutputs()`, `fetchUpdateProductionTask()`, `fetchUpdateProductionTaskStatus()`, `fetchUpdateTaskStep()`, `production-task.ts`
-  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 28`** (10 nodes): `fetchBatchStockInQcMaterial()`, `fetchBatchStockOutQcMaterial()`, `fetchCreateQcMaterial()`, `fetchDeleteQcMaterial()`, `fetchQcMaterialList()`, `fetchQcMaterialOptions()`, `fetchStockInQcMaterial()`, `fetchStockOutQcMaterial()`, `fetchUpdateQcMaterial()`, `qc-material.ts`
-  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 29`** (9 nodes): `FireworkSystem`, `.constructor()`, `.createFirework()`, `.createParticle()`, `.getAvailableParticle()`, `.initializePool()`, `.preloadAllImages()`, `.preloadImage()`, `index.vue`
-  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 32`** (8 nodes): `TableHeightCalculator`, `.calculate()`, `.calculateOffset()`, `.calculatePaginationOffset()`, `.constructor()`, `.getHeaderHeight()`, `useTableHeight()`, `useTableHeight.ts`
-  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 33`** (7 nodes): `App.vue`, `main.ts`, `refreshButtonAutoIcons()`, `refreshPanelTitleIcons()`, `logApiCryptoBootInfo()`, `maskSecret()`, `console.ts`
-  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 34`** (7 nodes): `fetchCreateLeave()`, `fetchDeleteLeave()`, `fetchLeaveDetail()`, `fetchLeaveList()`, `fetchSubmitLeave()`, `fetchUpdateLeave()`, `leave.ts`
-  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 36`** (6 nodes): `fetchCreateSampleReceptionCheckItem()`, `fetchDeleteSampleReceptionCheckItem()`, `fetchSampleReceptionCheckItemList()`, `fetchSampleReceptionCheckItemsActive()`, `fetchUpdateSampleReceptionCheckItem()`, `sample-reception-check-item.ts`
-  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 37`** (6 nodes): `fetchCreateQcThirdPartyCompany()`, `fetchDeleteQcThirdPartyCompany()`, `fetchQcThirdPartyCompanyList()`, `fetchQcThirdPartyCompanyOptions()`, `fetchUpdateQcThirdPartyCompany()`, `qc-third-party-company.ts`
-  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 38`** (6 nodes): `fetchCreateQcLabTestItem()`, `fetchDeleteQcLabTestItem()`, `fetchQcLabTestItemList()`, `fetchQcLabTestItemsActive()`, `fetchUpdateQcLabTestItem()`, `qc-lab-test-item.ts`
+- **Thin community `Community 23`** (14 nodes): `fetchAddTaskStep()`, `fetchCreateProductionTask()`, `fetchDeleteTaskStep()`, `fetchProductionTaskDetail()`, `fetchProductionTaskList()`, `fetchReplaceStepConsumables()`, `fetchReplaceStepDataCollection()`, `fetchReplaceStepReagents()`, `fetchSetTaskInputs()`, `fetchSetTaskOutputs()`, `fetchUpdateProductionTask()`, `fetchUpdateProductionTaskStatus()`, `fetchUpdateTaskStep()`, `production-task.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 39`** (6 nodes): `fetchCompleteQcTask()`, `fetchQcTaskDetail()`, `fetchQcTaskList()`, `fetchStartQcTask()`, `fetchUpdateQcTaskAttachments()`, `qc-task.ts`
+- **Thin community `Community 26`** (10 nodes): `fetchBatchStockInQcMaterial()`, `fetchBatchStockOutQcMaterial()`, `fetchCreateQcMaterial()`, `fetchDeleteQcMaterial()`, `fetchQcMaterialList()`, `fetchQcMaterialOptions()`, `fetchStockInQcMaterial()`, `fetchStockOutQcMaterial()`, `fetchUpdateQcMaterial()`, `qc-material.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 41`** (5 nodes): `focusInput()`, `handleKeydown()`, `if()`, `search()`, `index.vue`
+- **Thin community `Community 29`** (8 nodes): `TableHeightCalculator`, `.calculate()`, `.calculateOffset()`, `.calculatePaginationOffset()`, `.constructor()`, `.getHeaderHeight()`, `useTableHeight()`, `useTableHeight.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 42`** (4 nodes): `if()`, `logSidebarMenu()`, `index.vue`, `SidebarSubmenu.vue`
+- **Thin community `Community 30`** (7 nodes): `App.vue`, `main.ts`, `refreshButtonAutoIcons()`, `refreshPanelTitleIcons()`, `logApiCryptoBootInfo()`, `maskSecret()`, `console.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 43`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
+- **Thin community `Community 33`** (5 nodes): `focusInput()`, `handleKeydown()`, `if()`, `search()`, `index.vue`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 45`** (3 nodes): `defaultAvatarPathByRole()`, `resolveUserAvatarUrl()`, `user-avatar.ts`
+- **Thin community `Community 34`** (4 nodes): `if()`, `logSidebarMenu()`, `index.vue`, `SidebarSubmenu.vue`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 53`** (3 nodes): `fetchQcItemParams()`, `fetchSaveQcItemParams()`, `qc-item-param.ts`
+- **Thin community `Community 35`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 54`** (3 nodes): `fetchDeleteProjectSystemItem()`, `if()`, `index.vue`
+- **Thin community `Community 37`** (3 nodes): `defaultAvatarPathByRole()`, `resolveUserAvatarUrl()`, `user-avatar.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
 
 ## Suggested Questions
 _Questions this graph is uniquely positioned to answer:_
 
-- **Why does `get()` connect `Community 0` to `Community 1`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 13`, `Community 14`, `Community 17`, `Community 18`, `Community 22`, `Community 24`, `Community 26`, `Community 27`, `Community 28`, `Community 34`, `Community 36`, `Community 37`, `Community 38`, `Community 39`, `Community 53`?**
-  _High betweenness centrality (0.270) - this node is a cross-community bridge._
-- **Why does `push()` connect `Community 3` to `Community 0`, `Community 2`, `Community 6`, `Community 10`, `Community 12`, `Community 29`?**
-  _High betweenness centrality (0.089) - this node is a cross-community bridge._
-- **Why does `fetchGetLatestUpgradeLog()` connect `Community 10` to `Community 0`?**
-  _High betweenness centrality (0.042) - this node is a cross-community bridge._
+- **Why does `get()` connect `Community 0` to `Community 1`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 10`, `Community 11`, `Community 12`, `Community 13`, `Community 16`, `Community 18`, `Community 19`, `Community 22`, `Community 23`, `Community 25`, `Community 26`?**
+  _High betweenness centrality (0.276) - this node is a cross-community bridge._
+- **Why does `push()` connect `Community 4` to `Community 1`, `Community 3`, `Community 6`, `Community 7`, `Community 13`, `Community 18`?**
+  _High betweenness centrality (0.083) - this node is a cross-community bridge._
+- **Why does `fetchGetLatestUpgradeLog()` connect `Community 18` to `Community 0`?**
+  _High betweenness centrality (0.043) - this node is a cross-community bridge._
 - **Are the 152 inferred relationships involving `get()` (e.g. with `buildTreePathLabelFromFlat()` and `alignFqPaymentDetailsForDisplay()`) actually correct?**
   _`get()` has 152 INFERRED edges - model-reasoned connections that need verification._
 - **Are the 17 inferred relationships involving `push()` (e.g. with `fetchAllPagedRecords()` and `calcInstallmentDueAmounts()`) actually correct?**

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1 - 1
web-admin/graphify-out/graph.html


+ 5 - 5
web-admin/graphify-out/manifest.json

@@ -17,8 +17,8 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/common/index.ts": 1778582068.0254738,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/directive/directive.d.ts": 1778582068.026978,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/api/api.d.ts": 1778582068.0246184,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts": 1779336687.830218,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/auto-imports.d.ts": 1779336630.5292668,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts": 1779338118.2632515,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/auto-imports.d.ts": 1779338103.9480238,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/store/index.ts": 1778582068.0293276,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/router/index.ts": 1778775532.895603,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/locales/index.ts": 1778582068.004279,
@@ -205,7 +205,7 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts": 1779310973.2465198,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/ethnic.ts": 1778929297.3919344,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-lab-test-item.ts": 1779017838.8231559,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/cell-product.ts": 1779336699.268955,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/cell-product.ts": 1779339624.2590876,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/research-donation.ts": 1779160004.1431847,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-task.ts": 1779182848.881475,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-item-param.ts": 1779022632.5658252,
@@ -269,8 +269,8 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/system/dict/index.vue": 1779259163.7560437,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/system/leave/index.vue": 1779257191.2768743,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/output-type/index.vue": 1779307917.5932443,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/cell-product/index.vue": 1779337180.3946729,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue": 1779337296.3522038,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/cell-product/index.vue": 1779339695.6019273,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue": 1779337462.617501,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/sop/index.vue": 1779309609.6630545,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-task/index.vue": 1779269660.0717294,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/reagent/index.vue": 1779307562.93293,

+ 5 - 2
web-admin/src/api/cell-product.ts

@@ -32,6 +32,9 @@ export function fetchDeleteCellProduct(id: string) {
   return request.del({ url: `/web/admin/cell-products/${id}` })
 }
 
-export function fetchNextCellProductCode() {
-  return request.get<{ code: string }>({ url: '/web/admin/cell-products/next-code' })
+export function fetchNextCellProductCode(count = 1) {
+  return request.get<{ code: string; codes?: string[] }>({
+    url: '/web/admin/cell-products/next-code',
+    params: count > 1 ? { count } : undefined
+  })
 }

+ 17 - 2
web-admin/src/api/production-work-order.ts

@@ -1,13 +1,28 @@
 import request from '@/utils/http'
 
+export interface ProductionWorkOrderItem {
+  id: string
+  workOrderNo: string
+  protocolNo?: string
+  sampleCode?: string
+  receptionSource?: string
+  receptionSourceLabel?: string
+  sampleTypeName?: string
+  handlerName?: string
+  receptionTime?: string
+  workOrderStatus: string
+  createTime?: string
+  remark?: string
+}
+
 export function fetchProductionWorkOrderList(params?: {
   page?: number
   pageSize?: number
-  keyword?: string
+  workOrderNo?: string
   workOrderStatus?: string
 }) {
   return request.get<{
-    list: any[]
+    list: ProductionWorkOrderItem[]
     total: number
     page: number
     pageSize: number

+ 1 - 0
web-admin/src/graphify-out/cache/ast/14ebe1091f507aeb5673689b60aaf94d39d2ccdbab76e56c0347a1eaf5d6ce0b.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_types_import_components_d_ts", "label": "components.d.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts", "source_location": "L1"}], "edges": [], "raw_calls": []}

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
web-admin/src/graphify-out/cache/ast/2afef016be933363785341d29a9843c505b71b7c0e269583d58d86d12118b6e6.json


+ 1 - 0
web-admin/src/graphify-out/cache/ast/49c1ae5fdafcc18bc3ac6d875445549d24b2de5460772ee6e0d40bd0e82aa280.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_cell_product_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/cell-product/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}

+ 1 - 0
web-admin/src/graphify-out/cache/ast/4f5b18636b0af27d04ccbc5253e89c716b5c301c4f0f2dc922737f2e241c4fd5.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_cell_product_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/cell-product/index.vue", "source_location": "L1"}, {"id": "cell_product_index_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/cell-product/index.vue", "source_location": "L828"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_cell_product_index_vue", "target": "cell_product_index_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/cell-product/index.vue", "source_location": "L828", "weight": 1.0}], "raw_calls": []}

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
web-admin/src/graphify-out/cache/ast/7801c3bdd26a9c3e0643a1ee9c5f31c9bc0a58d51ddc913ecd1dd93cf9fb9028.json


+ 194 - 10
web-admin/src/views/production/cell-product/index.vue

@@ -269,6 +269,7 @@
                 v-model="formData.productCode"
                 placeholder="请输入产品编码"
                 :readonly="isEdit"
+                @input="onBaseProductCodeInput"
               />
             </el-form-item>
             <el-form-item label="产品名称" prop="productName">
@@ -322,7 +323,7 @@
             <el-form-item label="批次号" prop="batchNo">
               <el-input v-model="formData.batchNo" placeholder="请输入批次号" />
             </el-form-item>
-            <el-form-item label="储物格位置" prop="storageSlotId">
+            <el-form-item v-if="isEdit" label="储物格位置" prop="storageSlotId">
               <el-select
                 v-model="formData.storageSlotId"
                 placeholder="请选择储物格"
@@ -365,13 +366,18 @@
                 style="width: 100%"
               />
             </el-form-item>
-            <el-form-item label="数量" prop="quantity">
+            <el-form-item
+              :label="isEdit ? '数量' : '入库份数'"
+              prop="quantity"
+            >
               <el-input-number
                 v-model="formData.quantity"
-                :min="0"
+                :min="isEdit ? 0 : 1"
+                :max="isEdit ? undefined : 50"
                 :precision="0"
                 controls-position="right"
                 style="width: 100%"
+                @change="onCreateCountChange"
               />
             </el-form-item>
             <el-form-item label="单位">
@@ -489,6 +495,49 @@
                 placeholder="可选"
               />
             </el-form-item>
+            <div v-if="!isEdit" class="product-lines-section">
+              <div class="product-lines-head">
+                <span class="product-lines-title">入库明细</span>
+                <span class="form-label-tip"
+                  >每份填写编码与储物格;多份时编码为「产品编码-01、-02…」</span
+                >
+              </div>
+              <el-table :data="productLines" size="small" class="product-lines-table">
+                <el-table-column
+                  type="index"
+                  label="序号"
+                  width="64"
+                  align="center"
+                />
+                <el-table-column label="产品编码" min-width="200">
+                  <template #default="{ row }">
+                    <el-input
+                      v-model="row.productCode"
+                      placeholder="产品编码"
+                      @input="onLineProductCodeInput"
+                    />
+                  </template>
+                </el-table-column>
+                <el-table-column label="储物格位置" min-width="280">
+                  <template #default="{ row }">
+                    <el-select
+                      v-model="row.storageSlotId"
+                      placeholder="请选择储物格"
+                      clearable
+                      filterable
+                      style="width: 100%"
+                    >
+                      <el-option
+                        v-for="item in storageSlotOptions"
+                        :key="item.id"
+                        :label="item.name"
+                        :value="item.id"
+                      />
+                    </el-select>
+                  </template>
+                </el-table-column>
+              </el-table>
+            </div>
           </div>
         </el-form>
         <template #footer>
@@ -605,6 +654,8 @@
 
   defineOptions({ name: 'ProductionCellProduct' })
 
+  type ProductLine = { productCode: string; storageSlotId: string }
+
   type ProductImageFile = {
     id?: string
     fileName: string
@@ -728,12 +779,101 @@
     remark: '',
   })
 
+  const productLines = ref<ProductLine[]>([{ productCode: '', storageSlotId: '' }])
+
   const formRules = {
     productCode: [{ required: true, message: '请输入产品编码', trigger: 'blur' }],
     productName: [{ required: true, message: '请输入产品名称', trigger: 'blur' }],
     batchNo: [{ required: true, message: '请输入批次号', trigger: 'blur' }],
   }
 
+  function createQuantityValue() {
+    const n = Math.floor(Number(formData.quantity) || 0)
+    if (n < 1) return 1
+    return Math.min(n, 50)
+  }
+
+  function stripProductCodeSuffix(code: string) {
+    const c = (code || '').trim()
+    const m = c.match(/^(.+)-(\d{2})$/)
+    return m ? m[1].trim() : c
+  }
+
+  function buildLineProductCode(base: string, index: number, total: number) {
+    const b = stripProductCodeSuffix(base)
+    if (!b) return ''
+    if (total <= 1) return b
+    return `${b}-${String(index + 1).padStart(2, '0')}`
+  }
+
+  function syncLineCodesFromBase() {
+    const total = createQuantityValue()
+    const base = formData.productCode
+    productLines.value.forEach((line, index) => {
+      line.productCode = buildLineProductCode(base, index, total)
+    })
+  }
+
+  function syncProductLines() {
+    const count = createQuantityValue()
+    const prev = productLines.value.map((l) => ({ ...l }))
+    const lines: ProductLine[] = []
+    for (let i = 0; i < count; i++) {
+      lines.push({
+        productCode:
+          count > 1
+            ? buildLineProductCode(formData.productCode, i, count)
+            : prev[i]?.productCode || formData.productCode || '',
+        storageSlotId: prev[i]?.storageSlotId || '',
+      })
+    }
+    productLines.value = lines.length ? lines : [{ productCode: '', storageSlotId: '' }]
+    if (count <= 1 && productLines.value[0]) {
+      formData.productCode = productLines.value[0].productCode
+    }
+  }
+
+  function onCreateCountChange() {
+    if (!isEdit.value) {
+      syncProductLines()
+      if (createQuantityValue() > 1) syncLineCodesFromBase()
+    }
+  }
+
+  function onBaseProductCodeInput() {
+    if (!isEdit.value && createQuantityValue() > 1) syncLineCodesFromBase()
+    else if (!isEdit.value && createQuantityValue() <= 1 && productLines.value[0]) {
+      productLines.value[0].productCode = formData.productCode
+    }
+  }
+
+  function onLineProductCodeInput() {
+    if (!isEdit.value && createQuantityValue() <= 1) {
+      formData.productCode = productLines.value[0]?.productCode || ''
+    }
+  }
+
+  async function validateProductLines(): Promise<boolean> {
+    if (isEdit.value) return true
+    const count = createQuantityValue()
+    for (let i = 0; i < productLines.value.length; i++) {
+      const line = productLines.value[i]
+      if (!line.productCode?.trim()) {
+        ElMessage.warning(`请填写第 ${i + 1} 份产品编码`)
+        return false
+      }
+      if (!line.storageSlotId) {
+        ElMessage.warning(`请选择第 ${i + 1} 份产品的储物格位置`)
+        return false
+      }
+    }
+    if (productLines.value.length !== count) {
+      ElMessage.warning('入库明细行数与入库份数不一致,请调整入库份数')
+      return false
+    }
+    return true
+  }
+
   /* ---- 选择 ---- */
   function handleSelectionChange(rows: any[]) {
     selectedRows.value = rows
@@ -985,6 +1125,7 @@
     formData.productImages = ''
     formData.remark = ''
     productImageFiles.value = []
+    productLines.value = [{ productCode: '', storageSlotId: '' }]
     editingId.value = null
   }
 
@@ -993,6 +1134,7 @@
     isEdit.value = false
     dialogTitle.value = '新增产品'
     resetForm()
+    formData.quantity = 1
     dialogVisible.value = true
     void searchUnitOptions('')
     formRef.value?.clearValidate?.()
@@ -1004,6 +1146,10 @@
     } catch {
       /* 自动生成失败时用户可手动输入 */
     }
+    syncProductLines()
+    if (productLines.value[0]) {
+      productLines.value[0].productCode = formData.productCode
+    }
   }
 
   /* ---- 编辑 ---- */
@@ -1036,22 +1182,20 @@
 
   /* ---- 提交 ---- */
   async function handleSubmit() {
+    if (!(await validateProductLines())) return
     await formRef.value?.validate?.()
     submitLoading.value = true
     try {
       syncImagesToForm()
-      const payload = {
-        productCode: formData.productCode.trim(),
+      const shared = {
         productName: formData.productName.trim(),
         productTypeId: formData.productTypeId || undefined,
         specId: formData.specId || undefined,
         passage: formData.passage || undefined,
         batchNo: formData.batchNo.trim(),
-        storageSlotId: formData.storageSlotId || undefined,
         expectedCellCount: formData.expectedCellCount ?? undefined,
         viability: formData.viability ?? undefined,
         freezeDate: formData.freezeDate || undefined,
-        quantity: formData.quantity,
         unit: formData.unit?.trim() || undefined,
         expiryDate: formData.expiryDate || undefined,
         productStatus: formData.productStatus || undefined,
@@ -1059,11 +1203,22 @@
         remark: formData.remark?.trim() || undefined,
       }
       if (isEdit.value && editingId.value) {
-        await fetchUpdateCellProduct(editingId.value, payload)
+        await fetchUpdateCellProduct(editingId.value, {
+          ...shared,
+          storageSlotId: formData.storageSlotId || undefined,
+          quantity: formData.quantity,
+        })
+        ElMessage.success('更新成功')
       } else {
-        await fetchCreateCellProduct(payload)
+        const qty = createQuantityValue()
+        const items = productLines.value.map((line) => ({
+          productCode: line.productCode.trim(),
+          storageSlotId: line.storageSlotId,
+        }))
+        const res = await fetchCreateCellProduct({ ...shared, items })
+        const n = (res as { count?: number })?.count ?? qty
+        ElMessage.success(n > 1 ? `成功创建 ${n} 份细胞产品` : '创建成功')
       }
-      ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
       dialogVisible.value = false
       await loadData()
     } catch {
@@ -1121,6 +1276,35 @@
     }
   }
 
+  .product-lines-section {
+    grid-column: 1 / -1;
+    margin-top: 4px;
+  }
+
+  .product-lines-head {
+    display: flex;
+    align-items: baseline;
+    flex-wrap: wrap;
+    gap: 8px;
+    margin-bottom: 10px;
+  }
+
+  .product-lines-title {
+    font-size: 13px;
+    font-weight: 600;
+    color: var(--mint-text, #334155);
+  }
+
+  .product-lines-table {
+    width: 100%;
+  }
+
+  .product-line-code {
+    font-size: 14px;
+    color: #334155;
+    line-height: 32px;
+  }
+
   .attachment-upload {
     width: 100%;
   }

+ 16 - 4
web-admin/src/views/production/production-work-order/index.vue

@@ -16,7 +16,7 @@
         <el-form :model="searchForm" class="filter-form" @submit.prevent>
           <div class="filter-grid">
             <el-form-item label="工单号">
-              <el-input v-model="searchForm.keyword" placeholder="请输入工单号" clearable />
+              <el-input v-model="searchForm.workOrderNo" placeholder="请输入工单号" clearable />
             </el-form-item>
             <el-form-item label="工单状态">
               <el-select v-model="searchForm.workOrderStatus" placeholder="全部" clearable style="width: 100%">
@@ -47,6 +47,15 @@
               </el-tag>
             </template>
           </el-table-column>
+          <el-table-column prop="sampleTypeName" label="样本类型" min-width="110" align="center">
+            <template #default="{ row }">{{ row.sampleTypeName || '—' }}</template>
+          </el-table-column>
+          <el-table-column prop="handlerName" label="接收人" min-width="100" align="center">
+            <template #default="{ row }">{{ row.handlerName || '—' }}</template>
+          </el-table-column>
+          <el-table-column prop="receptionTime" label="接收时间" min-width="170" align="center">
+            <template #default="{ row }">{{ row.receptionTime || '—' }}</template>
+          </el-table-column>
           <el-table-column prop="workOrderStatus" label="工单状态" width="100" align="center">
             <template #default="{ row }">
               <el-tag :type="woStatusTag(row.workOrderStatus).type" size="small">
@@ -123,7 +132,7 @@
   }
 
   /* ---- 搜索 & 列表 ---- */
-  const searchForm = reactive({ keyword: '', workOrderStatus: '' })
+  const searchForm = reactive({ workOrderNo: '', workOrderStatus: '' })
   const tableData = ref<any[]>([])
   const loading = ref(false)
   const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
@@ -134,7 +143,7 @@
       const res = await fetchProductionWorkOrderList({
         page: pagination.page,
         pageSize: pagination.pageSize,
-        keyword: searchForm.keyword || undefined,
+        workOrderNo: searchForm.workOrderNo || undefined,
         workOrderStatus: searchForm.workOrderStatus || undefined
       })
       tableData.value = res.list || []
@@ -150,7 +159,7 @@
   }
 
   function handleReset() {
-    searchForm.keyword = ''
+    searchForm.workOrderNo = ''
     searchForm.workOrderStatus = ''
     pagination.page = 1
     loadData()
@@ -175,6 +184,9 @@
             value: sourceTag(d.receptionSource).label,
             tagType: sourceTag(d.receptionSource).type as any
           },
+          { label: '样本类型', value: d.sampleTypeName || '—' },
+          { label: '接收人', value: d.handlerName || '—' },
+          { label: '接收时间', value: d.receptionTime || '—' },
           {
             label: '工单状态',
             value: woStatusTag(d.workOrderStatus).label,

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov