fix(consumable): 修复最大库存字段逻辑并改进耗材管理
将 maxStock 字段从允许 null 改为默认 0 表示无限制 添加耗材创建时的数据验证和错误处理 优化前端耗材管理页面的表单初始化和显示逻辑 添加数据库迁移脚本处理现有数据
This commit is contained in:
@@ -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();
|
||||
@@ -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),
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user