import html
import shutil
import tempfile
import uuid
from pathlib import Path
from urllib.parse import quote
from fastapi import FastAPI, File, Form, HTTPException, Query, UploadFile
from fastapi.responses import FileResponse, HTMLResponse
from .processor import (
ProcessingError,
ProcessingResult,
create_result_zip,
find_output_file,
run_processing,
)
WORK_ROOT = Path(tempfile.gettempdir()) / "his_sur_data_deal_jobs"
WORK_ROOT.mkdir(parents=True, exist_ok=True)
app = FastAPI(title="检测数据处理")
@app.get("/", response_class=HTMLResponse)
def index() -> str:
return _page_shell(
"""
V1 适用于含有 Patients_info.csv、Tests_List、Tests_Detail_List 的批量数据;V2 适用于每个患者单独目录的数据。
"""
)
@app.post("/process", response_class=HTMLResponse)
async def process(
file: UploadFile = File(...),
mode: str = Form("auto"),
data_type: str = Form("pat_no"),
result_name: str = Form("Result"),
show_not_match: str | None = Form(None),
show_all_infos: str | None = Form(None),
) -> str:
if not file.filename or not file.filename.lower().endswith(".zip"):
raise HTTPException(status_code=400, detail="请上传 zip 文件。")
job_dir = WORK_ROOT / uuid.uuid4().hex
job_dir.mkdir(parents=True, exist_ok=True)
upload_path = job_dir / "input.zip"
try:
with upload_path.open("wb") as out:
shutil.copyfileobj(file.file, out)
result = run_processing(
zip_path=upload_path,
job_dir=job_dir,
mode=mode,
data_type=data_type,
result_name=result_name,
show_not_match=show_not_match == "true",
show_all_infos=show_all_infos == "true",
)
except ProcessingError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(status_code=500, detail=f"处理失败:{exc}") from exc
return _render_result(result)
@app.get("/download/all/{job_id}")
def download_all(job_id: str) -> FileResponse:
job_dir = _get_job_dir(job_id)
try:
result_zip = create_result_zip(job_dir)
except ProcessingError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
return FileResponse(
result_zip,
media_type="application/zip",
filename="检测数据处理结果.zip",
)
@app.get("/download/file/{job_id}")
def download_file(job_id: str, path: str = Query(...)) -> FileResponse:
job_dir = _get_job_dir(job_id)
try:
target = find_output_file(job_dir, path)
except ProcessingError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
return FileResponse(
target,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
filename=target.name,
)
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}
def _get_job_dir(job_id: str) -> Path:
if not job_id.isalnum():
raise HTTPException(status_code=404, detail="结果不存在。")
job_dir = (WORK_ROOT / job_id).resolve()
if not str(job_dir).startswith(str(WORK_ROOT.resolve())) or not job_dir.exists():
raise HTTPException(status_code=404, detail="结果不存在。")
return job_dir
def _render_result(result: ProcessingResult) -> str:
total_sheets = sum(len(file.sheets) for file in result.files)
total_rows = sum(max(sheet.rows - 1, 0) for file in result.files for sheet in file.sheets)
file_items = "\n".join(_render_file(file, result.job_id) for file in result.files)
body = f"""
处理模式{html.escape(result.mode.upper())}
Excel 文件{len(result.files)}
工作表{total_sheets}
数据行{total_rows}
"""
return _page_shell(body, subtitle="处理完成,可先查看结果摘要和部分预览,再选择导出。")
def _render_file(file, job_id: str) -> str:
sheet_items = "\n".join(_render_sheet(sheet) for sheet in file.sheets[:6])
more = ""
if len(file.sheets) > 6:
more = f'
还有 {len(file.sheets) - 6} 个工作表未展开预览,可导出 Excel 查看完整内容。
'
file_url = f"/download/file/{html.escape(job_id)}?path={quote(file.relpath)}"
return f"""
{html.escape(file.filename)}
{len(file.sheets)} 个工作表
导出此 Excel
{sheet_items}
{more}
"""
def _render_sheet(sheet) -> str:
preview = sheet.preview[:6]
table = '此工作表没有可预览的数据。
'
if preview:
max_cols = min(max((len(row) for row in preview), default=0), 12)
rows = []
for index, row in enumerate(preview):
cells = []
for value in (row + [""] * max_cols)[:max_cols]:
tag = "th" if index == 0 else "td"
cells.append(f"<{tag}>{html.escape(value)}{tag}>")
rows.append("" + "".join(cells) + "
")
table = f""
return f"""
{html.escape(sheet.name)}
{sheet.rows} 行 · {sheet.columns} 列
{table}
"""
def _page_shell(body: str, subtitle: str = "上传“待处理检测数据.zip”,处理完成后在网页中查看结果。") -> str:
return f"""
检测数据处理
检测数据处理
{html.escape(subtitle)}
{body}
"""