feat(耗材管理): 添加SN序列号管理功能
This commit is contained in:
@@ -52,6 +52,12 @@ const Consumable = sequelize.define('Consumable', {
|
||||
description: {
|
||||
type: DataTypes.TEXT
|
||||
},
|
||||
snList: {
|
||||
type: DataTypes.JSON,
|
||||
allowNull: true,
|
||||
defaultValue: [],
|
||||
comment: 'SN序列号列表,JSON数组格式'
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: 'active'
|
||||
|
||||
@@ -87,6 +87,12 @@ const ConsumableLog = sequelize.define('ConsumableLog', {
|
||||
type: DataTypes.JSON,
|
||||
allowNull: true,
|
||||
comment: '耗材快照信息(分类、单位、供应商等),用于耗材删除后追溯'
|
||||
},
|
||||
snList: {
|
||||
type: DataTypes.JSON,
|
||||
allowNull: true,
|
||||
defaultValue: [],
|
||||
comment: '本次操作的SN序列号列表'
|
||||
}
|
||||
}, {
|
||||
tableName: 'consumable_logs',
|
||||
|
||||
@@ -46,6 +46,12 @@ const ConsumableRecord = sequelize.define('ConsumableRecord', {
|
||||
},
|
||||
notes: {
|
||||
type: DataTypes.TEXT
|
||||
},
|
||||
snList: {
|
||||
type: DataTypes.JSON,
|
||||
allowNull: true,
|
||||
defaultValue: [],
|
||||
comment: '本次操作的SN序列号列表'
|
||||
}
|
||||
}, {
|
||||
tableName: 'consumable_records',
|
||||
|
||||
@@ -287,7 +287,7 @@ router.post('/quick-inout', async (req, res) => {
|
||||
while (attempt < MAX_RETRIES) {
|
||||
const transaction = await sequelize.transaction();
|
||||
try {
|
||||
const { consumableId, type, quantity, operator, reason, notes } = req.body;
|
||||
const { consumableId, type, quantity, operator, reason, notes, snList } = req.body;
|
||||
|
||||
const consumable = await Consumable.findByPk(consumableId, { transaction });
|
||||
if (!consumable) {
|
||||
@@ -297,15 +297,34 @@ router.post('/quick-inout', async (req, res) => {
|
||||
|
||||
const previousStock = parseFloat(consumable.currentStock);
|
||||
let newStock;
|
||||
let currentSnList = consumable.snList || [];
|
||||
let updatedSnList = [...currentSnList];
|
||||
let operationSnList = snList || [];
|
||||
|
||||
if (type === 'in') {
|
||||
newStock = previousStock + parseFloat(quantity);
|
||||
if (operationSnList.length > 0) {
|
||||
operationSnList.forEach(sn => {
|
||||
if (!updatedSnList.includes(sn)) {
|
||||
updatedSnList.push(sn);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (type === 'out') {
|
||||
newStock = previousStock - parseFloat(quantity);
|
||||
if (newStock < 0) {
|
||||
await transaction.rollback();
|
||||
return res.status(400).json({ error: '库存不足' });
|
||||
}
|
||||
if (operationSnList.length > 0) {
|
||||
for (const sn of operationSnList) {
|
||||
if (!updatedSnList.includes(sn)) {
|
||||
await transaction.rollback();
|
||||
return res.status(400).json({ error: `SN "${sn}" 不存在于当前耗材中` });
|
||||
}
|
||||
}
|
||||
updatedSnList = updatedSnList.filter(sn => !operationSnList.includes(sn));
|
||||
}
|
||||
} else {
|
||||
await transaction.rollback();
|
||||
return res.status(400).json({ error: '操作类型无效' });
|
||||
@@ -314,6 +333,7 @@ router.post('/quick-inout', async (req, res) => {
|
||||
const [affectedRows] = await Consumable.update(
|
||||
{
|
||||
currentStock: newStock,
|
||||
snList: updatedSnList,
|
||||
version: sequelize.literal('version + 1')
|
||||
},
|
||||
{
|
||||
@@ -342,10 +362,10 @@ router.post('/quick-inout', async (req, res) => {
|
||||
currentStock: newStock,
|
||||
operator,
|
||||
reason,
|
||||
notes
|
||||
notes,
|
||||
snList: operationSnList
|
||||
}, { transaction });
|
||||
|
||||
// 系统生成的出入库记录不可编辑
|
||||
await ConsumableLog.create({
|
||||
consumableId,
|
||||
consumableName: consumable.name,
|
||||
@@ -357,6 +377,7 @@ router.post('/quick-inout', async (req, res) => {
|
||||
reason,
|
||||
notes,
|
||||
isEditable: false,
|
||||
snList: operationSnList,
|
||||
consumableSnapshot: {
|
||||
category: consumable.category,
|
||||
unit: consumable.unit,
|
||||
@@ -391,7 +412,7 @@ router.post('/inout', async (req, res) => {
|
||||
while (attempt < MAX_RETRIES) {
|
||||
const transaction = await sequelize.transaction();
|
||||
try {
|
||||
const { consumableId, type, quantity, operator, reason, recipient, notes } = req.body;
|
||||
const { consumableId, type, quantity, operator, reason, recipient, notes, snList } = req.body;
|
||||
|
||||
const consumable = await Consumable.findByPk(consumableId, { transaction });
|
||||
if (!consumable) {
|
||||
@@ -401,20 +422,40 @@ router.post('/inout', async (req, res) => {
|
||||
|
||||
const previousStock = parseFloat(consumable.currentStock);
|
||||
let newStock;
|
||||
let currentSnList = consumable.snList || [];
|
||||
let updatedSnList = [...currentSnList];
|
||||
let operationSnList = snList || [];
|
||||
|
||||
if (type === 'in') {
|
||||
newStock = previousStock + parseFloat(quantity);
|
||||
if (operationSnList.length > 0) {
|
||||
operationSnList.forEach(sn => {
|
||||
if (!updatedSnList.includes(sn)) {
|
||||
updatedSnList.push(sn);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
newStock = previousStock - parseFloat(quantity);
|
||||
if (newStock < 0) {
|
||||
await transaction.rollback();
|
||||
return res.status(400).json({ error: '库存不足' });
|
||||
}
|
||||
if (operationSnList.length > 0) {
|
||||
for (const sn of operationSnList) {
|
||||
if (!updatedSnList.includes(sn)) {
|
||||
await transaction.rollback();
|
||||
return res.status(400).json({ error: `SN "${sn}" 不存在于当前耗材中` });
|
||||
}
|
||||
}
|
||||
updatedSnList = updatedSnList.filter(sn => !operationSnList.includes(sn));
|
||||
}
|
||||
}
|
||||
|
||||
const [affectedRows] = await Consumable.update(
|
||||
{
|
||||
currentStock: newStock,
|
||||
snList: updatedSnList,
|
||||
version: sequelize.literal('version + 1')
|
||||
},
|
||||
{
|
||||
@@ -444,10 +485,10 @@ router.post('/inout', async (req, res) => {
|
||||
operator,
|
||||
reason,
|
||||
recipient,
|
||||
notes
|
||||
notes,
|
||||
snList: operationSnList
|
||||
}, { transaction });
|
||||
|
||||
// 系统生成的出入库记录不可编辑
|
||||
await ConsumableLog.create({
|
||||
consumableId,
|
||||
consumableName: consumable.name,
|
||||
@@ -459,6 +500,7 @@ router.post('/inout', async (req, res) => {
|
||||
reason,
|
||||
notes,
|
||||
isEditable: false,
|
||||
snList: operationSnList,
|
||||
consumableSnapshot: {
|
||||
category: consumable.category,
|
||||
unit: consumable.unit,
|
||||
@@ -596,8 +638,13 @@ router.get('/logs', async (req, res) => {
|
||||
where.consumableId = consumableId;
|
||||
}
|
||||
|
||||
if (operationType && operationType !== 'all') {
|
||||
where.operationType = operationType;
|
||||
if (operationType) {
|
||||
const types = operationType.split(',').map(t => t.trim()).filter(t => t);
|
||||
if (types.length === 1) {
|
||||
where.operationType = types[0];
|
||||
} else if (types.length > 1) {
|
||||
where.operationType = { [Op.in]: types };
|
||||
}
|
||||
}
|
||||
|
||||
if (startDate && endDate) {
|
||||
|
||||
@@ -42,6 +42,11 @@ const migrations = [
|
||||
name: '耗材日志归档表',
|
||||
description: '创建 consumable_log_archives 归档表',
|
||||
migrate: migrateConsumableLogArchive
|
||||
},
|
||||
{
|
||||
name: '耗材SN序列号字段',
|
||||
description: '为 consumables、consumable_records、consumable_logs 添加 snList 字段',
|
||||
migrate: migrateSnList
|
||||
}
|
||||
];
|
||||
|
||||
@@ -366,6 +371,36 @@ async function migrateConsumableLogArchive() {
|
||||
await sequelize.query(`CREATE INDEX idx_archive_deleted_at ON consumable_log_archives(deletedAt)`);
|
||||
}
|
||||
|
||||
async function migrateSnList() {
|
||||
const dialect = sequelize.getDialect();
|
||||
|
||||
const tables = ['consumables', 'consumable_records', 'consumable_logs'];
|
||||
|
||||
for (const table of tables) {
|
||||
const tableInfo = await sequelize.query(
|
||||
dialect === 'sqlite'
|
||||
? `PRAGMA table_info(${table})`
|
||||
: `SHOW COLUMNS FROM ${table}`,
|
||||
{ type: sequelize.QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
const columns = dialect === 'sqlite'
|
||||
? tableInfo.map(col => col.name)
|
||||
: tableInfo.map(col => col.Field);
|
||||
|
||||
if (!columns.includes('snList')) {
|
||||
if (dialect === 'sqlite') {
|
||||
await sequelize.query(`ALTER TABLE ${table} ADD COLUMN snList TEXT DEFAULT '[]'`);
|
||||
} else {
|
||||
await sequelize.query(`ALTER TABLE ${table} ADD COLUMN snList JSON DEFAULT '[]'`);
|
||||
}
|
||||
console.log(` ${table} 表添加 snList 字段成功`);
|
||||
} else {
|
||||
console.log(` ${table} 表 snList 字段已存在,跳过`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 执行迁移
|
||||
runMigrations().catch(error => {
|
||||
console.error('迁移执行失败:', error);
|
||||
|
||||
Reference in New Issue
Block a user