调整列名并添加输入验证

This commit is contained in:
JIANG
2026-07-07 10:36:09 +08:00
parent 9688ee208c
commit 22dd364405
5 changed files with 53 additions and 22 deletions
+13 -13
View File
@@ -1,16 +1,16 @@
__pycache__/ .git
.agents
.codex
__pycache__
*.pyc *.pyc
*.pyo *.pyo
.git/ .pytest_cache
.agents/ node_modules
.codex/ data
node_modules/ uploads
.env static/images
app.log
server.out.log
server.err.log
data/
pipe_survival_0331.db pipe_survival_0331.db
uploads/ app.log
static/images/ server.err.log
server.out.log
.env
+1
View File
@@ -22,6 +22,7 @@ COPY templates ./templates
COPY static ./static COPY static ./static
COPY main.py . COPY main.py .
COPY example.xlsx . COPY example.xlsx .
COPY 20260630标准文本——供水管道健康状态与剩余寿命评估技术导则.pdf .
COPY my_survival_forest_model_quxi-10-0331.joblib . COPY my_survival_forest_model_quxi-10-0331.joblib .
RUN mkdir -p data static/images uploads RUN mkdir -p data static/images uploads
+24 -7
View File
@@ -57,7 +57,17 @@ FEATURES = [
] ]
ID_COLUMN = "管道编号" ID_COLUMN = "管道编号"
PIPE_AGE_COLUMN = "管龄" PIPE_AGE_COLUMN = "管龄(年)"
LEGACY_PIPE_AGE_COLUMN = "管龄"
STATUS_COLUMN = "状态"
EVENT_AGE_COLUMN = "事件/观察管龄(年)"
INPUT_COLUMNS = [
ID_COLUMN,
PIPE_AGE_COLUMN,
STATUS_COLUMN,
EVENT_AGE_COLUMN,
*FEATURES,
]
MATERIAL_COLUMN = "管材" MATERIAL_COLUMN = "管材"
MATERIAL_CODE_OPTIONS = [ MATERIAL_CODE_OPTIONS = [
(1, "镀锌"), (1, "镀锌"),
@@ -183,11 +193,19 @@ def secure_upload_name(original_filename: str, run_id: str) -> tuple[str, str]:
def read_input_file(path: Path, suffix: str) -> pd.DataFrame: def read_input_file(path: Path, suffix: str) -> pd.DataFrame:
try: try:
if suffix == ".csv": if suffix == ".csv":
return pd.read_csv(path, dtype={ID_COLUMN: "string"}) df = pd.read_csv(path, dtype={ID_COLUMN: "string"})
return pd.read_excel(path, dtype={ID_COLUMN: "string"}) else:
df = pd.read_excel(path, dtype={ID_COLUMN: "string"})
except Exception as exc: except Exception as exc:
logging.exception("文件解析失败: %s", exc) logging.exception("文件解析失败: %s", exc)
raise PredictionError("文件解析失败,请检查编码或表格格式。") raise PredictionError("文件解析失败,请检查编码或表格格式。")
return normalize_input_columns(df)
def normalize_input_columns(df: pd.DataFrame) -> pd.DataFrame:
if PIPE_AGE_COLUMN not in df.columns and LEGACY_PIPE_AGE_COLUMN in df.columns:
return df.rename(columns={LEGACY_PIPE_AGE_COLUMN: PIPE_AGE_COLUMN})
return df
def normalize_id_value(value: Any, fallback: str) -> str: def normalize_id_value(value: Any, fallback: str) -> str:
@@ -201,8 +219,7 @@ def normalize_id_value(value: Any, fallback: str) -> str:
def validate_input_frame(df: pd.DataFrame) -> None: def validate_input_frame(df: pd.DataFrame) -> None:
if df.empty: if df.empty:
raise PredictionError("上传文件没有可预测的数据。") raise PredictionError("上传文件没有可预测的数据。")
required_columns = [ID_COLUMN, *FEATURES] missing = [col for col in INPUT_COLUMNS if col not in df.columns]
missing = [col for col in required_columns if col not in df.columns]
if missing: if missing:
raise PredictionError(f"缺少必要字段: {', '.join(missing)}") raise PredictionError(f"缺少必要字段: {', '.join(missing)}")
@@ -447,8 +464,8 @@ def render_survival_chart(df: pd.DataFrame, curves, image_path: Path) -> tuple[l
) )
summary_sheet_rows.append( summary_sheet_rows.append(
{ {
"管道编号": pipe_id, ID_COLUMN: pipe_id,
"管龄": pipe_age, PIPE_AGE_COLUMN: pipe_age,
"健康等级": grade_label, "健康等级": grade_label,
} }
) )
BIN
View File
Binary file not shown.
+15 -2
View File
@@ -11,6 +11,7 @@ from openpyxl import load_workbook
from app.prediction import ( from app.prediction import (
FEATURES, FEATURES,
ID_COLUMN, ID_COLUMN,
INPUT_COLUMNS,
PIPE_AGE_COLUMN, PIPE_AGE_COLUMN,
PredictionError, PredictionError,
estimate_remaining_life, estimate_remaining_life,
@@ -59,6 +60,18 @@ class PredictionHelpersTest(unittest.TestCase):
self.assertIn("缺少必要字段", ctx.exception.message) self.assertIn("缺少必要字段", ctx.exception.message)
def test_validate_input_frame_accepts_current_template_columns(self) -> None:
df = pd.DataFrame([{column: 1 for column in INPUT_COLUMNS}])
validate_input_frame(df)
def test_example_workbook_columns_match_backend_contract(self) -> None:
workbook = load_workbook(Path(__file__).resolve().parents[1] / "example.xlsx", read_only=True)
worksheet = workbook["Template"]
headers = [cell.value for cell in next(worksheet.iter_rows(min_row=1, max_row=1))]
self.assertEqual(headers, INPUT_COLUMNS)
def test_read_input_file_preserves_text_pipe_ids(self) -> None: def test_read_input_file_preserves_text_pipe_ids(self) -> None:
with TemporaryDirectory() as temp_dir: with TemporaryDirectory() as temp_dir:
output_path = Path(temp_dir) / "input.xlsx" output_path = Path(temp_dir) / "input.xlsx"
@@ -97,8 +110,8 @@ class PredictionHelpersTest(unittest.TestCase):
] ]
summary_rows = [{"pipe_id": "P001"}, {"pipe_id": "P002"}] summary_rows = [{"pipe_id": "P001"}, {"pipe_id": "P002"}]
summary_sheet_rows = [ summary_sheet_rows = [
{"管道编号": "P001", "管龄": "10 年", "健康概率": 0.7, "预计剩余寿命": 2.0, "健康等级": "IV级"}, {ID_COLUMN: "P001", PIPE_AGE_COLUMN: "10 年", "健康概率": 0.7, "预计剩余寿命": 2.0, "健康等级": "IV级"},
{"管道编号": "P002", "管龄": "12 年", "健康概率": 0.6, "预计剩余寿命": 1.5, "健康等级": "III级"}, {ID_COLUMN: "P002", PIPE_AGE_COLUMN: "12 年", "健康概率": 0.6, "预计剩余寿命": 1.5, "健康等级": "III级"},
] ]
with TemporaryDirectory() as temp_dir: with TemporaryDirectory() as temp_dir: