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