This commit is contained in:
2026-07-20 10:16:27 +08:00
parent 8493f0df03
commit f3d2951dc3
24 changed files with 23623 additions and 1546 deletions
+180
View File
@@ -0,0 +1,180 @@
"""库存查询业务服务层
将原 inventory_routes 中的业务编排(库存列表/建/改/删)下沉到此,
路由层只做参数校验与响应组装。
"""
from typing import Optional
from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from sqlalchemy.exc import IntegrityError
from shared.models.database import User, Product, Warehouse, Inventory
from ..schemas import InventoryResponse, InventoryCreate, InventoryUpdate, PaginatedResponse
def _build_inventory_response(inv: Inventory, product: Product, warehouse: Warehouse) -> InventoryResponse:
return InventoryResponse(
id=inv.id,
product_id=inv.product_id,
product_name=product.name,
product_sku=product.sku,
warehouse_id=inv.warehouse_id,
warehouse_name=warehouse.name,
quantity=inv.quantity,
locked_quantity=inv.locked_quantity,
available_quantity=inv.available_quantity
)
class InventoryService:
"""库存查询业务服务"""
@staticmethod
async def list_inventory(
db_session: AsyncSession,
warehouse_id: Optional[int],
product_id: Optional[int],
low_stock: bool,
skip: int,
limit: int,
) -> PaginatedResponse:
base_query = (
select(Inventory, Product, Warehouse)
.join(Product, Inventory.product_id == Product.id)
.join(Warehouse, Inventory.warehouse_id == Warehouse.id)
.where(Product.is_active == True)
.where(Product.item_type == "material")
.where(Warehouse.is_active == True)
)
if warehouse_id:
base_query = base_query.where(Inventory.warehouse_id == warehouse_id)
if product_id:
base_query = base_query.where(Inventory.product_id == product_id)
if low_stock:
base_query = base_query.where(Inventory.quantity <= Product.min_stock)
count_query = select(func.count()).select_from(base_query.subquery())
total = await db_session.scalar(count_query) or 0
query = base_query.offset(skip).limit(limit)
result = await db_session.execute(query)
inventory_list = [_build_inventory_response(inv, product, warehouse) for inv, product, warehouse in result.all()]
return PaginatedResponse(items=inventory_list, total=total, skip=skip, limit=limit)
@staticmethod
async def create_inventory(
db_session: AsyncSession,
payload: InventoryCreate,
current_user: User,
) -> InventoryResponse:
if payload.quantity < 0 or payload.locked_quantity < 0:
raise HTTPException(status_code=400, detail="库存数量不能为负数")
if payload.locked_quantity > payload.quantity:
raise HTTPException(status_code=400, detail="锁定数量不能大于库存数量")
product_result = await db_session.execute(
select(Product).where(Product.id == payload.product_id, Product.is_active == True)
)
product = product_result.scalar_one_or_none()
if not product:
raise HTTPException(status_code=404, detail="物料不存在")
if product.item_type != "material":
raise HTTPException(status_code=400, detail="库存仅支持物料")
warehouse_result = await db_session.execute(
select(Warehouse).where(Warehouse.id == payload.warehouse_id, Warehouse.is_active == True)
)
warehouse = warehouse_result.scalar_one_or_none()
if not warehouse:
raise HTTPException(status_code=404, detail="仓库不存在")
exists_result = await db_session.execute(
select(Inventory).where(
Inventory.product_id == payload.product_id,
Inventory.warehouse_id == payload.warehouse_id
)
)
if exists_result.scalar_one_or_none():
raise HTTPException(status_code=400, detail="该仓库已存在该物料库存记录")
inventory = Inventory(
product_id=payload.product_id,
warehouse_id=payload.warehouse_id,
quantity=payload.quantity,
locked_quantity=payload.locked_quantity,
batch_number=payload.batch_number,
location=payload.location
)
db_session.add(inventory)
try:
await db_session.commit()
except IntegrityError:
# 并发创建命中 (product_id, warehouse_id) 唯一约束
await db_session.rollback()
raise HTTPException(status_code=400, detail="该仓库已存在该物料库存记录")
await db_session.refresh(inventory)
return _build_inventory_response(inventory, product, warehouse)
@staticmethod
async def update_inventory(
db_session: AsyncSession,
inventory_id: int,
payload: InventoryUpdate,
current_user: User,
) -> InventoryResponse:
result = await db_session.execute(
select(Inventory, Product, Warehouse)
.join(Product, Inventory.product_id == Product.id)
.join(Warehouse, Inventory.warehouse_id == Warehouse.id)
.where(Inventory.id == inventory_id)
.where(Product.item_type == "material")
.with_for_update(of=Inventory)
)
row = result.first()
if not row:
raise HTTPException(status_code=404, detail="库存记录不存在")
inventory, product, warehouse = row
if payload.quantity is not None:
if payload.quantity < 0:
raise HTTPException(status_code=400, detail="库存数量不能为负数")
inventory.quantity = payload.quantity
if payload.locked_quantity is not None:
if payload.locked_quantity < 0:
raise HTTPException(status_code=400, detail="锁定数量不能为负数")
inventory.locked_quantity = payload.locked_quantity
if inventory.locked_quantity > inventory.quantity:
raise HTTPException(status_code=400, detail="锁定数量不能大于库存数量")
if payload.batch_number is not None:
inventory.batch_number = payload.batch_number
if payload.location is not None:
inventory.location = payload.location
await db_session.commit()
await db_session.refresh(inventory)
return _build_inventory_response(inventory, product, warehouse)
@staticmethod
async def delete_inventory(
db_session: AsyncSession,
inventory_id: int,
current_user: User,
) -> dict:
result = await db_session.execute(select(Inventory).where(Inventory.id == inventory_id))
inventory = result.scalar_one_or_none()
if not inventory:
raise HTTPException(status_code=404, detail="库存记录不存在")
if inventory.quantity > 0:
raise HTTPException(status_code=400, detail="库存数量不为零,无法删除库存记录")
await db_session.delete(inventory)
await db_session.commit()
return {"message": "库存记录已删除"}
inventory_service = InventoryService()