feat: demo CORS, demo client, openpyxl catalog load

- Load consumable catalog XLSX with openpyxl and drop the pandas dependency.
- Add optional demo CORS settings and FastAPI CORSMiddleware for browser clients.
- Add scripts/demo_client static page and local server for API smoke tests.

Made-with: Cursor
This commit is contained in:
Kevin
2026-04-22 17:00:56 +08:00
parent 132702aea9
commit 42720f81cf
8 changed files with 991 additions and 84 deletions

View File

@@ -5,6 +5,7 @@
from __future__ import annotations
import math
import os
import sys
from collections import Counter
@@ -13,8 +14,8 @@ from pathlib import Path
from threading import Lock
import numpy as np
import pandas as pd
from loguru import logger
from openpyxl import load_workbook
from ultralytics import YOLO
from app.config import Settings, settings
@@ -74,14 +75,22 @@ class ClsTop3:
t3_pid: str
def _find_col(df: pd.DataFrame, want: str) -> str | None:
def _find_col_idx(headers: list[object], want: str) -> int | None:
want = want.strip()
for c in df.columns:
if str(c).strip() == want:
return str(c)
for i, h in enumerate(headers):
if str(h).strip() == want:
return i
return None
def _cell_empty(value: object) -> bool:
if value is None:
return True
if isinstance(value, float) and math.isnan(value):
return True
return False
def _norm_product_name(name: str) -> str:
s = (name or "").strip()
if s == "一次性医用垫单":
@@ -91,29 +100,41 @@ def _norm_product_name(name: str) -> str:
def load_name_to_product_code(xlsx: Path) -> dict[str, str]:
"""商品名称 -> 产品编码(白名单键为归一化后的名称)。"""
df = pd.read_excel(xlsx, sheet_name=0)
c_code = _find_col(df, "产品编码")
c_name = _find_col(df, "商品名称")
if c_code is None or c_name is None:
raise ValueError("Excel 缺少「产品编码」或「商品名称」列")
m: dict[str, str] = {}
dups: set[str] = set()
for _, row in df.iterrows():
raw = row.get(c_name)
if raw is None or (isinstance(raw, float) and pd.isna(raw)):
continue
n = _norm_product_name(str(raw).strip())
if not n:
continue
code = row.get(c_code)
if code is None or (isinstance(code, float) and pd.isna(code)):
continue
sc = str(code).strip()
if n in m and m[n] != sc:
dups.add(n)
continue
if n not in m:
m[n] = sc
wb = load_workbook(filename=str(xlsx), read_only=True, data_only=True)
try:
ws = wb.worksheets[0]
rows = ws.iter_rows(values_only=True)
header = next(rows, None)
if header is None:
raise ValueError("Excel 为空")
headers = list(header)
i_code = _find_col_idx(headers, "产品编码")
i_name = _find_col_idx(headers, "商品名称")
if i_code is None or i_name is None:
raise ValueError("Excel 缺少「产品编码」或「商品名称」列")
m: dict[str, str] = {}
dups: set[str] = set()
for row in rows:
if not row:
continue
raw = row[i_name] if i_name < len(row) else None
if _cell_empty(raw):
continue
n = _norm_product_name(str(raw).strip())
if not n:
continue
code = row[i_code] if i_code < len(row) else None
if _cell_empty(code):
continue
sc = str(code).strip()
if n in m and m[n] != sc:
dups.add(n)
continue
if n not in m:
m[n] = sc
finally:
wb.close()
if dups:
logger.warning(
"Excel 中以下商品名称对应多组产品编码,已保留首次映射: {}",