"""耗材管理路由 实现耗材的 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, ) )