chore(scripts): 归档旧迁移脚本并添加迁移汇总脚本
将多个单独的数据库迁移脚本归档到archive目录 添加migrate-all.js汇总脚本统一执行所有迁移
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
const { sequelize } = require('../db');
|
||||
|
||||
async function migrate() {
|
||||
try {
|
||||
console.log('开始迁移:添加 pending 状态到用户表...');
|
||||
|
||||
const queryInterface = sequelize.getQueryInterface();
|
||||
const dialect = sequelize.getDialect();
|
||||
|
||||
if (dialect === 'sqlite') {
|
||||
// SQLite 不支持 ALTER COLUMN,需要重建表
|
||||
console.log('检测到 SQLite 数据库,使用重建表方式迁移...');
|
||||
|
||||
// 1. 创建新表
|
||||
await queryInterface.createTable('users_new', {
|
||||
userId: {
|
||||
type: sequelize.Sequelize.STRING,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
username: {
|
||||
type: sequelize.Sequelize.STRING,
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
password: {
|
||||
type: sequelize.Sequelize.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
email: {
|
||||
type: sequelize.Sequelize.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
phone: {
|
||||
type: sequelize.Sequelize.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
realName: {
|
||||
type: sequelize.Sequelize.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
avatar: {
|
||||
type: sequelize.Sequelize.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
status: {
|
||||
type: sequelize.Sequelize.ENUM('active', 'inactive', 'locked', 'pending'),
|
||||
defaultValue: 'active'
|
||||
},
|
||||
lastLoginTime: {
|
||||
type: sequelize.Sequelize.DATE,
|
||||
allowNull: true
|
||||
},
|
||||
lastLoginIp: {
|
||||
type: sequelize.Sequelize.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
loginCount: {
|
||||
type: sequelize.Sequelize.INTEGER,
|
||||
defaultValue: 0
|
||||
},
|
||||
remark: {
|
||||
type: sequelize.Sequelize.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
createdAt: {
|
||||
type: sequelize.Sequelize.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
updatedAt: {
|
||||
type: sequelize.Sequelize.DATE,
|
||||
allowNull: false
|
||||
}
|
||||
});
|
||||
|
||||
// 2. 复制数据
|
||||
await sequelize.query(`
|
||||
INSERT INTO users_new (
|
||||
userId, username, password, email, phone, realName, avatar,
|
||||
status, lastLoginTime, lastLoginIp, loginCount, remark, createdAt, updatedAt
|
||||
)
|
||||
SELECT
|
||||
userId, username, password, email, phone, realName, avatar,
|
||||
status, lastLoginTime, lastLoginIp, loginCount, remark, createdAt, updatedAt
|
||||
FROM users
|
||||
`);
|
||||
|
||||
// 3. 删除旧表
|
||||
await queryInterface.dropTable('users');
|
||||
|
||||
// 4. 重命名新表
|
||||
await queryInterface.renameTable('users_new', 'users');
|
||||
|
||||
// 5. 重新创建索引
|
||||
await queryInterface.addIndex('users', ['status']);
|
||||
await queryInterface.addIndex('users', ['username']);
|
||||
await queryInterface.addIndex('users', ['email']);
|
||||
|
||||
console.log('SQLite 迁移完成');
|
||||
} else if (dialect === 'mysql') {
|
||||
// MySQL 可以直接修改 ENUM
|
||||
console.log('检测到 MySQL 数据库,直接修改 ENUM...');
|
||||
await sequelize.query(`
|
||||
ALTER TABLE users
|
||||
MODIFY COLUMN status ENUM('active', 'inactive', 'locked', 'pending') DEFAULT 'active'
|
||||
`);
|
||||
console.log('MySQL 迁移完成');
|
||||
} else {
|
||||
console.log(`不支持的数据库类型: ${dialect},请手动迁移`);
|
||||
}
|
||||
|
||||
console.log('迁移成功完成!');
|
||||
} catch (error) {
|
||||
console.error('迁移失败:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
migrate();
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 耗材操作日志归档表迁移脚本
|
||||
* 创建归档表用于存储被删除耗材的历史操作记录
|
||||
*/
|
||||
|
||||
const { sequelize, DB_TYPE } = require('../db');
|
||||
|
||||
async function migrate() {
|
||||
try {
|
||||
console.log('开始创建耗材操作日志归档表...');
|
||||
console.log(`数据库类型: ${DB_TYPE}`);
|
||||
|
||||
if (DB_TYPE === 'mysql') {
|
||||
await migrateMySQL();
|
||||
} else {
|
||||
await migrateSQLite();
|
||||
}
|
||||
|
||||
console.log('\n归档表创建完成!');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('迁移失败:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function migrateMySQL() {
|
||||
console.log('\n执行 MySQL 迁移...');
|
||||
|
||||
try {
|
||||
await sequelize.query(`
|
||||
CREATE TABLE IF NOT EXISTS consumable_log_archives (
|
||||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||||
archiveId VARCHAR(255) NOT NULL UNIQUE COMMENT '归档记录唯一标识',
|
||||
consumableId VARCHAR(255) NOT NULL COMMENT '被删除的耗材ID',
|
||||
consumableName VARCHAR(255) NOT NULL COMMENT '耗材名称',
|
||||
consumableSnapshot JSON DEFAULT NULL COMMENT '耗材快照信息',
|
||||
totalOperations INTEGER DEFAULT 0 COMMENT '操作记录总数',
|
||||
firstOperationAt DATETIME COMMENT '首次操作时间',
|
||||
lastOperationAt DATETIME COMMENT '最后操作时间',
|
||||
totalInQuantity INTEGER DEFAULT 0 COMMENT '总入库数量',
|
||||
totalOutQuantity INTEGER DEFAULT 0 COMMENT '总出库数量',
|
||||
finalStock INTEGER DEFAULT 0 COMMENT '删除时库存',
|
||||
deletedBy VARCHAR(255) COMMENT '删除人',
|
||||
deletedAt DATETIME COMMENT '删除时间',
|
||||
deleteReason VARCHAR(255) COMMENT '删除原因',
|
||||
createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_consumable_id (consumableId),
|
||||
INDEX idx_archive_id (archiveId),
|
||||
INDEX idx_deleted_at (deletedAt),
|
||||
INDEX idx_consumable_deleted (consumableId, deletedAt)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='耗材操作日志归档表'
|
||||
`);
|
||||
console.log('✓ 创建归档表成功');
|
||||
} catch (err) {
|
||||
if (err.message.includes('already exists')) {
|
||||
console.log('✓ 归档表已存在');
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function migrateSQLite() {
|
||||
console.log('\n执行 SQLite 迁移...');
|
||||
|
||||
try {
|
||||
// 检查表是否已存在
|
||||
const tables = await sequelize.query(
|
||||
`SELECT name FROM sqlite_master WHERE type='table' AND name='consumable_log_archives'`,
|
||||
{ type: sequelize.QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
if (tables && tables.length > 0) {
|
||||
console.log('✓ 归档表已存在');
|
||||
return;
|
||||
}
|
||||
|
||||
await sequelize.query(`
|
||||
CREATE TABLE consumable_log_archives (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
archiveId VARCHAR(255) NOT NULL UNIQUE,
|
||||
consumableId VARCHAR(255) NOT NULL,
|
||||
consumableName VARCHAR(255) NOT NULL,
|
||||
consumableSnapshot TEXT,
|
||||
totalOperations INTEGER DEFAULT 0,
|
||||
firstOperationAt DATETIME,
|
||||
lastOperationAt DATETIME,
|
||||
totalInQuantity INTEGER DEFAULT 0,
|
||||
totalOutQuantity INTEGER DEFAULT 0,
|
||||
finalStock INTEGER DEFAULT 0,
|
||||
deletedBy VARCHAR(255),
|
||||
deletedAt DATETIME,
|
||||
deleteReason VARCHAR(255),
|
||||
createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
console.log('✓ 创建归档表成功');
|
||||
|
||||
// 创建索引
|
||||
const indexes = [
|
||||
{ name: 'idx_archive_consumable_id', fields: 'consumableId' },
|
||||
{ name: 'idx_archive_archive_id', fields: 'archiveId' },
|
||||
{ name: 'idx_archive_deleted_at', fields: 'deletedAt' },
|
||||
{ name: 'idx_archive_consumable_deleted', fields: 'consumableId, deletedAt' }
|
||||
];
|
||||
|
||||
for (const idx of indexes) {
|
||||
try {
|
||||
await sequelize.query(
|
||||
`CREATE INDEX ${idx.name} ON consumable_log_archives(${idx.fields})`
|
||||
);
|
||||
console.log(`✓ 创建索引: ${idx.name}`);
|
||||
} catch (err) {
|
||||
console.log(`! 创建索引失败: ${idx.name}`, err.message);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('创建归档表失败:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
migrate();
|
||||
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* 耗材操作日志解耦迁移脚本
|
||||
* 添加 isConsumableDeleted 和 consumableSnapshot 字段
|
||||
* 实现耗材删除后日志仍可保留
|
||||
*/
|
||||
|
||||
const { sequelize, DB_TYPE } = require('../db');
|
||||
const Consumable = require('../models/Consumable');
|
||||
const ConsumableLog = require('../models/ConsumableLog');
|
||||
|
||||
async function migrate() {
|
||||
try {
|
||||
console.log('开始迁移耗材操作日志解耦...');
|
||||
console.log(`数据库类型: ${DB_TYPE}`);
|
||||
|
||||
const isMySQL = DB_TYPE === 'mysql';
|
||||
|
||||
if (isMySQL) {
|
||||
await migrateMySQL();
|
||||
} else {
|
||||
await migrateSQLite();
|
||||
}
|
||||
|
||||
await updateExistingLogs();
|
||||
|
||||
console.log('\n迁移完成!耗材删除后日志将被保留。');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('迁移失败:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function migrateMySQL() {
|
||||
console.log('\n执行 MySQL 迁移...');
|
||||
|
||||
try {
|
||||
await sequelize.query(`
|
||||
ALTER TABLE consumable_logs
|
||||
ADD COLUMN isConsumableDeleted TINYINT(1) DEFAULT 0 COMMENT '关联耗材是否已被删除'
|
||||
`);
|
||||
console.log('✓ 添加 isConsumableDeleted 字段成功');
|
||||
} catch (err) {
|
||||
if (err.message.includes('Duplicate column')) {
|
||||
console.log('✓ isConsumableDeleted 字段已存在');
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await sequelize.query(`
|
||||
ALTER TABLE consumable_logs
|
||||
ADD COLUMN consumableSnapshot JSON DEFAULT NULL COMMENT '耗材快照信息'
|
||||
`);
|
||||
console.log('✓ 添加 consumableSnapshot 字段成功');
|
||||
} catch (err) {
|
||||
if (err.message.includes('Duplicate column')) {
|
||||
console.log('✓ consumableSnapshot 字段已存在');
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await sequelize.query(`
|
||||
CREATE INDEX idx_consumable_logs_is_deleted ON consumable_logs(isConsumableDeleted)
|
||||
`);
|
||||
console.log('✓ 创建 isConsumableDeleted 索引成功');
|
||||
} catch (err) {
|
||||
if (err.message.includes('Duplicate key name')) {
|
||||
console.log('✓ isConsumableDeleted 索引已存在');
|
||||
} else {
|
||||
console.log('! isConsumableDeleted 索引创建失败:', err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function migrateSQLite() {
|
||||
console.log('\n执行 SQLite 迁移...');
|
||||
|
||||
try {
|
||||
await sequelize.query(`
|
||||
ALTER TABLE consumable_logs ADD COLUMN isConsumableDeleted BOOLEAN DEFAULT 0
|
||||
`);
|
||||
console.log('✓ 添加 isConsumableDeleted 字段成功');
|
||||
} catch (err) {
|
||||
if (err.message.includes('duplicate column name')) {
|
||||
console.log('✓ isConsumableDeleted 字段已存在');
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await sequelize.query(`
|
||||
ALTER TABLE consumable_logs ADD COLUMN consumableSnapshot TEXT
|
||||
`);
|
||||
console.log('✓ 添加 consumableSnapshot 字段成功');
|
||||
} catch (err) {
|
||||
if (err.message.includes('duplicate column name')) {
|
||||
console.log('✓ consumableSnapshot 字段已存在');
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await sequelize.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_consumable_logs_is_deleted ON consumable_logs(isConsumableDeleted)
|
||||
`);
|
||||
console.log('✓ 创建 isConsumableDeleted 索引成功');
|
||||
} catch (err) {
|
||||
console.log('! isConsumableDeleted 索引创建失败:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateExistingLogs() {
|
||||
console.log('\n更新现有日志数据...');
|
||||
|
||||
try {
|
||||
const consumables = await Consumable.findAll({
|
||||
attributes: ['consumableId', 'category', 'unit', 'unitPrice', 'supplier', 'location', 'minStock', 'maxStock', 'status']
|
||||
});
|
||||
|
||||
const consumableMap = new Map();
|
||||
consumables.forEach(c => {
|
||||
consumableMap.set(c.consumableId, {
|
||||
category: c.category,
|
||||
unit: c.unit,
|
||||
unitPrice: c.unitPrice,
|
||||
supplier: c.supplier,
|
||||
location: c.location,
|
||||
minStock: c.minStock,
|
||||
maxStock: c.maxStock,
|
||||
status: c.status
|
||||
});
|
||||
});
|
||||
|
||||
const logs = await ConsumableLog.findAll({
|
||||
where: {
|
||||
consumableSnapshot: null
|
||||
}
|
||||
});
|
||||
|
||||
let updatedCount = 0;
|
||||
for (const log of logs) {
|
||||
const snapshot = consumableMap.get(log.consumableId);
|
||||
if (snapshot) {
|
||||
await log.update({ consumableSnapshot: snapshot });
|
||||
updatedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✓ 更新了 ${updatedCount} 条日志的快照信息`);
|
||||
|
||||
const deletedLogsCount = await ConsumableLog.count({
|
||||
where: { operationType: 'delete' }
|
||||
});
|
||||
console.log(`✓ 当前有 ${deletedLogsCount} 条删除类型日志`);
|
||||
|
||||
} catch (error) {
|
||||
console.log('! 更新现有日志数据时出错:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
migrate();
|
||||
@@ -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();
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* 耗材管理乐观锁迁移脚本
|
||||
* 为 consumables 表添加 version 字段
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
const { sequelize, dbDialect } = require('../db');
|
||||
|
||||
// 从环境变量重新确定数据库类型
|
||||
const actualDbType = process.env.DB_TYPE || 'sqlite';
|
||||
|
||||
async function migrate() {
|
||||
try {
|
||||
console.log('开始执行耗材乐观锁迁移...');
|
||||
console.log('数据库类型:', actualDbType);
|
||||
|
||||
if (actualDbType === 'sqlite') {
|
||||
// SQLite: 检查字段是否存在
|
||||
const tableInfo = await sequelize.query(
|
||||
"PRAGMA table_info(consumables)",
|
||||
{ type: sequelize.QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
const hasVersion = tableInfo.some(col => col.name === 'version');
|
||||
|
||||
if (!hasVersion) {
|
||||
console.log('添加 version 字段...');
|
||||
await sequelize.query(
|
||||
"ALTER TABLE consumables ADD COLUMN version INTEGER DEFAULT 0"
|
||||
);
|
||||
console.log('version 字段添加成功');
|
||||
} else {
|
||||
console.log('version 字段已存在,跳过');
|
||||
}
|
||||
|
||||
// 检查 updatedAt 索引
|
||||
const indexes = await sequelize.query(
|
||||
"SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='consumables'",
|
||||
{ type: sequelize.QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
const hasUpdatedAtIndex = indexes.some(idx => idx.name === 'consumables_updatedAt');
|
||||
|
||||
if (!hasUpdatedAtIndex) {
|
||||
console.log('添加 updatedAt 索引...');
|
||||
await sequelize.query(
|
||||
"CREATE INDEX consumables_updatedAt ON consumables(updatedAt)"
|
||||
);
|
||||
console.log('updatedAt 索引添加成功');
|
||||
} else {
|
||||
console.log('updatedAt 索引已存在,跳过');
|
||||
}
|
||||
|
||||
} else if (actualDbType === 'mysql') {
|
||||
// MySQL: 检查并添加字段
|
||||
try {
|
||||
console.log('添加 version 字段...');
|
||||
await sequelize.query(
|
||||
"ALTER TABLE consumables ADD COLUMN version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号'"
|
||||
);
|
||||
console.log('version 字段添加成功');
|
||||
} catch (err) {
|
||||
if (err.message.includes('Duplicate column')) {
|
||||
console.log('version 字段已存在,跳过');
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加索引
|
||||
try {
|
||||
console.log('添加 updatedAt 索引...');
|
||||
await sequelize.query(
|
||||
"CREATE INDEX idx_consumables_updatedAt ON consumables(updatedAt)"
|
||||
);
|
||||
console.log('updatedAt 索引添加成功');
|
||||
} catch (err) {
|
||||
if (err.message.includes('Duplicate key')) {
|
||||
console.log('updatedAt 索引已存在,跳过');
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化现有数据的 version 值
|
||||
console.log('初始化现有数据的 version 值...');
|
||||
await sequelize.query(
|
||||
"UPDATE consumables SET version = 0 WHERE version IS NULL"
|
||||
);
|
||||
console.log('version 值初始化完成');
|
||||
|
||||
console.log('迁移完成!');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('迁移失败:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
migrate();
|
||||
@@ -0,0 +1,170 @@
|
||||
const { sequelize } = require('../db');
|
||||
const NetworkCard = require('../models/NetworkCard');
|
||||
const DevicePort = require('../models/DevicePort');
|
||||
const Device = require('../models/Device');
|
||||
|
||||
async function migrate() {
|
||||
console.log('========================================');
|
||||
console.log(' IDC管理系统 - 数据库迁移脚本 v2.0 ');
|
||||
console.log('========================================');
|
||||
console.log('');
|
||||
|
||||
try {
|
||||
console.log('🔍 检测数据库类型...');
|
||||
const dbType = sequelize.getDialect();
|
||||
console.log(` 数据库类型: ${dbType}`);
|
||||
console.log('');
|
||||
|
||||
console.log('📋 开始迁移...');
|
||||
console.log(' 1. 创建 network_cards 表');
|
||||
console.log(' 2. 为 device_ports 添加 nic_id 字段');
|
||||
console.log(' 3. 创建相关索引');
|
||||
console.log('');
|
||||
|
||||
if (dbType === 'sqlite') {
|
||||
await migrateSQLite();
|
||||
} else if (dbType === 'mysql') {
|
||||
await migrateMySQL();
|
||||
} else {
|
||||
console.log(`⚠️ 不支持的数据库类型: ${dbType}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log('✅ 迁移完成!');
|
||||
console.log('');
|
||||
console.log('📊 验证迁移结果...');
|
||||
|
||||
const [tables] = await sequelize.query("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name");
|
||||
console.log(` 数据库表: ${tables.map(t => t.name).join(', ')}`);
|
||||
|
||||
const portCount = await DevicePort.count();
|
||||
const cardCount = await NetworkCard.count();
|
||||
console.log(` device_ports: ${portCount} 条记录`);
|
||||
console.log(` network_cards: ${cardCount} 条记录`);
|
||||
|
||||
console.log('');
|
||||
console.log('========================================');
|
||||
console.log(' 迁移成功完成!🎉');
|
||||
console.log('========================================');
|
||||
|
||||
} catch (error) {
|
||||
console.error('');
|
||||
console.error('❌ 迁移失败:', error.message);
|
||||
console.error('');
|
||||
console.error('错误详情:', error.stack);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await sequelize.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function migrateSQLite() {
|
||||
console.log('');
|
||||
console.log('🔄 执行 SQLite 迁移...');
|
||||
|
||||
await sequelize.query('PRAGMA foreign_keys = OFF');
|
||||
|
||||
try {
|
||||
console.log(' → 删除旧的 device_ports 表...');
|
||||
await sequelize.query('DROP TABLE IF EXISTS `device_ports`');
|
||||
|
||||
console.log(' → 删除旧的 network_cards 表...');
|
||||
await sequelize.query('DROP TABLE IF EXISTS `network_cards`');
|
||||
|
||||
console.log(' → 同步 DevicePort 模型...');
|
||||
await DevicePort.sync({ force: false });
|
||||
|
||||
console.log(' → 同步 NetworkCard 模型...');
|
||||
await NetworkCard.sync({ force: false });
|
||||
|
||||
console.log(' → 同步 Device 模型(确保外键关系)...');
|
||||
await Device.sync({ force: false });
|
||||
|
||||
console.log(' → 重新同步 DevicePort 模型(含外键)...');
|
||||
await DevicePort.sync({ force: true });
|
||||
|
||||
console.log(' → 重新同步 NetworkCard 模型...');
|
||||
await NetworkCard.sync({ force: false });
|
||||
|
||||
await sequelize.query('PRAGMA foreign_keys = ON');
|
||||
|
||||
} catch (error) {
|
||||
await sequelize.query('PRAGMA foreign_keys = ON');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function migrateMySQL() {
|
||||
console.log('');
|
||||
console.log('🔄 执行 MySQL 迁移...');
|
||||
|
||||
const tableName = 'network_cards';
|
||||
console.log(` → 创建表: ${tableName}`);
|
||||
|
||||
const createTableSQL = `
|
||||
CREATE TABLE IF NOT EXISTS \`${tableName}\` (
|
||||
\`nic_id\` VARCHAR(255) NOT NULL PRIMARY KEY,
|
||||
\`device_id\` VARCHAR(255) NOT NULL,
|
||||
\`name\` VARCHAR(255) NOT NULL,
|
||||
\`description\` TEXT,
|
||||
\`slot_number\` INT,
|
||||
\`port_count\` INT DEFAULT 0,
|
||||
\`model\` VARCHAR(255),
|
||||
\`manufacturer\` VARCHAR(255),
|
||||
\`status\` ENUM('normal', 'warning', 'fault', 'offline') DEFAULT 'normal',
|
||||
\`created_at\` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
\`updated_at\` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX \`idx_device_id\` (\`device_id\`),
|
||||
INDEX \`idx_slot_number\` (\`slot_number\`),
|
||||
UNIQUE INDEX \`idx_device_name\` (\`device_id\`, \`name\`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`;
|
||||
|
||||
await sequelize.query(createTableSQL);
|
||||
|
||||
console.log(' → 检查 nic_id 字段是否存在...');
|
||||
const [columns] = await sequelize.query(
|
||||
"SHOW COLUMNS FROM `device_ports` LIKE 'nic_id'",
|
||||
{ type: sequelize.QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
if (columns.length === 0) {
|
||||
console.log(' → 添加 nic_id 字段...');
|
||||
await sequelize.query(
|
||||
'ALTER TABLE `device_ports` ADD COLUMN `nic_id` VARCHAR(255) NULL AFTER `device_id`'
|
||||
);
|
||||
} else {
|
||||
console.log(' → nic_id 字段已存在,跳过');
|
||||
}
|
||||
|
||||
console.log(' → 创建 nic_id 索引...');
|
||||
try {
|
||||
await sequelize.query('CREATE INDEX `idx_port_nic_id` ON `device_ports`(`nic_id`)');
|
||||
} catch (error) {
|
||||
if (error.message.includes('Duplicate key name')) {
|
||||
console.log(' → 索引已存在,跳过');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(' → 同步模型以确保关系正确...');
|
||||
await NetworkCard.sync({ force: false });
|
||||
await DevicePort.sync({ force: false });
|
||||
|
||||
console.log(' → 添加外键约束...');
|
||||
try {
|
||||
await sequelize.query(
|
||||
'ALTER TABLE `device_ports` ADD CONSTRAINT `fk_port_nic` FOREIGN KEY (`nic_id`) REFERENCES `network_cards`(`nic_id`)'
|
||||
);
|
||||
} catch (error) {
|
||||
if (error.message.includes('Duplicate key name') || error.message.includes('already exists')) {
|
||||
console.log(' → 外键约束已存在,跳过');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
migrate();
|
||||
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* 移除 consumable_logs 表的外键约束
|
||||
* 解决删除耗材时日志被级联删除的问题
|
||||
*/
|
||||
|
||||
const { sequelize, DB_TYPE } = require('../db');
|
||||
|
||||
async function removeForeignKey() {
|
||||
try {
|
||||
console.log('开始移除 consumable_logs 表的外键约束...');
|
||||
console.log(`数据库类型: ${DB_TYPE}`);
|
||||
|
||||
if (DB_TYPE === 'mysql') {
|
||||
await removeMySQLForeignKey();
|
||||
} else {
|
||||
await removeSQLiteForeignKey();
|
||||
}
|
||||
|
||||
console.log('\n外键约束移除完成!');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('移除外键约束失败:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeMySQLForeignKey() {
|
||||
console.log('\n执行 MySQL 外键移除...');
|
||||
|
||||
// 获取外键名称
|
||||
const [fks] = await sequelize.query(`
|
||||
SELECT CONSTRAINT_NAME
|
||||
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_NAME = 'consumable_logs'
|
||||
AND TABLE_SCHEMA = DATABASE()
|
||||
AND REFERENCED_TABLE_NAME IS NOT NULL
|
||||
`);
|
||||
|
||||
console.log('发现外键约束:', fks);
|
||||
|
||||
for (const fk of fks) {
|
||||
try {
|
||||
await sequelize.query(`
|
||||
ALTER TABLE consumable_logs DROP FOREIGN KEY ${fk.CONSTRAINT_NAME}
|
||||
`);
|
||||
console.log(`✓ 移除外键约束: ${fk.CONSTRAINT_NAME}`);
|
||||
} catch (err) {
|
||||
console.log(`! 移除外键约束失败: ${fk.CONSTRAINT_NAME}`, err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function removeSQLiteForeignKey() {
|
||||
console.log('\n执行 SQLite 外键移除...');
|
||||
console.log('SQLite 不支持直接删除外键,需要重建表');
|
||||
|
||||
// 检查外键是否存在
|
||||
const fks = await sequelize.query(
|
||||
`PRAGMA foreign_key_list(consumable_logs);`,
|
||||
{ type: sequelize.QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
if (!fks || fks.length === 0) {
|
||||
console.log('✓ 没有外键约束需要移除');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('发现外键约束:', fks);
|
||||
|
||||
// SQLite 不支持 ALTER TABLE DROP FOREIGN KEY,需要重建表
|
||||
await sequelize.transaction(async (transaction) => {
|
||||
// 获取表结构
|
||||
const columns = await sequelize.query(
|
||||
`PRAGMA table_info(consumable_logs);`,
|
||||
{ type: sequelize.QueryTypes.SELECT, transaction }
|
||||
);
|
||||
|
||||
console.log('\n当前表列:', columns.map(c => c.name));
|
||||
|
||||
// 创建新表(不包含外键约束)
|
||||
await sequelize.query(`
|
||||
CREATE TABLE consumable_logs_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
consumableId VARCHAR(255) NOT NULL,
|
||||
consumableName VARCHAR(255) NOT NULL,
|
||||
operationType TEXT NOT NULL,
|
||||
quantity INTEGER DEFAULT 0,
|
||||
previousStock INTEGER NOT NULL,
|
||||
currentStock INTEGER NOT NULL,
|
||||
operator VARCHAR(255),
|
||||
reason VARCHAR(255),
|
||||
notes TEXT,
|
||||
relatedId VARCHAR(255),
|
||||
isEditable TINYINT(1) DEFAULT 1,
|
||||
originalLogId INTEGER,
|
||||
modifiedBy VARCHAR(255),
|
||||
modifiedAt DATETIME,
|
||||
modificationReason VARCHAR(255),
|
||||
createdAt DATETIME NOT NULL,
|
||||
updatedAt DATETIME NOT NULL,
|
||||
isConsumableDeleted BOOLEAN DEFAULT 0,
|
||||
consumableSnapshot TEXT
|
||||
)
|
||||
`, { transaction });
|
||||
|
||||
console.log('✓ 创建新表成功');
|
||||
|
||||
// 复制数据
|
||||
await sequelize.query(`
|
||||
INSERT INTO consumable_logs_new
|
||||
SELECT * FROM consumable_logs
|
||||
`, { transaction });
|
||||
|
||||
const countResult = await sequelize.query(
|
||||
`SELECT COUNT(*) as count FROM consumable_logs_new`,
|
||||
{ type: sequelize.QueryTypes.SELECT, transaction }
|
||||
);
|
||||
console.log(`✓ 复制了 ${countResult[0].count} 条数据`);
|
||||
|
||||
// 删除旧表
|
||||
await sequelize.query(`DROP TABLE consumable_logs`, { transaction });
|
||||
console.log('✓ 删除旧表成功');
|
||||
|
||||
// 重命名新表
|
||||
await sequelize.query(`ALTER TABLE consumable_logs_new RENAME TO consumable_logs`, { transaction });
|
||||
console.log('✓ 重命名新表成功');
|
||||
|
||||
// 创建索引
|
||||
const indexes = [
|
||||
{ name: 'consumable_logs_consumable_id', fields: ['consumableId'] },
|
||||
{ name: 'consumable_logs_operation_type', fields: ['operationType'] },
|
||||
{ name: 'consumable_logs_created_at', fields: ['createdAt'] },
|
||||
{ name: 'consumable_logs_consumable_id_created_at', fields: ['consumableId', 'createdAt'] },
|
||||
{ name: 'consumable_logs_original_log_id', fields: ['originalLogId'] },
|
||||
{ name: 'consumable_logs_is_editable', fields: ['isEditable'] },
|
||||
{ name: 'idx_consumable_logs_is_deleted', fields: ['isConsumableDeleted'] }
|
||||
];
|
||||
|
||||
for (const idx of indexes) {
|
||||
try {
|
||||
await sequelize.query(
|
||||
`CREATE INDEX IF NOT EXISTS ${idx.name} ON consumable_logs(${idx.fields.join(', ')})`,
|
||||
{ transaction }
|
||||
);
|
||||
console.log(`✓ 创建索引: ${idx.name}`);
|
||||
} catch (err) {
|
||||
console.log(`! 创建索引失败: ${idx.name}`, err.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('\n✓ 表重建完成,外键约束已移除');
|
||||
}
|
||||
|
||||
removeForeignKey();
|
||||
Reference in New Issue
Block a user