/** * 库存控制器 * * 功能:处理库存管理的业务逻辑 */ const Inventory = require('../models/Inventory'); const { validateRequired, validateBatch } = require('../utils/validator'); const { success } = require('../utils/response'); const { BusinessError, ValidationError } = require('../middleware/errorHandler'); /** * 获取库存列表(分页、筛选) * GET /api/inventory */ const getInventory = async (req, res, next) => { try { const { page = 1, pageSize = 10, item_name, search, low_stock_only } = req.query; const result = await Inventory.findAll({ page: parseInt(page), pageSize: parseInt(pageSize), search: item_name || search, low_stock_only: low_stock_only === 'true' || low_stock_only === '1' }); // Map database fields to frontend fields const mappedList = result.list.map(item => ({ ...item, item_name: item.name })); res.json(success({ list: mappedList, pagination: { total: result.total, page: result.page, pageSize: result.pageSize, totalPages: Math.ceil(result.total / result.pageSize) } }, '获取库存列表成功')); } catch (err) { next(err); } }; /** * 获取单个库存项详情 * GET /api/inventory/:id */ const getInventoryById = async (req, res, next) => { try { const { id } = req.params; const item = await Inventory.findById(id); if (!item) { throw new BusinessError('库存项不存在', 404); } // Map database fields to frontend fields const mappedItem = { ...item, item_name: item.name }; res.json(success(mappedItem, '获取库存详情成功')); } catch (err) { next(err); } }; /** * 创建库存项 * POST /api/inventory * 需要管理员权限 */ const createInventory = async (req, res, next) => { try { const { item_name, name, quantity, unit, unit_price, expiry_date, min_quantity } = req.body; // Use item_name if provided, otherwise use name const itemName = item_name || name; // 数据验证 const validation = validateBatch([ validateRequired(itemName, '商品名称'), validateRequired(quantity, '数量'), validateRequired(unit, '单位'), validateRequired(unit_price, '单价'), validateRequired(min_quantity, '最低库存') ]); if (!validation.valid) { throw new ValidationError('数据验证失败', validation.errors); } // 验证数量和价格 if (parseFloat(quantity) < 0) { throw new ValidationError('数量不能为负数'); } if (parseFloat(unit_price) <= 0) { throw new ValidationError('单价必须大于0'); } if (parseFloat(min_quantity) < 0) { throw new ValidationError('最低库存不能为负数'); } // 检查名称是否已存在 const exists = await Inventory.isNameExists(itemName); if (exists) { throw new ValidationError('该商品名称已存在'); } const item = await Inventory.create({ name: itemName, quantity: parseFloat(quantity), unit, unit_price: parseFloat(unit_price), expiry_date: expiry_date || null, min_quantity: parseFloat(min_quantity), operator_id: req.user.id }); // Map response fields const mappedItem = { ...item, item_name: item.name }; res.status(201).json(success(mappedItem, '创建库存项成功')); } catch (err) { next(err); } }; /** * 更新库存项 * PUT /api/inventory/:id * 需要管理员权限 */ const updateInventory = async (req, res, next) => { try { const { id } = req.params; const { item_name, name, unit, unit_price, expiry_date, min_quantity, quantity } = req.body; // Use item_name if provided, otherwise use name const itemName = item_name || name; // 检查库存项是否存在 const item = await Inventory.findById(id); if (!item) { throw new BusinessError('库存项不存在', 404); } // 验证数据 if (unit_price !== undefined && parseFloat(unit_price) <= 0) { throw new ValidationError('单价必须大于0'); } if (min_quantity !== undefined && parseFloat(min_quantity) < 0) { throw new ValidationError('最低库存不能为负数'); } if (quantity !== undefined && parseFloat(quantity) < 0) { throw new ValidationError('库存数量不能为负数'); } // 检查名称是否已存在 if (itemName && itemName !== item.name) { const exists = await Inventory.isNameExists(itemName, id); if (exists) { throw new ValidationError('该商品名称已存在'); } } const updatedItem = await Inventory.update(id, { name: itemName, unit, unit_price: unit_price !== undefined ? parseFloat(unit_price) : undefined, expiry_date: expiry_date !== undefined ? expiry_date : undefined, min_quantity: min_quantity !== undefined ? parseFloat(min_quantity) : undefined, quantity: quantity !== undefined ? parseFloat(quantity) : undefined }); // Map response fields const mappedItem = { ...updatedItem, item_name: updatedItem.name }; res.json(success(mappedItem, '更新库存项成功')); } catch (err) { next(err); } }; /** * 删除库存项 * DELETE /api/inventory/:id * 需要管理员权限 */ const deleteInventory = async (req, res, next) => { try { const { id } = req.params; // 检查库存项是否存在 const item = await Inventory.findById(id); if (!item) { throw new BusinessError('库存项不存在', 404); } await Inventory.delete(id); res.json(success(null, '删除库存项成功')); } catch (err) { next(err); } }; /** * 调整库存数量(入库/出库/调整) * POST /api/inventory/:id/adjust * 需要管理员或服务员权限 */ const adjustInventory = async (req, res, next) => { try { const { id } = req.params; const { change_type, quantity, remark } = req.body; // 数据验证 const validation = validateBatch([ validateRequired(change_type, '变更类型'), validateRequired(quantity, '数量') ]); if (!validation.valid) { throw new ValidationError('数据验证失败', validation.errors); } // 验证变更类型 const validTypes = ['in', 'out', 'adjust']; if (!validTypes.includes(change_type)) { throw new ValidationError('变更类型必须是 in、out 或 adjust'); } // 验证数量 if (parseFloat(quantity) <= 0) { throw new ValidationError('数量必须大于0'); } // 检查库存项是否存在 const item = await Inventory.findById(id); if (!item) { throw new BusinessError('库存项不存在', 404); } const updatedItem = await Inventory.adjustQuantity( id, change_type, parseFloat(quantity), req.user.id, remark || null ); const messages = { in: '入库成功', out: '出库成功', adjust: '库存调整成功' }; res.json(success(updatedItem, messages[change_type])); } catch (err) { next(err); } }; /** * 库存补货 * POST /api/inventory/:id/restock * 需要管理员或服务员权限 */ const restockInventory = async (req, res, next) => { try { const { id } = req.params; const { quantity } = req.body; // 数据验证 const validation = validateBatch([ validateRequired(quantity, '补货数量') ]); if (!validation.valid) { throw new ValidationError('数据验证失败', validation.errors); } // 验证数量 if (parseFloat(quantity) <= 0) { throw new ValidationError('补货数量必须大于0'); } // 检查库存项是否存在 const item = await Inventory.findById(id); if (!item) { throw new BusinessError('库存项不存在', 404); } const updatedItem = await Inventory.adjustQuantity( id, 'in', parseFloat(quantity), req.user.id, '库存补货' ); // Map response fields const mappedItem = { ...updatedItem, item_name: updatedItem.name }; res.json(success(mappedItem, '补货成功')); } catch (err) { next(err); } }; /** * 获取低库存预警列表 * GET /api/inventory/alerts/low-stock */ const getLowStockAlerts = async (req, res, next) => { try { const items = await Inventory.getLowStockItems(); res.json(success({ count: items.length, items }, '获取低库存预警成功')); } catch (err) { next(err); } }; /** * 获取即将过期的库存列表 * GET /api/inventory/alerts/expiring */ const getExpiringAlerts = async (req, res, next) => { try { const { days = 7 } = req.query; const items = await Inventory.getExpiringItems(parseInt(days)); res.json(success({ count: items.length, days: parseInt(days), items }, '获取即将过期库存成功')); } catch (err) { next(err); } }; /** * 获取已过期的库存列表 * GET /api/inventory/alerts/expired */ const getExpiredAlerts = async (req, res, next) => { try { const items = await Inventory.getExpiredItems(); res.json(success({ count: items.length, items }, '获取已过期库存成功')); } catch (err) { next(err); } }; /** * 获取库存日志 * GET /api/inventory/logs 或 GET /api/inventory/:id/logs */ const getInventoryLogs = async (req, res, next) => { try { const { id } = req.params; const { page = 1, pageSize = 20, change_type } = req.query; const result = await Inventory.getLogs(id || null, { page: parseInt(page), pageSize: parseInt(pageSize), change_type }); res.json(success({ list: result.list, pagination: { total: result.total, page: result.page, pageSize: result.pageSize, totalPages: Math.ceil(result.total / result.pageSize) } }, '获取库存日志成功')); } catch (err) { next(err); } }; /** * 获取库存统计信息 * GET /api/inventory/statistics */ const getStatistics = async (req, res, next) => { try { const stats = await Inventory.getStatistics(); res.json(success(stats, '获取库存统计成功')); } catch (err) { next(err); } }; // 导出所有控制器函数 module.exports = { getInventory, getInventoryById, createInventory, updateInventory, deleteInventory, adjustInventory, restockInventory, getLowStockAlerts, getExpiringAlerts, getExpiredAlerts, getInventoryLogs, getStatistics };