Files
HIS_Sur_Data_Deal/app/main.py
2026-05-08 21:28:39 +08:00

245 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import html
import shutil
import tempfile
import uuid
from pathlib import Path
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
from fastapi.responses import FileResponse, HTMLResponse
from .processor import ProcessingError, run_processing
APP_ROOT = Path(__file__).resolve().parent
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 """
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>检测数据处理</title>
<style>
:root {
color-scheme: light;
--bg: #f6f7f9;
--panel: #ffffff;
--text: #1f2937;
--muted: #64748b;
--line: #d7dde5;
--primary: #146c5c;
--primary-dark: #0f574b;
--danger: #b42318;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif;
background: var(--bg);
color: var(--text);
}
.shell {
width: min(960px, calc(100vw - 32px));
margin: 0 auto;
padding: 36px 0;
}
header {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 24px;
margin-bottom: 24px;
}
h1 {
margin: 0;
font-size: 28px;
line-height: 1.2;
font-weight: 750;
letter-spacing: 0;
}
.sub {
margin-top: 8px;
color: var(--muted);
font-size: 14px;
}
.panel {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 8px;
padding: 24px;
box-shadow: 0 10px 26px rgba(17, 24, 39, 0.06);
}
form {
display: grid;
gap: 18px;
}
label {
display: block;
margin-bottom: 8px;
font-size: 14px;
font-weight: 650;
}
input[type="file"], select, input[type="text"] {
width: 100%;
border: 1px solid var(--line);
border-radius: 6px;
padding: 11px 12px;
font: inherit;
background: #fff;
color: var(--text);
}
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
}
.checks {
display: flex;
flex-wrap: wrap;
gap: 16px;
color: var(--text);
font-size: 14px;
}
.checks label {
display: inline-flex;
align-items: center;
gap: 8px;
margin: 0;
font-weight: 500;
}
button {
width: fit-content;
min-width: 148px;
border: 0;
border-radius: 6px;
padding: 12px 18px;
background: var(--primary);
color: #fff;
font: inherit;
font-weight: 700;
cursor: pointer;
}
button:hover { background: var(--primary-dark); }
.note {
color: var(--muted);
font-size: 13px;
line-height: 1.6;
margin-top: 18px;
border-top: 1px solid var(--line);
padding-top: 16px;
}
.error {
margin-bottom: 16px;
color: var(--danger);
font-weight: 650;
}
@media (max-width: 720px) {
header { display: block; }
.grid { grid-template-columns: 1fr; }
button { width: 100%; }
}
</style>
</head>
<body>
<main class="shell">
<header>
<div>
<h1>检测数据处理</h1>
<div class="sub">上传“待处理检测数据.zip”处理完成后自动下载结果压缩包。</div>
</div>
</header>
<section class="panel">
<form action="/process" method="post" enctype="multipart/form-data">
<div>
<label for="file">待处理检测数据.zip</label>
<input id="file" name="file" type="file" accept=".zip,application/zip" required>
</div>
<div class="grid">
<div>
<label for="mode">处理模式</label>
<select id="mode" name="mode">
<option value="auto">自动识别</option>
<option value="v1">V1 整批汇总</option>
<option value="v2">V2 单患者文件</option>
</select>
</div>
<div>
<label for="data_type">患者编号类型</label>
<select id="data_type" name="data_type">
<option value="pat_no">患者号 pat_no</option>
<option value="zhuyuanhao">住院号 zhuyuanhao</option>
</select>
</div>
<div>
<label for="result_name">结果文件名</label>
<input id="result_name" name="result_name" type="text" value="Result">
</div>
</div>
<div class="checks">
<label><input type="checkbox" name="show_not_match" value="true" checked> 输出未匹配内容</label>
<label><input type="checkbox" name="show_all_infos" value="true"> 输出全部检测记录</label>
</div>
<button type="submit">开始处理</button>
</form>
<div class="note">V1 适用于含有 Patients_info.csv、Tests_List、Tests_Detail_List 的批量数据V2 适用于每个患者单独目录的数据。</div>
</section>
</main>
</body>
</html>
"""
@app.post("/process")
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),
) -> FileResponse:
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_zip = 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:
safe_detail = html.escape(str(exc))
raise HTTPException(status_code=400, detail=safe_detail) from exc
except Exception as exc:
raise HTTPException(status_code=500, detail=f"处理失败:{exc}") from exc
return FileResponse(
result_zip,
media_type="application/zip",
filename="检测数据处理结果.zip",
)
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}