fix(consumable): 修复最大库存字段逻辑并改进耗材管理

将 maxStock 字段从允许 null 改为默认 0 表示无限制
添加耗材创建时的数据验证和错误处理
优化前端耗材管理页面的表单初始化和显示逻辑
添加数据库迁移脚本处理现有数据
This commit is contained in:
zhang1106
2026-02-05 16:26:51 +08:00
parent ebd4b5faa9
commit 384a1a9246
4 changed files with 135 additions and 14 deletions
+99
View File
@@ -0,0 +1,99 @@
const { sequelize } = require('./db');
async function fixMaxStock() {
try {
console.log('开始修复 maxStock 字段...');
// 检查当前表结构
const [results] = await sequelize.query(
"PRAGMA table_info(consumables);"
);
console.log('当前表结构:');
results.forEach(col => {
console.log(` ${col.name}: ${col.type} ${col.notnull ? 'NOT NULL' : 'NULL'} default=${col.dflt_value}`);
});
const maxStockCol = results.find(c => c.name === 'maxStock');
if (maxStockCol) {
console.log('\n当前 maxStock 字段:', maxStockCol);
// SQLite 不支持直接修改列,需要创建新表
console.log('\n需要重建表结构...');
// 1. 创建新表
await sequelize.query(`
CREATE TABLE consumables_new (
consumableId VARCHAR(255) PRIMARY KEY NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
category VARCHAR(255) NOT NULL,
unit VARCHAR(255) NOT NULL DEFAULT '个',
currentStock INTEGER NOT NULL DEFAULT 0,
minStock INTEGER NOT NULL DEFAULT 10,
maxStock INTEGER NOT NULL DEFAULT 0,
unitPrice DECIMAL(10,2) NOT NULL DEFAULT 0,
supplier VARCHAR(255),
location VARCHAR(255),
description TEXT,
status VARCHAR(255) DEFAULT 'active',
version INTEGER NOT NULL DEFAULT 0,
createdAt DATETIME,
updatedAt DATETIME
)
`);
// 2. 复制数据(将 null 转换为 0)
await sequelize.query(`
INSERT INTO consumables_new
SELECT
consumableId,
name,
category,
unit,
currentStock,
minStock,
COALESCE(maxStock, 0) as maxStock,
unitPrice,
supplier,
location,
description,
status,
version,
createdAt,
updatedAt
FROM consumables
`);
// 3. 删除旧表
await sequelize.query('DROP TABLE consumables');
// 4. 重命名新表
await sequelize.query('ALTER TABLE consumables_new RENAME TO consumables');
// 5. 创建索引
await sequelize.query('CREATE INDEX consumables_category ON consumables(category)');
await sequelize.query('CREATE INDEX consumables_status ON consumables(status)');
await sequelize.query('CREATE INDEX consumables_category_status ON consumables(category, status)');
await sequelize.query('CREATE INDEX consumables_updatedAt ON consumables(updatedAt)');
console.log('\n表结构修复完成!');
// 验证新表结构
const [newResults] = await sequelize.query(
"PRAGMA table_info(consumables);"
);
console.log('\n新表结构:');
newResults.forEach(col => {
console.log(` ${col.name}: ${col.type} ${col.notnull ? 'NOT NULL' : 'NULL'} default=${col.dflt_value}`);
});
}
console.log('\n修复完成!');
process.exit(0);
} catch (error) {
console.error('修复失败:', error);
process.exit(1);
}
}
fixMaxStock();
+3 -3
View File
@@ -33,9 +33,9 @@ const Consumable = sequelize.define('Consumable', {
},
maxStock: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: null,
comment: '最大库存,null表示无限制'
allowNull: false,
defaultValue: 0,
comment: '最大库存,0表示无限制'
},
unitPrice: {
type: DataTypes.DECIMAL(10, 2),
+14 -2
View File
@@ -53,7 +53,13 @@ router.get('/', async (req, res) => {
router.post('/', async (req, res) => {
const transaction = await sequelize.transaction();
try {
const consumable = await Consumable.create(req.body, { transaction });
console.log('接收到的数据:', JSON.stringify(req.body, null, 2));
const consumableData = {
...req.body,
consumableId: req.body.consumableId || `CON${Date.now()}`
};
console.log('处理后的数据:', JSON.stringify(consumableData, null, 2));
const consumable = await Consumable.create(consumableData, { transaction });
await ConsumableLog.create({
consumableId: consumable.consumableId,
@@ -71,7 +77,13 @@ router.post('/', async (req, res) => {
res.status(201).json(consumable);
} catch (error) {
await transaction.rollback();
res.status(400).json({ error: error.message });
console.error('创建耗材错误:', error);
if (error.name === 'SequelizeValidationError') {
const messages = error.errors.map(e => `${e.path}: ${e.message}`).join(', ');
res.status(400).json({ error: `Validation error: ${messages}` });
} else {
res.status(400).json({ error: error.message });
}
}
});
+19 -9
View File
@@ -67,7 +67,7 @@ function ConsumableManagement() {
const showModal = useCallback((consumable = null) => {
setEditingConsumable(consumable);
if (consumable) {
const isUnlimited = consumable.maxStock === null || consumable.maxStock === undefined;
const isUnlimited = consumable.maxStock === 0 || consumable.maxStock === null || consumable.maxStock === undefined;
setMaxStockUnlimited(isUnlimited);
form.setFieldsValue({
...consumable,
@@ -76,6 +76,13 @@ function ConsumableManagement() {
} else {
setMaxStockUnlimited(true);
form.resetFields();
form.setFieldsValue({
unit: '个',
currentStock: 0,
minStock: 0,
status: 'active',
unitPrice: 0
});
}
setModalVisible(true);
}, [form]);
@@ -89,7 +96,8 @@ function ConsumableManagement() {
try {
const submitData = {
...values,
maxStock: maxStockUnlimited ? null : values.maxStock
maxStock: maxStockUnlimited ? 0 : values.maxStock,
unitPrice: values.unitPrice || 0
};
if (editingConsumable) {
await axios.put(`/api/consumables/${editingConsumable.consumableId}`, submitData);
@@ -410,12 +418,6 @@ function ConsumableManagement() {
}, [stockRecord, stockType, fetchConsumables]);
const columns = useMemo(() => [
{
title: '耗材ID',
dataIndex: 'consumableId',
key: 'consumableId',
width: 150
},
{
title: '名称',
dataIndex: 'name',
@@ -459,7 +461,7 @@ function ConsumableManagement() {
dataIndex: 'maxStock',
key: 'maxStock',
width: 100,
render: (value) => value === null || value === undefined ? '无限制' : value
render: (value) => value === 0 || value === null || value === undefined ? '无限制' : value
},
{
title: '单价(元)',
@@ -482,6 +484,14 @@ function ConsumableManagement() {
width: 120,
render: (value) => value || '-'
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
width: 200,
render: (value) => value || '-',
ellipsis: true
},
{
title: '状态',
dataIndex: 'status',