From 384a1a9246a62a7c6e4e299e185c8b7fdbc32f8a Mon Sep 17 00:00:00 2001 From: zhang1106 <849185023@qq.com> Date: Thu, 5 Feb 2026 16:26:51 +0800 Subject: [PATCH] =?UTF-8?q?fix(consumable):=20=E4=BF=AE=E5=A4=8D=E6=9C=80?= =?UTF-8?q?=E5=A4=A7=E5=BA=93=E5=AD=98=E5=AD=97=E6=AE=B5=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E5=B9=B6=E6=94=B9=E8=BF=9B=E8=80=97=E6=9D=90=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 maxStock 字段从允许 null 改为默认 0 表示无限制 添加耗材创建时的数据验证和错误处理 优化前端耗材管理页面的表单初始化和显示逻辑 添加数据库迁移脚本处理现有数据 --- backend/fix-maxstock.js | 99 +++++++++++++++++++++ backend/models/Consumable.js | 6 +- backend/routes/consumables.js | 16 +++- frontend/src/pages/ConsumableManagement.jsx | 28 ++++-- 4 files changed, 135 insertions(+), 14 deletions(-) create mode 100644 backend/fix-maxstock.js diff --git a/backend/fix-maxstock.js b/backend/fix-maxstock.js new file mode 100644 index 0000000..7ca34da --- /dev/null +++ b/backend/fix-maxstock.js @@ -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(); diff --git a/backend/models/Consumable.js b/backend/models/Consumable.js index 6d16a55..af67bc5 100644 --- a/backend/models/Consumable.js +++ b/backend/models/Consumable.js @@ -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), diff --git a/backend/routes/consumables.js b/backend/routes/consumables.js index a0cf1fa..fe7a6a4 100644 --- a/backend/routes/consumables.js +++ b/backend/routes/consumables.js @@ -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 }); + } } }); diff --git a/frontend/src/pages/ConsumableManagement.jsx b/frontend/src/pages/ConsumableManagement.jsx index 7e921c7..652918f 100644 --- a/frontend/src/pages/ConsumableManagement.jsx +++ b/frontend/src/pages/ConsumableManagement.jsx @@ -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',