feat: 添加耗材操作日志修改记录功能; feat: 最大库存支持无限制选项; fix: 修复Ant Design Card组件bodyStyle废弃警告和字体预加载警告
This commit is contained in:
@@ -33,8 +33,9 @@ const Consumable = sequelize.define('Consumable', {
|
||||
},
|
||||
maxStock: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 100
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
comment: '最大库存,null表示无限制'
|
||||
},
|
||||
unitPrice: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
|
||||
@@ -53,6 +53,31 @@ const ConsumableLog = sequelize.define('ConsumableLog', {
|
||||
relatedId: {
|
||||
type: DataTypes.STRING,
|
||||
comment: '关联ID(如订单号、盘点ID等)'
|
||||
},
|
||||
isEditable: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true,
|
||||
comment: '是否可编辑(创建、导入的记录可编辑,系统生成的出入库记录不可编辑)'
|
||||
},
|
||||
originalLogId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '原始日志ID(用于追踪修改历史链)'
|
||||
},
|
||||
modifiedBy: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: '修改人'
|
||||
},
|
||||
modifiedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '修改时间'
|
||||
},
|
||||
modificationReason: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: '修改原因'
|
||||
}
|
||||
}, {
|
||||
tableName: 'consumable_logs',
|
||||
@@ -62,7 +87,9 @@ const ConsumableLog = sequelize.define('ConsumableLog', {
|
||||
{ fields: ['consumableId'] },
|
||||
{ fields: ['operationType'] },
|
||||
{ fields: ['createdAt'] },
|
||||
{ fields: ['consumableId', 'createdAt'] }
|
||||
{ fields: ['consumableId', 'createdAt'] },
|
||||
{ fields: ['originalLogId'] },
|
||||
{ fields: ['isEditable'] }
|
||||
]
|
||||
});
|
||||
|
||||
|
||||
+105
-24
@@ -252,21 +252,21 @@ router.get('/inout/records', async (req, res) => {
|
||||
router.post('/quick-inout', async (req, res) => {
|
||||
const MAX_RETRIES = 3;
|
||||
let attempt = 0;
|
||||
|
||||
|
||||
while (attempt < MAX_RETRIES) {
|
||||
const transaction = await sequelize.transaction();
|
||||
try {
|
||||
const { consumableId, type, quantity, operator, reason, notes } = req.body;
|
||||
|
||||
|
||||
const consumable = await Consumable.findByPk(consumableId, { transaction });
|
||||
if (!consumable) {
|
||||
await transaction.rollback();
|
||||
return res.status(404).json({ error: '耗材不存在' });
|
||||
}
|
||||
|
||||
|
||||
const previousStock = parseFloat(consumable.currentStock);
|
||||
let newStock;
|
||||
|
||||
|
||||
if (type === 'in') {
|
||||
newStock = previousStock + parseFloat(quantity);
|
||||
} else if (type === 'out') {
|
||||
@@ -279,21 +279,21 @@ router.post('/quick-inout', async (req, res) => {
|
||||
await transaction.rollback();
|
||||
return res.status(400).json({ error: '操作类型无效' });
|
||||
}
|
||||
|
||||
|
||||
const [affectedRows] = await Consumable.update(
|
||||
{
|
||||
{
|
||||
currentStock: newStock,
|
||||
version: sequelize.literal('version + 1')
|
||||
},
|
||||
{
|
||||
where: {
|
||||
{
|
||||
where: {
|
||||
consumableId,
|
||||
version: consumable.version
|
||||
},
|
||||
transaction
|
||||
transaction
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
if (affectedRows === 0) {
|
||||
await transaction.rollback();
|
||||
attempt++;
|
||||
@@ -302,7 +302,7 @@ router.post('/quick-inout', async (req, res) => {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
const record = await ConsumableRecord.create({
|
||||
consumableId,
|
||||
type,
|
||||
@@ -313,7 +313,8 @@ router.post('/quick-inout', async (req, res) => {
|
||||
reason,
|
||||
notes
|
||||
}, { transaction });
|
||||
|
||||
|
||||
// 系统生成的出入库记录不可编辑
|
||||
await ConsumableLog.create({
|
||||
consumableId,
|
||||
consumableName: consumable.name,
|
||||
@@ -323,11 +324,12 @@ router.post('/quick-inout', async (req, res) => {
|
||||
currentStock: newStock,
|
||||
operator,
|
||||
reason,
|
||||
notes
|
||||
notes,
|
||||
isEditable: false
|
||||
}, { transaction });
|
||||
|
||||
|
||||
await transaction.commit();
|
||||
|
||||
|
||||
res.json({
|
||||
message: '操作成功',
|
||||
record,
|
||||
@@ -407,6 +409,7 @@ router.post('/inout', async (req, res) => {
|
||||
notes
|
||||
}, { transaction });
|
||||
|
||||
// 系统生成的出入库记录不可编辑
|
||||
await ConsumableLog.create({
|
||||
consumableId,
|
||||
consumableName: consumable.name,
|
||||
@@ -416,11 +419,12 @@ router.post('/inout', async (req, res) => {
|
||||
currentStock: newStock,
|
||||
operator,
|
||||
reason,
|
||||
notes
|
||||
notes,
|
||||
isEditable: false
|
||||
}, { transaction });
|
||||
|
||||
|
||||
await transaction.commit();
|
||||
|
||||
|
||||
res.json({
|
||||
message: '操作成功',
|
||||
record,
|
||||
@@ -499,6 +503,7 @@ router.post('/adjust', async (req, res) => {
|
||||
|
||||
const changeQuantity = newStock - previousStock;
|
||||
|
||||
// 系统生成的调整记录不可编辑
|
||||
await ConsumableLog.create({
|
||||
consumableId,
|
||||
consumableName: consumable.name,
|
||||
@@ -508,7 +513,8 @@ router.post('/adjust', async (req, res) => {
|
||||
currentStock: newStock,
|
||||
operator,
|
||||
reason: reason || (adjustType === 'set' ? '库存调整为 ' + newStock : reason),
|
||||
notes: adjustType === 'set' ? `库存从 ${previousStock} 调整为 ${newStock}` : notes
|
||||
notes: adjustType === 'set' ? `库存从 ${previousStock} 调整为 ${newStock}` : notes,
|
||||
isEditable: false
|
||||
}, { transaction });
|
||||
|
||||
await transaction.commit();
|
||||
@@ -755,23 +761,23 @@ router.delete('/:id', async (req, res) => {
|
||||
await transaction.rollback();
|
||||
return res.status(404).json({ error: '耗材不存在' });
|
||||
}
|
||||
|
||||
|
||||
const consumableId = consumable.consumableId;
|
||||
const consumableName = consumable.name;
|
||||
const currentStock = consumable.currentStock;
|
||||
|
||||
|
||||
await ConsumableRecord.destroy({
|
||||
where: { consumableId },
|
||||
transaction
|
||||
});
|
||||
|
||||
|
||||
await ConsumableLog.destroy({
|
||||
where: { consumableId },
|
||||
transaction
|
||||
});
|
||||
|
||||
|
||||
await consumable.destroy({ transaction });
|
||||
|
||||
|
||||
await transaction.commit();
|
||||
res.json({ message: '删除成功' });
|
||||
} catch (error) {
|
||||
@@ -780,4 +786,79 @@ router.delete('/:id', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 修改日志记录
|
||||
router.put('/logs/:id', async (req, res) => {
|
||||
const transaction = await sequelize.transaction();
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { reason, notes, operator, modificationReason } = req.body;
|
||||
|
||||
const log = await ConsumableLog.findByPk(id, { transaction });
|
||||
if (!log) {
|
||||
await transaction.rollback();
|
||||
return res.status(404).json({ error: '日志记录不存在' });
|
||||
}
|
||||
|
||||
// 检查是否可编辑
|
||||
if (!log.isEditable) {
|
||||
await transaction.rollback();
|
||||
return res.status(403).json({ error: '该记录为系统自动生成,不可修改' });
|
||||
}
|
||||
|
||||
// 保存原始日志ID(用于追踪修改历史)
|
||||
const originalLogId = log.originalLogId || log.id;
|
||||
|
||||
// 更新当前记录,并标记为已修改
|
||||
await log.update({
|
||||
reason: reason !== undefined ? reason : log.reason,
|
||||
notes: notes !== undefined ? notes : log.notes,
|
||||
modifiedBy: operator || '系统',
|
||||
modifiedAt: new Date(),
|
||||
modificationReason: modificationReason || '用户修改'
|
||||
}, { transaction });
|
||||
|
||||
await transaction.commit();
|
||||
|
||||
res.json({
|
||||
message: '日志修改成功',
|
||||
log: await ConsumableLog.findByPk(id)
|
||||
});
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取日志修改历史
|
||||
router.get('/logs/:id/history', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const log = await ConsumableLog.findByPk(id);
|
||||
if (!log) {
|
||||
return res.status(404).json({ error: '日志记录不存在' });
|
||||
}
|
||||
|
||||
// 查询该日志的所有修改历史(包括原始记录)
|
||||
const originalLogId = log.originalLogId || log.id;
|
||||
|
||||
const history = await ConsumableLog.findAll({
|
||||
where: {
|
||||
[Op.or]: [
|
||||
{ id: originalLogId },
|
||||
{ originalLogId: originalLogId }
|
||||
]
|
||||
},
|
||||
order: [['createdAt', 'ASC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
current: log,
|
||||
history: history
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* 耗材操作日志表结构迁移脚本
|
||||
* 添加修改记录相关字段
|
||||
*/
|
||||
|
||||
const { sequelize } = require('../db');
|
||||
|
||||
async function migrate() {
|
||||
try {
|
||||
console.log('开始迁移耗材操作日志表...');
|
||||
|
||||
// 检查并添加 isEditable 字段
|
||||
try {
|
||||
await sequelize.query(`
|
||||
ALTER TABLE consumable_logs ADD COLUMN isEditable BOOLEAN DEFAULT 1
|
||||
`);
|
||||
console.log('✓ 添加 isEditable 字段成功');
|
||||
} catch (err) {
|
||||
if (err.message.includes('duplicate column name')) {
|
||||
console.log('✓ isEditable 字段已存在');
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查并添加 originalLogId 字段
|
||||
try {
|
||||
await sequelize.query(`
|
||||
ALTER TABLE consumable_logs ADD COLUMN originalLogId INTEGER
|
||||
`);
|
||||
console.log('✓ 添加 originalLogId 字段成功');
|
||||
} catch (err) {
|
||||
if (err.message.includes('duplicate column name')) {
|
||||
console.log('✓ originalLogId 字段已存在');
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查并添加 modifiedBy 字段
|
||||
try {
|
||||
await sequelize.query(`
|
||||
ALTER TABLE consumable_logs ADD COLUMN modifiedBy VARCHAR(255)
|
||||
`);
|
||||
console.log('✓ 添加 modifiedBy 字段成功');
|
||||
} catch (err) {
|
||||
if (err.message.includes('duplicate column name')) {
|
||||
console.log('✓ modifiedBy 字段已存在');
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查并添加 modifiedAt 字段
|
||||
try {
|
||||
await sequelize.query(`
|
||||
ALTER TABLE consumable_logs ADD COLUMN modifiedAt DATETIME
|
||||
`);
|
||||
console.log('✓ 添加 modifiedAt 字段成功');
|
||||
} catch (err) {
|
||||
if (err.message.includes('duplicate column name')) {
|
||||
console.log('✓ modifiedAt 字段已存在');
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查并添加 modificationReason 字段
|
||||
try {
|
||||
await sequelize.query(`
|
||||
ALTER TABLE consumable_logs ADD COLUMN modificationReason VARCHAR(255)
|
||||
`);
|
||||
console.log('✓ 添加 modificationReason 字段成功');
|
||||
} catch (err) {
|
||||
if (err.message.includes('duplicate column name')) {
|
||||
console.log('✓ modificationReason 字段已存在');
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建索引
|
||||
try {
|
||||
await sequelize.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_consumable_logs_original_log_id ON consumable_logs(originalLogId)
|
||||
`);
|
||||
console.log('✓ 创建 originalLogId 索引成功');
|
||||
} catch (err) {
|
||||
console.log('! originalLogId 索引创建失败:', err.message);
|
||||
}
|
||||
|
||||
try {
|
||||
await sequelize.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_consumable_logs_is_editable ON consumable_logs(isEditable)
|
||||
`);
|
||||
console.log('✓ 创建 isEditable 索引成功');
|
||||
} catch (err) {
|
||||
console.log('! isEditable 索引创建失败:', err.message);
|
||||
}
|
||||
|
||||
// 更新现有记录:将出入库、调整等系统生成的记录标记为不可编辑
|
||||
const [result] = await sequelize.query(`
|
||||
UPDATE consumable_logs
|
||||
SET isEditable = 0
|
||||
WHERE operationType IN ('in', 'out', 'adjust')
|
||||
AND (isEditable IS NULL OR isEditable = 1)
|
||||
`);
|
||||
console.log(`✓ 更新 ${result.changes || 0} 条系统生成记录为不可编辑状态`);
|
||||
|
||||
console.log('\n迁移完成!');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('迁移失败:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
migrate();
|
||||
Reference in New Issue
Block a user