81bafaf557
- Sync: fix GiteaSync constructor + add push()/pull() methods - Barcode: two-tab layout matching GUI (mapping + special rules) - Memory: spec→specification unification, manual add, confidence/price tracking - Processing: TaskLogHandler captures detailed logs (barcode mapping, unit conversion) - Preview: fullscreen dialog for file preview (image/Excel) in Orders/Tables/Images - Detail: per-file log filtering in file pages - Tasks: result files now per-task, add copy path button - Config: reactive edited state + save_config fix - Dashboard: sync task isolation, log limit 10 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
202 lines
5.5 KiB
Python
202 lines
5.5 KiB
Python
"""Product memory CRUD endpoints."""
|
|
|
|
from typing import Optional, List, Dict
|
|
from pathlib import Path
|
|
|
|
from fastapi import APIRouter, HTTPException, Depends, Query
|
|
from pydantic import BaseModel
|
|
|
|
from ..auth.dependencies import get_current_user
|
|
|
|
router = APIRouter(prefix="/api/memory", tags=["memory"])
|
|
|
|
_project_root = Path(__file__).resolve().parent.parent.parent.parent
|
|
_db_path = str(_project_root / "data" / "product_cache.db")
|
|
_excel_source = str(_project_root / "templates" / "商品资料.xlsx")
|
|
|
|
|
|
class MemoryItem(BaseModel):
|
|
barcode: str
|
|
name: str
|
|
specification: Optional[str] = None
|
|
unit: Optional[str] = None
|
|
price: Optional[float] = None
|
|
avg_price: Optional[float] = None
|
|
min_price: Optional[float] = None
|
|
max_price: Optional[float] = None
|
|
price_count: int = 0
|
|
confidence: int = 0
|
|
source: str = "ocr"
|
|
last_used: Optional[str] = None
|
|
use_count: int = 0
|
|
|
|
|
|
class MemoryCreate(BaseModel):
|
|
barcode: str
|
|
name: Optional[str] = ""
|
|
specification: Optional[str] = None
|
|
unit: Optional[str] = None
|
|
price: Optional[float] = None
|
|
confidence: int = 50
|
|
|
|
|
|
class MemoryUpdate(BaseModel):
|
|
name: Optional[str] = None
|
|
specification: Optional[str] = None
|
|
unit: Optional[str] = None
|
|
price: Optional[float] = None
|
|
confidence: Optional[int] = None
|
|
|
|
|
|
class MemoryListResponse(BaseModel):
|
|
items: List[MemoryItem]
|
|
total: int
|
|
page: int
|
|
page_size: int
|
|
|
|
|
|
def _get_db():
|
|
from app.core.db.product_db import ProductDatabase
|
|
return ProductDatabase(_db_path, _excel_source)
|
|
|
|
|
|
def _row_to_item(row: Dict) -> MemoryItem:
|
|
return MemoryItem(
|
|
barcode=row.get("barcode", ""),
|
|
name=row.get("name", ""),
|
|
specification=row.get("specification"),
|
|
unit=row.get("unit"),
|
|
price=row.get("price"),
|
|
avg_price=row.get("avg_price"),
|
|
min_price=row.get("min_price"),
|
|
max_price=row.get("max_price"),
|
|
price_count=row.get("price_count", 0),
|
|
confidence=row.get("confidence", 0),
|
|
source=row.get("source", "ocr"),
|
|
last_used=row.get("last_used"),
|
|
use_count=row.get("use_count", 0),
|
|
)
|
|
|
|
|
|
@router.get("", response_model=MemoryListResponse)
|
|
async def list_memory(
|
|
search: str = "",
|
|
page: int = Query(1, ge=1),
|
|
page_size: int = Query(50, ge=1, le=200),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
db = _get_db()
|
|
results = db.get_all_memories()
|
|
|
|
if search:
|
|
s = search.lower()
|
|
results = [r for r in results if s in r.get("barcode", "").lower() or s in r.get("name", "").lower()]
|
|
|
|
total = len(results)
|
|
start = (page - 1) * page_size
|
|
page_items = results[start:start + page_size]
|
|
|
|
return MemoryListResponse(
|
|
items=[_row_to_item(r) for r in page_items],
|
|
total=total,
|
|
page=page,
|
|
page_size=page_size,
|
|
)
|
|
|
|
|
|
@router.get("/{barcode}")
|
|
async def get_memory(
|
|
barcode: str,
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
db = _get_db()
|
|
product = db.get_memory(barcode)
|
|
if not product:
|
|
raise HTTPException(404, f"未找到条码 {barcode} 的记忆记录")
|
|
return product
|
|
|
|
|
|
@router.post("")
|
|
async def create_memory(
|
|
body: MemoryCreate,
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
db = _get_db()
|
|
existing = db.get_memory(body.barcode)
|
|
if existing:
|
|
raise HTTPException(409, f"条码 {body.barcode} 已存在,请使用编辑功能")
|
|
db.learn_from_product({
|
|
"barcode": body.barcode,
|
|
"name": body.name or "",
|
|
"specification": body.specification or "",
|
|
"unit": body.unit or "",
|
|
"price": body.price or 0,
|
|
}, source="user_confirmed")
|
|
return {"message": f"已创建记忆记录 {body.barcode}"}
|
|
|
|
|
|
@router.put("/{barcode}")
|
|
async def update_memory(
|
|
barcode: str,
|
|
body: MemoryUpdate,
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
db = _get_db()
|
|
existing = db.get_memory(barcode)
|
|
if not existing:
|
|
raise HTTPException(404, f"未找到条码 {barcode}")
|
|
|
|
update_data = body.model_dump(exclude_none=True)
|
|
if not update_data:
|
|
raise HTTPException(400, "没有提供更新数据")
|
|
|
|
db.update_memory(barcode, update_data)
|
|
return {"message": f"已更新 {barcode}", "updated_fields": list(update_data.keys())}
|
|
|
|
|
|
@router.delete("/{barcode}")
|
|
async def delete_memory(
|
|
barcode: str,
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
db = _get_db()
|
|
existing = db.get_memory(barcode)
|
|
if not existing:
|
|
raise HTTPException(404, f"未找到条码 {barcode}")
|
|
db.delete_memory(barcode)
|
|
return {"message": f"已删除 {barcode}"}
|
|
|
|
|
|
@router.post("/reimport")
|
|
async def reimport_memory(
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
db = _get_db()
|
|
try:
|
|
count = db.reimport()
|
|
return {"message": f"重新导入完成,共导入 {count} 条记录", "count": count}
|
|
except Exception as e:
|
|
raise HTTPException(500, f"导入失败: {e}")
|
|
|
|
|
|
@router.get("/export/sync")
|
|
async def export_memory(
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
db = _get_db()
|
|
data = db.export_for_sync()
|
|
return {"data": data, "count": len(data)}
|
|
|
|
|
|
@router.post("/import/sync")
|
|
async def import_memory(
|
|
data: dict,
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
db = _get_db()
|
|
try:
|
|
count = db.import_from_sync(data.get("data", []))
|
|
return {"message": f"导入完成,共 {count} 条", "count": count}
|
|
except Exception as e:
|
|
raise HTTPException(500, f"导入失败: {e}")
|