first commit

This commit is contained in:
2026-05-08 21:28:29 +08:00
commit 8598df4930
11 changed files with 1236 additions and 0 deletions

244
app/main.py Normal file
View File

@@ -0,0 +1,244 @@
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"}