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: {
|
maxStock: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: true,
|
allowNull: false,
|
||||||
defaultValue: null,
|
defaultValue: 0,
|
||||||
comment: '最大库存,null表示无限制'
|
comment: '最大库存,0表示无限制'
|
||||||
},
|
},
|
||||||
unitPrice: {
|
unitPrice: {
|
||||||
type: DataTypes.DECIMAL(10, 2),
|
type: DataTypes.DECIMAL(10, 2),
|
||||||
|
|||||||
@@ -53,7 +53,13 @@ router.get('/', async (req, res) => {
|
|||||||
router.post('/', async (req, res) => {
|
router.post('/', async (req, res) => {
|
||||||
const transaction = await sequelize.transaction();
|
const transaction = await sequelize.transaction();
|
||||||
try {
|
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({
|
await ConsumableLog.create({
|
||||||
consumableId: consumable.consumableId,
|
consumableId: consumable.consumableId,
|
||||||
@@ -71,7 +77,13 @@ router.post('/', async (req, res) => {
|
|||||||
res.status(201).json(consumable);
|
res.status(201).json(consumable);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await transaction.rollback();
|
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) => {
|
const showModal = useCallback((consumable = null) => {
|
||||||
setEditingConsumable(consumable);
|
setEditingConsumable(consumable);
|
||||||
if (consumable) {
|
if (consumable) {
|
||||||
const isUnlimited = consumable.maxStock === null || consumable.maxStock === undefined;
|
const isUnlimited = consumable.maxStock === 0 || consumable.maxStock === null || consumable.maxStock === undefined;
|
||||||
setMaxStockUnlimited(isUnlimited);
|
setMaxStockUnlimited(isUnlimited);
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
...consumable,
|
...consumable,
|
||||||
@@ -76,6 +76,13 @@ function ConsumableManagement() {
|
|||||||
} else {
|
} else {
|
||||||
setMaxStockUnlimited(true);
|
setMaxStockUnlimited(true);
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
|
form.setFieldsValue({
|
||||||
|
unit: '个',
|
||||||
|
currentStock: 0,
|
||||||
|
minStock: 0,
|
||||||
|
status: 'active',
|
||||||
|
unitPrice: 0
|
||||||
|
});
|
||||||
}
|
}
|
||||||
setModalVisible(true);
|
setModalVisible(true);
|
||||||
}, [form]);
|
}, [form]);
|
||||||
@@ -89,7 +96,8 @@ function ConsumableManagement() {
|
|||||||
try {
|
try {
|
||||||
const submitData = {
|
const submitData = {
|
||||||
...values,
|
...values,
|
||||||
maxStock: maxStockUnlimited ? null : values.maxStock
|
maxStock: maxStockUnlimited ? 0 : values.maxStock,
|
||||||
|
unitPrice: values.unitPrice || 0
|
||||||
};
|
};
|
||||||
if (editingConsumable) {
|
if (editingConsumable) {
|
||||||
await axios.put(`/api/consumables/${editingConsumable.consumableId}`, submitData);
|
await axios.put(`/api/consumables/${editingConsumable.consumableId}`, submitData);
|
||||||
@@ -410,12 +418,6 @@ function ConsumableManagement() {
|
|||||||
}, [stockRecord, stockType, fetchConsumables]);
|
}, [stockRecord, stockType, fetchConsumables]);
|
||||||
|
|
||||||
const columns = useMemo(() => [
|
const columns = useMemo(() => [
|
||||||
{
|
|
||||||
title: '耗材ID',
|
|
||||||
dataIndex: 'consumableId',
|
|
||||||
key: 'consumableId',
|
|
||||||
width: 150
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: '名称',
|
title: '名称',
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
@@ -459,7 +461,7 @@ function ConsumableManagement() {
|
|||||||
dataIndex: 'maxStock',
|
dataIndex: 'maxStock',
|
||||||
key: 'maxStock',
|
key: 'maxStock',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (value) => value === null || value === undefined ? '无限制' : value
|
render: (value) => value === 0 || value === null || value === undefined ? '无限制' : value
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '单价(元)',
|
title: '单价(元)',
|
||||||
@@ -482,6 +484,14 @@ function ConsumableManagement() {
|
|||||||
width: 120,
|
width: 120,
|
||||||
render: (value) => value || '-'
|
render: (value) => value || '-'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '描述',
|
||||||
|
dataIndex: 'description',
|
||||||
|
key: 'description',
|
||||||
|
width: 200,
|
||||||
|
render: (value) => value || '-',
|
||||||
|
ellipsis: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
dataIndex: 'status',
|
dataIndex: 'status',
|
||||||
|
|||||||
Reference in New Issue
Block a user