367 lines
16 KiB
Python
367 lines
16 KiB
Python
"""
|
||
产品管理路由模块
|
||
|
||
职责:
|
||
处理产品和产品分类的增删改查接口,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)})
|