""" app/api/v1/product_images.py — R5 产品多图管理(列/删/改场景/设主图) 上传走 products.py 的 /products/{id}/upload-image(带 scene)。 本文件管已上传图的增删改。product.image_path 始终同步当前主图(向后兼容)。 """ import logging from typing import Annotated from fastapi import APIRouter, Depends from pydantic import BaseModel from sqlalchemy.orm import Session from app.constants.enums import ProductImageScene from app.core.database import get_db from app.core.response import ok, raise_not_found, raise_business from app.middleware.workspace_guard import CurrentUser, require_write_permission from app.models.product import Product, ProductImage logger = logging.getLogger(__name__) router = APIRouter(tags=["product-images"]) class SceneUpdate(BaseModel): scene: str def _get_product(db: Session, product_id: int, workspace_id: int) -> Product: p = db.query(Product).filter( Product.id == product_id, Product.workspace_id == workspace_id ).first() if not p: raise_not_found("产品不存在") return p def _get_image(db: Session, product_id: int, image_id: int) -> ProductImage: img = db.query(ProductImage).filter( ProductImage.id == image_id, ProductImage.product_id == product_id ).first() if not img: raise_not_found("产品图不存在") return img @router.get("/products/{product_id}/images") def list_images( product_id: int, current_user: Annotated[CurrentUser, Depends(require_write_permission)] = None, db: Session = Depends(get_db), ): _get_product(db, product_id, current_user.workspace_id) imgs = db.query(ProductImage).filter( ProductImage.product_id == product_id ).order_by(ProductImage.sort_order).all() return ok([ {"id": im.id, "path": im.path, "scene": im.scene, "is_primary": im.is_primary, "sort_order": im.sort_order} for im in imgs ]) @router.put("/products/{product_id}/images/{image_id}/scene") def update_scene( product_id: int, image_id: int, body: SceneUpdate, current_user: Annotated[CurrentUser, Depends(require_write_permission)] = None, db: Session = Depends(get_db), ): _get_product(db, product_id, current_user.workspace_id) if body.scene not in {s.value for s in ProductImageScene}: raise_business(f"非法场景类型 {body.scene}") img = _get_image(db, product_id, image_id) img.scene = body.scene db.commit() logger.info("product image scene updated: id=%s scene=%s", image_id, body.scene) return ok({"id": image_id, "scene": body.scene}) @router.put("/products/{product_id}/images/{image_id}/primary") def set_primary( product_id: int, image_id: int, current_user: Annotated[CurrentUser, Depends(require_write_permission)] = None, db: Session = Depends(get_db), ): p = _get_product(db, product_id, current_user.workspace_id) img = _get_image(db, product_id, image_id) db.query(ProductImage).filter( ProductImage.product_id == product_id, ProductImage.is_primary == True ).update({"is_primary": False}) img.is_primary = True p.image_path = img.path # 同步主图字段(管道/校验读此字段) db.commit() logger.info("product primary image set: product_id=%s image_id=%s", product_id, image_id) return ok({"product_id": product_id, "primary_image_id": image_id}) @router.delete("/products/{product_id}/images/{image_id}") def delete_image( product_id: int, image_id: int, current_user: Annotated[CurrentUser, Depends(require_write_permission)] = None, db: Session = Depends(get_db), ): p = _get_product(db, product_id, current_user.workspace_id) img = _get_image(db, product_id, image_id) was_primary = img.is_primary db.delete(img) db.flush() # 删的是主图:把剩余 sort_order 最小的一张顶上来当主图,保证 image_path 不悬空 if was_primary: nxt = db.query(ProductImage).filter( ProductImage.product_id == product_id ).order_by(ProductImage.sort_order).first() if nxt: nxt.is_primary = True p.image_path = nxt.path else: p.image_path = None db.commit() logger.info("product image deleted: product_id=%s image_id=%s was_primary=%s", product_id, image_id, was_primary) return ok({"deleted": image_id})