Files
beige/backend/app/api/v1/product_images.py
yangqianqian 4bed7425a8 A8多套打包+M4归档+R5多图:存量功能备份
A8 多套交付包(packaging_task.py):
- 修复交付包只打第1条混乱note的bug,按ImageCandidate.strategy分A/B/C组
- 每组生独立note_0N夹(6图+文案.txt),同seq留最新去重,老数据兼容
- task74端到端验:3套各6图,独立agent7项交叉验证全过

M4 归档(tasks.py/exports.py/前端):
- list_tasks加date_from/date_to/product_id筛选+product_name批量填(防N+1)
- 新增exports.py:产品JSON导出+标杆CSV导出(UTF-8 BOM)
- 前端HistoryFilters日期/产品筛选+产品列+打回原因红banner
- response.py加raise_param_error;独立agent验A1/A2/A9通过

R5 产品多图(product_images.py/020迁移/前端):
- product_images表+5端点(上传/列/改场景/设主图/删图)
- 生图按ROLE_SCENE_PREFERENCE选对应场景图,回落primary
- 前端ProductImageManager多图画廊

R6 账号config拆页(settings/):
- 配置页按角色拆/settings(运营+组长+admin)+/config(仅admin)
- Key只显末4位不显余额(守红线)

核销表对齐真实代码状态:D1改稿框/M7裂变/E12评图分纠偏为已完成(曾漏回写)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 17:32:49 +08:00

122 lines
4.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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})