dingdanquanliucheng/backend/app/api/products.py

367 lines
16 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, require_roles
from backend.app.core.cache import cache_delete_pattern
from backend.app.core.error_codes import ErrorCode
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.post("/api/products/{product_id}/specifications")
def add_product_specification(
product_id: int, # 产品组中任一规格的 ID
payload: UpdateProductSpecificationRequest, # 新增规格的请求体(复用更新规格的字段)
product_service: ProductService = Depends(get_product_service),
session: Session = Depends(get_db_session),
) -> dict:
"""给已有产品追加规格
用途:在已有产品下新增一条规格明细。
请求参数product_id产品组中任一规格的 ID+ 规格数据。
返回值:新增规格的信息。
"""
return success_payload(product_service.add_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),
current_user: dict = Depends(require_roles("manager", "admin")),
) -> dict:
"""删除产品分类(软删除)"""
cat = session.get(ProductCategory, category_id)
if cat is None or cat.deleted == 1:
raise AppException(code=ErrorCode.NOT_FOUND, message="产品分类不存在", status_code=404)
cat.deleted = 1
session.commit()
cache_delete_pattern("product:*")
cache_delete_pattern("category:*")
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),
current_user: dict = Depends(require_roles("manager", "admin")),
) -> dict:
"""永久删除产品分类(硬删除)"""
cat = session.get(ProductCategory, category_id)
if cat is None or cat.deleted == 1:
raise AppException(code=ErrorCode.NOT_FOUND, message="产品分类不存在", status_code=404)
count = session.execute(
select(func.count(Product.id)).where(
Product.category_id == category_id, Product.deleted == 0
)
).scalar()
if count > 0:
raise AppException(code=ErrorCode.BUSINESS_RULE_FAILED, message="该分类下还有产品,无法删除")
session.delete(cat)
session.commit()
cache_delete_pattern("product:*")
cache_delete_pattern("category:*")
return success_payload({"deleted": True})
@router.delete("/api/products/{product_id}")
def delete_product(
product_id: int,
session: Session = Depends(get_db_session),
current_user: dict = Depends(require_roles("manager", "admin")),
) -> dict:
"""删除产品(软删除)"""
product = session.get(Product, product_id)
if product is None or product.deleted == 1:
raise AppException(code=ErrorCode.NOT_FOUND, message="产品不存在", status_code=404)
product.deleted = 1
session.commit()
cache_delete_pattern("product:*")
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),
current_user: dict = Depends(require_roles("manager", "admin")),
) -> dict:
"""永久删除产品(硬删除)"""
product = session.get(Product, product_id)
if product is None or product.deleted == 1:
raise AppException(code=ErrorCode.NOT_FOUND, message="产品不存在", status_code=404)
count = session.execute(
select(func.count(ProductPricingRule.id)).where(
ProductPricingRule.product_id == product_id
)
).scalar()
if count > 0:
raise AppException(code=ErrorCode.BUSINESS_RULE_FAILED, message="该产品还有定价规则,无法删除")
session.delete(product)
session.commit()
cache_delete_pattern("product:*")
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),
current_user: dict = Depends(require_roles("manager", "admin")),
) -> dict:
"""批量删除产品分类"""
cats = session.execute(
select(ProductCategory).where(ProductCategory.id.in_(ids), ProductCategory.deleted == 0)
).scalars().all()
if not cats:
raise AppException(code=ErrorCode.NOT_FOUND, message="未找到可删除的分类", status_code=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(code=ErrorCode.BUSINESS_RULE_FAILED, message="所选分类下还有产品,无法删除")
for c in cats:
session.delete(c)
else:
for c in cats:
c.deleted = 1
session.commit()
cache_delete_pattern("product:*")
cache_delete_pattern("category:*")
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),
current_user: dict = Depends(require_roles("manager", "admin")),
) -> dict:
"""批量删除产品"""
products = session.execute(
select(Product).where(Product.id.in_(ids), Product.deleted == 0)
).scalars().all()
if not products:
raise AppException(code=ErrorCode.NOT_FOUND, message="未找到可删除的产品", status_code=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(code=ErrorCode.BUSINESS_RULE_FAILED, message="所选产品还有定价规则,无法删除")
for p in products:
session.delete(p)
else:
for p in products:
p.deleted = 1
session.commit()
cache_delete_pattern("product:*")
return success_payload({"deleted": len(products)})