You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

539 lines
14 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* 库存数据模型
*
* 功能:封装库存表和库存日志表的数据库操作
*/
const { pool } = require('../config/database');
/**
* 库存模型类
*/
class Inventory {
/**
* 根据ID查找库存项
* @param {number} id - 库存ID
* @returns {Promise<object|null>} 库存对象或null
*/
static async findById(id) {
try {
const [rows] = await pool.query(
'SELECT * FROM inventory WHERE id = ?',
[id]
);
return rows[0] || null;
} catch (error) {
throw new Error('查询库存失败: ' + error.message);
}
}
/**
* 根据原材料名称查找库存项
* @param {string} name - 原材料名称
* @returns {Promise<object|null>} 库存对象或null
*/
static async findByName(name) {
try {
const [rows] = await pool.query(
'SELECT * FROM inventory WHERE name = ?',
[name]
);
return rows[0] || null;
} catch (error) {
throw new Error('查询库存失败: ' + error.message);
}
}
/**
* 获取库存列表(分页、筛选)
* @param {object} options - 查询选项
* @returns {Promise<object>} 库存列表和总数
*/
static async findAll(options = {}) {
try {
const {
page = 1,
pageSize = 10,
category,
search,
low_stock_only = false
} = options;
const offset = (page - 1) * pageSize;
// 构建WHERE条件
const conditions = [];
const params = [];
if (category) {
conditions.push('category = ?');
params.push(category);
}
if (search) {
conditions.push('(name LIKE ? OR supplier LIKE ?)');
params.push(`%${search}%`, `%${search}%`);
}
// 低库存筛选
if (low_stock_only) {
conditions.push('quantity <= alert_quantity');
}
const whereClause = conditions.length > 0
? 'WHERE ' + conditions.join(' AND ')
: '';
// 查询总数
const [countResult] = await pool.query(
`SELECT COUNT(*) as total FROM inventory ${whereClause}`,
params
);
const total = countResult[0].total;
// 查询列表
const [rows] = await pool.query(
`SELECT * FROM inventory ${whereClause}
ORDER BY created_at DESC
LIMIT ? OFFSET ?`,
[...params, pageSize, offset]
);
return {
list: rows,
total,
page: parseInt(page),
pageSize: parseInt(pageSize)
};
} catch (error) {
throw new Error('查询库存列表失败: ' + error.message);
}
}
/**
* 创建新库存项
* @param {object} inventoryData - 库存数据
* @returns {Promise<object>} 创建的库存项
*/
static async create(inventoryData) {
try {
const {
name,
category,
quantity,
unit,
purchase_price,
supplier,
expiry_date,
alert_quantity
} = inventoryData;
const [result] = await pool.query(
`INSERT INTO inventory
(name, category, quantity, unit, purchase_price, supplier, expiry_date, alert_quantity)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[name, category, quantity, unit, purchase_price, supplier, expiry_date || null, alert_quantity]
);
// 记录库存日志
await this.addLog({
inventory_id: result.insertId,
change_type: 'in',
quantity_change: quantity,
quantity_after: quantity,
operator_id: inventoryData.operator_id,
remark: '初始入库'
});
return await this.findById(result.insertId);
} catch (error) {
throw new Error('创建库存失败: ' + error.message);
}
}
/**
* 更新库存项
* @param {number} id - 库存ID
* @param {object} inventoryData - 要更新的数据
* @returns {Promise<object>} 更新后的库存项
*/
static async update(id, inventoryData) {
try {
const {
name,
category,
unit,
purchase_price,
supplier,
expiry_date,
alert_quantity
} = inventoryData;
const updates = [];
const values = [];
if (name !== undefined) {
updates.push('name = ?');
values.push(name);
}
if (category !== undefined) {
updates.push('category = ?');
values.push(category);
}
if (unit !== undefined) {
updates.push('unit = ?');
values.push(unit);
}
if (purchase_price !== undefined) {
updates.push('purchase_price = ?');
values.push(purchase_price);
}
if (supplier !== undefined) {
updates.push('supplier = ?');
values.push(supplier);
}
if (expiry_date !== undefined) {
updates.push('expiry_date = ?');
values.push(expiry_date);
}
if (alert_quantity !== undefined) {
updates.push('alert_quantity = ?');
values.push(alert_quantity);
}
if (updates.length === 0) {
throw new Error('没有要更新的数据');
}
values.push(id);
await pool.query(
`UPDATE inventory SET ${updates.join(', ')} WHERE id = ?`,
values
);
return await this.findById(id);
} catch (error) {
throw new Error('更新库存失败: ' + error.message);
}
}
/**
* 删除库存项
* @param {number} id - 库存ID
* @returns {Promise<boolean>} 是否成功
*/
static async delete(id) {
try {
await pool.query('DELETE FROM inventory WHERE id = ?', [id]);
return true;
} catch (error) {
throw new Error('删除库存失败: ' + error.message);
}
}
/**
* 调整库存数量(入库/出库)
* @param {number} id - 库存ID
* @param {string} changeType - 变更类型 (in/out/adjust)
* @param {number} quantityChange - 数量变化(正数表示增加,负数表示减少)
* @param {number} operatorId - 操作人ID
* @param {string} remark - 备注
* @returns {Promise<object>} 更新后的库存项
*/
static async adjustQuantity(id, changeType, quantityChange, operatorId, remark = null) {
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
// 获取当前库存
const [rows] = await connection.query(
'SELECT * FROM inventory WHERE id = ?',
[id]
);
if (rows.length === 0) {
throw new Error('库存项不存在');
}
const currentItem = rows[0];
let newQuantity;
// 计算新数量
if (changeType === 'in') {
newQuantity = parseFloat(currentItem.quantity) + parseFloat(quantityChange);
} else if (changeType === 'out') {
newQuantity = parseFloat(currentItem.quantity) - parseFloat(quantityChange);
if (newQuantity < 0) {
throw new Error('库存不足,无法出库');
}
} else if (changeType === 'adjust') {
// 直接调整到目标数量
newQuantity = parseFloat(quantityChange);
} else {
throw new Error('无效的变更类型');
}
// 更新库存数量
await connection.query(
'UPDATE inventory SET quantity = ?, updated_at = NOW() WHERE id = ?',
[newQuantity, id]
);
// 记录库存日志
const actualChange = changeType === 'adjust'
? (newQuantity - parseFloat(currentItem.quantity))
: (changeType === 'in' ? quantityChange : -quantityChange);
await connection.query(
`INSERT INTO inventory_logs
(inventory_id, change_type, quantity_change, quantity_after, operator_id, remark)
VALUES (?, ?, ?, ?, ?, ?)`,
[id, changeType, actualChange, newQuantity, operatorId, remark]
);
await connection.commit();
return await this.findById(id);
} catch (error) {
await connection.rollback();
throw new Error('调整库存失败: ' + error.message);
} finally {
connection.release();
}
}
/**
* 获取低库存预警列表
* @returns {Promise<array>} 低库存项列表
*/
static async getLowStockItems() {
try {
const [rows] = await pool.query(
`SELECT * FROM inventory
WHERE quantity <= alert_quantity
ORDER BY (quantity / alert_quantity) ASC`
);
return rows;
} catch (error) {
throw new Error('查询低库存项失败: ' + error.message);
}
}
/**
* 获取即将过期的库存列表
* @param {number} days - 预警天数默认7天
* @returns {Promise<array>} 即将过期的库存列表
*/
static async getExpiringItems(days = 7) {
try {
const [rows] = await pool.query(
`SELECT * FROM inventory
WHERE expiry_date IS NOT NULL
AND expiry_date <= DATE_ADD(CURDATE(), INTERVAL ? DAY)
AND expiry_date >= CURDATE()
ORDER BY expiry_date ASC`,
[days]
);
return rows;
} catch (error) {
throw new Error('查询即将过期库存失败: ' + error.message);
}
}
/**
* 获取已过期的库存列表
* @returns {Promise<array>} 已过期的库存列表
*/
static async getExpiredItems() {
try {
const [rows] = await pool.query(
`SELECT * FROM inventory
WHERE expiry_date IS NOT NULL
AND expiry_date < CURDATE()
ORDER BY expiry_date ASC`
);
return rows;
} catch (error) {
throw new Error('查询已过期库存失败: ' + error.message);
}
}
/**
* 获取库存日志
* @param {number} inventoryId - 库存ID可选
* @param {object} options - 查询选项
* @returns {Promise<object>} 日志列表和总数
*/
static async getLogs(inventoryId = null, options = {}) {
try {
const {
page = 1,
pageSize = 20,
change_type
} = options;
const offset = (page - 1) * pageSize;
// 构建WHERE条件
const conditions = [];
const params = [];
if (inventoryId) {
conditions.push('il.inventory_id = ?');
params.push(inventoryId);
}
if (change_type) {
conditions.push('il.change_type = ?');
params.push(change_type);
}
const whereClause = conditions.length > 0
? 'WHERE ' + conditions.join(' AND ')
: '';
// 查询总数
const [countResult] = await pool.query(
`SELECT COUNT(*) as total FROM inventory_logs il ${whereClause}`,
params
);
const total = countResult[0].total;
// 查询日志列表
const [rows] = await pool.query(
`SELECT
il.*,
i.name as inventory_name,
i.unit,
u.username as operator_name
FROM inventory_logs il
LEFT JOIN inventory i ON il.inventory_id = i.id
LEFT JOIN users u ON il.operator_id = u.id
${whereClause}
ORDER BY il.created_at DESC
LIMIT ? OFFSET ?`,
[...params, pageSize, offset]
);
return {
list: rows,
total,
page: parseInt(page),
pageSize: parseInt(pageSize)
};
} catch (error) {
throw new Error('查询库存日志失败: ' + error.message);
}
}
/**
* 添加库存日志
* @param {object} logData - 日志数据
* @returns {Promise<number>} 插入的日志ID
*/
static async addLog(logData) {
try {
const {
inventory_id,
change_type,
quantity_change,
quantity_after,
operator_id,
remark
} = logData;
const [result] = await pool.query(
`INSERT INTO inventory_logs
(inventory_id, change_type, quantity_change, quantity_after, operator_id, remark)
VALUES (?, ?, ?, ?, ?, ?)`,
[inventory_id, change_type, quantity_change, quantity_after, operator_id, remark || null]
);
return result.insertId;
} catch (error) {
throw new Error('添加库存日志失败: ' + error.message);
}
}
/**
* 检查名称是否已存在
* @param {string} name - 原材料名称
* @param {number} excludeId - 排除的ID用于更新时
* @returns {Promise<boolean>} 是否存在
*/
static async isNameExists(name, excludeId = null) {
try {
const query = excludeId
? 'SELECT id FROM inventory WHERE name = ? AND id != ?'
: 'SELECT id FROM inventory WHERE name = ?';
const params = excludeId ? [name, excludeId] : [name];
const [rows] = await pool.query(query, params);
return rows.length > 0;
} catch (error) {
throw new Error('检查名称失败: ' + error.message);
}
}
/**
* 获取库存分类列表
* @returns {Promise<array>} 分类列表
*/
static async getCategories() {
try {
const [rows] = await pool.query(
'SELECT DISTINCT category FROM inventory ORDER BY category'
);
return rows.map(row => row.category);
} catch (error) {
throw new Error('查询分类列表失败: ' + error.message);
}
}
/**
* 获取库存统计信息
* @returns {Promise<object>} 统计信息
*/
static async getStatistics() {
try {
const [totalResult] = await pool.query(
'SELECT COUNT(*) as total_items, SUM(quantity * purchase_price) as total_value FROM inventory'
);
const [lowStockResult] = await pool.query(
'SELECT COUNT(*) as low_stock_count FROM inventory WHERE quantity <= alert_quantity'
);
const [expiringResult] = await pool.query(
`SELECT COUNT(*) as expiring_count FROM inventory
WHERE expiry_date IS NOT NULL
AND expiry_date <= DATE_ADD(CURDATE(), INTERVAL 7 DAY)
AND expiry_date >= CURDATE()`
);
const [expiredResult] = await pool.query(
`SELECT COUNT(*) as expired_count FROM inventory
WHERE expiry_date IS NOT NULL AND expiry_date < CURDATE()`
);
return {
total_items: totalResult[0].total_items || 0,
total_value: parseFloat(totalResult[0].total_value || 0),
low_stock_count: lowStockResult[0].low_stock_count || 0,
expiring_count: expiringResult[0].expiring_count || 0,
expired_count: expiredResult[0].expired_count || 0
};
} catch (error) {
throw new Error('查询库存统计失败: ' + error.message);
}
}
}
module.exports = Inventory;