187 lines
5.9 KiB
Python
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)
|