dingdanquanliucheng/backend/app/api/products.py
taiyi 1a3e9b0197 feat: 为 6 个后台管理模块添加删除和批量删除功能
后端新增 18 个 DELETE 端点(软删除/硬删除/批量删除),
覆盖产品分类、产品、客户、供应商、用户、角色。
硬删除带前置检查,有关联数据时拒绝删除。
前端每个表格新增全选复选框和批量删除按钮。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 20:45:00 +08:00

334 lines
13 KiB
Python
Raw 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.

"""
产品管理路由模块
职责:
处理产品和产品分类的增删改查接口URL 前缀为空(路由在路径中定义)。
包括:产品分类的列表/创建/更新、产品的列表/创建/更新/详情、
产品规格的更新和默认规格设置。
注意:本模块使用 /api/product-categories 和 /api/products 作为路径前缀,
而非使用 APIRouter(prefix=...) 统一设置。
"""
from fastapi import APIRouter, Body, Depends, Query
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from backend.app.api.deps import get_product_service
from backend.app.core.exceptions import AppException
from backend.app.db import get_db_session
from backend.app.models.business import Product, ProductCategory, ProductPricingRule
from backend.app.schemas.common import success_payload
from backend.app.schemas.products import (
CreateCategoryRequest,
CreateProductRequest,
UpdateCategoryRequest,
UpdateProductCategoryRequest,
UpdateProductRequest,
UpdateProductSpecificationRequest,
)
from backend.app.services.product_service import ProductService
router = APIRouter(tags=["products"])
@router.get("/api/product-categories")
def list_categories(
category_name: str | None = Query(default=None), # 分类名称模糊搜索
status: int | None = Query(default=None), # 分类状态筛选(启用/停用)
page_no: int = Query(default=1), # 页码,默认第 1 页
page_size: int = Query(default=20), # 每页条数,默认 20 条
product_service: ProductService = Depends(get_product_service), # 注入产品服务
session: Session = Depends(get_db_session), # 注入数据库会话
) -> dict:
"""分页查询产品分类列表
用途:获取所有产品分类,支持按名称和状态筛选。
请求参数Query 参数筛选 + 分页参数。
返回值:分页分类列表,包含 total、page_no、page_size、list。
权限要求:无特殊权限限制(未配置角色鉴权)。
"""
result = product_service.list_categories(
session=session,
filters={"category_name": category_name, "status": status},
)
start = max(page_no - 1, 0) * page_size
result["list"] = result["list"][start : start + page_size]
result["page_no"] = page_no
result["page_size"] = page_size
return success_payload(result)
@router.post("/api/product-categories")
def create_category(
payload: CreateCategoryRequest, # 创建分类的请求体
product_service: ProductService = Depends(get_product_service), # 注入产品服务
session: Session = Depends(get_db_session), # 注入数据库会话
) -> dict:
"""创建产品分类
用途:新增一个产品分类。
请求参数CreateCategoryRequest分类名称、描述等
返回值:创建成功后的分类信息。
权限要求:无特殊权限限制(未配置角色鉴权)。
"""
return success_payload(product_service.create_category(payload.model_dump(), session))
@router.put("/api/product-categories/{category_id}")
def update_category(
category_id: int, # 分类 ID路径参数
payload: UpdateProductCategoryRequest, # 更新分类的请求体
product_service: ProductService = Depends(get_product_service), # 注入产品服务
session: Session = Depends(get_db_session), # 注入数据库会话
) -> dict:
"""更新产品分类
用途:修改已有产品分类的名称、状态等信息。
请求参数category_id路径参数+ UpdateProductCategoryRequest更新字段
返回值:更新后的分类信息。
权限要求:无特殊权限限制(未配置角色鉴权)。
"""
return success_payload(product_service.update_category(category_id, payload.model_dump(), session))
@router.get("/api/products")
def list_products(
product_name: str | None = Query(default=None), # 产品名称模糊搜索
specification: str | None = Query(default=None), # 规格模糊搜索
category_id: int | None = Query(default=None), # 产品分类 ID 筛选
status: int | None = Query(default=None), # 产品状态筛选(启用/停用)
page_no: int = Query(default=1), # 页码,默认第 1 页
page_size: int = Query(default=20), # 每页条数,默认 20 条
product_service: ProductService = Depends(get_product_service), # 注入产品服务
session: Session = Depends(get_db_session), # 注入数据库会话
) -> dict:
"""分页查询产品列表
用途:获取所有产品信息,支持按名称、规格、分类、状态筛选。
请求参数Query 参数筛选 + 分页参数。
返回值:分页产品列表,包含 total、page_no、page_size、list。
权限要求:无特殊权限限制(未配置角色鉴权)。
"""
result = product_service.list_products(
{
"product_name": product_name,
"specification": specification,
"category_id": category_id,
"status": status,
},
session,
)
start = max(page_no - 1, 0) * page_size
result["list"] = result["list"][start : start + page_size]
result["page_no"] = page_no
result["page_size"] = page_size
return success_payload(result)
@router.post("/api/products")
def create_product(
payload: CreateProductRequest, # 创建产品的请求体
product_service: ProductService = Depends(get_product_service), # 注入产品服务
session: Session = Depends(get_db_session), # 注入数据库会话
) -> dict:
"""创建新产品
用途:新增一个产品记录。
请求参数CreateProductRequest产品名称、分类、规格等
返回值:创建成功后的产品信息。
权限要求:无特殊权限限制(未配置角色鉴权)。
"""
return success_payload(product_service.create_product(payload.model_dump(), session))
@router.put("/api/products/{product_id}")
def update_product(
product_id: int, # 产品 ID路径参数
payload: UpdateProductRequest, # 更新产品的请求体
product_service: ProductService = Depends(get_product_service), # 注入产品服务
session: Session = Depends(get_db_session), # 注入数据库会话
) -> dict:
"""更新产品信息
用途:修改已有产品的基本信息。
请求参数product_id路径参数+ UpdateProductRequest更新字段
返回值:更新后的产品信息。
权限要求:无特殊权限限制(未配置角色鉴权)。
"""
return success_payload(product_service.update_product(product_id, payload.model_dump(), session))
@router.put("/api/products/specifications/{product_id}")
def update_product_specification(
product_id: int, # 产品 ID路径参数
payload: UpdateProductSpecificationRequest, # 更新规格的请求体
product_service: ProductService = Depends(get_product_service), # 注入产品服务
session: Session = Depends(get_db_session), # 注入数据库会话
) -> dict:
"""更新产品规格
用途:修改指定产品的规格信息(如尺寸、材质等)。
请求参数product_id路径参数+ UpdateProductSpecificationRequest规格数据
返回值:更新后的规格信息。
权限要求:无特殊权限限制(未配置角色鉴权)。
"""
return success_payload(product_service.update_product_specification(product_id, payload.model_dump(), session))
@router.put("/api/products/specifications/{product_id}/default")
def set_default_specification(
product_id: int, # 产品 ID路径参数
product_service: ProductService = Depends(get_product_service), # 注入产品服务
session: Session = Depends(get_db_session), # 注入数据库会话
) -> dict:
"""设置默认规格
用途:将指定产品的某个规格设为默认规格,用于报价计算时的默认取值。
请求参数product_id - 产品 ID路径参数
返回值:设置结果。
权限要求:无特殊权限限制(未配置角色鉴权)。
"""
return success_payload(product_service.set_default_specification(product_id, session))
@router.get("/api/products/{product_id}")
def get_product(
product_id: int, # 产品 ID路径参数
product_service: ProductService = Depends(get_product_service), # 注入产品服务
session: Session = Depends(get_db_session), # 注入数据库会话
) -> dict:
"""查询产品详情
用途:根据产品 ID 获取单个产品的完整信息(含规格、分类等)。
请求参数product_id - 产品 ID路径参数
返回值:产品详情信息。
权限要求:无特殊权限限制(未配置角色鉴权)。
"""
return success_payload(product_service.get_product(product_id, session))
@router.delete("/api/product-categories/{category_id}")
def delete_category(
category_id: int,
session: Session = Depends(get_db_session),
) -> dict:
"""删除产品分类(软删除)"""
cat = session.get(ProductCategory, category_id)
if cat is None or cat.deleted == 1:
raise AppException(404, "产品分类不存在")
cat.deleted = 1
session.commit()
return success_payload({"deleted": True})
@router.delete("/api/product-categories/{category_id}/permanent")
def permanent_delete_category(
category_id: int,
session: Session = Depends(get_db_session),
) -> dict:
"""永久删除产品分类(硬删除)"""
cat = session.get(ProductCategory, category_id)
if cat is None or cat.deleted == 1:
raise AppException(404, "产品分类不存在")
count = session.execute(
select(func.count(Product.id)).where(
Product.category_id == category_id, Product.deleted == 0
)
).scalar()
if count > 0:
raise AppException("该分类下还有产品,无法删除")
session.delete(cat)
session.commit()
return success_payload({"deleted": True})
@router.delete("/api/products/{product_id}")
def delete_product(
product_id: int,
session: Session = Depends(get_db_session),
) -> dict:
"""删除产品(软删除)"""
product = session.get(Product, product_id)
if product is None or product.deleted == 1:
raise AppException(404, "产品不存在")
product.deleted = 1
session.commit()
return success_payload({"deleted": True})
@router.delete("/api/products/{product_id}/permanent")
def permanent_delete_product(
product_id: int,
session: Session = Depends(get_db_session),
) -> dict:
"""永久删除产品(硬删除)"""
product = session.get(Product, product_id)
if product is None or product.deleted == 1:
raise AppException(404, "产品不存在")
count = session.execute(
select(func.count(ProductPricingRule.id)).where(
ProductPricingRule.product_id == product_id
)
).scalar()
if count > 0:
raise AppException("该产品还有定价规则,无法删除")
session.delete(product)
session.commit()
return success_payload({"deleted": True})
@router.post("/api/product-categories/batch-delete")
def batch_delete_categories(
ids: list[int] = Body(..., embed=True),
permanent: bool = Body(default=False, embed=True),
session: Session = Depends(get_db_session),
) -> dict:
"""批量删除产品分类"""
cats = session.execute(
select(ProductCategory).where(ProductCategory.id.in_(ids), ProductCategory.deleted == 0)
).scalars().all()
if not cats:
raise AppException(404, "未找到可删除的分类")
if permanent:
cat_ids = [c.id for c in cats]
count = session.execute(
select(func.count(Product.id)).where(Product.category_id.in_(cat_ids), Product.deleted == 0)
).scalar()
if count > 0:
raise AppException("所选分类下还有产品,无法删除")
for c in cats:
session.delete(c)
else:
for c in cats:
c.deleted = 1
session.commit()
return success_payload({"deleted": len(cats)})
@router.post("/api/products/batch-delete")
def batch_delete_products(
ids: list[int] = Body(..., embed=True),
permanent: bool = Body(default=False, embed=True),
session: Session = Depends(get_db_session),
) -> dict:
"""批量删除产品"""
products = session.execute(
select(Product).where(Product.id.in_(ids), Product.deleted == 0)
).scalars().all()
if not products:
raise AppException(404, "未找到可删除的产品")
if permanent:
prod_ids = [p.id for p in products]
count = session.execute(
select(func.count(ProductPricingRule.id)).where(ProductPricingRule.product_id.in_(prod_ids))
).scalar()
if count > 0:
raise AppException("所选产品还有定价规则,无法删除")
for p in products:
session.delete(p)
else:
for p in products:
p.deleted = 1
session.commit()
return success_payload({"deleted": len(products)})