from fastapi import FastAPI, UploadFile, File, Query from fastapi.responses import HTMLResponse, JSONResponse from fastapi.staticfiles import StaticFiles import sqlite3 import pandas as pd import os from datetime import datetime DB_PATH = os.path.join(os.getcwd(), "data", "products.db") os.makedirs(os.path.join(os.getcwd(), "data"), exist_ok=True) def get_conn(): conn = sqlite3.connect(DB_PATH, check_same_thread=False) try: conn.execute("PRAGMA journal_mode=WAL") conn.execute("PRAGMA synchronous=NORMAL") except Exception: pass return conn def init_db(): conn = get_conn() cur = conn.cursor() cur.execute( """ create table if not exists products ( barcode text primary key, name text, purchase_price real, sale_price real, category text, created_at text, source_file text ) """ ) cur.execute("create index if not exists idx_products_name on products(name)") conn.commit() conn.close() init_db() app = FastAPI() app.mount("/static", StaticFiles(directory="static"), name="static") @app.get("/", response_class=HTMLResponse) def root(): return open(os.path.join("static", "index.html"), "r", encoding="utf-8").read() @app.get("/health") def health(): return {"status": "ok"} def normalize_price(v): if v is None: return None try: s = str(v).strip() if s == "": return None return float(s) except: return None def normalize_text(v): if v is None: return None s = str(v).strip() return s if s != "" else None def import_df(df: pd.DataFrame, source_file: str): required = ["名称(必填)", "进货价(必填)", "销售价(必填)", "条码"] for col in required: if col not in df.columns: return {"error": f"missing column: {col}"} df = df[["名称(必填)", "进货价(必填)", "销售价(必填)", "条码", "分类(必填)"]].copy() if "分类(必填)" in df.columns else df[["名称(必填)", "进货价(必填)", "销售价(必填)", "条码"]].copy() df.columns = ["name", "purchase_price", "sale_price", "barcode"] + (["category"] if df.shape[1] == 5 else []) df["name"] = df["name"].apply(normalize_text) df["barcode"] = df["barcode"].apply(normalize_text) df["purchase_price"] = df["purchase_price"].apply(normalize_price) df["sale_price"] = df["sale_price"].apply(normalize_price) if "category" in df.columns: df["category"] = df["category"].apply(normalize_text) now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") inserted = 0 updated = 0 skipped = 0 conn = get_conn() cur = conn.cursor() for _, row in df.iterrows(): bc = row.get("barcode") nm = row.get("name") pp = row.get("purchase_price") sp = row.get("sale_price") ct = row.get("category") if "category" in df.columns else None if bc is None: skipped += 1 continue cur.execute("select barcode from products where barcode=?", (bc,)) exists = cur.fetchone() if exists: cur.execute( "update products set name=?, purchase_price=?, sale_price=?, category=?, created_at=?, source_file=? where barcode=?", (nm, pp, sp, ct, now, source_file, bc), ) updated += 1 else: cur.execute( "insert into products(barcode,name,purchase_price,sale_price,category,created_at,source_file) values(?,?,?,?,?,?,?)", (bc, nm, pp, sp, ct, now, source_file), ) inserted += 1 conn.commit() conn.close() return {"inserted": inserted, "updated": updated, "skipped": skipped} @app.post("/import") async def import_excel(file: UploadFile = File(...)): tmp = os.path.join(os.getcwd(), "data", f"upload_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{file.filename}") with open(tmp, "wb") as f: f.write(await file.read()) try: df = pd.read_excel(tmp, sheet_name="Sheet1") result = import_df(df, os.path.basename(file.filename)) return result if isinstance(result, dict) else {"error": "import failed"} finally: pass @app.get("/products/{barcode}") def get_product(barcode: str): conn = get_conn() cur = conn.cursor() cur.execute("select name,purchase_price,sale_price,barcode from products where barcode=?", (barcode,)) row = cur.fetchone() conn.close() if not row: return JSONResponse(status_code=404, content={"error": "not found"}) return {"name": row[0], "purchase_price": row[1], "sale_price": row[2], "barcode": row[3]} @app.get("/products") def search_products(q: str = Query("", min_length=0), limit: int = 20, page: int = 1, sort: str = "", order: str = "asc"): conn = get_conn() cur = conn.cursor() offset = max((page - 1), 0) * max(limit, 1) sort_map = {"name": "name", "sale_price": "sale_price", "purchase_price": "purchase_price", "created_at": "created_at", "barcode": "barcode"} sort_col = sort_map.get(sort, "") order_sql = "DESC" if str(order).lower() == "desc" else "ASC" if q: s = q.strip() if s.isdigit(): if sort_col: cur.execute( f"select distinct name,purchase_price,sale_price,barcode from products where barcode like ? or barcode like ? or barcode like ? or name like ? order by {sort_col} {order_sql} limit ? offset ?", (f"{s}%", f"%{s}", f"%{s}%", f"%{s}%", limit, offset), ) else: cur.execute( "select name,purchase_price,sale_price,barcode, case when barcode like ? then 0 when barcode like ? then 1 when barcode like ? then 2 when name like ? then 3 else 4 end as score from products where barcode like ? or barcode like ? or barcode like ? or name like ? order by score, name limit ? offset ?", (f"{s}%", f"%{s}", f"%{s}%", f"%{s}%", f"{s}%", f"%{s}", f"%{s}%", f"%{s}%", limit, offset), ) else: if sort_col: cur.execute( f"select name,purchase_price,sale_price,barcode from products where name like ? order by {sort_col} {order_sql} limit ? offset ?", (f"%{s}%", limit, offset), ) else: cur.execute( "select name,purchase_price,sale_price,barcode, case when instr(name, ?) = 1 then 0 when instr(name, ?) > 0 then 1 else 2 end as score from products where name like ? order by score, name limit ? offset ?", (s, s, f"%{s}%", limit, offset), ) else: if sort_col: cur.execute(f"select name,purchase_price,sale_price,barcode from products order by {sort_col} {order_sql} limit ? offset ?", (limit, offset)) else: cur.execute("select name,purchase_price,sale_price,barcode from products order by name limit ? offset ?", (limit, offset)) rows = cur.fetchall() conn.close() return [{"name": r[0], "purchase_price": r[1], "sale_price": r[2], "barcode": r[3]} for r in rows]