feat(lab-sample): 实验室样本处理三表台账与部门字段 ACL

按造血/单个核/间充质聚合展示,部门员工只改本部门、主管可改全部,并支持筛选与 Excel 导出;同步纠正采集接受人串权与严格菜单守卫。
This commit is contained in:
zlwu committed 2026-11-12 12:48:05 +08:00
1 parent 58b252ce6b
commit 5fe1193a2c
22 files changed
+2136 -5

No files matched your search

+193
View File
@@ -0,0 +1,193 @@
"""实验室样本处理数据 API"""
from __future__ import annotations
import csv
import io
from typing import Any, Dict, Optional
from fastapi import APIRouter, Depends, Query
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.menu_guard import require_strict_any_menu_code
from app.core.response import error_response, success_response
from app.models.models import SysUser
from app.services import lab_sample_processing as svc
from app.services.lab_sample_processing_fields import (
DATASETS,
DATASET_LABELS,
PERM_LIST,
PERM_MANAGER,
PERM_MARKET_EDIT,
PERM_PRODUCTION_EDIT,
PERM_QC_EDIT,
schema_payload,
)
router = APIRouter(prefix="/web/admin/lab-sample-processing", tags=["实验室样本处理数据"])
# 列表/导出:有列表、主管或任一部门编辑权即可查看全部字段
READ_MENUS = (
PERM_LIST,
PERM_MANAGER,
PERM_MARKET_EDIT,
PERM_PRODUCTION_EDIT,
PERM_QC_EDIT,
)
# 更新:至少要有主管或某一部门编辑权(字段级 ACL 在服务内再收紧)
WRITE_MENUS = (
PERM_MANAGER,
PERM_MARKET_EDIT,
PERM_PRODUCTION_EDIT,
PERM_QC_EDIT,
)
def _tenant_no(db: Session, user: dict) -> str:
user_id = user.get("user_id")
if not user_id:
raise PermissionError("用户身份信息缺失")
row = db.query(SysUser).filter(SysUser.id == user_id, SysUser.del_flag == 0).first()
if not row or not row.tenant_no:
raise PermissionError("用户租户信息缺失")
return row.tenant_no
def _ensure_dataset(dataset: str) -> Optional[str]:
ds = (dataset or "").strip().lower()
return ds if ds in DATASETS else None
class UpdateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
payload: Dict[str, Any] = Field(default_factory=dict)
@router.get("/meta/{dataset}")
async def get_meta(
dataset: str,
user: dict = Depends(require_strict_any_menu_code(*READ_MENUS)),
db: Session = Depends(get_db),
):
ds = _ensure_dataset(dataset)
if not ds:
return error_response(400, "未知数据集")
acl = svc.user_acl(db, user)
return success_response(
{
"dataset": ds,
"label": DATASET_LABELS.get(ds),
"fields": schema_payload(ds),
"acl": acl,
}
)
@router.get("/{dataset}/list")
async def list_dataset(
dataset: str,
keyword: Optional[str] = Query(None),
dateFrom: Optional[str] = Query(None),
dateTo: Optional[str] = Query(None),
page: int = Query(1, ge=1),
pageSize: int = Query(20, ge=1, le=200),
user: dict = Depends(require_strict_any_menu_code(*READ_MENUS)),
db: Session = Depends(get_db),
):
ds = _ensure_dataset(dataset)
if not ds:
return error_response(400, "未知数据集")
tenant_no = _tenant_no(db, user)
rows, total = svc.list_rows(
db,
tenant_no=tenant_no,
dataset=ds,
keyword=keyword,
date_from=dateFrom,
date_to=dateTo,
page=page,
page_size=pageSize,
)
acl = svc.user_acl(db, user)
return success_response(
{
"list": rows,
"total": total,
"page": page,
"pageSize": pageSize,
"fields": schema_payload(ds),
"acl": acl,
}
)
@router.put("/{dataset}/{sample_code}")
async def update_dataset_row(
dataset: str,
sample_code: str,
body: UpdateBody,
user: dict = Depends(require_strict_any_menu_code(*WRITE_MENUS)),
db: Session = Depends(get_db),
):
ds = _ensure_dataset(dataset)
if not ds:
return error_response(400, "未知数据集")
tenant_no = _tenant_no(db, user)
acl = svc.user_acl(db, user)
editable = set(acl.get("editableDepts") or [])
if not editable:
return error_response(403, "无编辑权限")
try:
row = svc.update_row(
db,
tenant_no=tenant_no,
dataset=ds,
sample_code=sample_code,
payload=body.payload or {},
editable_depts=editable,
operator_id=str(user.get("user_id") or ""),
)
except PermissionError as exc:
return error_response(403, str(exc))
except LookupError as exc:
return error_response(404, str(exc))
except ValueError as exc:
return error_response(400, str(exc))
return success_response(row)
@router.get("/{dataset}/export")
async def export_dataset(
dataset: str,
keyword: Optional[str] = Query(None),
dateFrom: Optional[str] = Query(None),
dateTo: Optional[str] = Query(None),
user: dict = Depends(require_strict_any_menu_code(*READ_MENUS)),
db: Session = Depends(get_db),
):
ds = _ensure_dataset(dataset)
if not ds:
return error_response(400, "未知数据集")
tenant_no = _tenant_no(db, user)
headers, body = svc.export_rows(
db,
tenant_no=tenant_no,
dataset=ds,
keyword=keyword,
date_from=dateFrom,
date_to=dateTo,
)
buf = io.StringIO()
writer = csv.writer(buf)
writer.writerow(headers)
for row in body:
writer.writerow(["" if v is None else v for v in row])
raw = ("\ufeff" + buf.getvalue()).encode("utf-8")
filename = f"lab-sample-{ds}.csv"
return StreamingResponse(
io.BytesIO(raw),
media_type="text/csv; charset=utf-8",
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
)