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:
@@ -135,6 +135,20 @@ class Settings(BaseSettings):
|
||||
#: 上传医生语音 WAV 的最大字节数(默认 10MB)。
|
||||
voice_upload_max_bytes: int = Field(default=10 * 1024 * 1024, ge=64, le=50 * 1024 * 1024)
|
||||
|
||||
# --- Demo 客户端跨源(仅用于 scripts/demo_client 联调;生产置 false) ---
|
||||
#: 为 true 时挂载 CORSMiddleware,便于浏览器 demo 从另一个端口访问本服务。
|
||||
demo_cors_enabled: bool = True
|
||||
#: 逗号分隔的允许来源;`*` 表示允许全部来源(demo/联调用,生产应显式指定)。
|
||||
demo_cors_origins: str = "*"
|
||||
|
||||
def parsed_demo_cors_origins(self) -> list[str]:
|
||||
raw = (self.demo_cors_origins or "").strip()
|
||||
if not raw:
|
||||
return []
|
||||
if raw == "*":
|
||||
return ["*"]
|
||||
return [item.strip() for item in raw.split(",") if item.strip()]
|
||||
|
||||
@field_validator("consumable_classifier_weights", mode="before")
|
||||
@classmethod
|
||||
def consumable_classifier_weights_default(cls, value: object) -> str:
|
||||
|
||||
@@ -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 中以下商品名称对应多组产品编码,已保留首次映射: {}",
|
||||
|
||||
Reference in New Issue
Block a user