Files

187 lines
5.9 KiB
Python

"""商品 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)