"""商品 CRUD(采集箱 / 编辑 / 删除)。""" from __future__ import annotations from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession from db import get_db from deps import get_current_user from models import Product, ProductAsset from models.enums import Stage from schemas.product import ProductDetail, ProductListItem, ProductUpdate router = APIRouter(prefix="/api/products", tags=["products"]) @router.get("") async def list_products( stage: str | None = None, q: str | None = None, page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), db: AsyncSession = Depends(get_db), _user: dict = Depends(get_current_user), ): stmt = select(Product) if stage: stmt = stmt.where(Product.stage == stage) if q: stmt = stmt.where(Product.name.ilike(f"%{q}%") | Product.offer_id.ilike(f"%{q}%")) total = (await db.execute(select(func.count()).select_from(stmt.subquery()))).scalar() or 0 rows = (await db.execute( stmt.order_by(Product.updated_at.desc()).offset((page - 1) * page_size).limit(page_size) )).scalars().all() items = [ProductListItem.model_validate(r) for r in rows] return {"total": total, "items": items} @router.get("/{product_id}", response_model=ProductDetail) async def get_product( product_id: str, db: AsyncSession = Depends(get_db), _user: dict = Depends(get_current_user), ): product = await db.get(Product, UUID(product_id)) if product is None: raise HTTPException(status_code=404, detail="商品不存在") return ProductDetail.model_validate(product) @router.post("", response_model=ProductDetail) async def create_product( body: ProductUpdate, db: AsyncSession = Depends(get_db), _user: dict = Depends(get_current_user), ): product = Product(stage=Stage.collected) _apply_update(product, body) db.add(product) await db.commit() await db.refresh(product) return ProductDetail.model_validate(product) @router.post("/{product_id}/copy", response_model=ProductDetail) async def copy_product( product_id: str, db: AsyncSession = Depends(get_db), _user: dict = Depends(get_current_user), ): """复制商品为新变体:继承标题/描述/属性/型号名称/计价,重置货号与图片。""" src = await db.get(Product, UUID(product_id)) if src is None: raise HTTPException(status_code=404, detail="商品不存在") clone = Product( stage=Stage.collected, shop_id=src.shop_id, source_platform=src.source_platform, source_item_id=None, # 新变体,不沿用源 itemId(避免去重冲突) source_url=src.source_url, offer_id="", # 重置货号 name=src.name, description=src.description, description_category_id=src.description_category_id, type_id=src.type_id, price=src.price, old_price=src.old_price, currency_code=src.currency_code, vat=src.vat, depth=src.depth, width=src.width, height=src.height, dimension_unit=src.dimension_unit, weight=src.weight, weight_unit=src.weight_unit, barcode=src.barcode, images=None, # 重置图片 primary_image=None, images360=None, color_image=None, attributes=src.attributes, complex_attributes=src.complex_attributes, raw=src.raw, # 含 model_name(型号名称) pricing=src.pricing, copy=src.copy, fx_rate=src.fx_rate, ) db.add(clone) await db.commit() await db.refresh(clone) return ProductDetail.model_validate(clone) @router.patch("/{product_id}", response_model=ProductDetail) async def update_product( product_id: str, body: ProductUpdate, db: AsyncSession = Depends(get_db), _user: dict = Depends(get_current_user), ): product = await db.get(Product, UUID(product_id)) if product is None: raise HTTPException(status_code=404, detail="商品不存在") _apply_update(product, body) await db.commit() await db.refresh(product) return ProductDetail.model_validate(product) @router.delete("/{product_id}") async def delete_product( product_id: str, hard: bool = False, db: AsyncSession = Depends(get_db), _user: dict = Depends(get_current_user), ): product = await db.get(Product, UUID(product_id)) if product is None: raise HTTPException(status_code=404, detail="商品不存在") if hard: await db.delete(product) else: product.stage = Stage.archived await db.commit() return {"deleted": True} @router.get("/{product_id}/assets") async def list_assets( product_id: str, db: AsyncSession = Depends(get_db), _user: dict = Depends(get_current_user), ): rows = (await db.scalars( select(ProductAsset) .where(ProductAsset.product_id == UUID(product_id)) .order_by(ProductAsset.group_key, ProductAsset.sort_order) )).all() return [ { "id": str(a.id), "group_key": a.group_key, "variant_name": a.variant_name, "sort_order": a.sort_order, "type": a.type, "source_url": a.source_url, "stored_url": a.stored_url, "status": a.status.value, "width": a.width, "height": a.height, "error": a.error, } for a in rows ] def _apply_update(product: Product, body: ProductUpdate) -> None: data = body.model_dump(exclude_unset=True) if "stage" in data and data["stage"]: data["stage"] = Stage(data["stage"]) for key, value in data.items(): if value is not None or key in ("raw", "pricing", "copy", "images", "attributes", "complex_attributes", "shop_id"): setattr(product, key, value)