dingdanquanliucheng/backend/app/api/products.py

367 lines
16 KiB
Python
Raw Normal View History

"""
产品管理路由模块
职责
处理产品和产品分类的增删改查接口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
2026-05-14 13:51:06 +08:00
2026-06-15 13:18:43 +08:00
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
2026-05-14 13:51:06 +08:00
from backend.app.schemas.common import success_payload
from backend.app.schemas.products import (
CreateCategoryRequest,
CreateProductRequest,
UpdateCategoryRequest,
UpdateProductCategoryRequest,
2026-05-21 14:59:38 +08:00
UpdateProductRequest,
UpdateProductSpecificationRequest,
)
from backend.app.services.product_service import ProductService
2026-05-14 13:51:06 +08:00
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 参数筛选 + 分页参数
返回值分页分类列表包含 totalpage_nopage_sizelist
权限要求无特殊权限限制未配置角色鉴权
"""
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)
2026-05-14 13:51:06 +08:00
@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))
2026-05-14 13:51:06 +08:00
@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))
2026-05-14 13:51:06 +08:00
@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 参数筛选 + 分页参数
返回值分页产品列表包含 totalpage_nopage_sizelist
权限要求无特殊权限限制未配置角色鉴权
"""
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)
2026-05-14 13:51:06 +08:00
@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))
2026-05-14 13:51:06 +08:00
2026-05-21 14:59:38 +08:00
@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), # 注入数据库会话
2026-05-21 14:59:38 +08:00
) -> dict:
"""更新产品信息
用途修改已有产品的基本信息
请求参数product_id路径参数+ UpdateProductRequest更新字段
返回值更新后的产品信息
权限要求无特殊权限限制未配置角色鉴权
"""
2026-05-21 14:59:38 +08:00
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), # 注入数据库会话
2026-05-21 14:59:38 +08:00
) -> dict:
"""更新产品规格
用途修改指定产品的规格信息如尺寸材质等
请求参数product_id路径参数+ UpdateProductSpecificationRequest规格数据
返回值更新后的规格信息
权限要求无特殊权限限制未配置角色鉴权
"""
2026-05-21 14:59:38 +08:00
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))
2026-05-21 14:59:38 +08:00
@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), # 注入数据库会话
2026-05-21 14:59:38 +08:00
) -> dict:
"""设置默认规格
用途将指定产品的某个规格设为默认规格用于报价计算时的默认取值
请求参数product_id - 产品 ID路径参数
返回值设置结果
权限要求无特殊权限限制未配置角色鉴权
"""
2026-05-21 14:59:38 +08:00
return success_payload(product_service.set_default_specification(product_id, session))
2026-05-14 13:51:06 +08:00
@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),
2026-06-15 13:18:43 +08:00
current_user: dict = Depends(require_roles("manager", "admin")),
) -> dict:
"""删除产品分类(软删除)"""
cat = session.get(ProductCategory, category_id)
if cat is None or cat.deleted == 1:
2026-06-15 13:18:43 +08:00
raise AppException(code=ErrorCode.NOT_FOUND, message="产品分类不存在", status_code=404)
cat.deleted = 1
session.commit()
2026-06-15 13:18:43 +08:00
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),
2026-06-15 13:18:43 +08:00
current_user: dict = Depends(require_roles("manager", "admin")),
) -> dict:
"""永久删除产品分类(硬删除)"""
cat = session.get(ProductCategory, category_id)
if cat is None or cat.deleted == 1:
2026-06-15 13:18:43 +08:00
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:
2026-06-15 13:18:43 +08:00
raise AppException(code=ErrorCode.BUSINESS_RULE_FAILED, message="该分类下还有产品,无法删除")
session.delete(cat)
session.commit()
2026-06-15 13:18:43 +08:00
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),
2026-06-15 13:18:43 +08:00
current_user: dict = Depends(require_roles("manager", "admin")),
) -> dict:
"""删除产品(软删除)"""
product = session.get(Product, product_id)
if product is None or product.deleted == 1:
2026-06-15 13:18:43 +08:00
raise AppException(code=ErrorCode.NOT_FOUND, message="产品不存在", status_code=404)
product.deleted = 1
session.commit()
2026-06-15 13:18:43 +08:00
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),
2026-06-15 13:18:43 +08:00
current_user: dict = Depends(require_roles("manager", "admin")),
) -> dict:
"""永久删除产品(硬删除)"""
product = session.get(Product, product_id)
if product is None or product.deleted == 1:
2026-06-15 13:18:43 +08:00
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:
2026-06-15 13:18:43 +08:00
raise AppException(code=ErrorCode.BUSINESS_RULE_FAILED, message="该产品还有定价规则,无法删除")
session.delete(product)
session.commit()
2026-06-15 13:18:43 +08:00
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),
2026-06-15 13:18:43 +08:00
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:
2026-06-15 13:18:43 +08:00
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:
2026-06-15 13:18:43 +08:00
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()
2026-06-15 13:18:43 +08:00
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),
2026-06-15 13:18:43 +08:00
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:
2026-06-15 13:18:43 +08:00
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:
2026-06-15 13:18:43 +08:00
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()
2026-06-15 13:18:43 +08:00
cache_delete_pattern("product:*")
return success_payload({"deleted": len(products)})