Files
smart-project-pricing/后端服务/app/routers/materials.py
2026-01-31 21:33:06 +08:00

273 lines
8.8 KiB
Python

"""耗材管理路由
实现耗材的 CRUD 操作和批量导入
"""
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File
from sqlalchemy import select, func, or_
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.models.material import Material
from app.schemas.common import ResponseModel, PaginatedData, ErrorCode
from app.schemas.material import (
MaterialCreate,
MaterialUpdate,
MaterialResponse,
MaterialImportResult,
MaterialType,
)
router = APIRouter()
@router.get("", response_model=ResponseModel[PaginatedData[MaterialResponse]])
async def get_materials(
page: int = Query(1, ge=1, description="页码"),
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
keyword: Optional[str] = Query(None, description="关键词搜索"),
material_type: Optional[MaterialType] = Query(None, description="类型筛选"),
is_active: Optional[bool] = Query(None, description="是否启用筛选"),
db: AsyncSession = Depends(get_db),
):
"""获取耗材列表"""
query = select(Material)
if keyword:
query = query.where(
or_(
Material.material_code.contains(keyword),
Material.material_name.contains(keyword),
)
)
if material_type:
query = query.where(Material.material_type == material_type.value)
if is_active is not None:
query = query.where(Material.is_active == is_active)
query = query.order_by(Material.id.desc())
# 分页
offset = (page - 1) * page_size
query = query.offset(offset).limit(page_size)
result = await db.execute(query)
materials = result.scalars().all()
# 统计总数
count_query = select(func.count(Material.id))
if keyword:
count_query = count_query.where(
or_(
Material.material_code.contains(keyword),
Material.material_name.contains(keyword),
)
)
if material_type:
count_query = count_query.where(Material.material_type == material_type.value)
if is_active is not None:
count_query = count_query.where(Material.is_active == is_active)
total_result = await db.execute(count_query)
total = total_result.scalar() or 0
return ResponseModel(
data=PaginatedData(
items=[MaterialResponse.model_validate(m) for m in materials],
total=total,
page=page,
page_size=page_size,
total_pages=(total + page_size - 1) // page_size,
)
)
@router.get("/{material_id}", response_model=ResponseModel[MaterialResponse])
async def get_material(
material_id: int,
db: AsyncSession = Depends(get_db),
):
"""获取单个耗材详情"""
result = await db.execute(select(Material).where(Material.id == material_id))
material = result.scalar_one_or_none()
if not material:
raise HTTPException(
status_code=404,
detail={"code": ErrorCode.NOT_FOUND, "message": "耗材不存在"}
)
return ResponseModel(data=MaterialResponse.model_validate(material))
@router.post("", response_model=ResponseModel[MaterialResponse])
async def create_material(
data: MaterialCreate,
db: AsyncSession = Depends(get_db),
):
"""创建耗材"""
# 检查编码是否已存在
existing = await db.execute(
select(Material).where(Material.material_code == data.material_code)
)
if existing.scalar_one_or_none():
raise HTTPException(
status_code=400,
detail={"code": ErrorCode.ALREADY_EXISTS, "message": "耗材编码已存在"}
)
material = Material(**data.model_dump())
db.add(material)
await db.flush()
await db.refresh(material)
return ResponseModel(message="创建成功", data=MaterialResponse.model_validate(material))
@router.put("/{material_id}", response_model=ResponseModel[MaterialResponse])
async def update_material(
material_id: int,
data: MaterialUpdate,
db: AsyncSession = Depends(get_db),
):
"""更新耗材"""
result = await db.execute(select(Material).where(Material.id == material_id))
material = result.scalar_one_or_none()
if not material:
raise HTTPException(
status_code=404,
detail={"code": ErrorCode.NOT_FOUND, "message": "耗材不存在"}
)
# 检查编码是否重复
if data.material_code and data.material_code != material.material_code:
existing = await db.execute(
select(Material).where(Material.material_code == data.material_code)
)
if existing.scalar_one_or_none():
raise HTTPException(
status_code=400,
detail={"code": ErrorCode.ALREADY_EXISTS, "message": "耗材编码已存在"}
)
update_data = data.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(material, field, value)
await db.flush()
await db.refresh(material)
return ResponseModel(message="更新成功", data=MaterialResponse.model_validate(material))
@router.delete("/{material_id}", response_model=ResponseModel)
async def delete_material(
material_id: int,
db: AsyncSession = Depends(get_db),
):
"""删除耗材"""
result = await db.execute(select(Material).where(Material.id == material_id))
material = result.scalar_one_or_none()
if not material:
raise HTTPException(
status_code=404,
detail={"code": ErrorCode.NOT_FOUND, "message": "耗材不存在"}
)
await db.delete(material)
return ResponseModel(message="删除成功")
@router.post("/import", response_model=ResponseModel[MaterialImportResult])
async def import_materials(
file: UploadFile = File(..., description="Excel 文件"),
update_existing: bool = Query(False, description="是否更新已存在的数据"),
db: AsyncSession = Depends(get_db),
):
"""批量导入耗材
Excel 格式:耗材编码 | 耗材名称 | 单位 | 单价 | 供应商 | 类型
"""
import openpyxl
from io import BytesIO
if not file.filename.endswith(('.xlsx', '.xls')):
raise HTTPException(
status_code=400,
detail={"code": ErrorCode.PARAM_ERROR, "message": "请上传 Excel 文件"}
)
content = await file.read()
wb = openpyxl.load_workbook(BytesIO(content))
ws = wb.active
total = 0
success = 0
errors = []
for row_num, row in enumerate(ws.iter_rows(min_row=2, values_only=True), start=2):
if not row[0]: # 跳过空行
continue
total += 1
try:
material_code = str(row[0]).strip()
material_name = str(row[1]).strip() if row[1] else ""
unit = str(row[2]).strip() if row[2] else ""
unit_price = float(row[3]) if row[3] else 0
supplier = str(row[4]).strip() if row[4] else None
material_type = str(row[5]).strip() if row[5] else "consumable"
if not material_name or not unit:
errors.append({"row": row_num, "error": "名称或单位不能为空"})
continue
# 检查是否已存在
existing = await db.execute(
select(Material).where(Material.material_code == material_code)
)
existing_material = existing.scalar_one_or_none()
if existing_material:
if update_existing:
existing_material.material_name = material_name
existing_material.unit = unit
existing_material.unit_price = unit_price
existing_material.supplier = supplier
existing_material.material_type = material_type
success += 1
else:
errors.append({"row": row_num, "error": f"耗材编码 {material_code} 已存在"})
else:
material = Material(
material_code=material_code,
material_name=material_name,
unit=unit,
unit_price=unit_price,
supplier=supplier,
material_type=material_type,
)
db.add(material)
success += 1
except Exception as e:
errors.append({"row": row_num, "error": str(e)})
await db.flush()
return ResponseModel(
message="导入完成",
data=MaterialImportResult(
total=total,
success=success,
failed=len(errors),
errors=errors,
)
)