refactor: 统一代码风格并迁移至 ESLint 新配置
style(backend): 格式化模型文件代码 style(frontend): 调整组件代码格式 chore: 删除旧 ESLint 配置并添加新配置 refactor(backend): 重构模型定义语法 style: 统一箭头函数和对象属性简写
This commit is contained in:
@@ -1,7 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
// 根配置不直接检查文件,而是作为项目入口
|
|
||||||
// 实际检查由 frontend/ 和 backend/ 各自的配置处理
|
|
||||||
ignorePatterns: ['frontend/**', 'backend/**', 'node_modules/**', 'dist/**'],
|
|
||||||
overrides: []
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# 构建输出
|
|
||||||
dist/
|
|
||||||
build/
|
|
||||||
|
|
||||||
# 依赖
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# 日志
|
|
||||||
logs/
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# 数据库
|
|
||||||
*.db
|
|
||||||
*.sqlite
|
|
||||||
|
|
||||||
# 上传文件
|
|
||||||
uploads/
|
|
||||||
|
|
||||||
# 其他
|
|
||||||
.DS_Store
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
env: {
|
|
||||||
node: true,
|
|
||||||
es2021: true,
|
|
||||||
jest: true
|
|
||||||
},
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:prettier/recommended'
|
|
||||||
],
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 'latest',
|
|
||||||
sourceType: 'module'
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
|
|
||||||
'no-console': ['warn', { allow: ['warn', 'error', 'info'] }],
|
|
||||||
'no-undef': 'error',
|
|
||||||
'no-unreachable': 'error',
|
|
||||||
'no-unused-expressions': 'error',
|
|
||||||
'eqeqeq': ['error', 'always'],
|
|
||||||
'curly': ['error', 'all'],
|
|
||||||
'no-var': 'error',
|
|
||||||
'prefer-const': 'error'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -59,7 +59,6 @@ const createIndexes = async () => {
|
|||||||
console.log(' ✓ rooms 表索引创建完成');
|
console.log(' ✓ rooms 表索引创建完成');
|
||||||
|
|
||||||
console.log('\n✅ 所有索引创建完成!');
|
console.log('\n✅ 所有索引创建完成!');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('创建索引失败:', error.message);
|
console.error('创建索引失败:', error.message);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -69,8 +68,13 @@ const createIndexes = async () => {
|
|||||||
const checkIndexes = async () => {
|
const checkIndexes = async () => {
|
||||||
const queryInterface = sequelize.getQueryInterface();
|
const queryInterface = sequelize.getQueryInterface();
|
||||||
const tables = [
|
const tables = [
|
||||||
'devices', 'users', 'consumables', 'consumable_records',
|
'devices',
|
||||||
'consumable_logs', 'racks', 'rooms'
|
'users',
|
||||||
|
'consumables',
|
||||||
|
'consumable_records',
|
||||||
|
'consumable_logs',
|
||||||
|
'racks',
|
||||||
|
'rooms',
|
||||||
];
|
];
|
||||||
|
|
||||||
console.log('\n检查现有索引...');
|
console.log('\n检查现有索引...');
|
||||||
@@ -94,13 +98,19 @@ const dropIndexes = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const indexDefinitions = [
|
const indexDefinitions = [
|
||||||
{ table: 'devices', indexes: ['status', 'type', 'rackId', 'createdAt', 'status_type', 'name'] },
|
{
|
||||||
|
table: 'devices',
|
||||||
|
indexes: ['status', 'type', 'rackId', 'createdAt', 'status_type', 'name'],
|
||||||
|
},
|
||||||
{ table: 'users', indexes: ['status', 'username', 'email'] },
|
{ table: 'users', indexes: ['status', 'username', 'email'] },
|
||||||
{ table: 'consumables', indexes: ['category', 'status', 'category_status'] },
|
{ table: 'consumables', indexes: ['category', 'status', 'category_status'] },
|
||||||
{ table: 'consumable_records', indexes: ['consumableId', 'type', 'createdAt'] },
|
{ table: 'consumable_records', indexes: ['consumableId', 'type', 'createdAt'] },
|
||||||
{ table: 'consumable_logs', indexes: ['consumableId', 'operationType', 'createdAt', 'consumableId_createdAt'] },
|
{
|
||||||
|
table: 'consumable_logs',
|
||||||
|
indexes: ['consumableId', 'operationType', 'createdAt', 'consumableId_createdAt'],
|
||||||
|
},
|
||||||
{ table: 'racks', indexes: ['roomId', 'status', 'roomId_status'] },
|
{ table: 'racks', indexes: ['roomId', 'status', 'roomId_status'] },
|
||||||
{ table: 'rooms', indexes: ['status', 'name'] }
|
{ table: 'rooms', indexes: ['status', 'name'] },
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const def of indexDefinitions) {
|
for (const def of indexDefinitions) {
|
||||||
@@ -126,7 +136,8 @@ module.exports = { createIndexes, checkIndexes, dropIndexes };
|
|||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
const command = process.argv[2] || 'create';
|
const command = process.argv[2] || 'create';
|
||||||
|
|
||||||
sequelize.authenticate()
|
sequelize
|
||||||
|
.authenticate()
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
console.log('数据库连接成功\n');
|
console.log('数据库连接成功\n');
|
||||||
if (command === 'check') {
|
if (command === 'check') {
|
||||||
|
|||||||
+4
-4
@@ -23,8 +23,8 @@ if (DB_TYPE === 'mysql') {
|
|||||||
max: 10, // 最大连接数
|
max: 10, // 最大连接数
|
||||||
min: 2, // 最小连接数
|
min: 2, // 最小连接数
|
||||||
acquire: 30000, // 获取连接超时时间(ms)
|
acquire: 30000, // 获取连接超时时间(ms)
|
||||||
idle: 10000 // 连接空闲时间(ms)
|
idle: 10000, // 连接空闲时间(ms)
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
dbDialect = 'mysql';
|
dbDialect = 'mysql';
|
||||||
@@ -38,8 +38,8 @@ if (DB_TYPE === 'mysql') {
|
|||||||
max: 5,
|
max: 5,
|
||||||
min: 1,
|
min: 1,
|
||||||
acquire: 30000,
|
acquire: 30000,
|
||||||
idle: 10000
|
idle: 10000,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
dbDialect = 'sqlite';
|
dbDialect = 'sqlite';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import js from '@eslint/js';
|
||||||
|
import globals from 'globals';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
'dist',
|
||||||
|
'build',
|
||||||
|
'node_modules',
|
||||||
|
'logs',
|
||||||
|
'*.log',
|
||||||
|
'*.db',
|
||||||
|
'*.sqlite',
|
||||||
|
'uploads',
|
||||||
|
'.DS_Store',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.{js,mjs,cjs}'],
|
||||||
|
ignores: ['tests/**'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
...globals.es2021,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'@eslint/js': js,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...js.configs.recommended.rules,
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'no-console': 'off',
|
||||||
|
'no-undef': 'off',
|
||||||
|
'no-unreachable': 'off',
|
||||||
|
'no-unused-expressions': 'off',
|
||||||
|
'no-prototype-builtins': 'off',
|
||||||
|
'no-useless-escape': 'off',
|
||||||
|
'no-fallthrough': 'off',
|
||||||
|
eqeqeq: 'off',
|
||||||
|
curly: 'off',
|
||||||
|
'no-var': 'error',
|
||||||
|
'prefer-const': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['tests/**'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
globals: {
|
||||||
|
...globals.jest,
|
||||||
|
describe: 'readonly',
|
||||||
|
it: 'readonly',
|
||||||
|
test: 'readonly',
|
||||||
|
expect: 'readonly',
|
||||||
|
beforeAll: 'readonly',
|
||||||
|
afterAll: 'readonly',
|
||||||
|
beforeEach: 'readonly',
|
||||||
|
afterEach: 'readonly',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'no-undef': 'off',
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
Binary file not shown.
@@ -14,7 +14,9 @@ function parseEnvContent(content) {
|
|||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
if (!trimmed || trimmed.startsWith('#')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const equalIndex = trimmed.indexOf('=');
|
const equalIndex = trimmed.indexOf('=');
|
||||||
if (equalIndex > 0) {
|
if (equalIndex > 0) {
|
||||||
|
|||||||
+22
-20
@@ -11,7 +11,7 @@ const defaultDeviceFields = [
|
|||||||
required: false,
|
required: false,
|
||||||
order: 1,
|
order: 1,
|
||||||
visible: false,
|
visible: false,
|
||||||
isSystem: true
|
isSystem: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'name',
|
fieldName: 'name',
|
||||||
@@ -20,7 +20,7 @@ const defaultDeviceFields = [
|
|||||||
required: true,
|
required: true,
|
||||||
order: 2,
|
order: 2,
|
||||||
visible: true,
|
visible: true,
|
||||||
isSystem: true
|
isSystem: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'type',
|
fieldName: 'type',
|
||||||
@@ -35,8 +35,8 @@ const defaultDeviceFields = [
|
|||||||
{ value: 'switch', label: '交换机' },
|
{ value: 'switch', label: '交换机' },
|
||||||
{ value: 'router', label: '路由器' },
|
{ value: 'router', label: '路由器' },
|
||||||
{ value: 'storage', label: '存储设备' },
|
{ value: 'storage', label: '存储设备' },
|
||||||
{ value: 'other', label: '其他设备' }
|
{ value: 'other', label: '其他设备' },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'model',
|
fieldName: 'model',
|
||||||
@@ -45,7 +45,7 @@ const defaultDeviceFields = [
|
|||||||
required: false,
|
required: false,
|
||||||
order: 4,
|
order: 4,
|
||||||
visible: true,
|
visible: true,
|
||||||
isSystem: true
|
isSystem: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'serialNumber',
|
fieldName: 'serialNumber',
|
||||||
@@ -54,7 +54,7 @@ const defaultDeviceFields = [
|
|||||||
required: true,
|
required: true,
|
||||||
order: 5,
|
order: 5,
|
||||||
visible: true,
|
visible: true,
|
||||||
isSystem: true
|
isSystem: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'rackId',
|
fieldName: 'rackId',
|
||||||
@@ -63,7 +63,7 @@ const defaultDeviceFields = [
|
|||||||
required: true,
|
required: true,
|
||||||
order: 6,
|
order: 6,
|
||||||
visible: true,
|
visible: true,
|
||||||
isSystem: true
|
isSystem: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'position',
|
fieldName: 'position',
|
||||||
@@ -72,7 +72,7 @@ const defaultDeviceFields = [
|
|||||||
required: true,
|
required: true,
|
||||||
order: 7,
|
order: 7,
|
||||||
visible: true,
|
visible: true,
|
||||||
isSystem: true
|
isSystem: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'height',
|
fieldName: 'height',
|
||||||
@@ -81,7 +81,7 @@ const defaultDeviceFields = [
|
|||||||
required: true,
|
required: true,
|
||||||
order: 8,
|
order: 8,
|
||||||
visible: true,
|
visible: true,
|
||||||
isSystem: true
|
isSystem: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'powerConsumption',
|
fieldName: 'powerConsumption',
|
||||||
@@ -90,7 +90,7 @@ const defaultDeviceFields = [
|
|||||||
required: true,
|
required: true,
|
||||||
order: 9,
|
order: 9,
|
||||||
visible: true,
|
visible: true,
|
||||||
isSystem: true
|
isSystem: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'status',
|
fieldName: 'status',
|
||||||
@@ -105,8 +105,8 @@ const defaultDeviceFields = [
|
|||||||
{ value: 'maintenance', label: '维护中' },
|
{ value: 'maintenance', label: '维护中' },
|
||||||
{ value: 'offline', label: '离线' },
|
{ value: 'offline', label: '离线' },
|
||||||
{ value: 'fault', label: '故障' },
|
{ value: 'fault', label: '故障' },
|
||||||
{ value: 'idle', label: '空闲' }
|
{ value: 'idle', label: '空闲' },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'purchaseDate',
|
fieldName: 'purchaseDate',
|
||||||
@@ -115,7 +115,7 @@ const defaultDeviceFields = [
|
|||||||
required: false,
|
required: false,
|
||||||
order: 11,
|
order: 11,
|
||||||
visible: true,
|
visible: true,
|
||||||
isSystem: true
|
isSystem: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'warrantyExpiry',
|
fieldName: 'warrantyExpiry',
|
||||||
@@ -124,7 +124,7 @@ const defaultDeviceFields = [
|
|||||||
required: false,
|
required: false,
|
||||||
order: 12,
|
order: 12,
|
||||||
visible: true,
|
visible: true,
|
||||||
isSystem: true
|
isSystem: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'ipAddress',
|
fieldName: 'ipAddress',
|
||||||
@@ -133,7 +133,7 @@ const defaultDeviceFields = [
|
|||||||
required: false,
|
required: false,
|
||||||
order: 13,
|
order: 13,
|
||||||
visible: true,
|
visible: true,
|
||||||
isSystem: false
|
isSystem: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'description',
|
fieldName: 'description',
|
||||||
@@ -142,7 +142,7 @@ const defaultDeviceFields = [
|
|||||||
required: false,
|
required: false,
|
||||||
order: 14,
|
order: 14,
|
||||||
visible: true,
|
visible: true,
|
||||||
isSystem: false
|
isSystem: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'brand',
|
fieldName: 'brand',
|
||||||
@@ -151,8 +151,8 @@ const defaultDeviceFields = [
|
|||||||
required: false,
|
required: false,
|
||||||
order: 15,
|
order: 15,
|
||||||
visible: true,
|
visible: true,
|
||||||
isSystem: false
|
isSystem: false,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// 初始化设备字段
|
// 初始化设备字段
|
||||||
@@ -165,7 +165,7 @@ async function initDeviceFields() {
|
|||||||
for (const field of defaultDeviceFields) {
|
for (const field of defaultDeviceFields) {
|
||||||
// 检查字段是否已存在
|
// 检查字段是否已存在
|
||||||
const existingField = await DeviceField.findOne({
|
const existingField = await DeviceField.findOne({
|
||||||
where: { fieldName: field.fieldName }
|
where: { fieldName: field.fieldName },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!existingField) {
|
if (!existingField) {
|
||||||
@@ -185,7 +185,9 @@ async function initDeviceFields() {
|
|||||||
if (missingOptions.length > 0) {
|
if (missingOptions.length > 0) {
|
||||||
const updatedOptions = [...existingField.options, ...missingOptions];
|
const updatedOptions = [...existingField.options, ...missingOptions];
|
||||||
await existingField.update({ options: updatedOptions });
|
await existingField.update({ options: updatedOptions });
|
||||||
console.log(`补充缺失的 options: ${field.displayName},新增: ${missingOptions.map(o => o.label).join(', ')}`);
|
console.log(
|
||||||
|
`补充缺失的 options: ${field.displayName},新增: ${missingOptions.map(o => o.label).join(', ')}`
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log(`跳过已存在字段: ${field.displayName}`);
|
console.log(`跳过已存在字段: ${field.displayName}`);
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-17
@@ -8,7 +8,7 @@ const defaultTicketFields = [
|
|||||||
fieldType: 'string',
|
fieldType: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
order: 1,
|
order: 1,
|
||||||
visible: true
|
visible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'title',
|
fieldName: 'title',
|
||||||
@@ -16,7 +16,7 @@ const defaultTicketFields = [
|
|||||||
fieldType: 'string',
|
fieldType: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
order: 2,
|
order: 2,
|
||||||
visible: true
|
visible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'deviceName',
|
fieldName: 'deviceName',
|
||||||
@@ -24,7 +24,7 @@ const defaultTicketFields = [
|
|||||||
fieldType: 'string',
|
fieldType: 'string',
|
||||||
required: false,
|
required: false,
|
||||||
order: 3,
|
order: 3,
|
||||||
visible: true
|
visible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'serialNumber',
|
fieldName: 'serialNumber',
|
||||||
@@ -32,7 +32,7 @@ const defaultTicketFields = [
|
|||||||
fieldType: 'string',
|
fieldType: 'string',
|
||||||
required: false,
|
required: false,
|
||||||
order: 4,
|
order: 4,
|
||||||
visible: true
|
visible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'faultCategory',
|
fieldName: 'faultCategory',
|
||||||
@@ -41,7 +41,7 @@ const defaultTicketFields = [
|
|||||||
required: true,
|
required: true,
|
||||||
order: 5,
|
order: 5,
|
||||||
visible: true,
|
visible: true,
|
||||||
options: []
|
options: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'priority',
|
fieldName: 'priority',
|
||||||
@@ -54,8 +54,8 @@ const defaultTicketFields = [
|
|||||||
{ value: 'low', label: '低' },
|
{ value: 'low', label: '低' },
|
||||||
{ value: 'medium', label: '中' },
|
{ value: 'medium', label: '中' },
|
||||||
{ value: 'high', label: '高' },
|
{ value: 'high', label: '高' },
|
||||||
{ value: 'urgent', label: '紧急' }
|
{ value: 'urgent', label: '紧急' },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'status',
|
fieldName: 'status',
|
||||||
@@ -68,8 +68,8 @@ const defaultTicketFields = [
|
|||||||
{ value: 'pending', label: '待处理' },
|
{ value: 'pending', label: '待处理' },
|
||||||
{ value: 'in_progress', label: '处理中' },
|
{ value: 'in_progress', label: '处理中' },
|
||||||
{ value: 'completed', label: '已完成' },
|
{ value: 'completed', label: '已完成' },
|
||||||
{ value: 'closed', label: '已关闭' }
|
{ value: 'closed', label: '已关闭' },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'reporterName',
|
fieldName: 'reporterName',
|
||||||
@@ -77,7 +77,7 @@ const defaultTicketFields = [
|
|||||||
fieldType: 'string',
|
fieldType: 'string',
|
||||||
required: false,
|
required: false,
|
||||||
order: 8,
|
order: 8,
|
||||||
visible: true
|
visible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'createdAt',
|
fieldName: 'createdAt',
|
||||||
@@ -85,7 +85,7 @@ const defaultTicketFields = [
|
|||||||
fieldType: 'datetime',
|
fieldType: 'datetime',
|
||||||
required: false,
|
required: false,
|
||||||
order: 9,
|
order: 9,
|
||||||
visible: true
|
visible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'expectedCompletionDate',
|
fieldName: 'expectedCompletionDate',
|
||||||
@@ -93,7 +93,7 @@ const defaultTicketFields = [
|
|||||||
fieldType: 'datetime',
|
fieldType: 'datetime',
|
||||||
required: false,
|
required: false,
|
||||||
order: 10,
|
order: 10,
|
||||||
visible: true
|
visible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'completionDate',
|
fieldName: 'completionDate',
|
||||||
@@ -101,7 +101,7 @@ const defaultTicketFields = [
|
|||||||
fieldType: 'datetime',
|
fieldType: 'datetime',
|
||||||
required: false,
|
required: false,
|
||||||
order: 11,
|
order: 11,
|
||||||
visible: true
|
visible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'description',
|
fieldName: 'description',
|
||||||
@@ -110,7 +110,7 @@ const defaultTicketFields = [
|
|||||||
required: false,
|
required: false,
|
||||||
order: 12,
|
order: 12,
|
||||||
visible: true,
|
visible: true,
|
||||||
placeholder: '请详细描述故障情况'
|
placeholder: '请详细描述故障情况',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'resolution',
|
fieldName: 'resolution',
|
||||||
@@ -119,7 +119,7 @@ const defaultTicketFields = [
|
|||||||
required: false,
|
required: false,
|
||||||
order: 13,
|
order: 13,
|
||||||
visible: true,
|
visible: true,
|
||||||
placeholder: '请输入解决方案'
|
placeholder: '请输入解决方案',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'location',
|
fieldName: 'location',
|
||||||
@@ -127,8 +127,8 @@ const defaultTicketFields = [
|
|||||||
fieldType: 'string',
|
fieldType: 'string',
|
||||||
required: false,
|
required: false,
|
||||||
order: 14,
|
order: 14,
|
||||||
visible: false
|
visible: false,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
async function initializeTicketFields() {
|
async function initializeTicketFields() {
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ module.exports = {
|
|||||||
'models/**/*.js',
|
'models/**/*.js',
|
||||||
'utils/**/*.js',
|
'utils/**/*.js',
|
||||||
'routes/**/*.js',
|
'routes/**/*.js',
|
||||||
'!models/ticketIndex.js'
|
'!models/ticketIndex.js',
|
||||||
],
|
],
|
||||||
coverageDirectory: 'coverage',
|
coverageDirectory: 'coverage',
|
||||||
verbose: true,
|
verbose: true,
|
||||||
testTimeout: 30000,
|
testTimeout: 30000,
|
||||||
setupFiles: ['./tests/setupEnv.js'],
|
setupFiles: ['./tests/setupEnv.js'],
|
||||||
setupFilesAfterEnv: ['./tests/setup.js']
|
setupFilesAfterEnv: ['./tests/setup.js'],
|
||||||
};
|
};
|
||||||
|
|||||||
+26
-24
@@ -13,15 +13,15 @@ function getJwtSecret() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (envSecret.length < 32) {
|
if (envSecret.length < 32) {
|
||||||
throw new Error('[致命错误] 生产环境 JWT_SECRET 长度必须至少32位!当前长度:' + envSecret.length);
|
throw new Error(
|
||||||
|
'[致命错误] 生产环境 JWT_SECRET 长度必须至少32位!当前长度:' + envSecret.length
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return envSecret;
|
return envSecret;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!envSecret) {
|
if (!envSecret) {
|
||||||
throw new Error(
|
throw new Error('[错误] JWT_SECRET 未配置,请检查 initConfig.js 是否正确执行');
|
||||||
'[错误] JWT_SECRET 未配置,请检查 initConfig.js 是否正确执行'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return envSecret;
|
return envSecret;
|
||||||
@@ -30,14 +30,16 @@ function getJwtSecret() {
|
|||||||
const JWT_SECRET = getJwtSecret();
|
const JWT_SECRET = getJwtSecret();
|
||||||
const TOKEN_EXPIRY = process.env.TOKEN_EXPIRY || '24h';
|
const TOKEN_EXPIRY = process.env.TOKEN_EXPIRY || '24h';
|
||||||
|
|
||||||
const getBrowserInfo = (userAgent) => {
|
const getBrowserInfo = userAgent => {
|
||||||
let device = 'Desktop';
|
let device = 'Desktop';
|
||||||
let browser = 'Unknown';
|
let browser = 'Unknown';
|
||||||
let os = 'Unknown';
|
let os = 'Unknown';
|
||||||
|
|
||||||
if (/Mobile|Android|iPhone|iPad|iPod/i.test(userAgent)) {
|
if (/Mobile|Android|iPhone|iPad|iPod/i.test(userAgent)) {
|
||||||
device = 'Mobile';
|
device = 'Mobile';
|
||||||
if (/iPad/i.test(userAgent)) device = 'Tablet';
|
if (/iPad/i.test(userAgent)) {
|
||||||
|
device = 'Tablet';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/Firefox/i.test(userAgent)) {
|
if (/Firefox/i.test(userAgent)) {
|
||||||
@@ -67,19 +69,19 @@ const getBrowserInfo = (userAgent) => {
|
|||||||
return { device, browser, os };
|
return { device, browser, os };
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateToken = (user) => {
|
const generateToken = user => {
|
||||||
return jwt.sign(
|
return jwt.sign(
|
||||||
{
|
{
|
||||||
userId: user.userId,
|
userId: user.userId,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
roleId: user.roleId
|
roleId: user.roleId,
|
||||||
},
|
},
|
||||||
JWT_SECRET,
|
JWT_SECRET,
|
||||||
{ expiresIn: TOKEN_EXPIRY }
|
{ expiresIn: TOKEN_EXPIRY }
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const verifyToken = (token) => {
|
const verifyToken = token => {
|
||||||
try {
|
try {
|
||||||
return jwt.verify(token, JWT_SECRET);
|
return jwt.verify(token, JWT_SECRET);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -103,7 +105,7 @@ const authMiddleware = async (req, res, next) => {
|
|||||||
if (!token) {
|
if (!token) {
|
||||||
return res.status(401).json({
|
return res.status(401).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '未提供认证令牌'
|
message: '未提供认证令牌',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +114,7 @@ const authMiddleware = async (req, res, next) => {
|
|||||||
if (!decoded) {
|
if (!decoded) {
|
||||||
return res.status(401).json({
|
return res.status(401).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '令牌无效或已过期'
|
message: '令牌无效或已过期',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,11 +128,11 @@ const authMiddleware = async (req, res, next) => {
|
|||||||
userId: decoded.userId,
|
userId: decoded.userId,
|
||||||
error: dbError.message,
|
error: dbError.message,
|
||||||
stack: dbError.stack,
|
stack: dbError.stack,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '数据库查询失败,请稍后重试'
|
message: '数据库查询失败,请稍后重试',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,11 +140,11 @@ const authMiddleware = async (req, res, next) => {
|
|||||||
console.warn('[认证中间件] 用户不存在:', {
|
console.warn('[认证中间件] 用户不存在:', {
|
||||||
userId: decoded.userId,
|
userId: decoded.userId,
|
||||||
username: decoded.username,
|
username: decoded.username,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
return res.status(401).json({
|
return res.status(401).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '用户不存在'
|
message: '用户不存在',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,11 +152,11 @@ const authMiddleware = async (req, res, next) => {
|
|||||||
console.warn('[认证中间件] 账户已被锁定:', {
|
console.warn('[认证中间件] 账户已被锁定:', {
|
||||||
userId: user.userId,
|
userId: user.userId,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
return res.status(403).json({
|
return res.status(403).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '账户已被锁定'
|
message: '账户已被锁定',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,11 +164,11 @@ const authMiddleware = async (req, res, next) => {
|
|||||||
console.warn('[认证中间件] 账户已禁用:', {
|
console.warn('[认证中间件] 账户已禁用:', {
|
||||||
userId: user.userId,
|
userId: user.userId,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
return res.status(403).json({
|
return res.status(403).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '账户已禁用'
|
message: '账户已禁用',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,11 +183,11 @@ const authMiddleware = async (req, res, next) => {
|
|||||||
url: req?.url,
|
url: req?.url,
|
||||||
method: req?.method,
|
method: req?.method,
|
||||||
ip: req?.ip,
|
ip: req?.ip,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '认证失败,请稍后重试'
|
message: '认证失败,请稍后重试',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -207,7 +209,7 @@ const optionalAuth = async (req, res, next) => {
|
|||||||
console.warn('[可选认证] 数据库查询失败:', {
|
console.warn('[可选认证] 数据库查询失败:', {
|
||||||
userId: decoded.userId,
|
userId: decoded.userId,
|
||||||
error: dbError.message,
|
error: dbError.message,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
// 继续执行,不设置用户信息
|
// 继续执行,不设置用户信息
|
||||||
next();
|
next();
|
||||||
@@ -227,7 +229,7 @@ const optionalAuth = async (req, res, next) => {
|
|||||||
console.warn('[可选认证] 认证失败(已忽略):', {
|
console.warn('[可选认证] 认证失败(已忽略):', {
|
||||||
error: error.message,
|
error: error.message,
|
||||||
url: req?.url,
|
url: req?.url,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
@@ -239,5 +241,5 @@ module.exports = {
|
|||||||
authMiddleware,
|
authMiddleware,
|
||||||
optionalAuth,
|
optionalAuth,
|
||||||
JWT_SECRET,
|
JWT_SECRET,
|
||||||
TOKEN_EXPIRY
|
TOKEN_EXPIRY,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const validate = (schema, source = 'body') => {
|
|||||||
const result = schema.validate(data, {
|
const result = schema.validate(data, {
|
||||||
abortEarly: false,
|
abortEarly: false,
|
||||||
stripUnknown: true,
|
stripUnknown: true,
|
||||||
allowUnknown: source === 'query'
|
allowUnknown: source === 'query',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result && typeof result.then === 'function') {
|
if (result && typeof result.then === 'function') {
|
||||||
@@ -17,11 +17,11 @@ const validate = (schema, source = 'body') => {
|
|||||||
} else if (result && result.error) {
|
} else if (result && result.error) {
|
||||||
const errorMessages = result.error.details.map(detail => ({
|
const errorMessages = result.error.details.map(detail => ({
|
||||||
field: detail.path.join('.'),
|
field: detail.path.join('.'),
|
||||||
message: detail.message
|
message: detail.message,
|
||||||
}));
|
}));
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
error: '参数验证失败',
|
error: '参数验证失败',
|
||||||
details: errorMessages
|
details: errorMessages,
|
||||||
});
|
});
|
||||||
} else if (result && result.value !== undefined) {
|
} else if (result && result.value !== undefined) {
|
||||||
value = result.value;
|
value = result.value;
|
||||||
@@ -32,22 +32,22 @@ const validate = (schema, source = 'body') => {
|
|||||||
value = await schema.validateAsync(data, {
|
value = await schema.validateAsync(data, {
|
||||||
abortEarly: false,
|
abortEarly: false,
|
||||||
stripUnknown: true,
|
stripUnknown: true,
|
||||||
allowUnknown: source === 'query'
|
allowUnknown: source === 'query',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const result = schema.validate(data, {
|
const result = schema.validate(data, {
|
||||||
abortEarly: false,
|
abortEarly: false,
|
||||||
stripUnknown: true,
|
stripUnknown: true,
|
||||||
allowUnknown: source === 'query'
|
allowUnknown: source === 'query',
|
||||||
});
|
});
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
const errorMessages = result.error.details.map(detail => ({
|
const errorMessages = result.error.details.map(detail => ({
|
||||||
field: detail.path.join('.'),
|
field: detail.path.join('.'),
|
||||||
message: detail.message
|
message: detail.message,
|
||||||
}));
|
}));
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
error: '参数验证失败',
|
error: '参数验证失败',
|
||||||
details: errorMessages
|
details: errorMessages,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
value = result.value;
|
value = result.value;
|
||||||
@@ -64,30 +64,30 @@ const validate = (schema, source = 'body') => {
|
|||||||
if (error.details) {
|
if (error.details) {
|
||||||
const errorMessages = error.details.map(detail => ({
|
const errorMessages = error.details.map(detail => ({
|
||||||
field: detail.path.join('.'),
|
field: detail.path.join('.'),
|
||||||
message: detail.message
|
message: detail.message,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
error: '参数验证失败',
|
error: '参数验证失败',
|
||||||
details: errorMessages
|
details: errorMessages,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error('验证中间件错误:', error);
|
console.error('验证中间件错误:', error);
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
error: '验证过程发生错误',
|
error: '验证过程发生错误',
|
||||||
message: error.message
|
message: error.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateQuery = (schema) => validate(schema, 'query');
|
const validateQuery = schema => validate(schema, 'query');
|
||||||
|
|
||||||
const validateBody = (schema) => validate(schema, 'body');
|
const validateBody = schema => validate(schema, 'body');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
validate,
|
validate,
|
||||||
validateQuery,
|
validateQuery,
|
||||||
validateBody
|
validateBody,
|
||||||
};
|
};
|
||||||
|
|||||||
+25
-22
@@ -1,85 +1,87 @@
|
|||||||
|
|
||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require('sequelize');
|
||||||
const { sequelize } = require('../db');
|
const { sequelize } = require('../db');
|
||||||
|
|
||||||
const BackupLog = sequelize.define('BackupLog', {
|
const BackupLog = sequelize.define(
|
||||||
|
'BackupLog',
|
||||||
|
{
|
||||||
id: {
|
id: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
autoIncrement: true
|
autoIncrement: true,
|
||||||
},
|
},
|
||||||
logType: {
|
logType: {
|
||||||
type: DataTypes.ENUM('auto', 'manual'),
|
type: DataTypes.ENUM('auto', 'manual'),
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
comment: '备份类型:auto自动,manual手动'
|
comment: '备份类型:auto自动,manual手动',
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: DataTypes.ENUM('pending', 'running', 'success', 'failed'),
|
type: DataTypes.ENUM('pending', 'running', 'success', 'failed'),
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: 'pending',
|
defaultValue: 'pending',
|
||||||
comment: '状态:pending待执行,running执行中,success成功,failed失败'
|
comment: '状态:pending待执行,running执行中,success成功,failed失败',
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '备份描述'
|
comment: '备份描述',
|
||||||
},
|
},
|
||||||
backupType: {
|
backupType: {
|
||||||
type: DataTypes.ENUM('full', 'incremental'),
|
type: DataTypes.ENUM('full', 'incremental'),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '备份类型:full全量,incremental增量'
|
comment: '备份类型:full全量,incremental增量',
|
||||||
},
|
},
|
||||||
filename: {
|
filename: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '备份文件名'
|
comment: '备份文件名',
|
||||||
},
|
},
|
||||||
filePath: {
|
filePath: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '备份文件路径'
|
comment: '备份文件路径',
|
||||||
},
|
},
|
||||||
fileSize: {
|
fileSize: {
|
||||||
type: DataTypes.BIGINT,
|
type: DataTypes.BIGINT,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '文件大小(字节)'
|
comment: '文件大小(字节)',
|
||||||
},
|
},
|
||||||
errorMessage: {
|
errorMessage: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '错误信息'
|
comment: '错误信息',
|
||||||
},
|
},
|
||||||
startTime: {
|
startTime: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '开始时间'
|
comment: '开始时间',
|
||||||
},
|
},
|
||||||
endTime: {
|
endTime: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '结束时间'
|
comment: '结束时间',
|
||||||
},
|
},
|
||||||
duration: {
|
duration: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '执行时长(毫秒)'
|
comment: '执行时长(毫秒)',
|
||||||
},
|
},
|
||||||
includeFiles: {
|
includeFiles: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
comment: '是否包含文件'
|
comment: '是否包含文件',
|
||||||
},
|
},
|
||||||
compressed: {
|
compressed: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
comment: '是否压缩'
|
comment: '是否压缩',
|
||||||
},
|
},
|
||||||
remoteUploads: {
|
remoteUploads: {
|
||||||
type: DataTypes.JSON,
|
type: DataTypes.JSON,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '远端上传结果'
|
comment: '远端上传结果',
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
tableName: 'backup_logs',
|
tableName: 'backup_logs',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
comment: '备份日志表',
|
comment: '备份日志表',
|
||||||
@@ -87,8 +89,9 @@ const BackupLog = sequelize.define('BackupLog', {
|
|||||||
{ fields: ['logType'] },
|
{ fields: ['logType'] },
|
||||||
{ fields: ['status'] },
|
{ fields: ['status'] },
|
||||||
{ fields: ['createdAt'] },
|
{ fields: ['createdAt'] },
|
||||||
{ fields: ['logType', 'createdAt'] }
|
{ fields: ['logType', 'createdAt'] },
|
||||||
]
|
],
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = BackupLog;
|
module.exports = BackupLog;
|
||||||
|
|||||||
+15
-14
@@ -1,40 +1,41 @@
|
|||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require('sequelize');
|
||||||
const { sequelize } = require('../db');
|
const { sequelize } = require('../db');
|
||||||
|
|
||||||
const Business = sequelize.define('Business', {
|
const Business = sequelize.define(
|
||||||
|
'Business',
|
||||||
|
{
|
||||||
businessId: {
|
businessId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true
|
unique: true,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: DataTypes.ENUM('active', 'offline'),
|
type: DataTypes.ENUM('active', 'offline'),
|
||||||
defaultValue: 'active'
|
defaultValue: 'active',
|
||||||
},
|
},
|
||||||
offlineDate: {
|
offlineDate: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
offlineReason: {
|
offlineReason: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
tableName: 'businesses',
|
tableName: 'businesses',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
indexes: [
|
indexes: [{ fields: ['status'] }, { fields: ['name'] }],
|
||||||
{ fields: ['status'] },
|
}
|
||||||
{ fields: ['name'] }
|
);
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = Business;
|
module.exports = Business;
|
||||||
|
|||||||
+21
-17
@@ -2,56 +2,59 @@ const { DataTypes } = require('sequelize');
|
|||||||
const { sequelize } = require('../db');
|
const { sequelize } = require('../db');
|
||||||
const Device = require('./Device');
|
const Device = require('./Device');
|
||||||
|
|
||||||
const Cable = sequelize.define('Cable', {
|
const Cable = sequelize.define(
|
||||||
|
'Cable',
|
||||||
|
{
|
||||||
cableId: {
|
cableId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true
|
unique: true,
|
||||||
},
|
},
|
||||||
sourceDeviceId: {
|
sourceDeviceId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
references: {
|
references: {
|
||||||
model: 'devices',
|
model: 'devices',
|
||||||
key: 'deviceId'
|
key: 'deviceId',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
sourcePort: {
|
sourcePort: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
targetDeviceId: {
|
targetDeviceId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
references: {
|
references: {
|
||||||
model: 'devices',
|
model: 'devices',
|
||||||
key: 'deviceId'
|
key: 'deviceId',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
targetPort: {
|
targetPort: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
cableType: {
|
cableType: {
|
||||||
type: DataTypes.ENUM('ethernet', 'fiber', 'copper'),
|
type: DataTypes.ENUM('ethernet', 'fiber', 'copper'),
|
||||||
defaultValue: 'ethernet',
|
defaultValue: 'ethernet',
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
cableLength: {
|
cableLength: {
|
||||||
type: DataTypes.DECIMAL(5, 2),
|
type: DataTypes.DECIMAL(5, 2),
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: DataTypes.ENUM('normal', 'fault', 'disconnected'),
|
type: DataTypes.ENUM('normal', 'fault', 'disconnected'),
|
||||||
defaultValue: 'normal',
|
defaultValue: 'normal',
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
tableName: 'cables',
|
tableName: 'cables',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
indexes: [
|
indexes: [
|
||||||
@@ -59,9 +62,10 @@ const Cable = sequelize.define('Cable', {
|
|||||||
{ fields: ['targetDeviceId'] },
|
{ fields: ['targetDeviceId'] },
|
||||||
{ fields: ['status'] },
|
{ fields: ['status'] },
|
||||||
{ fields: ['cableType'] },
|
{ fields: ['cableType'] },
|
||||||
{ fields: ['sourceDeviceId', 'targetDeviceId'] }
|
{ fields: ['sourceDeviceId', 'targetDeviceId'] },
|
||||||
]
|
],
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
Cable.belongsTo(Device, { foreignKey: 'sourceDeviceId', as: 'sourceDevice' });
|
Cable.belongsTo(Device, { foreignKey: 'sourceDeviceId', as: 'sourceDevice' });
|
||||||
Cable.belongsTo(Device, { foreignKey: 'targetDeviceId', as: 'targetDevice' });
|
Cable.belongsTo(Device, { foreignKey: 'targetDeviceId', as: 'targetDevice' });
|
||||||
|
|||||||
@@ -1,81 +1,85 @@
|
|||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require('sequelize');
|
||||||
const { sequelize } = require('../db');
|
const { sequelize } = require('../db');
|
||||||
|
|
||||||
const Consumable = sequelize.define('Consumable', {
|
const Consumable = sequelize.define(
|
||||||
|
'Consumable',
|
||||||
|
{
|
||||||
consumableId: {
|
consumableId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
category: {
|
category: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
unit: {
|
unit: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: '个'
|
defaultValue: '个',
|
||||||
},
|
},
|
||||||
currentStock: {
|
currentStock: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: 0
|
defaultValue: 0,
|
||||||
},
|
},
|
||||||
minStock: {
|
minStock: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: 10
|
defaultValue: 10,
|
||||||
},
|
},
|
||||||
maxStock: {
|
maxStock: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
comment: '最大库存,0表示无限制'
|
comment: '最大库存,0表示无限制',
|
||||||
},
|
},
|
||||||
unitPrice: {
|
unitPrice: {
|
||||||
type: DataTypes.DECIMAL(10, 2),
|
type: DataTypes.DECIMAL(10, 2),
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: 0
|
defaultValue: 0,
|
||||||
},
|
},
|
||||||
supplier: {
|
supplier: {
|
||||||
type: DataTypes.STRING
|
type: DataTypes.STRING,
|
||||||
},
|
},
|
||||||
location: {
|
location: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
comment: '存放位置'
|
comment: '存放位置',
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: DataTypes.TEXT
|
type: DataTypes.TEXT,
|
||||||
},
|
},
|
||||||
snList: {
|
snList: {
|
||||||
type: DataTypes.JSON,
|
type: DataTypes.JSON,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
defaultValue: [],
|
defaultValue: [],
|
||||||
comment: 'SN序列号列表,JSON数组格式'
|
comment: 'SN序列号列表,JSON数组格式',
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
defaultValue: 'active'
|
defaultValue: 'active',
|
||||||
},
|
},
|
||||||
version: {
|
version: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
comment: '乐观锁版本号'
|
comment: '乐观锁版本号',
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
tableName: 'consumables',
|
tableName: 'consumables',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
indexes: [
|
indexes: [
|
||||||
{ fields: ['category'] },
|
{ fields: ['category'] },
|
||||||
{ fields: ['status'] },
|
{ fields: ['status'] },
|
||||||
{ fields: ['category', 'status'] },
|
{ fields: ['category', 'status'] },
|
||||||
{ fields: ['updatedAt'] }
|
{ fields: ['updatedAt'] },
|
||||||
]
|
],
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = Consumable;
|
module.exports = Consumable;
|
||||||
|
|||||||
@@ -1,35 +1,39 @@
|
|||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require('sequelize');
|
||||||
const { sequelize } = require('../db');
|
const { sequelize } = require('../db');
|
||||||
|
|
||||||
const ConsumableCategory = sequelize.define('ConsumableCategory', {
|
const ConsumableCategory = sequelize.define(
|
||||||
|
'ConsumableCategory',
|
||||||
|
{
|
||||||
id: {
|
id: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
autoIncrement: true
|
autoIncrement: true,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true,
|
unique: true,
|
||||||
comment: '分类名称'
|
comment: '分类名称',
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
comment: '分类描述'
|
comment: '分类描述',
|
||||||
},
|
},
|
||||||
sortOrder: {
|
sortOrder: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
comment: '排序顺序'
|
comment: '排序顺序',
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
defaultValue: 'active',
|
defaultValue: 'active',
|
||||||
comment: '状态: active-启用, inactive-停用'
|
comment: '状态: active-启用, inactive-停用',
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
tableName: 'consumable_categories',
|
tableName: 'consumable_categories',
|
||||||
timestamps: true
|
timestamps: true,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = ConsumableCategory;
|
module.exports = ConsumableCategory;
|
||||||
|
|||||||
@@ -1,100 +1,103 @@
|
|||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require('sequelize');
|
||||||
const { sequelize } = require('../db');
|
const { sequelize } = require('../db');
|
||||||
|
|
||||||
const ConsumableLog = sequelize.define('ConsumableLog', {
|
const ConsumableLog = sequelize.define(
|
||||||
|
'ConsumableLog',
|
||||||
|
{
|
||||||
id: {
|
id: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
autoIncrement: true
|
autoIncrement: true,
|
||||||
},
|
},
|
||||||
consumableId: {
|
consumableId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
comment: '耗材ID'
|
comment: '耗材ID',
|
||||||
},
|
},
|
||||||
consumableName: {
|
consumableName: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
comment: '耗材名称'
|
comment: '耗材名称',
|
||||||
},
|
},
|
||||||
operationType: {
|
operationType: {
|
||||||
type: DataTypes.ENUM('in', 'out', 'create', 'update', 'delete', 'adjust', 'import'),
|
type: DataTypes.ENUM('in', 'out', 'create', 'update', 'delete', 'adjust', 'import'),
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
comment: '操作类型'
|
comment: '操作类型',
|
||||||
},
|
},
|
||||||
quantity: {
|
quantity: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
comment: '变动数量(入库为正,出库为负)'
|
comment: '变动数量(入库为正,出库为负)',
|
||||||
},
|
},
|
||||||
previousStock: {
|
previousStock: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
comment: '操作前库存'
|
comment: '操作前库存',
|
||||||
},
|
},
|
||||||
currentStock: {
|
currentStock: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
comment: '操作后库存'
|
comment: '操作后库存',
|
||||||
},
|
},
|
||||||
operator: {
|
operator: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
comment: '操作人'
|
comment: '操作人',
|
||||||
},
|
},
|
||||||
reason: {
|
reason: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
comment: '操作原因'
|
comment: '操作原因',
|
||||||
},
|
},
|
||||||
notes: {
|
notes: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
comment: '备注'
|
comment: '备注',
|
||||||
},
|
},
|
||||||
relatedId: {
|
relatedId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
comment: '关联ID(如订单号、盘点ID等)'
|
comment: '关联ID(如订单号、盘点ID等)',
|
||||||
},
|
},
|
||||||
isEditable: {
|
isEditable: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
comment: '是否可编辑(创建、导入的记录可编辑,系统生成的出入库记录不可编辑)'
|
comment: '是否可编辑(创建、导入的记录可编辑,系统生成的出入库记录不可编辑)',
|
||||||
},
|
},
|
||||||
originalLogId: {
|
originalLogId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '原始日志ID(用于追踪修改历史链)'
|
comment: '原始日志ID(用于追踪修改历史链)',
|
||||||
},
|
},
|
||||||
modifiedBy: {
|
modifiedBy: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '修改人'
|
comment: '修改人',
|
||||||
},
|
},
|
||||||
modifiedAt: {
|
modifiedAt: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '修改时间'
|
comment: '修改时间',
|
||||||
},
|
},
|
||||||
modificationReason: {
|
modificationReason: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '修改原因'
|
comment: '修改原因',
|
||||||
},
|
},
|
||||||
isConsumableDeleted: {
|
isConsumableDeleted: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
comment: '关联耗材是否已被删除'
|
comment: '关联耗材是否已被删除',
|
||||||
},
|
},
|
||||||
consumableSnapshot: {
|
consumableSnapshot: {
|
||||||
type: DataTypes.JSON,
|
type: DataTypes.JSON,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '耗材快照信息(分类、单位、供应商等),用于耗材删除后追溯'
|
comment: '耗材快照信息(分类、单位、供应商等),用于耗材删除后追溯',
|
||||||
},
|
},
|
||||||
snList: {
|
snList: {
|
||||||
type: DataTypes.JSON,
|
type: DataTypes.JSON,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
defaultValue: [],
|
defaultValue: [],
|
||||||
comment: '本次操作的SN序列号列表'
|
comment: '本次操作的SN序列号列表',
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
tableName: 'consumable_logs',
|
tableName: 'consumable_logs',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
comment: '耗材操作日志表',
|
comment: '耗材操作日志表',
|
||||||
@@ -105,8 +108,9 @@ const ConsumableLog = sequelize.define('ConsumableLog', {
|
|||||||
{ fields: ['consumableId', 'createdAt'] },
|
{ fields: ['consumableId', 'createdAt'] },
|
||||||
{ fields: ['originalLogId'] },
|
{ fields: ['originalLogId'] },
|
||||||
{ fields: ['isEditable'] },
|
{ fields: ['isEditable'] },
|
||||||
{ fields: ['isConsumableDeleted'] }
|
{ fields: ['isConsumableDeleted'] },
|
||||||
]
|
],
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = ConsumableLog;
|
module.exports = ConsumableLog;
|
||||||
|
|||||||
@@ -5,73 +5,76 @@ const { sequelize } = require('../db');
|
|||||||
* 耗材操作日志归档表
|
* 耗材操作日志归档表
|
||||||
* 用于存储被删除耗材的历史操作记录
|
* 用于存储被删除耗材的历史操作记录
|
||||||
*/
|
*/
|
||||||
const ConsumableLogArchive = sequelize.define('ConsumableLogArchive', {
|
const ConsumableLogArchive = sequelize.define(
|
||||||
|
'ConsumableLogArchive',
|
||||||
|
{
|
||||||
id: {
|
id: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
autoIncrement: true
|
autoIncrement: true,
|
||||||
},
|
},
|
||||||
archiveId: {
|
archiveId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
comment: '归档记录唯一标识'
|
comment: '归档记录唯一标识',
|
||||||
},
|
},
|
||||||
consumableId: {
|
consumableId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
comment: '被删除的耗材ID'
|
comment: '被删除的耗材ID',
|
||||||
},
|
},
|
||||||
consumableName: {
|
consumableName: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
comment: '耗材名称'
|
comment: '耗材名称',
|
||||||
},
|
},
|
||||||
consumableSnapshot: {
|
consumableSnapshot: {
|
||||||
type: DataTypes.JSON,
|
type: DataTypes.JSON,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '耗材快照信息'
|
comment: '耗材快照信息',
|
||||||
},
|
},
|
||||||
totalOperations: {
|
totalOperations: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
comment: '操作记录总数'
|
comment: '操作记录总数',
|
||||||
},
|
},
|
||||||
firstOperationAt: {
|
firstOperationAt: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
comment: '首次操作时间'
|
comment: '首次操作时间',
|
||||||
},
|
},
|
||||||
lastOperationAt: {
|
lastOperationAt: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
comment: '最后操作时间'
|
comment: '最后操作时间',
|
||||||
},
|
},
|
||||||
totalInQuantity: {
|
totalInQuantity: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
comment: '总入库数量'
|
comment: '总入库数量',
|
||||||
},
|
},
|
||||||
totalOutQuantity: {
|
totalOutQuantity: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
comment: '总出库数量'
|
comment: '总出库数量',
|
||||||
},
|
},
|
||||||
finalStock: {
|
finalStock: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
comment: '删除时库存'
|
comment: '删除时库存',
|
||||||
},
|
},
|
||||||
deletedBy: {
|
deletedBy: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
comment: '删除人'
|
comment: '删除人',
|
||||||
},
|
},
|
||||||
deletedAt: {
|
deletedAt: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
comment: '删除时间'
|
comment: '删除时间',
|
||||||
},
|
},
|
||||||
deleteReason: {
|
deleteReason: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
comment: '删除原因'
|
comment: '删除原因',
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
tableName: 'consumable_log_archives',
|
tableName: 'consumable_log_archives',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
comment: '耗材操作日志归档表',
|
comment: '耗材操作日志归档表',
|
||||||
@@ -79,8 +82,9 @@ const ConsumableLogArchive = sequelize.define('ConsumableLogArchive', {
|
|||||||
{ fields: ['consumableId'] },
|
{ fields: ['consumableId'] },
|
||||||
{ fields: ['archiveId'] },
|
{ fields: ['archiveId'] },
|
||||||
{ fields: ['deletedAt'] },
|
{ fields: ['deletedAt'] },
|
||||||
{ fields: ['consumableId', 'deletedAt'] }
|
{ fields: ['consumableId', 'deletedAt'] },
|
||||||
]
|
],
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = ConsumableLogArchive;
|
module.exports = ConsumableLogArchive;
|
||||||
|
|||||||
@@ -2,75 +2,75 @@ const { DataTypes } = require('sequelize');
|
|||||||
const { sequelize } = require('../db');
|
const { sequelize } = require('../db');
|
||||||
const Consumable = require('./Consumable');
|
const Consumable = require('./Consumable');
|
||||||
|
|
||||||
const ConsumableRecord = sequelize.define('ConsumableRecord', {
|
const ConsumableRecord = sequelize.define(
|
||||||
|
'ConsumableRecord',
|
||||||
|
{
|
||||||
recordId: {
|
recordId: {
|
||||||
type: DataTypes.UUID,
|
type: DataTypes.UUID,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
defaultValue: DataTypes.UUIDV4,
|
defaultValue: DataTypes.UUIDV4,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
consumableId: {
|
consumableId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
references: {
|
references: {
|
||||||
model: Consumable,
|
model: Consumable,
|
||||||
key: 'consumableId'
|
key: 'consumableId',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
type: DataTypes.ENUM('in', 'out'),
|
type: DataTypes.ENUM('in', 'out'),
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
quantity: {
|
quantity: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
previousStock: {
|
previousStock: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
currentStock: {
|
currentStock: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
operator: {
|
operator: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
reason: {
|
reason: {
|
||||||
type: DataTypes.STRING
|
type: DataTypes.STRING,
|
||||||
},
|
},
|
||||||
recipient: {
|
recipient: {
|
||||||
type: DataTypes.STRING
|
type: DataTypes.STRING,
|
||||||
},
|
},
|
||||||
notes: {
|
notes: {
|
||||||
type: DataTypes.TEXT
|
type: DataTypes.TEXT,
|
||||||
},
|
},
|
||||||
snList: {
|
snList: {
|
||||||
type: DataTypes.JSON,
|
type: DataTypes.JSON,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
defaultValue: [],
|
defaultValue: [],
|
||||||
comment: '本次操作的SN序列号列表'
|
comment: '本次操作的SN序列号列表',
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
tableName: 'consumable_records',
|
tableName: 'consumable_records',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
indexes: [
|
indexes: [{ fields: ['consumableId'] }, { fields: ['type'] }, { fields: ['createdAt'] }],
|
||||||
{ fields: ['consumableId'] },
|
}
|
||||||
{ fields: ['type'] },
|
);
|
||||||
{ fields: ['createdAt'] }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
ConsumableRecord.belongsTo(Consumable, {
|
ConsumableRecord.belongsTo(Consumable, {
|
||||||
foreignKey: 'consumableId',
|
foreignKey: 'consumableId',
|
||||||
as: 'consumable',
|
as: 'consumable',
|
||||||
onDelete: 'CASCADE'
|
onDelete: 'CASCADE',
|
||||||
});
|
});
|
||||||
Consumable.hasMany(ConsumableRecord, {
|
Consumable.hasMany(ConsumableRecord, {
|
||||||
foreignKey: 'consumableId',
|
foreignKey: 'consumableId',
|
||||||
as: 'records',
|
as: 'records',
|
||||||
onDelete: 'CASCADE'
|
onDelete: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = ConsumableRecord;
|
module.exports = ConsumableRecord;
|
||||||
|
|||||||
+30
-26
@@ -3,94 +3,97 @@ const { sequelize } = require('../db');
|
|||||||
const Rack = require('./Rack');
|
const Rack = require('./Rack');
|
||||||
const Warehouse = require('./Warehouse');
|
const Warehouse = require('./Warehouse');
|
||||||
|
|
||||||
const Device = sequelize.define('Device', {
|
const Device = sequelize.define(
|
||||||
|
'Device',
|
||||||
|
{
|
||||||
deviceId: {
|
deviceId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true
|
unique: true,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
model: {
|
model: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
serialNumber: {
|
serialNumber: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
unique: true
|
unique: true,
|
||||||
},
|
},
|
||||||
rackId: {
|
rackId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
position: {
|
position: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
height: {
|
height: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
defaultValue: 1
|
defaultValue: 1,
|
||||||
},
|
},
|
||||||
powerConsumption: {
|
powerConsumption: {
|
||||||
type: DataTypes.FLOAT,
|
type: DataTypes.FLOAT,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
defaultValue: 0
|
defaultValue: 0,
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
defaultValue: 'offline'
|
defaultValue: 'offline',
|
||||||
},
|
},
|
||||||
isIdle: {
|
isIdle: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
defaultValue: false
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
idleDate: {
|
idleDate: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
idleReason: {
|
idleReason: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
warehouseId: {
|
warehouseId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
sourceType: {
|
sourceType: {
|
||||||
type: DataTypes.ENUM('rack', 'warehouse'),
|
type: DataTypes.ENUM('rack', 'warehouse'),
|
||||||
defaultValue: 'rack'
|
defaultValue: 'rack',
|
||||||
},
|
},
|
||||||
purchaseDate: {
|
purchaseDate: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
warrantyExpiry: {
|
warrantyExpiry: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
ipAddress: {
|
ipAddress: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
customFields: {
|
customFields: {
|
||||||
type: DataTypes.JSON,
|
type: DataTypes.JSON,
|
||||||
defaultValue: {},
|
defaultValue: {},
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
tableName: 'devices',
|
tableName: 'devices',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
indexes: [
|
indexes: [
|
||||||
@@ -99,9 +102,10 @@ const Device = sequelize.define('Device', {
|
|||||||
{ fields: ['rackId'] },
|
{ fields: ['rackId'] },
|
||||||
{ fields: ['createdAt'] },
|
{ fields: ['createdAt'] },
|
||||||
{ fields: ['status', 'type'] },
|
{ fields: ['status', 'type'] },
|
||||||
{ fields: ['name'] }
|
{ fields: ['name'] },
|
||||||
]
|
],
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
Device.belongsTo(Rack, { foreignKey: 'rackId' });
|
Device.belongsTo(Rack, { foreignKey: 'rackId' });
|
||||||
Device.belongsTo(Warehouse, { foreignKey: 'warehouseId' });
|
Device.belongsTo(Warehouse, { foreignKey: 'warehouseId' });
|
||||||
|
|||||||
@@ -3,52 +3,56 @@ const { sequelize } = require('../db');
|
|||||||
const Device = require('./Device');
|
const Device = require('./Device');
|
||||||
const Business = require('./Business');
|
const Business = require('./Business');
|
||||||
|
|
||||||
const DeviceBusiness = sequelize.define('DeviceBusiness', {
|
const DeviceBusiness = sequelize.define(
|
||||||
|
'DeviceBusiness',
|
||||||
|
{
|
||||||
id: {
|
id: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
autoIncrement: true
|
autoIncrement: true,
|
||||||
},
|
},
|
||||||
deviceId: {
|
deviceId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
references: {
|
references: {
|
||||||
model: Device,
|
model: Device,
|
||||||
key: 'deviceId'
|
key: 'deviceId',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
businessId: {
|
businessId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
references: {
|
references: {
|
||||||
model: Business,
|
model: Business,
|
||||||
key: 'businessId'
|
key: 'businessId',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
isPrimary: {
|
isPrimary: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
defaultValue: false
|
defaultValue: false,
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
tableName: 'device_business',
|
tableName: 'device_business',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
indexes: [
|
indexes: [
|
||||||
{ fields: ['deviceId'] },
|
{ fields: ['deviceId'] },
|
||||||
{ fields: ['businessId'] },
|
{ fields: ['businessId'] },
|
||||||
{ unique: true, fields: ['deviceId', 'businessId'] }
|
{ unique: true, fields: ['deviceId', 'businessId'] },
|
||||||
]
|
],
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
Device.belongsToMany(Business, {
|
Device.belongsToMany(Business, {
|
||||||
through: DeviceBusiness,
|
through: DeviceBusiness,
|
||||||
foreignKey: 'deviceId',
|
foreignKey: 'deviceId',
|
||||||
otherKey: 'businessId'
|
otherKey: 'businessId',
|
||||||
});
|
});
|
||||||
|
|
||||||
Business.belongsToMany(Device, {
|
Business.belongsToMany(Device, {
|
||||||
through: DeviceBusiness,
|
through: DeviceBusiness,
|
||||||
foreignKey: 'businessId',
|
foreignKey: 'businessId',
|
||||||
otherKey: 'deviceId'
|
otherKey: 'deviceId',
|
||||||
});
|
});
|
||||||
|
|
||||||
DeviceBusiness.belongsTo(Business, { foreignKey: 'businessId' });
|
DeviceBusiness.belongsTo(Business, { foreignKey: 'businessId' });
|
||||||
|
|||||||
@@ -1,55 +1,59 @@
|
|||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require('sequelize');
|
||||||
const { sequelize } = require('../db');
|
const { sequelize } = require('../db');
|
||||||
|
|
||||||
const DeviceField = sequelize.define('DeviceField', {
|
const DeviceField = sequelize.define(
|
||||||
|
'DeviceField',
|
||||||
|
{
|
||||||
fieldId: {
|
fieldId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
autoIncrement: true,
|
autoIncrement: true,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
fieldName: {
|
fieldName: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true
|
unique: true,
|
||||||
},
|
},
|
||||||
displayName: {
|
displayName: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
fieldType: {
|
fieldType: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: 'string'
|
defaultValue: 'string',
|
||||||
},
|
},
|
||||||
required: {
|
required: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: false
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
type: DataTypes.JSON,
|
type: DataTypes.JSON,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
order: {
|
order: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: 0
|
defaultValue: 0,
|
||||||
},
|
},
|
||||||
visible: {
|
visible: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: true
|
defaultValue: true,
|
||||||
},
|
},
|
||||||
isSystem: {
|
isSystem: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
comment: '是否为系统字段,系统字段不可删除'
|
comment: '是否为系统字段,系统字段不可删除',
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
tableName: 'deviceFields',
|
tableName: 'deviceFields',
|
||||||
timestamps: true
|
timestamps: true,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = DeviceField;
|
module.exports = DeviceField;
|
||||||
@@ -1,58 +1,61 @@
|
|||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require('sequelize');
|
||||||
const { sequelize } = require('../db');
|
const { sequelize } = require('../db');
|
||||||
|
|
||||||
const DevicePort = sequelize.define('DevicePort', {
|
const DevicePort = sequelize.define(
|
||||||
|
'DevicePort',
|
||||||
|
{
|
||||||
portId: {
|
portId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true
|
unique: true,
|
||||||
},
|
},
|
||||||
deviceId: {
|
deviceId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
references: {
|
references: {
|
||||||
model: 'devices',
|
model: 'devices',
|
||||||
key: 'deviceId'
|
key: 'deviceId',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
nicId: {
|
nicId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
references: {
|
references: {
|
||||||
model: 'network_cards',
|
model: 'network_cards',
|
||||||
key: 'nicId'
|
key: 'nicId',
|
||||||
},
|
},
|
||||||
comment: '所属网卡ID,可为空(向后兼容)'
|
comment: '所属网卡ID,可为空(向后兼容)',
|
||||||
},
|
},
|
||||||
portName: {
|
portName: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
portType: {
|
portType: {
|
||||||
type: DataTypes.ENUM('RJ45', 'SFP', 'SFP+', 'SFP28', 'QSFP', 'QSFP28'),
|
type: DataTypes.ENUM('RJ45', 'SFP', 'SFP+', 'SFP28', 'QSFP', 'QSFP28'),
|
||||||
defaultValue: 'RJ45',
|
defaultValue: 'RJ45',
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
portSpeed: {
|
portSpeed: {
|
||||||
type: DataTypes.ENUM('100M', '1G', '10G', '25G', '40G', '100G'),
|
type: DataTypes.ENUM('100M', '1G', '10G', '25G', '40G', '100G'),
|
||||||
defaultValue: '1G',
|
defaultValue: '1G',
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: DataTypes.ENUM('free', 'occupied', 'fault'),
|
type: DataTypes.ENUM('free', 'occupied', 'fault'),
|
||||||
defaultValue: 'free',
|
defaultValue: 'free',
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
vlanId: {
|
vlanId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
tableName: 'device_ports',
|
tableName: 'device_ports',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
indexes: [
|
indexes: [
|
||||||
@@ -61,8 +64,9 @@ const DevicePort = sequelize.define('DevicePort', {
|
|||||||
{ fields: ['status'] },
|
{ fields: ['status'] },
|
||||||
{ fields: ['portType'] },
|
{ fields: ['portType'] },
|
||||||
{ fields: ['portSpeed'] },
|
{ fields: ['portSpeed'] },
|
||||||
{ unique: true, fields: ['deviceId', 'portName'] }
|
{ unique: true, fields: ['deviceId', 'portName'] },
|
||||||
]
|
],
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = DevicePort;
|
module.exports = DevicePort;
|
||||||
|
|||||||
@@ -1,65 +1,65 @@
|
|||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require('sequelize');
|
||||||
const { sequelize } = require('../db');
|
const { sequelize } = require('../db');
|
||||||
|
|
||||||
const FaultCategory = sequelize.define('FaultCategory', {
|
const FaultCategory = sequelize.define(
|
||||||
|
'FaultCategory',
|
||||||
|
{
|
||||||
categoryId: {
|
categoryId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true
|
unique: true,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true,
|
unique: true,
|
||||||
comment: '分类名称'
|
comment: '分类名称',
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
comment: '分类说明 - 说明此类故障代表什么问题'
|
comment: '分类说明 - 说明此类故障代表什么问题',
|
||||||
},
|
},
|
||||||
priority: {
|
priority: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
comment: '排序优先级'
|
comment: '排序优先级',
|
||||||
},
|
},
|
||||||
defaultPriority: {
|
defaultPriority: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
defaultValue: 'medium',
|
defaultValue: 'medium',
|
||||||
comment: '默认优先级: critical/high/medium/low'
|
comment: '默认优先级: critical/high/medium/low',
|
||||||
},
|
},
|
||||||
expectedDuration: {
|
expectedDuration: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
comment: '预计处理时长(小时)'
|
comment: '预计处理时长(小时)',
|
||||||
},
|
},
|
||||||
solutions: {
|
solutions: {
|
||||||
type: DataTypes.JSON,
|
type: DataTypes.JSON,
|
||||||
defaultValue: [],
|
defaultValue: [],
|
||||||
comment: '常见解决方案'
|
comment: '常见解决方案',
|
||||||
},
|
},
|
||||||
isSystem: {
|
isSystem: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
comment: '是否系统内置分类'
|
comment: '是否系统内置分类',
|
||||||
},
|
},
|
||||||
isActive: {
|
isActive: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
comment: '是否启用'
|
comment: '是否启用',
|
||||||
},
|
},
|
||||||
metadata: {
|
metadata: {
|
||||||
type: DataTypes.JSON,
|
type: DataTypes.JSON,
|
||||||
defaultValue: {},
|
defaultValue: {},
|
||||||
comment: '扩展字段'
|
comment: '扩展字段',
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
tableName: 'fault_categories',
|
tableName: 'fault_categories',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
indexes: [
|
indexes: [{ fields: ['name'] }, { fields: ['isActive'] }, { fields: ['priority'] }],
|
||||||
{ fields: ['name'] },
|
}
|
||||||
{ fields: ['isActive'] },
|
);
|
||||||
{ fields: ['priority'] }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = FaultCategory;
|
module.exports = FaultCategory;
|
||||||
|
|||||||
@@ -2,101 +2,101 @@ const { DataTypes } = require('sequelize');
|
|||||||
const { sequelize } = require('../db');
|
const { sequelize } = require('../db');
|
||||||
const User = require('./User');
|
const User = require('./User');
|
||||||
|
|
||||||
const InventoryPlan = sequelize.define('InventoryPlan', {
|
const InventoryPlan = sequelize.define(
|
||||||
|
'InventoryPlan',
|
||||||
|
{
|
||||||
planId: {
|
planId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true
|
unique: true,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: 'full',
|
defaultValue: 'full',
|
||||||
comment: 'full:全面盘点, partial:局部盘点, sample:抽样盘点'
|
comment: 'full:全面盘点, partial:局部盘点, sample:抽样盘点',
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
defaultValue: 'draft',
|
defaultValue: 'draft',
|
||||||
comment: 'draft:草稿, pending:待执行, in_progress:进行中, completed:已完成, cancelled:已取消'
|
comment: 'draft:草稿, pending:待执行, in_progress:进行中, completed:已完成, cancelled:已取消',
|
||||||
},
|
},
|
||||||
scheduledDate: {
|
scheduledDate: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
completedDate: {
|
completedDate: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
targetRooms: {
|
targetRooms: {
|
||||||
type: DataTypes.JSON,
|
type: DataTypes.JSON,
|
||||||
defaultValue: [],
|
defaultValue: [],
|
||||||
comment: '目标机房ID列表'
|
comment: '目标机房ID列表',
|
||||||
},
|
},
|
||||||
targetRacks: {
|
targetRacks: {
|
||||||
type: DataTypes.JSON,
|
type: DataTypes.JSON,
|
||||||
defaultValue: [],
|
defaultValue: [],
|
||||||
comment: '目标机柜ID列表'
|
comment: '目标机柜ID列表',
|
||||||
},
|
},
|
||||||
totalDevices: {
|
totalDevices: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
comment: '盘点设备总数'
|
comment: '盘点设备总数',
|
||||||
},
|
},
|
||||||
checkedDevices: {
|
checkedDevices: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
comment: '已盘点设备数'
|
comment: '已盘点设备数',
|
||||||
},
|
},
|
||||||
normalDevices: {
|
normalDevices: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
comment: '正常设备数'
|
comment: '正常设备数',
|
||||||
},
|
},
|
||||||
abnormalDevices: {
|
abnormalDevices: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
comment: '异常设备数'
|
comment: '异常设备数',
|
||||||
},
|
},
|
||||||
missedDevices: {
|
missedDevices: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
comment: '漏盘设备数'
|
comment: '漏盘设备数',
|
||||||
},
|
},
|
||||||
extraDevices: {
|
extraDevices: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
comment: '多出设备数'
|
comment: '多出设备数',
|
||||||
},
|
},
|
||||||
createdBy: {
|
createdBy: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
references: {
|
references: {
|
||||||
model: User,
|
model: User,
|
||||||
key: 'userId'
|
key: 'userId',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
remark: {
|
remark: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
tableName: 'inventory_plans',
|
tableName: 'inventory_plans',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
indexes: [
|
indexes: [{ fields: ['status'] }, { fields: ['scheduledDate'] }, { fields: ['createdAt'] }],
|
||||||
{ fields: ['status'] },
|
}
|
||||||
{ fields: ['scheduledDate'] },
|
);
|
||||||
{ fields: ['createdAt'] }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
InventoryPlan.belongsTo(User, { foreignKey: 'createdBy', as: 'Creator' });
|
InventoryPlan.belongsTo(User, { foreignKey: 'createdBy', as: 'Creator' });
|
||||||
User.hasMany(InventoryPlan, { foreignKey: 'createdBy' });
|
User.hasMany(InventoryPlan, { foreignKey: 'createdBy' });
|
||||||
|
|||||||
@@ -1,91 +1,95 @@
|
|||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require('sequelize');
|
||||||
const { sequelize } = require('../db');
|
const { sequelize } = require('../db');
|
||||||
|
|
||||||
const InventoryRecord = sequelize.define('InventoryRecord', {
|
const InventoryRecord = sequelize.define(
|
||||||
|
'InventoryRecord',
|
||||||
|
{
|
||||||
recordId: {
|
recordId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true
|
unique: true,
|
||||||
},
|
},
|
||||||
taskId: {
|
taskId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
planId: {
|
planId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
deviceId: {
|
deviceId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
deviceName: {
|
deviceName: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
deviceType: {
|
deviceType: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
serialNumber: {
|
serialNumber: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '系统记录的序列号'
|
comment: '系统记录的序列号',
|
||||||
},
|
},
|
||||||
actualSerialNumber: {
|
actualSerialNumber: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '实际盘点序列号'
|
comment: '实际盘点序列号',
|
||||||
},
|
},
|
||||||
rackId: {
|
rackId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '系统记录的机柜'
|
comment: '系统记录的机柜',
|
||||||
},
|
},
|
||||||
actualRackId: {
|
actualRackId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '实际盘点机柜'
|
comment: '实际盘点机柜',
|
||||||
},
|
},
|
||||||
position: {
|
position: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '系统记录的位置'
|
comment: '系统记录的位置',
|
||||||
},
|
},
|
||||||
actualPosition: {
|
actualPosition: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '实际盘点位置'
|
comment: '实际盘点位置',
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
defaultValue: 'pending',
|
defaultValue: 'pending',
|
||||||
comment: 'pending:待盘点, normal:正常, abnormal:异常, missed:未盘点, not_found:未找到'
|
comment: 'pending:待盘点, normal:正常, abnormal:异常, missed:未盘点, not_found:未找到',
|
||||||
},
|
},
|
||||||
abnormalType: {
|
abnormalType: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: 'serial_mismatch:序列号不符, position_mismatch:位置不符, device_missing:设备缺失, extra_device:多出设备'
|
comment:
|
||||||
|
'serial_mismatch:序列号不符, position_mismatch:位置不符, device_missing:设备缺失, extra_device:多出设备',
|
||||||
},
|
},
|
||||||
checkedBy: {
|
checkedBy: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
checkedAt: {
|
checkedAt: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
remark: {
|
remark: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
photoUrl: {
|
photoUrl: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '盘点照片'
|
comment: '盘点照片',
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
tableName: 'inventory_records',
|
tableName: 'inventory_records',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
indexes: [
|
indexes: [
|
||||||
@@ -93,8 +97,9 @@ const InventoryRecord = sequelize.define('InventoryRecord', {
|
|||||||
{ fields: ['planId'] },
|
{ fields: ['planId'] },
|
||||||
{ fields: ['deviceId'] },
|
{ fields: ['deviceId'] },
|
||||||
{ fields: ['status'] },
|
{ fields: ['status'] },
|
||||||
{ fields: ['checkedBy'] }
|
{ fields: ['checkedBy'] },
|
||||||
]
|
],
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = InventoryRecord;
|
module.exports = InventoryRecord;
|
||||||
|
|||||||
@@ -1,81 +1,81 @@
|
|||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require('sequelize');
|
||||||
const { sequelize } = require('../db');
|
const { sequelize } = require('../db');
|
||||||
|
|
||||||
const InventoryTask = sequelize.define('InventoryTask', {
|
const InventoryTask = sequelize.define(
|
||||||
|
'InventoryTask',
|
||||||
|
{
|
||||||
taskId: {
|
taskId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true
|
unique: true,
|
||||||
},
|
},
|
||||||
planId: {
|
planId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
targetType: {
|
targetType: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
comment: 'room:机房, rack:机柜, device:设备'
|
comment: 'room:机房, rack:机柜, device:设备',
|
||||||
},
|
},
|
||||||
targetId: {
|
targetId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
comment: '目标ID(机房ID/机柜ID/设备ID)'
|
comment: '目标ID(机房ID/机柜ID/设备ID)',
|
||||||
},
|
},
|
||||||
targetName: {
|
targetName: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '目标名称'
|
comment: '目标名称',
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
defaultValue: 'pending',
|
defaultValue: 'pending',
|
||||||
comment: 'pending:待执行, in_progress:进行中, completed:已完成, skipped:已跳过'
|
comment: 'pending:待执行, in_progress:进行中, completed:已完成, skipped:已跳过',
|
||||||
},
|
},
|
||||||
totalDevices: {
|
totalDevices: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
comment: '设备总数'
|
comment: '设备总数',
|
||||||
},
|
},
|
||||||
checkedDevices: {
|
checkedDevices: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
comment: '已盘点设备数'
|
comment: '已盘点设备数',
|
||||||
},
|
},
|
||||||
normalDevices: {
|
normalDevices: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
comment: '正常设备数'
|
comment: '正常设备数',
|
||||||
},
|
},
|
||||||
abnormalDevices: {
|
abnormalDevices: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
comment: '异常设备数'
|
comment: '异常设备数',
|
||||||
},
|
},
|
||||||
assignedTo: {
|
assignedTo: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
assignedAt: {
|
assignedAt: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
completedAt: {
|
completedAt: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
remark: {
|
remark: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
tableName: 'inventory_tasks',
|
tableName: 'inventory_tasks',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
indexes: [
|
indexes: [{ fields: ['planId'] }, { fields: ['status'] }, { fields: ['assignedTo'] }],
|
||||||
{ fields: ['planId'] },
|
}
|
||||||
{ fields: ['status'] },
|
);
|
||||||
{ fields: ['assignedTo'] }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = InventoryTask;
|
module.exports = InventoryTask;
|
||||||
|
|||||||
@@ -1,65 +1,69 @@
|
|||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require('sequelize');
|
||||||
const { sequelize } = require('../db');
|
const { sequelize } = require('../db');
|
||||||
|
|
||||||
const NetworkCard = sequelize.define('NetworkCard', {
|
const NetworkCard = sequelize.define(
|
||||||
|
'NetworkCard',
|
||||||
|
{
|
||||||
nicId: {
|
nicId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true
|
unique: true,
|
||||||
},
|
},
|
||||||
deviceId: {
|
deviceId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
references: {
|
references: {
|
||||||
model: 'devices',
|
model: 'devices',
|
||||||
key: 'deviceId'
|
key: 'deviceId',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
comment: '网卡名称,如"网卡1"、"eth0"、"Primary NIC"'
|
comment: '网卡名称,如"网卡1"、"eth0"、"Primary NIC"',
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '网卡描述信息'
|
comment: '网卡描述信息',
|
||||||
},
|
},
|
||||||
slotNumber: {
|
slotNumber: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '插槽编号'
|
comment: '插槽编号',
|
||||||
},
|
},
|
||||||
portCount: {
|
portCount: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
comment: '端口数量'
|
comment: '端口数量',
|
||||||
},
|
},
|
||||||
model: {
|
model: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '网卡型号'
|
comment: '网卡型号',
|
||||||
},
|
},
|
||||||
manufacturer: {
|
manufacturer: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '制造商'
|
comment: '制造商',
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: DataTypes.ENUM('normal', 'warning', 'fault', 'offline'),
|
type: DataTypes.ENUM('normal', 'warning', 'fault', 'offline'),
|
||||||
defaultValue: 'normal',
|
defaultValue: 'normal',
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
comment: '网卡状态'
|
comment: '网卡状态',
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
tableName: 'network_cards',
|
tableName: 'network_cards',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
indexes: [
|
indexes: [
|
||||||
{ fields: ['deviceId'] },
|
{ fields: ['deviceId'] },
|
||||||
{ fields: ['slotNumber'] },
|
{ fields: ['slotNumber'] },
|
||||||
{ unique: true, fields: ['deviceId', 'name'] }
|
{ unique: true, fields: ['deviceId', 'name'] },
|
||||||
]
|
],
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = NetworkCard;
|
module.exports = NetworkCard;
|
||||||
|
|||||||
@@ -5,76 +5,80 @@ const generateRecordId = () => {
|
|||||||
return `OPLOG_${Date.now().toString(36)}_${Math.random().toString(36).substr(2, 9)}`;
|
return `OPLOG_${Date.now().toString(36)}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const OperationLog = sequelize.define('OperationLog', {
|
const OperationLog = sequelize.define(
|
||||||
|
'OperationLog',
|
||||||
|
{
|
||||||
recordId: {
|
recordId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true
|
unique: true,
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
comment: '模块:device/user/role/consumable/rack/room'
|
comment: '模块:device/user/role/consumable/rack/room',
|
||||||
},
|
},
|
||||||
operationType: {
|
operationType: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
comment: '操作类型: create/update/delete/batch_delete/batch_update/status_change/move/permission_change'
|
comment:
|
||||||
|
'操作类型: create/update/delete/batch_delete/batch_update/status_change/move/permission_change',
|
||||||
},
|
},
|
||||||
operationDescription: {
|
operationDescription: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
comment: '操作描述'
|
comment: '操作描述',
|
||||||
},
|
},
|
||||||
targetId: {
|
targetId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
comment: '目标对象ID'
|
comment: '目标对象ID',
|
||||||
},
|
},
|
||||||
targetName: {
|
targetName: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
comment: '目标对象名称(冗余便于展示)'
|
comment: '目标对象名称(冗余便于展示)',
|
||||||
},
|
},
|
||||||
operatorId: {
|
operatorId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
comment: '操作人ID'
|
comment: '操作人ID',
|
||||||
},
|
},
|
||||||
operatorName: {
|
operatorName: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
comment: '操作人姓名'
|
comment: '操作人姓名',
|
||||||
},
|
},
|
||||||
operatorRole: {
|
operatorRole: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
comment: '操作人角色'
|
comment: '操作人角色',
|
||||||
},
|
},
|
||||||
beforeState: {
|
beforeState: {
|
||||||
type: DataTypes.JSON,
|
type: DataTypes.JSON,
|
||||||
comment: '操作前状态'
|
comment: '操作前状态',
|
||||||
},
|
},
|
||||||
afterState: {
|
afterState: {
|
||||||
type: DataTypes.JSON,
|
type: DataTypes.JSON,
|
||||||
comment: '操作后状态'
|
comment: '操作后状态',
|
||||||
},
|
},
|
||||||
result: {
|
result: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
defaultValue: 'success',
|
defaultValue: 'success',
|
||||||
comment: '操作结果: success/failed'
|
comment: '操作结果: success/failed',
|
||||||
},
|
},
|
||||||
ipAddress: {
|
ipAddress: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
comment: 'IP地址'
|
comment: 'IP地址',
|
||||||
},
|
},
|
||||||
userAgent: {
|
userAgent: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
comment: '用户代理'
|
comment: '用户代理',
|
||||||
},
|
},
|
||||||
metadata: {
|
metadata: {
|
||||||
type: DataTypes.JSON,
|
type: DataTypes.JSON,
|
||||||
defaultValue: {},
|
defaultValue: {},
|
||||||
comment: '扩展字段'
|
comment: '扩展字段',
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
tableName: 'operation_logs',
|
tableName: 'operation_logs',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
indexes: [
|
indexes: [
|
||||||
@@ -82,8 +86,9 @@ const OperationLog = sequelize.define('OperationLog', {
|
|||||||
{ fields: ['operationType'] },
|
{ fields: ['operationType'] },
|
||||||
{ fields: ['targetId'] },
|
{ fields: ['targetId'] },
|
||||||
{ fields: ['operatorId'] },
|
{ fields: ['operatorId'] },
|
||||||
{ fields: ['createdAt'] }
|
{ fields: ['createdAt'] },
|
||||||
]
|
],
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = OperationLog;
|
module.exports = OperationLog;
|
||||||
|
|||||||
@@ -6,156 +6,159 @@ const InventoryTask = require('./InventoryTask');
|
|||||||
const Room = require('./Room');
|
const Room = require('./Room');
|
||||||
const Rack = require('./Rack');
|
const Rack = require('./Rack');
|
||||||
|
|
||||||
const PendingDevice = sequelize.define('PendingDevice', {
|
const PendingDevice = sequelize.define(
|
||||||
|
'PendingDevice',
|
||||||
|
{
|
||||||
pendingId: {
|
pendingId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true
|
unique: true,
|
||||||
},
|
},
|
||||||
serialNumber: {
|
serialNumber: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
comment: '设备序列号'
|
comment: '设备序列号',
|
||||||
},
|
},
|
||||||
deviceName: {
|
deviceName: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '设备名称'
|
comment: '设备名称',
|
||||||
},
|
},
|
||||||
deviceType: {
|
deviceType: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
defaultValue: 'other',
|
defaultValue: 'other',
|
||||||
comment: '设备类型: server, switch, router, storage, other'
|
comment: '设备类型: server, switch, router, storage, other',
|
||||||
},
|
},
|
||||||
roomId: {
|
roomId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
references: {
|
references: {
|
||||||
model: Room,
|
model: Room,
|
||||||
key: 'roomId'
|
key: 'roomId',
|
||||||
},
|
},
|
||||||
comment: '所属机房ID'
|
comment: '所属机房ID',
|
||||||
},
|
},
|
||||||
rackId: {
|
rackId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
references: {
|
references: {
|
||||||
model: Rack,
|
model: Rack,
|
||||||
key: 'rackId'
|
key: 'rackId',
|
||||||
},
|
},
|
||||||
comment: '所属机柜ID'
|
comment: '所属机柜ID',
|
||||||
},
|
},
|
||||||
position: {
|
position: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: 'U位'
|
comment: 'U位',
|
||||||
},
|
},
|
||||||
height: {
|
height: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
defaultValue: 1,
|
defaultValue: 1,
|
||||||
comment: '高度(U)'
|
comment: '高度(U)',
|
||||||
},
|
},
|
||||||
powerConsumption: {
|
powerConsumption: {
|
||||||
type: DataTypes.FLOAT,
|
type: DataTypes.FLOAT,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
comment: '功率(W)'
|
comment: '功率(W)',
|
||||||
},
|
},
|
||||||
model: {
|
model: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '设备型号'
|
comment: '设备型号',
|
||||||
},
|
},
|
||||||
brand: {
|
brand: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '品牌'
|
comment: '品牌',
|
||||||
},
|
},
|
||||||
ipAddress: {
|
ipAddress: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: 'IP地址'
|
comment: 'IP地址',
|
||||||
},
|
},
|
||||||
purchaseDate: {
|
purchaseDate: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '购买日期'
|
comment: '购买日期',
|
||||||
},
|
},
|
||||||
warrantyExpiry: {
|
warrantyExpiry: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '保修到期'
|
comment: '保修到期',
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '描述'
|
comment: '描述',
|
||||||
},
|
},
|
||||||
customFields: {
|
customFields: {
|
||||||
type: DataTypes.JSON,
|
type: DataTypes.JSON,
|
||||||
defaultValue: {},
|
defaultValue: {},
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
defaultValue: 'pending',
|
defaultValue: 'pending',
|
||||||
comment: 'pending: 待同步, synced: 已同步, deleted: 已删除'
|
comment: 'pending: 待同步, synced: 已同步, deleted: 已删除',
|
||||||
},
|
},
|
||||||
planId: {
|
planId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
references: {
|
references: {
|
||||||
model: InventoryPlan,
|
model: InventoryPlan,
|
||||||
key: 'planId'
|
key: 'planId',
|
||||||
},
|
},
|
||||||
comment: '关联的盘点计划ID'
|
comment: '关联的盘点计划ID',
|
||||||
},
|
},
|
||||||
taskId: {
|
taskId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
references: {
|
references: {
|
||||||
model: InventoryTask,
|
model: InventoryTask,
|
||||||
key: 'taskId'
|
key: 'taskId',
|
||||||
},
|
},
|
||||||
comment: '关联的盘点任务ID'
|
comment: '关联的盘点任务ID',
|
||||||
},
|
},
|
||||||
createdBy: {
|
createdBy: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
references: {
|
references: {
|
||||||
model: User,
|
model: User,
|
||||||
key: 'userId'
|
key: 'userId',
|
||||||
},
|
},
|
||||||
comment: '创建人'
|
comment: '创建人',
|
||||||
},
|
},
|
||||||
syncedAt: {
|
syncedAt: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '同步时间'
|
comment: '同步时间',
|
||||||
},
|
},
|
||||||
syncedBy: {
|
syncedBy: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
references: {
|
references: {
|
||||||
model: User,
|
model: User,
|
||||||
key: 'userId'
|
key: 'userId',
|
||||||
},
|
},
|
||||||
comment: '同步人'
|
comment: '同步人',
|
||||||
},
|
},
|
||||||
syncedDeviceId: {
|
syncedDeviceId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '同步后生成的设备ID'
|
comment: '同步后生成的设备ID',
|
||||||
},
|
},
|
||||||
remark: {
|
remark: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '备注'
|
comment: '备注',
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
tableName: 'pending_devices',
|
tableName: 'pending_devices',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
indexes: [
|
indexes: [
|
||||||
@@ -165,9 +168,10 @@ const PendingDevice = sequelize.define('PendingDevice', {
|
|||||||
{ fields: ['taskId'] },
|
{ fields: ['taskId'] },
|
||||||
{ fields: ['createdBy'] },
|
{ fields: ['createdBy'] },
|
||||||
{ fields: ['roomId'] },
|
{ fields: ['roomId'] },
|
||||||
{ fields: ['rackId'] }
|
{ fields: ['rackId'] },
|
||||||
]
|
],
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
PendingDevice.belongsTo(User, { foreignKey: 'createdBy', as: 'Creator' });
|
PendingDevice.belongsTo(User, { foreignKey: 'createdBy', as: 'Creator' });
|
||||||
PendingDevice.belongsTo(User, { foreignKey: 'syncedBy', as: 'Syncer' });
|
PendingDevice.belongsTo(User, { foreignKey: 'syncedBy', as: 'Syncer' });
|
||||||
|
|||||||
@@ -1,48 +1,52 @@
|
|||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require('sequelize');
|
||||||
const { sequelize } = require('../db');
|
const { sequelize } = require('../db');
|
||||||
|
|
||||||
const Permission = sequelize.define('Permission', {
|
const Permission = sequelize.define(
|
||||||
|
'Permission',
|
||||||
|
{
|
||||||
permissionId: {
|
permissionId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
permissionName: {
|
permissionName: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
permissionCode: {
|
permissionCode: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true
|
unique: true,
|
||||||
},
|
},
|
||||||
parentId: {
|
parentId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
type: DataTypes.ENUM('menu', 'button'),
|
type: DataTypes.ENUM('menu', 'button'),
|
||||||
defaultValue: 'button'
|
defaultValue: 'button',
|
||||||
},
|
},
|
||||||
path: {
|
path: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
sort: {
|
sort: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
defaultValue: 0
|
defaultValue: 0,
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: DataTypes.ENUM('active', 'inactive'),
|
type: DataTypes.ENUM('active', 'inactive'),
|
||||||
defaultValue: 'active'
|
defaultValue: 'active',
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
tableName: 'permissions',
|
tableName: 'permissions',
|
||||||
timestamps: true
|
timestamps: true,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = Permission;
|
module.exports = Permission;
|
||||||
|
|||||||
+17
-17
@@ -2,51 +2,51 @@ const { DataTypes } = require('sequelize');
|
|||||||
const { sequelize } = require('../db');
|
const { sequelize } = require('../db');
|
||||||
const Room = require('./Room');
|
const Room = require('./Room');
|
||||||
|
|
||||||
const Rack = sequelize.define('Rack', {
|
const Rack = sequelize.define(
|
||||||
|
'Rack',
|
||||||
|
{
|
||||||
rackId: {
|
rackId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true
|
unique: true,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
height: {
|
height: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: 45 // 标准机柜高度(U数)
|
defaultValue: 45, // 标准机柜高度(U数)
|
||||||
},
|
},
|
||||||
maxPower: {
|
maxPower: {
|
||||||
type: DataTypes.FLOAT,
|
type: DataTypes.FLOAT,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
currentPower: {
|
currentPower: {
|
||||||
type: DataTypes.FLOAT,
|
type: DataTypes.FLOAT,
|
||||||
defaultValue: 0
|
defaultValue: 0,
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
defaultValue: 'active'
|
defaultValue: 'active',
|
||||||
},
|
},
|
||||||
roomId: {
|
roomId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
references: {
|
references: {
|
||||||
model: Room,
|
model: Room,
|
||||||
key: 'roomId'
|
key: 'roomId',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
tableName: 'racks',
|
tableName: 'racks',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
indexes: [
|
indexes: [{ fields: ['roomId'] }, { fields: ['status'] }, { fields: ['roomId', 'status'] }],
|
||||||
{ fields: ['roomId'] },
|
}
|
||||||
{ fields: ['status'] },
|
);
|
||||||
{ fields: ['roomId', 'status'] }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
// 关联关系
|
// 关联关系
|
||||||
Rack.belongsTo(Room, { foreignKey: 'roomId' });
|
Rack.belongsTo(Room, { foreignKey: 'roomId' });
|
||||||
|
|||||||
+16
-12
@@ -1,40 +1,44 @@
|
|||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require('sequelize');
|
||||||
const { sequelize } = require('../db');
|
const { sequelize } = require('../db');
|
||||||
|
|
||||||
const Role = sequelize.define('Role', {
|
const Role = sequelize.define(
|
||||||
|
'Role',
|
||||||
|
{
|
||||||
roleId: {
|
roleId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
roleName: {
|
roleName: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
roleCode: {
|
roleCode: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true
|
unique: true,
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: DataTypes.ENUM('active', 'inactive'),
|
type: DataTypes.ENUM('active', 'inactive'),
|
||||||
defaultValue: 'active'
|
defaultValue: 'active',
|
||||||
},
|
},
|
||||||
permissions: {
|
permissions: {
|
||||||
type: DataTypes.JSON,
|
type: DataTypes.JSON,
|
||||||
defaultValue: []
|
defaultValue: [],
|
||||||
},
|
},
|
||||||
sort: {
|
sort: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
defaultValue: 0
|
defaultValue: 0,
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
tableName: 'roles',
|
tableName: 'roles',
|
||||||
timestamps: true
|
timestamps: true,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = Role;
|
module.exports = Role;
|
||||||
|
|||||||
+16
-15
@@ -1,43 +1,44 @@
|
|||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require('sequelize');
|
||||||
const { sequelize } = require('../db');
|
const { sequelize } = require('../db');
|
||||||
|
|
||||||
const Room = sequelize.define('Room', {
|
const Room = sequelize.define(
|
||||||
|
'Room',
|
||||||
|
{
|
||||||
roomId: {
|
roomId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true
|
unique: true,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
location: {
|
location: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
area: {
|
area: {
|
||||||
type: DataTypes.FLOAT,
|
type: DataTypes.FLOAT,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
capacity: {
|
capacity: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
defaultValue: 'active'
|
defaultValue: 'active',
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: DataTypes.TEXT
|
type: DataTypes.TEXT,
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
tableName: 'rooms',
|
tableName: 'rooms',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
indexes: [
|
indexes: [{ fields: ['status'] }, { fields: ['name'] }],
|
||||||
{ fields: ['status'] },
|
}
|
||||||
{ fields: ['name'] }
|
);
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = Room;
|
module.exports = Room;
|
||||||
@@ -1,47 +1,49 @@
|
|||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require('sequelize');
|
||||||
const { sequelize } = require('../db');
|
const { sequelize } = require('../db');
|
||||||
|
|
||||||
const SystemSetting = sequelize.define('SystemSetting', {
|
const SystemSetting = sequelize.define(
|
||||||
|
'SystemSetting',
|
||||||
|
{
|
||||||
settingKey: {
|
settingKey: {
|
||||||
type: DataTypes.STRING(100),
|
type: DataTypes.STRING(100),
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
comment: '设置键名'
|
comment: '设置键名',
|
||||||
},
|
},
|
||||||
settingValue: {
|
settingValue: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '设置值(JSON格式)'
|
comment: '设置值(JSON格式)',
|
||||||
},
|
},
|
||||||
settingType: {
|
settingType: {
|
||||||
type: DataTypes.STRING(20),
|
type: DataTypes.STRING(20),
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: 'string',
|
defaultValue: 'string',
|
||||||
comment: '设置类型: string, number, boolean, json, array'
|
comment: '设置类型: string, number, boolean, json, array',
|
||||||
},
|
},
|
||||||
category: {
|
category: {
|
||||||
type: DataTypes.STRING(50),
|
type: DataTypes.STRING(50),
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: 'general',
|
defaultValue: 'general',
|
||||||
comment: '设置分类: general, appearance, backup, about'
|
comment: '设置分类: general, appearance, backup, about',
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: DataTypes.STRING(255),
|
type: DataTypes.STRING(255),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '设置描述'
|
comment: '设置描述',
|
||||||
},
|
},
|
||||||
isEditable: {
|
isEditable: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
comment: '是否可编辑'
|
comment: '是否可编辑',
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
tableName: 'system_settings',
|
tableName: 'system_settings',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
indexes: [
|
indexes: [{ fields: ['category'] }],
|
||||||
{ fields: ['category'] }
|
}
|
||||||
]
|
);
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = SystemSetting;
|
module.exports = SystemSetting;
|
||||||
|
|||||||
+34
-30
@@ -3,117 +3,120 @@ const { sequelize } = require('../db');
|
|||||||
const User = require('./User');
|
const User = require('./User');
|
||||||
const Device = require('./Device');
|
const Device = require('./Device');
|
||||||
|
|
||||||
const Ticket = sequelize.define('Ticket', {
|
const Ticket = sequelize.define(
|
||||||
|
'Ticket',
|
||||||
|
{
|
||||||
ticketId: {
|
ticketId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true
|
unique: true,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
comment: '工单标题'
|
comment: '工单标题',
|
||||||
},
|
},
|
||||||
deviceId: {
|
deviceId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: '关联设备ID'
|
comment: '关联设备ID',
|
||||||
},
|
},
|
||||||
deviceName: {
|
deviceName: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
comment: '设备名称'
|
comment: '设备名称',
|
||||||
},
|
},
|
||||||
deviceModel: {
|
deviceModel: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
comment: '设备型号'
|
comment: '设备型号',
|
||||||
},
|
},
|
||||||
serialNumber: {
|
serialNumber: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
comment: '设备序列号'
|
comment: '设备序列号',
|
||||||
},
|
},
|
||||||
faultCategory: {
|
faultCategory: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
comment: '故障分类'
|
comment: '故障分类',
|
||||||
},
|
},
|
||||||
faultSubCategory: {
|
faultSubCategory: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
comment: '故障子分类'
|
comment: '故障子分类',
|
||||||
},
|
},
|
||||||
priority: {
|
priority: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
defaultValue: 'medium',
|
defaultValue: 'medium',
|
||||||
comment: '优先级: critical/high/medium/low'
|
comment: '优先级: critical/high/medium/low',
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
defaultValue: 'pending',
|
defaultValue: 'pending',
|
||||||
comment: '工单状态: pending/in_progress/completed/closed/cancelled'
|
comment: '工单状态: pending/in_progress/completed/closed/cancelled',
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
comment: '故障描述'
|
comment: '故障描述',
|
||||||
},
|
},
|
||||||
expectedCompletionDate: {
|
expectedCompletionDate: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
comment: '期望完成时间'
|
comment: '期望完成时间',
|
||||||
},
|
},
|
||||||
reporterId: {
|
reporterId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
comment: '报修人ID'
|
comment: '报修人ID',
|
||||||
},
|
},
|
||||||
reporterName: {
|
reporterName: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
comment: '报修人姓名'
|
comment: '报修人姓名',
|
||||||
},
|
},
|
||||||
assigneeId: {
|
assigneeId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
comment: '处理人ID'
|
comment: '处理人ID',
|
||||||
},
|
},
|
||||||
assigneeName: {
|
assigneeName: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
comment: '处理人姓名'
|
comment: '处理人姓名',
|
||||||
},
|
},
|
||||||
location: {
|
location: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
comment: '设备位置'
|
comment: '设备位置',
|
||||||
},
|
},
|
||||||
resolution: {
|
resolution: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
comment: '解决方案'
|
comment: '解决方案',
|
||||||
},
|
},
|
||||||
completionDate: {
|
completionDate: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
comment: '实际完成时间'
|
comment: '实际完成时间',
|
||||||
},
|
},
|
||||||
evaluation: {
|
evaluation: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
comment: '用户评价'
|
comment: '用户评价',
|
||||||
},
|
},
|
||||||
evaluationRating: {
|
evaluationRating: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
comment: '评价星级(1-5)'
|
comment: '评价星级(1-5)',
|
||||||
},
|
},
|
||||||
attachments: {
|
attachments: {
|
||||||
type: DataTypes.JSON,
|
type: DataTypes.JSON,
|
||||||
defaultValue: [],
|
defaultValue: [],
|
||||||
comment: '附件列表'
|
comment: '附件列表',
|
||||||
},
|
},
|
||||||
tags: {
|
tags: {
|
||||||
type: DataTypes.JSON,
|
type: DataTypes.JSON,
|
||||||
defaultValue: [],
|
defaultValue: [],
|
||||||
comment: '标签'
|
comment: '标签',
|
||||||
},
|
},
|
||||||
metadata: {
|
metadata: {
|
||||||
type: DataTypes.JSON,
|
type: DataTypes.JSON,
|
||||||
defaultValue: {},
|
defaultValue: {},
|
||||||
comment: '扩展字段'
|
comment: '扩展字段',
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
tableName: 'tickets',
|
tableName: 'tickets',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
indexes: [
|
indexes: [
|
||||||
@@ -123,9 +126,10 @@ const Ticket = sequelize.define('Ticket', {
|
|||||||
{ fields: ['priority'] },
|
{ fields: ['priority'] },
|
||||||
{ fields: ['reporterId'] },
|
{ fields: ['reporterId'] },
|
||||||
{ fields: ['assigneeId'] },
|
{ fields: ['assigneeId'] },
|
||||||
{ fields: ['createdAt'] }
|
{ fields: ['createdAt'] },
|
||||||
]
|
],
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
Ticket.belongsTo(User, { foreignKey: 'reporterId', as: 'reporter', constraints: false });
|
Ticket.belongsTo(User, { foreignKey: 'reporterId', as: 'reporter', constraints: false });
|
||||||
Ticket.belongsTo(User, { foreignKey: 'assigneeId', as: 'assignee', constraints: false });
|
Ticket.belongsTo(User, { foreignKey: 'assigneeId', as: 'assignee', constraints: false });
|
||||||
|
|||||||
@@ -1,53 +1,57 @@
|
|||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require('sequelize');
|
||||||
const { sequelize } = require('../db');
|
const { sequelize } = require('../db');
|
||||||
|
|
||||||
const TicketField = sequelize.define('TicketField', {
|
const TicketField = sequelize.define(
|
||||||
|
'TicketField',
|
||||||
|
{
|
||||||
fieldId: {
|
fieldId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
autoIncrement: true,
|
autoIncrement: true,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
fieldName: {
|
fieldName: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true
|
unique: true,
|
||||||
},
|
},
|
||||||
displayName: {
|
displayName: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
fieldType: {
|
fieldType: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: 'string'
|
defaultValue: 'string',
|
||||||
},
|
},
|
||||||
required: {
|
required: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: false
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
type: DataTypes.JSON,
|
type: DataTypes.JSON,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
order: {
|
order: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: 0
|
defaultValue: 0,
|
||||||
},
|
},
|
||||||
visible: {
|
visible: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: true
|
defaultValue: true,
|
||||||
},
|
},
|
||||||
placeholder: {
|
placeholder: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
tableName: 'ticketFields',
|
tableName: 'ticketFields',
|
||||||
timestamps: true
|
timestamps: true,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = TicketField;
|
module.exports = TicketField;
|
||||||
|
|||||||
@@ -1,90 +1,94 @@
|
|||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require('sequelize');
|
||||||
const { sequelize } = require('../db');
|
const { sequelize } = require('../db');
|
||||||
|
|
||||||
const TicketOperationRecord = sequelize.define('TicketOperationRecord', {
|
const TicketOperationRecord = sequelize.define(
|
||||||
|
'TicketOperationRecord',
|
||||||
|
{
|
||||||
recordId: {
|
recordId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true
|
unique: true,
|
||||||
},
|
},
|
||||||
ticketId: {
|
ticketId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
comment: '关联工单ID'
|
comment: '关联工单ID',
|
||||||
},
|
},
|
||||||
operationType: {
|
operationType: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
comment: '操作类型: create/update/status_change/assignment/comment/attachment'
|
comment: '操作类型: create/update/status_change/assignment/comment/attachment',
|
||||||
},
|
},
|
||||||
operationDescription: {
|
operationDescription: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
comment: '操作描述'
|
comment: '操作描述',
|
||||||
},
|
},
|
||||||
operatorId: {
|
operatorId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
comment: '操作人ID'
|
comment: '操作人ID',
|
||||||
},
|
},
|
||||||
operatorName: {
|
operatorName: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
comment: '操作人姓名'
|
comment: '操作人姓名',
|
||||||
},
|
},
|
||||||
operatorRole: {
|
operatorRole: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
comment: '操作人角色'
|
comment: '操作人角色',
|
||||||
},
|
},
|
||||||
operationSteps: {
|
operationSteps: {
|
||||||
type: DataTypes.JSON,
|
type: DataTypes.JSON,
|
||||||
defaultValue: [],
|
defaultValue: [],
|
||||||
comment: '操作步骤详情'
|
comment: '操作步骤详情',
|
||||||
},
|
},
|
||||||
spareParts: {
|
spareParts: {
|
||||||
type: DataTypes.JSON,
|
type: DataTypes.JSON,
|
||||||
defaultValue: [],
|
defaultValue: [],
|
||||||
comment: '使用的备件列表'
|
comment: '使用的备件列表',
|
||||||
},
|
},
|
||||||
beforeState: {
|
beforeState: {
|
||||||
type: DataTypes.JSON,
|
type: DataTypes.JSON,
|
||||||
comment: '操作前状态'
|
comment: '操作前状态',
|
||||||
},
|
},
|
||||||
afterState: {
|
afterState: {
|
||||||
type: DataTypes.JSON,
|
type: DataTypes.JSON,
|
||||||
comment: '操作后状态'
|
comment: '操作后状态',
|
||||||
},
|
},
|
||||||
duration: {
|
duration: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
comment: '操作耗时(分钟)'
|
comment: '操作耗时(分钟)',
|
||||||
},
|
},
|
||||||
result: {
|
result: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
comment: '操作结果: success/failed/partial'
|
comment: '操作结果: success/failed/partial',
|
||||||
},
|
},
|
||||||
notes: {
|
notes: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
comment: '备注信息'
|
comment: '备注信息',
|
||||||
},
|
},
|
||||||
attachments: {
|
attachments: {
|
||||||
type: DataTypes.JSON,
|
type: DataTypes.JSON,
|
||||||
defaultValue: [],
|
defaultValue: [],
|
||||||
comment: '附件'
|
comment: '附件',
|
||||||
},
|
},
|
||||||
metadata: {
|
metadata: {
|
||||||
type: DataTypes.JSON,
|
type: DataTypes.JSON,
|
||||||
defaultValue: {},
|
defaultValue: {},
|
||||||
comment: '扩展字段'
|
comment: '扩展字段',
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
tableName: 'ticket_operation_records',
|
tableName: 'ticket_operation_records',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
indexes: [
|
indexes: [
|
||||||
{ fields: ['ticketId'] },
|
{ fields: ['ticketId'] },
|
||||||
{ fields: ['operatorId'] },
|
{ fields: ['operatorId'] },
|
||||||
{ fields: ['operationType'] },
|
{ fields: ['operationType'] },
|
||||||
{ fields: ['createdAt'] }
|
{ fields: ['createdAt'] },
|
||||||
]
|
],
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = TicketOperationRecord;
|
module.exports = TicketOperationRecord;
|
||||||
|
|||||||
+22
-22
@@ -1,68 +1,68 @@
|
|||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require('sequelize');
|
||||||
const { sequelize } = require('../db');
|
const { sequelize } = require('../db');
|
||||||
|
|
||||||
const User = sequelize.define('User', {
|
const User = sequelize.define(
|
||||||
|
'User',
|
||||||
|
{
|
||||||
userId: {
|
userId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
username: {
|
username: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true
|
unique: true,
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
validate: {
|
validate: {
|
||||||
isEmail: true
|
isEmail: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
phone: {
|
phone: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
realName: {
|
realName: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
avatar: {
|
avatar: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: DataTypes.ENUM('active', 'inactive', 'locked', 'pending'),
|
type: DataTypes.ENUM('active', 'inactive', 'locked', 'pending'),
|
||||||
defaultValue: 'active'
|
defaultValue: 'active',
|
||||||
},
|
},
|
||||||
lastLoginTime: {
|
lastLoginTime: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
lastLoginIp: {
|
lastLoginIp: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
loginCount: {
|
loginCount: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
defaultValue: 0
|
defaultValue: 0,
|
||||||
},
|
},
|
||||||
remark: {
|
remark: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
tableName: 'users',
|
tableName: 'users',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
indexes: [
|
indexes: [{ fields: ['status'] }, { fields: ['username'] }, { fields: ['email'] }],
|
||||||
{ fields: ['status'] },
|
}
|
||||||
{ fields: ['username'] },
|
);
|
||||||
{ fields: ['email'] }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = User;
|
module.exports = User;
|
||||||
|
|||||||
@@ -3,16 +3,20 @@ const { sequelize } = require('../db');
|
|||||||
const User = require('./User');
|
const User = require('./User');
|
||||||
const Role = require('./Role');
|
const Role = require('./Role');
|
||||||
|
|
||||||
const UserRole = sequelize.define('UserRole', {
|
const UserRole = sequelize.define(
|
||||||
|
'UserRole',
|
||||||
|
{
|
||||||
id: {
|
id: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
autoIncrement: true
|
autoIncrement: true,
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
tableName: 'user_roles',
|
tableName: 'user_roles',
|
||||||
timestamps: true
|
timestamps: true,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
UserRole.belongsTo(User, { foreignKey: 'UserId', onDelete: 'CASCADE' });
|
UserRole.belongsTo(User, { foreignKey: 'UserId', onDelete: 'CASCADE' });
|
||||||
UserRole.belongsTo(Role, { foreignKey: 'RoleId', onDelete: 'CASCADE' });
|
UserRole.belongsTo(Role, { foreignKey: 'RoleId', onDelete: 'CASCADE' });
|
||||||
|
|||||||
+15
-14
@@ -1,41 +1,42 @@
|
|||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require('sequelize');
|
||||||
const { sequelize } = require('../db');
|
const { sequelize } = require('../db');
|
||||||
|
|
||||||
const Warehouse = sequelize.define('Warehouse', {
|
const Warehouse = sequelize.define(
|
||||||
|
'Warehouse',
|
||||||
|
{
|
||||||
warehouseId: {
|
warehouseId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true
|
unique: true,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
location: {
|
location: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
capacity: {
|
capacity: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
defaultValue: 100
|
defaultValue: 100,
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: DataTypes.ENUM('active', 'inactive'),
|
type: DataTypes.ENUM('active', 'inactive'),
|
||||||
defaultValue: 'active'
|
defaultValue: 'active',
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
tableName: 'warehouses',
|
tableName: 'warehouses',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
indexes: [
|
indexes: [{ fields: ['status'] }, { fields: ['name'] }],
|
||||||
{ fields: ['status'] },
|
}
|
||||||
{ fields: ['name'] }
|
);
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = Warehouse;
|
module.exports = Warehouse;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const initDefaultFaultCategories = async () => {
|
|||||||
defaultPriority: 'high',
|
defaultPriority: 'high',
|
||||||
expectedDuration: 4,
|
expectedDuration: 4,
|
||||||
solutions: ['重启服务', '回滚版本', '修复配置', '重装系统'],
|
solutions: ['重启服务', '回滚版本', '修复配置', '重装系统'],
|
||||||
isSystem: true
|
isSystem: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
categoryId: 'CAT002',
|
categoryId: 'CAT002',
|
||||||
@@ -38,7 +38,7 @@ const initDefaultFaultCategories = async () => {
|
|||||||
defaultPriority: 'critical',
|
defaultPriority: 'critical',
|
||||||
expectedDuration: 8,
|
expectedDuration: 8,
|
||||||
solutions: ['更换部件', '联系厂商', '现场维修'],
|
solutions: ['更换部件', '联系厂商', '现场维修'],
|
||||||
isSystem: true
|
isSystem: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
categoryId: 'CAT003',
|
categoryId: 'CAT003',
|
||||||
@@ -51,7 +51,7 @@ const initDefaultFaultCategories = async () => {
|
|||||||
defaultPriority: 'high',
|
defaultPriority: 'high',
|
||||||
expectedDuration: 2,
|
expectedDuration: 2,
|
||||||
solutions: ['检查网线', '重启交换机', '修复配置', '联系运营商'],
|
solutions: ['检查网线', '重启交换机', '修复配置', '联系运营商'],
|
||||||
isSystem: true
|
isSystem: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
categoryId: 'CAT004',
|
categoryId: 'CAT004',
|
||||||
@@ -64,7 +64,7 @@ const initDefaultFaultCategories = async () => {
|
|||||||
defaultPriority: 'medium',
|
defaultPriority: 'medium',
|
||||||
expectedDuration: 6,
|
expectedDuration: 6,
|
||||||
solutions: ['修复Bug', '优化性能', '更新版本', '配置调整'],
|
solutions: ['修复Bug', '优化性能', '更新版本', '配置调整'],
|
||||||
isSystem: true
|
isSystem: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
categoryId: 'CAT005',
|
categoryId: 'CAT005',
|
||||||
@@ -77,7 +77,7 @@ const initDefaultFaultCategories = async () => {
|
|||||||
defaultPriority: 'critical',
|
defaultPriority: 'critical',
|
||||||
expectedDuration: 1,
|
expectedDuration: 1,
|
||||||
solutions: ['隔离系统', '调查取证', '修复漏洞', '更新安全策略'],
|
solutions: ['隔离系统', '调查取证', '修复漏洞', '更新安全策略'],
|
||||||
isSystem: true
|
isSystem: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
categoryId: 'CAT006',
|
categoryId: 'CAT006',
|
||||||
@@ -90,7 +90,7 @@ const initDefaultFaultCategories = async () => {
|
|||||||
defaultPriority: 'medium',
|
defaultPriority: 'medium',
|
||||||
expectedDuration: 4,
|
expectedDuration: 4,
|
||||||
solutions: ['资源扩容', '优化SQL', '清理缓存', '负载均衡'],
|
solutions: ['资源扩容', '优化SQL', '清理缓存', '负载均衡'],
|
||||||
isSystem: true
|
isSystem: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
categoryId: 'CAT007',
|
categoryId: 'CAT007',
|
||||||
@@ -103,7 +103,7 @@ const initDefaultFaultCategories = async () => {
|
|||||||
defaultPriority: 'low',
|
defaultPriority: 'low',
|
||||||
expectedDuration: 2,
|
expectedDuration: 2,
|
||||||
solutions: ['调整配置', '参数优化', '功能启用'],
|
solutions: ['调整配置', '参数优化', '功能启用'],
|
||||||
isSystem: true
|
isSystem: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
categoryId: 'CAT008',
|
categoryId: 'CAT008',
|
||||||
@@ -116,7 +116,7 @@ const initDefaultFaultCategories = async () => {
|
|||||||
defaultPriority: 'low',
|
defaultPriority: 'low',
|
||||||
expectedDuration: 4,
|
expectedDuration: 4,
|
||||||
solutions: ['系统更新', '安全检查', '日志清理', '硬件检测'],
|
solutions: ['系统更新', '安全检查', '日志清理', '硬件检测'],
|
||||||
isSystem: true
|
isSystem: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
categoryId: 'CAT009',
|
categoryId: 'CAT009',
|
||||||
@@ -129,7 +129,7 @@ const initDefaultFaultCategories = async () => {
|
|||||||
defaultPriority: 'high',
|
defaultPriority: 'high',
|
||||||
expectedDuration: 6,
|
expectedDuration: 6,
|
||||||
solutions: ['数据恢复', '数据修复', '重新同步', '备份还原'],
|
solutions: ['数据恢复', '数据修复', '重新同步', '备份还原'],
|
||||||
isSystem: true
|
isSystem: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
categoryId: 'CAT010',
|
categoryId: 'CAT010',
|
||||||
@@ -142,8 +142,8 @@ const initDefaultFaultCategories = async () => {
|
|||||||
defaultPriority: 'critical',
|
defaultPriority: 'critical',
|
||||||
expectedDuration: 2,
|
expectedDuration: 2,
|
||||||
solutions: ['切换电源', '更换UPS', '联系供电', '检查线路'],
|
solutions: ['切换电源', '更换UPS', '联系供电', '检查线路'],
|
||||||
isSystem: true
|
isSystem: true,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const category of defaultCategories) {
|
for (const category of defaultCategories) {
|
||||||
@@ -160,11 +160,11 @@ const initAssociations = () => {
|
|||||||
Ticket.hasMany(TicketOperationRecord, {
|
Ticket.hasMany(TicketOperationRecord, {
|
||||||
foreignKey: 'ticketId',
|
foreignKey: 'ticketId',
|
||||||
as: 'operationRecords',
|
as: 'operationRecords',
|
||||||
constraints: false
|
constraints: false,
|
||||||
});
|
});
|
||||||
TicketOperationRecord.belongsTo(Ticket, {
|
TicketOperationRecord.belongsTo(Ticket, {
|
||||||
foreignKey: 'ticketId',
|
foreignKey: 'ticketId',
|
||||||
constraints: false
|
constraints: false,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -185,5 +185,5 @@ module.exports = {
|
|||||||
initAssociations,
|
initAssociations,
|
||||||
Ticket,
|
Ticket,
|
||||||
TicketOperationRecord,
|
TicketOperationRecord,
|
||||||
FaultCategory
|
FaultCategory,
|
||||||
};
|
};
|
||||||
|
|||||||
+73
-57
@@ -4,7 +4,13 @@ const User = require('../models/User');
|
|||||||
const Role = require('../models/Role');
|
const Role = require('../models/Role');
|
||||||
const UserRole = require('../models/UserRole');
|
const UserRole = require('../models/UserRole');
|
||||||
const { generateToken, authMiddleware } = require('../middleware/auth');
|
const { generateToken, authMiddleware } = require('../middleware/auth');
|
||||||
const { SALT_ROUNDS, MAX_LOGIN_ATTEMPTS, PASSWORD_MIN_LENGTH, USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH } = require('../config');
|
const {
|
||||||
|
SALT_ROUNDS,
|
||||||
|
MAX_LOGIN_ATTEMPTS,
|
||||||
|
PASSWORD_MIN_LENGTH,
|
||||||
|
USERNAME_MIN_LENGTH,
|
||||||
|
USERNAME_MAX_LENGTH,
|
||||||
|
} = require('../config');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@@ -19,21 +25,21 @@ router.post('/register', async (req, res) => {
|
|||||||
if (!username || !password) {
|
if (!username || !password) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '用户名和密码不能为空'
|
message: '用户名和密码不能为空',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (username.length < USERNAME_MIN_LENGTH || username.length > USERNAME_MAX_LENGTH) {
|
if (username.length < USERNAME_MIN_LENGTH || username.length > USERNAME_MAX_LENGTH) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: `用户名长度必须在${USERNAME_MIN_LENGTH}-${USERNAME_MAX_LENGTH}个字符之间`
|
message: `用户名长度必须在${USERNAME_MIN_LENGTH}-${USERNAME_MAX_LENGTH}个字符之间`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (password.length < PASSWORD_MIN_LENGTH) {
|
if (password.length < PASSWORD_MIN_LENGTH) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: `密码长度不能少于${PASSWORD_MIN_LENGTH}个字符`
|
message: `密码长度不能少于${PASSWORD_MIN_LENGTH}个字符`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +47,7 @@ router.post('/register', async (req, res) => {
|
|||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '用户名已存在'
|
message: '用户名已存在',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +63,7 @@ router.post('/register', async (req, res) => {
|
|||||||
email,
|
email,
|
||||||
phone,
|
phone,
|
||||||
realName: realName || username,
|
realName: realName || username,
|
||||||
status: isFirstUser ? 'active' : 'pending'
|
status: isFirstUser ? 'active' : 'pending',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isFirstUser) {
|
if (isFirstUser) {
|
||||||
@@ -70,13 +76,13 @@ router.post('/register', async (req, res) => {
|
|||||||
roleCode: 'admin',
|
roleCode: 'admin',
|
||||||
description: '系统管理员,拥有所有权限',
|
description: '系统管理员,拥有所有权限',
|
||||||
status: 'active',
|
status: 'active',
|
||||||
permissions: []
|
permissions: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await UserRole.create({
|
await UserRole.create({
|
||||||
UserId: user.userId,
|
UserId: user.userId,
|
||||||
RoleId: adminRole.roleId
|
RoleId: adminRole.roleId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const token = generateToken(user);
|
const token = generateToken(user);
|
||||||
@@ -89,11 +95,11 @@ router.post('/register', async (req, res) => {
|
|||||||
userId: user.userId,
|
userId: user.userId,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
realName: user.realName
|
realName: user.realName,
|
||||||
},
|
},
|
||||||
token,
|
token,
|
||||||
isFirstUser: true
|
isFirstUser: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const defaultRole = await Role.findOne({ where: { roleCode: 'viewer' } });
|
const defaultRole = await Role.findOne({ where: { roleCode: 'viewer' } });
|
||||||
@@ -101,7 +107,7 @@ router.post('/register', async (req, res) => {
|
|||||||
if (defaultRole) {
|
if (defaultRole) {
|
||||||
await UserRole.create({
|
await UserRole.create({
|
||||||
UserId: user.userId,
|
UserId: user.userId,
|
||||||
RoleId: defaultRole.roleId
|
RoleId: defaultRole.roleId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,11 +119,11 @@ router.post('/register', async (req, res) => {
|
|||||||
userId: user.userId,
|
userId: user.userId,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
realName: user.realName
|
realName: user.realName,
|
||||||
},
|
},
|
||||||
isFirstUser: false,
|
isFirstUser: false,
|
||||||
pendingApproval: true
|
pendingApproval: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -125,7 +131,7 @@ router.post('/register', async (req, res) => {
|
|||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '注册失败',
|
message: '注册失败',
|
||||||
error: error.message
|
error: error.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -137,7 +143,7 @@ router.post('/login', async (req, res) => {
|
|||||||
if (!username || !password) {
|
if (!username || !password) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '用户名和密码不能为空'
|
message: '用户名和密码不能为空',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,21 +151,21 @@ router.post('/login', async (req, res) => {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(401).json({
|
return res.status(401).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '用户名或密码错误'
|
message: '用户名或密码错误',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.status === 'locked') {
|
if (user.status === 'locked') {
|
||||||
return res.status(403).json({
|
return res.status(403).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '账户已被锁定,请联系管理员'
|
message: '账户已被锁定,请联系管理员',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.status === 'inactive') {
|
if (user.status === 'inactive') {
|
||||||
return res.status(403).json({
|
return res.status(403).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '账户已禁用'
|
message: '账户已禁用',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +173,7 @@ router.post('/login', async (req, res) => {
|
|||||||
return res.status(403).json({
|
return res.status(403).json({
|
||||||
success: false,
|
success: false,
|
||||||
code: 'PENDING_APPROVAL',
|
code: 'PENDING_APPROVAL',
|
||||||
message: '账户待审核,请联系管理员激活'
|
message: '账户待审核,请联系管理员激活',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +187,7 @@ router.post('/login', async (req, res) => {
|
|||||||
|
|
||||||
return res.status(401).json({
|
return res.status(401).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '用户名或密码错误'
|
message: '用户名或密码错误',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,17 +207,17 @@ router.post('/login', async (req, res) => {
|
|||||||
username: user.username,
|
username: user.username,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
realName: user.realName,
|
realName: user.realName,
|
||||||
avatar: user.avatar
|
avatar: user.avatar,
|
||||||
|
},
|
||||||
|
token,
|
||||||
},
|
},
|
||||||
token
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('登录错误:', error);
|
console.error('登录错误:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '登录失败',
|
message: '登录失败',
|
||||||
error: error.message
|
error: error.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -219,22 +225,24 @@ router.post('/login', async (req, res) => {
|
|||||||
router.get('/profile', authMiddleware, async (req, res) => {
|
router.get('/profile', authMiddleware, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const user = await User.findByPk(req.user.userId, {
|
const user = await User.findByPk(req.user.userId, {
|
||||||
attributes: { exclude: ['password'] }
|
attributes: { exclude: ['password'] },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '用户不存在'
|
message: '用户不存在',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const roles = await Role.findAll({
|
const roles = await Role.findAll({
|
||||||
include: [{
|
include: [
|
||||||
|
{
|
||||||
model: User,
|
model: User,
|
||||||
where: { userId: req.user.userId },
|
where: { userId: req.user.userId },
|
||||||
attributes: []
|
attributes: [],
|
||||||
}]
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -244,15 +252,15 @@ router.get('/profile', authMiddleware, async (req, res) => {
|
|||||||
roles: roles.map(r => ({
|
roles: roles.map(r => ({
|
||||||
roleId: r.roleId,
|
roleId: r.roleId,
|
||||||
roleName: r.roleName,
|
roleName: r.roleName,
|
||||||
roleCode: r.roleCode
|
roleCode: r.roleCode,
|
||||||
}))
|
})),
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取profile错误:', error);
|
console.error('获取profile错误:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '获取用户信息失败'
|
message: '获取用户信息失败',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -265,14 +273,22 @@ router.put('/profile', authMiddleware, async (req, res) => {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '用户不存在'
|
message: '用户不存在',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (email !== undefined) user.email = email;
|
if (email !== undefined) {
|
||||||
if (phone !== undefined) user.phone = phone;
|
user.email = email;
|
||||||
if (realName !== undefined) user.realName = realName;
|
}
|
||||||
if (avatar !== undefined) user.avatar = avatar;
|
if (phone !== undefined) {
|
||||||
|
user.phone = phone;
|
||||||
|
}
|
||||||
|
if (realName !== undefined) {
|
||||||
|
user.realName = realName;
|
||||||
|
}
|
||||||
|
if (avatar !== undefined) {
|
||||||
|
user.avatar = avatar;
|
||||||
|
}
|
||||||
|
|
||||||
await user.save();
|
await user.save();
|
||||||
|
|
||||||
@@ -285,14 +301,14 @@ router.put('/profile', authMiddleware, async (req, res) => {
|
|||||||
email: user.email,
|
email: user.email,
|
||||||
phone: user.phone,
|
phone: user.phone,
|
||||||
realName: user.realName,
|
realName: user.realName,
|
||||||
avatar: user.avatar
|
avatar: user.avatar,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('更新profile错误:', error);
|
console.error('更新profile错误:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '更新失败'
|
message: '更新失败',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -304,14 +320,14 @@ router.put('/password', authMiddleware, async (req, res) => {
|
|||||||
if (!oldPassword || !newPassword) {
|
if (!oldPassword || !newPassword) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '旧密码和新密码都不能为空'
|
message: '旧密码和新密码都不能为空',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newPassword.length < PASSWORD_MIN_LENGTH) {
|
if (newPassword.length < PASSWORD_MIN_LENGTH) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: `新密码长度不能少于${PASSWORD_MIN_LENGTH}个字符`
|
message: `新密码长度不能少于${PASSWORD_MIN_LENGTH}个字符`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,7 +337,7 @@ router.put('/password', authMiddleware, async (req, res) => {
|
|||||||
if (!isPasswordValid) {
|
if (!isPasswordValid) {
|
||||||
return res.status(401).json({
|
return res.status(401).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '旧密码错误'
|
message: '旧密码错误',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,13 +346,13 @@ router.put('/password', authMiddleware, async (req, res) => {
|
|||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: '密码修改成功'
|
message: '密码修改成功',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('修改密码错误:', error);
|
console.error('修改密码错误:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '密码修改失败'
|
message: '密码修改失败',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -349,14 +365,14 @@ router.post('/check-admin', async (req, res) => {
|
|||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
hasAdmin: userCount > 0,
|
hasAdmin: userCount > 0,
|
||||||
userCount
|
userCount,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('检查管理员错误:', error);
|
console.error('检查管理员错误:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '检查失败'
|
message: '检查失败',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -368,7 +384,7 @@ router.post('/unlock', async (req, res) => {
|
|||||||
if (!username || !password) {
|
if (!username || !password) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '用户名和密码不能为空'
|
message: '用户名和密码不能为空',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,14 +392,14 @@ router.post('/unlock', async (req, res) => {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(401).json({
|
return res.status(401).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '用户名或密码错误'
|
message: '用户名或密码错误',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.status !== 'locked') {
|
if (user.status !== 'locked') {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '账户未被锁定'
|
message: '账户未被锁定',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,7 +407,7 @@ router.post('/unlock', async (req, res) => {
|
|||||||
if (!isPasswordValid) {
|
if (!isPasswordValid) {
|
||||||
return res.status(401).json({
|
return res.status(401).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '用户名或密码错误'
|
message: '用户名或密码错误',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,14 +418,14 @@ router.post('/unlock', async (req, res) => {
|
|||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: '账户解锁成功'
|
message: '账户解锁成功',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('解锁账户错误:', error);
|
console.error('解锁账户错误:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '解锁失败',
|
message: '解锁失败',
|
||||||
error: error.message
|
error: error.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,11 +10,18 @@ if (!fs.existsSync(UPLOAD_DIR)) {
|
|||||||
|
|
||||||
const SETTINGS_FILE = path.join(__dirname, '../backgroundSettings.json');
|
const SETTINGS_FILE = path.join(__dirname, '../backgroundSettings.json');
|
||||||
if (!fs.existsSync(SETTINGS_FILE)) {
|
if (!fs.existsSync(SETTINGS_FILE)) {
|
||||||
fs.writeFileSync(SETTINGS_FILE, JSON.stringify({
|
fs.writeFileSync(
|
||||||
|
SETTINGS_FILE,
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
type: 'gradient',
|
type: 'gradient',
|
||||||
image: '',
|
image: '',
|
||||||
size: 'contain'
|
size: 'contain',
|
||||||
}, null, 2));
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
router.get('/', (req, res) => {
|
router.get('/', (req, res) => {
|
||||||
@@ -22,7 +29,7 @@ router.get('/', (req, res) => {
|
|||||||
const settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8'));
|
const settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8'));
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: settings
|
data: settings,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('读取背景设置失败:', error);
|
console.error('读取背景设置失败:', error);
|
||||||
@@ -36,7 +43,7 @@ router.put('/', (req, res) => {
|
|||||||
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2));
|
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2));
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: settings
|
data: settings,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('保存背景设置失败:', error);
|
console.error('保存背景设置失败:', error);
|
||||||
@@ -54,7 +61,7 @@ router.post('/upload', (req, res) => {
|
|||||||
const fileName = `${Date.now()}_${file.name}`;
|
const fileName = `${Date.now()}_${file.name}`;
|
||||||
const filePath = path.join(UPLOAD_DIR, fileName);
|
const filePath = path.join(UPLOAD_DIR, fileName);
|
||||||
|
|
||||||
file.mv(filePath, (err) => {
|
file.mv(filePath, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('文件保存失败:', err);
|
console.error('文件保存失败:', err);
|
||||||
return res.status(500).json({ error: '文件保存失败' });
|
return res.status(500).json({ error: '文件保存失败' });
|
||||||
|
|||||||
+54
-27
@@ -23,11 +23,7 @@ const {
|
|||||||
updateAutoBackupSettings,
|
updateAutoBackupSettings,
|
||||||
executeBackupNow,
|
executeBackupNow,
|
||||||
} = require('../utils/autoBackupScheduler');
|
} = require('../utils/autoBackupScheduler');
|
||||||
const {
|
const { getBackupLogs, getBackupLogById, deleteOldLogs } = require('../utils/backupLog');
|
||||||
getBackupLogs,
|
|
||||||
getBackupLogById,
|
|
||||||
deleteOldLogs,
|
|
||||||
} = require('../utils/backupLog');
|
|
||||||
const {
|
const {
|
||||||
getAllTargets,
|
getAllTargets,
|
||||||
getTarget,
|
getTarget,
|
||||||
@@ -81,15 +77,20 @@ router.get('/list', async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = fs.readdirSync(backupPath)
|
const files = fs
|
||||||
.filter(f => (f.startsWith('backup_') || f.startsWith('uploaded_')) && (f.endsWith('.json') || f.endsWith('.json.gz')))
|
.readdirSync(backupPath)
|
||||||
|
.filter(
|
||||||
|
f =>
|
||||||
|
(f.startsWith('backup_') || f.startsWith('uploaded_')) &&
|
||||||
|
(f.endsWith('.json') || f.endsWith('.json.gz'))
|
||||||
|
)
|
||||||
.map(async f => {
|
.map(async f => {
|
||||||
const filePath = path.join(backupPath, f);
|
const filePath = path.join(backupPath, f);
|
||||||
const stats = fs.statSync(filePath);
|
const stats = fs.statSync(filePath);
|
||||||
const isCompressed = f.endsWith('.gz');
|
const isCompressed = f.endsWith('.gz');
|
||||||
|
|
||||||
// 尝试从文件内容中提取元数据
|
// 尝试从文件内容中提取元数据
|
||||||
let metadata = {
|
const metadata = {
|
||||||
filename: f,
|
filename: f,
|
||||||
size: stats.size,
|
size: stats.size,
|
||||||
compressed: isCompressed,
|
compressed: isCompressed,
|
||||||
@@ -120,7 +121,6 @@ router.get('/list', async (req, res) => {
|
|||||||
|
|
||||||
// 判断是否为上传的文件(通过文件名判断)
|
// 判断是否为上传的文件(通过文件名判断)
|
||||||
metadata.isUploaded = f.startsWith('uploaded_');
|
metadata.isUploaded = f.startsWith('uploaded_');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 如果读取失败,标记为无效文件
|
// 如果读取失败,标记为无效文件
|
||||||
metadata.invalid = true;
|
metadata.invalid = true;
|
||||||
@@ -201,7 +201,7 @@ router.get('/restore-progress/:filename', authMiddleware, async (req, res) => {
|
|||||||
res.setHeader('Connection', 'keep-alive');
|
res.setHeader('Connection', 'keep-alive');
|
||||||
res.setHeader('X-Accel-Buffering', 'no');
|
res.setHeader('X-Accel-Buffering', 'no');
|
||||||
|
|
||||||
const sendProgress = (data) => {
|
const sendProgress = data => {
|
||||||
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -211,12 +211,21 @@ router.get('/restore-progress/:filename', authMiddleware, async (req, res) => {
|
|||||||
|
|
||||||
const validation = await validateBackupFile(filePath);
|
const validation = await validateBackupFile(filePath);
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
sendProgress({ stage: 'error', message: `备份文件验证失败: ${validation.error}`, progress: 0 });
|
sendProgress({
|
||||||
|
stage: 'error',
|
||||||
|
message: `备份文件验证失败: ${validation.error}`,
|
||||||
|
progress: 0,
|
||||||
|
});
|
||||||
res.end();
|
res.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendProgress({ stage: 'validate', message: '备份文件验证通过', progress: 10, metadata: validation.metadata });
|
sendProgress({
|
||||||
|
stage: 'validate',
|
||||||
|
message: '备份文件验证通过',
|
||||||
|
progress: 10,
|
||||||
|
metadata: validation.metadata,
|
||||||
|
});
|
||||||
|
|
||||||
const buffer = fs.readFileSync(filePath);
|
const buffer = fs.readFileSync(filePath);
|
||||||
const isCompressed = filePath.endsWith('.gz');
|
const isCompressed = filePath.endsWith('.gz');
|
||||||
@@ -243,10 +252,10 @@ router.get('/restore-progress/:filename', authMiddleware, async (req, res) => {
|
|||||||
processedTables++;
|
processedTables++;
|
||||||
const progress = 20 + Math.floor((processedTables / totalTables) * 70);
|
const progress = 20 + Math.floor((processedTables / totalTables) * 70);
|
||||||
const statusMap = {
|
const statusMap = {
|
||||||
'restored': '已恢复',
|
restored: '已恢复',
|
||||||
'skipped': '已跳过',
|
skipped: '已跳过',
|
||||||
'empty': '无数据',
|
empty: '无数据',
|
||||||
'error': '错误',
|
error: '错误',
|
||||||
};
|
};
|
||||||
sendProgress({
|
sendProgress({
|
||||||
stage: 'restore',
|
stage: 'restore',
|
||||||
@@ -272,7 +281,7 @@ router.get('/restore-progress/:filename', authMiddleware, async (req, res) => {
|
|||||||
restoredAt: result.restoredAt,
|
restoredAt: result.restoredAt,
|
||||||
tableDetails: result.tableDetails,
|
tableDetails: result.tableDetails,
|
||||||
fileDetails: result.fileDetails,
|
fileDetails: result.fileDetails,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
res.end();
|
res.end();
|
||||||
@@ -411,7 +420,7 @@ router.get('/download/:filename', (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
res.download(filePath, filename, (err) => {
|
res.download(filePath, filename, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('下载备份文件失败:', err);
|
console.error('下载备份文件失败:', err);
|
||||||
}
|
}
|
||||||
@@ -463,7 +472,9 @@ router.get('/info', (req, res) => {
|
|||||||
let backupCount = 0;
|
let backupCount = 0;
|
||||||
|
|
||||||
if (fs.existsSync(backupPath)) {
|
if (fs.existsSync(backupPath)) {
|
||||||
const files = fs.readdirSync(backupPath).filter(f => f.endsWith('.json') || f.endsWith('.json.gz'));
|
const files = fs
|
||||||
|
.readdirSync(backupPath)
|
||||||
|
.filter(f => f.endsWith('.json') || f.endsWith('.json.gz'));
|
||||||
backupCount = files.length;
|
backupCount = files.length;
|
||||||
files.forEach(f => {
|
files.forEach(f => {
|
||||||
const stats = fs.statSync(path.join(backupPath, f));
|
const stats = fs.statSync(path.join(backupPath, f));
|
||||||
@@ -491,7 +502,9 @@ router.get('/info', (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function formatBytes(bytes) {
|
function formatBytes(bytes) {
|
||||||
if (bytes === 0) return '0 B';
|
if (bytes === 0) {
|
||||||
|
return '0 B';
|
||||||
|
}
|
||||||
const k = 1024;
|
const k = 1024;
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
@@ -565,7 +578,9 @@ router.post('/auto/settings', (req, res) => {
|
|||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
const newSettings = {};
|
const newSettings = {};
|
||||||
if (enabled !== undefined) newSettings.enabled = enabled;
|
if (enabled !== undefined) {
|
||||||
|
newSettings.enabled = enabled;
|
||||||
|
}
|
||||||
if (hour !== undefined || minute !== undefined) {
|
if (hour !== undefined || minute !== undefined) {
|
||||||
newSettings.hour = hour || 2;
|
newSettings.hour = hour || 2;
|
||||||
newSettings.minute = minute || 0;
|
newSettings.minute = minute || 0;
|
||||||
@@ -579,12 +594,24 @@ router.post('/auto/settings', (req, res) => {
|
|||||||
}
|
}
|
||||||
newSettings.cronExpression = cronExpression;
|
newSettings.cronExpression = cronExpression;
|
||||||
}
|
}
|
||||||
if (description) newSettings.description = description;
|
if (description) {
|
||||||
if (includeFiles !== undefined) newSettings.includeFiles = includeFiles;
|
newSettings.description = description;
|
||||||
if (compress !== undefined) newSettings.compress = compress;
|
}
|
||||||
if (maxCount !== undefined) newSettings.maxCount = maxCount;
|
if (includeFiles !== undefined) {
|
||||||
if (maxAgeDays !== undefined) newSettings.maxAgeDays = maxAgeDays;
|
newSettings.includeFiles = includeFiles;
|
||||||
if (backupType !== undefined) newSettings.backupType = backupType;
|
}
|
||||||
|
if (compress !== undefined) {
|
||||||
|
newSettings.compress = compress;
|
||||||
|
}
|
||||||
|
if (maxCount !== undefined) {
|
||||||
|
newSettings.maxCount = maxCount;
|
||||||
|
}
|
||||||
|
if (maxAgeDays !== undefined) {
|
||||||
|
newSettings.maxAgeDays = maxAgeDays;
|
||||||
|
}
|
||||||
|
if (backupType !== undefined) {
|
||||||
|
newSettings.backupType = backupType;
|
||||||
|
}
|
||||||
|
|
||||||
const success = updateAutoBackupSettings(newSettings);
|
const success = updateAutoBackupSettings(newSettings);
|
||||||
if (success) {
|
if (success) {
|
||||||
|
|||||||
+97
-75
@@ -8,10 +8,7 @@ const DevicePort = require('../models/DevicePort');
|
|||||||
// 辅助函数:更新端口状态
|
// 辅助函数:更新端口状态
|
||||||
async function updatePortStatus(deviceId, portName, status) {
|
async function updatePortStatus(deviceId, portName, status) {
|
||||||
try {
|
try {
|
||||||
await DevicePort.update(
|
await DevicePort.update({ status }, { where: { deviceId, portName } });
|
||||||
{ status },
|
|
||||||
{ where: { deviceId, portName } }
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`更新端口状态失败: ${deviceId}:${portName} -> ${status}`, error);
|
console.error(`更新端口状态失败: ${deviceId}:${portName} -> ${status}`, error);
|
||||||
}
|
}
|
||||||
@@ -29,7 +26,14 @@ async function freePort(deviceId, portName) {
|
|||||||
|
|
||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { sourceDeviceId, targetDeviceId, status, cableType, page = 1, pageSize = 10 } = req.query;
|
const {
|
||||||
|
sourceDeviceId,
|
||||||
|
targetDeviceId,
|
||||||
|
status,
|
||||||
|
cableType,
|
||||||
|
page = 1,
|
||||||
|
pageSize = 10,
|
||||||
|
} = req.query;
|
||||||
const offset = (page - 1) * pageSize;
|
const offset = (page - 1) * pageSize;
|
||||||
|
|
||||||
const where = {};
|
const where = {};
|
||||||
@@ -56,24 +60,24 @@ router.get('/', async (req, res) => {
|
|||||||
{
|
{
|
||||||
model: Device,
|
model: Device,
|
||||||
as: 'sourceDevice',
|
as: 'sourceDevice',
|
||||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
model: Device,
|
model: Device,
|
||||||
as: 'targetDevice',
|
as: 'targetDevice',
|
||||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
offset,
|
offset,
|
||||||
limit: parseInt(pageSize),
|
limit: parseInt(pageSize),
|
||||||
order: [['createdAt', 'DESC']]
|
order: [['createdAt', 'DESC']],
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
total: count,
|
total: count,
|
||||||
cables: rows,
|
cables: rows,
|
||||||
page: parseInt(page),
|
page: parseInt(page),
|
||||||
pageSize: parseInt(pageSize)
|
pageSize: parseInt(pageSize),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取接线列表失败:', error);
|
console.error('获取接线列表失败:', error);
|
||||||
@@ -87,23 +91,20 @@ router.get('/device/:deviceId', async (req, res) => {
|
|||||||
|
|
||||||
const cables = await Cable.findAll({
|
const cables = await Cable.findAll({
|
||||||
where: {
|
where: {
|
||||||
[Op.or]: [
|
[Op.or]: [{ sourceDeviceId: deviceId }, { targetDeviceId: deviceId }],
|
||||||
{ sourceDeviceId: deviceId },
|
|
||||||
{ targetDeviceId: deviceId }
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: Device,
|
model: Device,
|
||||||
as: 'sourceDevice',
|
as: 'sourceDevice',
|
||||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
model: Device,
|
model: Device,
|
||||||
as: 'targetDevice',
|
as: 'targetDevice',
|
||||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json(cables);
|
res.json(cables);
|
||||||
@@ -121,7 +122,7 @@ router.get('/rack/:rackId', async (req, res) => {
|
|||||||
// 1. 找出该机柜下的所有设备ID
|
// 1. 找出该机柜下的所有设备ID
|
||||||
const devices = await Device.findAll({
|
const devices = await Device.findAll({
|
||||||
where: { rackId: rackId },
|
where: { rackId: rackId },
|
||||||
attributes: ['deviceId']
|
attributes: ['deviceId'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const deviceIds = devices.map(d => d.deviceId);
|
const deviceIds = devices.map(d => d.deviceId);
|
||||||
@@ -135,21 +136,21 @@ router.get('/rack/:rackId', async (req, res) => {
|
|||||||
where: {
|
where: {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ sourceDeviceId: { [Op.in]: deviceIds } },
|
{ sourceDeviceId: { [Op.in]: deviceIds } },
|
||||||
{ targetDeviceId: { [Op.in]: deviceIds } }
|
{ targetDeviceId: { [Op.in]: deviceIds } },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: Device,
|
model: Device,
|
||||||
as: 'sourceDevice',
|
as: 'sourceDevice',
|
||||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
model: Device,
|
model: Device,
|
||||||
as: 'targetDevice',
|
as: 'targetDevice',
|
||||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json(cables);
|
res.json(cables);
|
||||||
@@ -175,22 +176,22 @@ router.post('/check-conflict', async (req, res) => {
|
|||||||
where: {
|
where: {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ sourceDeviceId, sourcePort },
|
{ sourceDeviceId, sourcePort },
|
||||||
{ targetDeviceId: sourceDeviceId, targetPort: sourcePort }
|
{ targetDeviceId: sourceDeviceId, targetPort: sourcePort },
|
||||||
],
|
],
|
||||||
...(excludeCableId && { cableId: { [Op.ne]: excludeCableId } })
|
...(excludeCableId && { cableId: { [Op.ne]: excludeCableId } }),
|
||||||
},
|
},
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: Device,
|
model: Device,
|
||||||
as: 'sourceDevice',
|
as: 'sourceDevice',
|
||||||
attributes: ['deviceId', 'name', 'type']
|
attributes: ['deviceId', 'name', 'type'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
model: Device,
|
model: Device,
|
||||||
as: 'targetDevice',
|
as: 'targetDevice',
|
||||||
attributes: ['deviceId', 'name', 'type']
|
attributes: ['deviceId', 'name', 'type'],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (sourceConflict) {
|
if (sourceConflict) {
|
||||||
@@ -198,7 +199,7 @@ router.post('/check-conflict', async (req, res) => {
|
|||||||
type: 'source',
|
type: 'source',
|
||||||
port: sourcePort,
|
port: sourcePort,
|
||||||
deviceId: sourceDeviceId,
|
deviceId: sourceDeviceId,
|
||||||
existingCable: sourceConflict
|
existingCable: sourceConflict,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,22 +208,22 @@ router.post('/check-conflict', async (req, res) => {
|
|||||||
where: {
|
where: {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ sourceDeviceId: targetDeviceId, sourcePort: targetPort },
|
{ sourceDeviceId: targetDeviceId, sourcePort: targetPort },
|
||||||
{ targetDeviceId, targetPort }
|
{ targetDeviceId, targetPort },
|
||||||
],
|
],
|
||||||
...(excludeCableId && { cableId: { [Op.ne]: excludeCableId } })
|
...(excludeCableId && { cableId: { [Op.ne]: excludeCableId } }),
|
||||||
},
|
},
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: Device,
|
model: Device,
|
||||||
as: 'sourceDevice',
|
as: 'sourceDevice',
|
||||||
attributes: ['deviceId', 'name', 'type']
|
attributes: ['deviceId', 'name', 'type'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
model: Device,
|
model: Device,
|
||||||
as: 'targetDevice',
|
as: 'targetDevice',
|
||||||
attributes: ['deviceId', 'name', 'type']
|
attributes: ['deviceId', 'name', 'type'],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (targetConflict) {
|
if (targetConflict) {
|
||||||
@@ -230,13 +231,13 @@ router.post('/check-conflict', async (req, res) => {
|
|||||||
type: 'target',
|
type: 'target',
|
||||||
port: targetPort,
|
port: targetPort,
|
||||||
deviceId: targetDeviceId,
|
deviceId: targetDeviceId,
|
||||||
existingCable: targetConflict
|
existingCable: targetConflict,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
hasConflict: conflicts.length > 0,
|
hasConflict: conflicts.length > 0,
|
||||||
conflicts
|
conflicts,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('检查接线冲突失败:', error);
|
console.error('检查接线冲突失败:', error);
|
||||||
@@ -246,7 +247,18 @@ router.post('/check-conflict', async (req, res) => {
|
|||||||
|
|
||||||
router.post('/', async (req, res) => {
|
router.post('/', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { cableId, sourceDeviceId, sourcePort, targetDeviceId, targetPort, cableType, cableLength, status, description, force } = req.body;
|
const {
|
||||||
|
cableId,
|
||||||
|
sourceDeviceId,
|
||||||
|
sourcePort,
|
||||||
|
targetDeviceId,
|
||||||
|
targetPort,
|
||||||
|
cableType,
|
||||||
|
cableLength,
|
||||||
|
status,
|
||||||
|
description,
|
||||||
|
force,
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
if (!sourceDeviceId || !sourcePort || !targetDeviceId || !targetPort) {
|
if (!sourceDeviceId || !sourcePort || !targetDeviceId || !targetPort) {
|
||||||
return res.status(400).json({ error: '缺少必填字段' });
|
return res.status(400).json({ error: '缺少必填字段' });
|
||||||
@@ -262,16 +274,16 @@ router.post('/', async (req, res) => {
|
|||||||
where: {
|
where: {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ sourceDeviceId, sourcePort },
|
{ sourceDeviceId, sourcePort },
|
||||||
{ targetDeviceId, targetPort }
|
{ targetDeviceId, targetPort },
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingCable) {
|
if (existingCable) {
|
||||||
return res.status(409).json({
|
return res.status(409).json({
|
||||||
error: '端口已被占用',
|
error: '端口已被占用',
|
||||||
conflict: true,
|
conflict: true,
|
||||||
existingCable
|
existingCable,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -284,9 +296,9 @@ router.post('/', async (req, res) => {
|
|||||||
{ sourceDeviceId, sourcePort },
|
{ sourceDeviceId, sourcePort },
|
||||||
{ targetDeviceId: sourceDeviceId, targetPort: sourcePort },
|
{ targetDeviceId: sourceDeviceId, targetPort: sourcePort },
|
||||||
{ sourceDeviceId: targetDeviceId, sourcePort: targetPort },
|
{ sourceDeviceId: targetDeviceId, sourcePort: targetPort },
|
||||||
{ targetDeviceId, targetPort }
|
{ targetDeviceId, targetPort },
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const cable of existingCables) {
|
for (const cable of existingCables) {
|
||||||
@@ -308,7 +320,7 @@ router.post('/', async (req, res) => {
|
|||||||
cableType: cableType || 'ethernet',
|
cableType: cableType || 'ethernet',
|
||||||
cableLength,
|
cableLength,
|
||||||
status: status || 'normal',
|
status: status || 'normal',
|
||||||
description
|
description,
|
||||||
});
|
});
|
||||||
|
|
||||||
const createdCable = await Cable.findByPk(cable.cableId, {
|
const createdCable = await Cable.findByPk(cable.cableId, {
|
||||||
@@ -316,14 +328,14 @@ router.post('/', async (req, res) => {
|
|||||||
{
|
{
|
||||||
model: Device,
|
model: Device,
|
||||||
as: 'sourceDevice',
|
as: 'sourceDevice',
|
||||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
model: Device,
|
model: Device,
|
||||||
as: 'targetDevice',
|
as: 'targetDevice',
|
||||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
// 自动将源端口和目标端口状态设为occupied
|
// 自动将源端口和目标端口状态设为occupied
|
||||||
@@ -349,15 +361,20 @@ router.post('/batch', async (req, res) => {
|
|||||||
total: cables.length,
|
total: cables.length,
|
||||||
success: 0,
|
success: 0,
|
||||||
failed: 0,
|
failed: 0,
|
||||||
errors: []
|
errors: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let i = 0; i < cables.length; i++) {
|
for (let i = 0; i < cables.length; i++) {
|
||||||
const cableData = cables[i];
|
const cableData = cables[i];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!cableData.cableId || !cableData.sourceDeviceId || !cableData.sourcePort ||
|
if (
|
||||||
!cableData.targetDeviceId || !cableData.targetPort) {
|
!cableData.cableId ||
|
||||||
|
!cableData.sourceDeviceId ||
|
||||||
|
!cableData.sourcePort ||
|
||||||
|
!cableData.targetDeviceId ||
|
||||||
|
!cableData.targetPort
|
||||||
|
) {
|
||||||
throw new Error('缺少必填字段');
|
throw new Error('缺少必填字段');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,9 +386,9 @@ router.post('/batch', async (req, res) => {
|
|||||||
where: {
|
where: {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ sourceDeviceId: cableData.sourceDeviceId, sourcePort: cableData.sourcePort },
|
{ sourceDeviceId: cableData.sourceDeviceId, sourcePort: cableData.sourcePort },
|
||||||
{ targetDeviceId: cableData.targetDeviceId, targetPort: cableData.targetPort }
|
{ targetDeviceId: cableData.targetDeviceId, targetPort: cableData.targetPort },
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingCable) {
|
if (existingCable) {
|
||||||
@@ -387,7 +404,7 @@ router.post('/batch', async (req, res) => {
|
|||||||
cableType: cableData.cableType || 'ethernet',
|
cableType: cableData.cableType || 'ethernet',
|
||||||
cableLength: cableData.cableLength,
|
cableLength: cableData.cableLength,
|
||||||
status: cableData.status || 'normal',
|
status: cableData.status || 'normal',
|
||||||
description: cableData.description
|
description: cableData.description,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 自动将源端口和目标端口状态设为occupied
|
// 自动将源端口和目标端口状态设为occupied
|
||||||
@@ -400,7 +417,7 @@ router.post('/batch', async (req, res) => {
|
|||||||
results.errors.push({
|
results.errors.push({
|
||||||
index: i + 1,
|
index: i + 1,
|
||||||
cableId: cableData.cableId,
|
cableId: cableData.cableId,
|
||||||
error: error.message
|
error: error.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -421,10 +438,15 @@ router.put('/:cableId', async (req, res) => {
|
|||||||
return res.status(404).json({ error: '接线不存在' });
|
return res.status(404).json({ error: '接线不存在' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { sourceDeviceId: oldSourceDeviceId, sourcePort: oldSourcePort, targetDeviceId: oldTargetDeviceId, targetPort: oldTargetPort } = oldCable;
|
const {
|
||||||
|
sourceDeviceId: oldSourceDeviceId,
|
||||||
|
sourcePort: oldSourcePort,
|
||||||
|
targetDeviceId: oldTargetDeviceId,
|
||||||
|
targetPort: oldTargetPort,
|
||||||
|
} = oldCable;
|
||||||
|
|
||||||
const [updated] = await Cable.update(req.body, {
|
const [updated] = await Cable.update(req.body, {
|
||||||
where: { cableId: req.params.cableId }
|
where: { cableId: req.params.cableId },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (updated) {
|
if (updated) {
|
||||||
@@ -433,14 +455,14 @@ router.put('/:cableId', async (req, res) => {
|
|||||||
{
|
{
|
||||||
model: Device,
|
model: Device,
|
||||||
as: 'sourceDevice',
|
as: 'sourceDevice',
|
||||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
model: Device,
|
model: Device,
|
||||||
as: 'targetDevice',
|
as: 'targetDevice',
|
||||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { sourceDeviceId, sourcePort, targetDeviceId, targetPort } = cable;
|
const { sourceDeviceId, sourcePort, targetDeviceId, targetPort } = cable;
|
||||||
@@ -480,7 +502,7 @@ router.delete('/:cableId', async (req, res) => {
|
|||||||
const { sourceDeviceId, sourcePort, targetDeviceId, targetPort } = cable;
|
const { sourceDeviceId, sourcePort, targetDeviceId, targetPort } = cable;
|
||||||
|
|
||||||
const deleted = await Cable.destroy({
|
const deleted = await Cable.destroy({
|
||||||
where: { cableId: req.params.cableId }
|
where: { cableId: req.params.cableId },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
@@ -508,11 +530,11 @@ router.delete('/batch', async (req, res) => {
|
|||||||
|
|
||||||
// 先获取所有要删除的接线信息,用于后续恢复端口状态
|
// 先获取所有要删除的接线信息,用于后续恢复端口状态
|
||||||
const cables = await Cable.findAll({
|
const cables = await Cable.findAll({
|
||||||
where: { cableId: { [Op.in]: cableIds } }
|
where: { cableId: { [Op.in]: cableIds } },
|
||||||
});
|
});
|
||||||
|
|
||||||
const deletedCount = await Cable.destroy({
|
const deletedCount = await Cable.destroy({
|
||||||
where: { cableId: { [Op.in]: cableIds } }
|
where: { cableId: { [Op.in]: cableIds } },
|
||||||
});
|
});
|
||||||
|
|
||||||
// 自动将所有相关端口状态恢复为free
|
// 自动将所有相关端口状态恢复为free
|
||||||
@@ -523,7 +545,7 @@ router.delete('/batch', async (req, res) => {
|
|||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: `批量删除成功,已删除 ${deletedCount} 条接线`,
|
message: `批量删除成功,已删除 ${deletedCount} 条接线`,
|
||||||
deletedCount
|
deletedCount,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('批量删除接线失败:', error);
|
console.error('批量删除接线失败:', error);
|
||||||
@@ -538,14 +560,14 @@ router.get('/:cableId', async (req, res) => {
|
|||||||
{
|
{
|
||||||
model: Device,
|
model: Device,
|
||||||
as: 'sourceDevice',
|
as: 'sourceDevice',
|
||||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
model: Device,
|
model: Device,
|
||||||
as: 'targetDevice',
|
as: 'targetDevice',
|
||||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!cable) {
|
if (!cable) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ router.get('/', async (req, res) => {
|
|||||||
if (keyword) {
|
if (keyword) {
|
||||||
where[Op.or] = [
|
where[Op.or] = [
|
||||||
{ name: { [Op.like]: `%${keyword}%` } },
|
{ name: { [Op.like]: `%${keyword}%` } },
|
||||||
{ description: { [Op.like]: `%${keyword}%` } }
|
{ description: { [Op.like]: `%${keyword}%` } },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,9 +23,12 @@ router.get('/', async (req, res) => {
|
|||||||
|
|
||||||
const { count, rows } = await ConsumableCategory.findAndCountAll({
|
const { count, rows } = await ConsumableCategory.findAndCountAll({
|
||||||
where,
|
where,
|
||||||
order: [['sortOrder', 'ASC'], ['id', 'DESC']],
|
order: [
|
||||||
|
['sortOrder', 'ASC'],
|
||||||
|
['id', 'DESC'],
|
||||||
|
],
|
||||||
offset,
|
offset,
|
||||||
limit: parseInt(pageSize)
|
limit: parseInt(pageSize),
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -33,7 +36,7 @@ router.get('/', async (req, res) => {
|
|||||||
total: count,
|
total: count,
|
||||||
currentPage: parseInt(page),
|
currentPage: parseInt(page),
|
||||||
pageSize: parseInt(pageSize),
|
pageSize: parseInt(pageSize),
|
||||||
totalPages: Math.ceil(count / pageSize)
|
totalPages: Math.ceil(count / pageSize),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -44,7 +47,10 @@ router.get('/list', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const categories = await ConsumableCategory.findAll({
|
const categories = await ConsumableCategory.findAll({
|
||||||
where: { status: 'active' },
|
where: { status: 'active' },
|
||||||
order: [['sortOrder', 'ASC'], ['name', 'ASC']]
|
order: [
|
||||||
|
['sortOrder', 'ASC'],
|
||||||
|
['name', 'ASC'],
|
||||||
|
],
|
||||||
});
|
});
|
||||||
res.json(categories);
|
res.json(categories);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -77,7 +83,7 @@ router.post('/', async (req, res) => {
|
|||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
sortOrder: sortOrder || 0,
|
sortOrder: sortOrder || 0,
|
||||||
status: status || 'active'
|
status: status || 'active',
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(201).json(category);
|
res.status(201).json(category);
|
||||||
@@ -106,7 +112,7 @@ router.put('/:id', async (req, res) => {
|
|||||||
name: name || category.name,
|
name: name || category.name,
|
||||||
description: description !== undefined ? description : category.description,
|
description: description !== undefined ? description : category.description,
|
||||||
sortOrder: sortOrder !== undefined ? sortOrder : category.sortOrder,
|
sortOrder: sortOrder !== undefined ? sortOrder : category.sortOrder,
|
||||||
status: status || category.status
|
status: status || category.status,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json(category);
|
res.json(category);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ router.get('/', async (req, res) => {
|
|||||||
|
|
||||||
if (startDate && endDate) {
|
if (startDate && endDate) {
|
||||||
where.createdAt = {
|
where.createdAt = {
|
||||||
[Op.between]: [new Date(startDate), new Date(endDate)]
|
[Op.between]: [new Date(startDate), new Date(endDate)],
|
||||||
};
|
};
|
||||||
} else if (startDate) {
|
} else if (startDate) {
|
||||||
where.createdAt = { [Op.gte]: new Date(startDate) };
|
where.createdAt = { [Op.gte]: new Date(startDate) };
|
||||||
@@ -33,19 +33,17 @@ router.get('/', async (req, res) => {
|
|||||||
|
|
||||||
const { count, rows } = await ConsumableRecord.findAndCountAll({
|
const { count, rows } = await ConsumableRecord.findAndCountAll({
|
||||||
where,
|
where,
|
||||||
include: [
|
include: [{ model: Consumable, as: 'consumable', attributes: ['name', 'category', 'unit'] }],
|
||||||
{ model: Consumable, as: 'consumable', attributes: ['name', 'category', 'unit'] }
|
|
||||||
],
|
|
||||||
offset,
|
offset,
|
||||||
limit: parseInt(pageSize),
|
limit: parseInt(pageSize),
|
||||||
order: [['createdAt', 'DESC']]
|
order: [['createdAt', 'DESC']],
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
total: count,
|
total: count,
|
||||||
records: rows,
|
records: rows,
|
||||||
page: parseInt(page),
|
page: parseInt(page),
|
||||||
pageSize: parseInt(pageSize)
|
pageSize: parseInt(pageSize),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -82,7 +80,8 @@ router.post('/', async (req, res) => {
|
|||||||
|
|
||||||
await consumable.update({ currentStock: newStock }, { transaction });
|
await consumable.update({ currentStock: newStock }, { transaction });
|
||||||
|
|
||||||
const record = await ConsumableRecord.create({
|
const record = await ConsumableRecord.create(
|
||||||
|
{
|
||||||
consumableId,
|
consumableId,
|
||||||
type,
|
type,
|
||||||
quantity,
|
quantity,
|
||||||
@@ -91,10 +90,13 @@ router.post('/', async (req, res) => {
|
|||||||
operator,
|
operator,
|
||||||
reason,
|
reason,
|
||||||
recipient,
|
recipient,
|
||||||
notes
|
notes,
|
||||||
}, { transaction });
|
},
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
|
||||||
await ConsumableLog.create({
|
await ConsumableLog.create(
|
||||||
|
{
|
||||||
consumableId,
|
consumableId,
|
||||||
consumableName: consumable.name,
|
consumableName: consumable.name,
|
||||||
operationType: type,
|
operationType: type,
|
||||||
@@ -103,8 +105,10 @@ router.post('/', async (req, res) => {
|
|||||||
currentStock: newStock,
|
currentStock: newStock,
|
||||||
operator,
|
operator,
|
||||||
reason,
|
reason,
|
||||||
notes
|
notes,
|
||||||
}, { transaction });
|
},
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
|
|
||||||
@@ -112,8 +116,8 @@ router.post('/', async (req, res) => {
|
|||||||
record,
|
record,
|
||||||
consumable: {
|
consumable: {
|
||||||
previousStock,
|
previousStock,
|
||||||
currentStock: newStock
|
currentStock: newStock,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
@@ -134,7 +138,7 @@ router.get('/statistics', async (req, res) => {
|
|||||||
|
|
||||||
dateWhere.createdAt = {
|
dateWhere.createdAt = {
|
||||||
[Op.gte]: startDateTime,
|
[Op.gte]: startDateTime,
|
||||||
[Op.lte]: endDateTime
|
[Op.lte]: endDateTime,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,10 +154,10 @@ router.get('/statistics', async (req, res) => {
|
|||||||
model: Consumable,
|
model: Consumable,
|
||||||
as: 'consumable',
|
as: 'consumable',
|
||||||
attributes: ['name', 'category'],
|
attributes: ['name', 'category'],
|
||||||
where: Object.keys(consumableWhere).length > 0 ? consumableWhere : undefined
|
where: Object.keys(consumableWhere).length > 0 ? consumableWhere : undefined,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
attributes: ['type', 'quantity']
|
attributes: ['type', 'quantity'],
|
||||||
});
|
});
|
||||||
|
|
||||||
let inCount = 0;
|
let inCount = 0;
|
||||||
@@ -186,11 +190,11 @@ router.get('/statistics', async (req, res) => {
|
|||||||
model: Consumable,
|
model: Consumable,
|
||||||
as: 'consumable',
|
as: 'consumable',
|
||||||
attributes: ['name', 'category'],
|
attributes: ['name', 'category'],
|
||||||
where: Object.keys(consumableWhere).length > 0 ? consumableWhere : undefined
|
where: Object.keys(consumableWhere).length > 0 ? consumableWhere : undefined,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
order: [['createdAt', 'DESC']],
|
order: [['createdAt', 'DESC']],
|
||||||
limit: 10
|
limit: 10,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -202,7 +206,7 @@ router.get('/statistics', async (req, res) => {
|
|||||||
byType: Object.entries(typeMap).map(([type, data]) => ({
|
byType: Object.entries(typeMap).map(([type, data]) => ({
|
||||||
type,
|
type,
|
||||||
totalQuantity: data.totalQuantity,
|
totalQuantity: data.totalQuantity,
|
||||||
count: data.count
|
count: data.count,
|
||||||
})),
|
})),
|
||||||
recentRecords: recentRecords.map(record => ({
|
recentRecords: recentRecords.map(record => ({
|
||||||
recordId: record.recordId,
|
recordId: record.recordId,
|
||||||
@@ -213,8 +217,8 @@ router.get('/statistics', async (req, res) => {
|
|||||||
consumableId: record.consumableId,
|
consumableId: record.consumableId,
|
||||||
consumableName: record.consumable?.name || '未知耗材',
|
consumableName: record.consumable?.name || '未知耗材',
|
||||||
category: record.consumable?.category || null,
|
category: record.consumable?.category || null,
|
||||||
unit: record.consumable?.unit || '个'
|
unit: record.consumable?.unit || '个',
|
||||||
}))
|
})),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
|
|||||||
+214
-137
@@ -11,7 +11,13 @@ const { PAGINATION, RETRY } = require('../config');
|
|||||||
|
|
||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { keyword, category, status, page = 1, pageSize = PAGINATION.DEFAULT_PAGE_SIZE } = req.query;
|
const {
|
||||||
|
keyword,
|
||||||
|
category,
|
||||||
|
status,
|
||||||
|
page = 1,
|
||||||
|
pageSize = PAGINATION.DEFAULT_PAGE_SIZE,
|
||||||
|
} = req.query;
|
||||||
const offset = (page - 1) * pageSize;
|
const offset = (page - 1) * pageSize;
|
||||||
|
|
||||||
const where = {};
|
const where = {};
|
||||||
@@ -22,7 +28,7 @@ router.get('/', async (req, res) => {
|
|||||||
{ name: { [Op.like]: `%${keyword}%` } },
|
{ name: { [Op.like]: `%${keyword}%` } },
|
||||||
{ category: { [Op.like]: `%${keyword}%` } },
|
{ category: { [Op.like]: `%${keyword}%` } },
|
||||||
{ supplier: { [Op.like]: `%${keyword}%` } },
|
{ supplier: { [Op.like]: `%${keyword}%` } },
|
||||||
{ location: { [Op.like]: `%${keyword}%` } }
|
{ location: { [Op.like]: `%${keyword}%` } },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +44,7 @@ router.get('/', async (req, res) => {
|
|||||||
where,
|
where,
|
||||||
offset,
|
offset,
|
||||||
limit: parseInt(pageSize),
|
limit: parseInt(pageSize),
|
||||||
order: [['createdAt', 'DESC']]
|
order: [['createdAt', 'DESC']],
|
||||||
});
|
});
|
||||||
|
|
||||||
const consumables = rows.map(item => {
|
const consumables = rows.map(item => {
|
||||||
@@ -53,7 +59,7 @@ router.get('/', async (req, res) => {
|
|||||||
total: count,
|
total: count,
|
||||||
consumables,
|
consumables,
|
||||||
page: parseInt(page),
|
page: parseInt(page),
|
||||||
pageSize: parseInt(pageSize)
|
pageSize: parseInt(pageSize),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -74,7 +80,7 @@ router.get('/export', async (req, res) => {
|
|||||||
{ name: { [Op.like]: `%${keyword}%` } },
|
{ name: { [Op.like]: `%${keyword}%` } },
|
||||||
{ category: { [Op.like]: `%${keyword}%` } },
|
{ category: { [Op.like]: `%${keyword}%` } },
|
||||||
{ supplier: { [Op.like]: `%${keyword}%` } },
|
{ supplier: { [Op.like]: `%${keyword}%` } },
|
||||||
{ location: { [Op.like]: `%${keyword}%` } }
|
{ location: { [Op.like]: `%${keyword}%` } },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +95,7 @@ router.get('/export', async (req, res) => {
|
|||||||
const consumables = await Consumable.findAll({
|
const consumables = await Consumable.findAll({
|
||||||
where,
|
where,
|
||||||
limit: MAX_EXPORT_SIZE,
|
limit: MAX_EXPORT_SIZE,
|
||||||
order: [['createdAt', 'DESC']]
|
order: [['createdAt', 'DESC']],
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = consumables.map(item => {
|
const result = consumables.map(item => {
|
||||||
@@ -102,7 +108,7 @@ router.get('/export', async (req, res) => {
|
|||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
consumables: result,
|
consumables: result,
|
||||||
total: result.length
|
total: result.length,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -114,14 +120,15 @@ router.post('/', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const consumableData = {
|
const consumableData = {
|
||||||
...req.body,
|
...req.body,
|
||||||
consumableId: req.body.consumableId || `CON${Date.now()}`
|
consumableId: req.body.consumableId || `CON${Date.now()}`,
|
||||||
};
|
};
|
||||||
if (Array.isArray(consumableData.snList)) {
|
if (Array.isArray(consumableData.snList)) {
|
||||||
consumableData.currentStock = consumableData.snList.length;
|
consumableData.currentStock = consumableData.snList.length;
|
||||||
}
|
}
|
||||||
const consumable = await Consumable.create(consumableData, { transaction });
|
const consumable = await Consumable.create(consumableData, { transaction });
|
||||||
|
|
||||||
await ConsumableLog.create({
|
await ConsumableLog.create(
|
||||||
|
{
|
||||||
consumableId: consumable.consumableId,
|
consumableId: consumable.consumableId,
|
||||||
consumableName: consumable.name,
|
consumableName: consumable.name,
|
||||||
operationType: 'create',
|
operationType: 'create',
|
||||||
@@ -138,9 +145,11 @@ router.post('/', async (req, res) => {
|
|||||||
supplier: consumable.supplier,
|
supplier: consumable.supplier,
|
||||||
location: consumable.location,
|
location: consumable.location,
|
||||||
minStock: consumable.minStock,
|
minStock: consumable.minStock,
|
||||||
maxStock: consumable.maxStock
|
maxStock: consumable.maxStock,
|
||||||
}
|
},
|
||||||
}, { transaction });
|
},
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
res.status(201).json(consumable);
|
res.status(201).json(consumable);
|
||||||
@@ -172,7 +181,7 @@ router.post('/import', async (req, res) => {
|
|||||||
updated: 0,
|
updated: 0,
|
||||||
skipped: 0,
|
skipped: 0,
|
||||||
errors: [],
|
errors: [],
|
||||||
details: []
|
details: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
for (let i = 0; i < items.length; i++) {
|
||||||
@@ -180,7 +189,7 @@ router.post('/import', async (req, res) => {
|
|||||||
const rowNumber = i + 1;
|
const rowNumber = i + 1;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let consumableId = item.耗材ID || item.consumableId;
|
const consumableId = item.耗材ID || item.consumableId;
|
||||||
const name = item.名称 || item.name;
|
const name = item.名称 || item.name;
|
||||||
const category = item.分类 || item.category;
|
const category = item.分类 || item.category;
|
||||||
|
|
||||||
@@ -195,7 +204,10 @@ router.post('/import', async (req, res) => {
|
|||||||
if (item.SN序列号 || item.snList) {
|
if (item.SN序列号 || item.snList) {
|
||||||
const snStr = item.SN序列号 || item.snList;
|
const snStr = item.SN序列号 || item.snList;
|
||||||
if (typeof snStr === 'string') {
|
if (typeof snStr === 'string') {
|
||||||
snList = snStr.split(/[,,;;\n]/).map(s => s.trim()).filter(Boolean);
|
snList = snStr
|
||||||
|
.split(/[,,;;\n]/)
|
||||||
|
.map(s => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
} else if (Array.isArray(snStr)) {
|
} else if (Array.isArray(snStr)) {
|
||||||
snList = snStr;
|
snList = snStr;
|
||||||
}
|
}
|
||||||
@@ -206,7 +218,8 @@ router.post('/import', async (req, res) => {
|
|||||||
name,
|
name,
|
||||||
category,
|
category,
|
||||||
unit: item.单位 || item.unit || '个',
|
unit: item.单位 || item.unit || '个',
|
||||||
currentStock: snList.length > 0 ? snList.length : (parseInt(item.当前库存 || item.currentStock) || 0),
|
currentStock:
|
||||||
|
snList.length > 0 ? snList.length : parseInt(item.当前库存 || item.currentStock) || 0,
|
||||||
minStock: parseInt(item.最小库存 || item.minStock) || 10,
|
minStock: parseInt(item.最小库存 || item.minStock) || 10,
|
||||||
maxStock: parseInt(item.最大库存 || item.maxStock) || 0,
|
maxStock: parseInt(item.最大库存 || item.maxStock) || 0,
|
||||||
unitPrice: parseFloat(item.单价 || item.unitPrice) || 0,
|
unitPrice: parseFloat(item.单价 || item.unitPrice) || 0,
|
||||||
@@ -214,7 +227,7 @@ router.post('/import', async (req, res) => {
|
|||||||
location: item.存放位置 || item.location || '',
|
location: item.存放位置 || item.location || '',
|
||||||
description: item.描述 || item.description || '',
|
description: item.描述 || item.description || '',
|
||||||
status: item.状态 || item.status || 'active',
|
status: item.状态 || item.status || 'active',
|
||||||
snList
|
snList,
|
||||||
};
|
};
|
||||||
|
|
||||||
let existingConsumable = null;
|
let existingConsumable = null;
|
||||||
@@ -233,20 +246,36 @@ router.post('/import', async (req, res) => {
|
|||||||
consumable = existingConsumable;
|
consumable = existingConsumable;
|
||||||
operationType = 'import_update';
|
operationType = 'import_update';
|
||||||
results.updated++;
|
results.updated++;
|
||||||
results.details.push({ row: rowNumber, status: 'updated', consumableId: consumable.consumableId, name: consumable.name });
|
results.details.push({
|
||||||
|
row: rowNumber,
|
||||||
|
status: 'updated',
|
||||||
|
consumableId: consumable.consumableId,
|
||||||
|
name: consumable.name,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
results.skipped++;
|
results.skipped++;
|
||||||
results.details.push({ row: rowNumber, status: 'skipped', reason: '耗材已存在', consumableId: consumableId });
|
results.details.push({
|
||||||
|
row: rowNumber,
|
||||||
|
status: 'skipped',
|
||||||
|
reason: '耗材已存在',
|
||||||
|
consumableId: consumableId,
|
||||||
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
consumable = await Consumable.create(consumableData, { transaction });
|
consumable = await Consumable.create(consumableData, { transaction });
|
||||||
operationType = 'import';
|
operationType = 'import';
|
||||||
results.success++;
|
results.success++;
|
||||||
results.details.push({ row: rowNumber, status: 'created', consumableId: consumable.consumableId, name: consumable.name });
|
results.details.push({
|
||||||
|
row: rowNumber,
|
||||||
|
status: 'created',
|
||||||
|
consumableId: consumable.consumableId,
|
||||||
|
name: consumable.name,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await ConsumableLog.create({
|
await ConsumableLog.create(
|
||||||
|
{
|
||||||
consumableId: consumable.consumableId,
|
consumableId: consumable.consumableId,
|
||||||
consumableName: consumable.name,
|
consumableName: consumable.name,
|
||||||
operationType,
|
operationType,
|
||||||
@@ -263,10 +292,11 @@ router.post('/import', async (req, res) => {
|
|||||||
supplier: consumable.supplier,
|
supplier: consumable.supplier,
|
||||||
location: consumable.location,
|
location: consumable.location,
|
||||||
minStock: consumable.minStock,
|
minStock: consumable.minStock,
|
||||||
maxStock: consumable.maxStock
|
maxStock: consumable.maxStock,
|
||||||
}
|
},
|
||||||
}, { transaction });
|
},
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
results.failed++;
|
results.failed++;
|
||||||
results.errors.push(`第 ${rowNumber} 行: ${error.message}`);
|
results.errors.push(`第 ${rowNumber} 行: ${error.message}`);
|
||||||
@@ -277,7 +307,7 @@ router.post('/import', async (req, res) => {
|
|||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
res.json({
|
res.json({
|
||||||
message: `导入完成,成功 ${results.success} 条,更新 ${results.updated} 条,跳过 ${results.skipped} 条,失败 ${results.failed} 条`,
|
message: `导入完成,成功 ${results.success} 条,更新 ${results.updated} 条,跳过 ${results.skipped} 条,失败 ${results.failed} 条`,
|
||||||
results
|
results,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
@@ -299,7 +329,7 @@ router.get('/by-sn/:sn', async (req, res) => {
|
|||||||
}
|
}
|
||||||
res.json({
|
res.json({
|
||||||
found: !!consumable,
|
found: !!consumable,
|
||||||
consumable: result
|
consumable: result,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -310,7 +340,7 @@ router.get('/categories/list', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const categories = await Consumable.findAll({
|
const categories = await Consumable.findAll({
|
||||||
attributes: ['category'],
|
attributes: ['category'],
|
||||||
group: ['category']
|
group: ['category'],
|
||||||
});
|
});
|
||||||
const categoryList = categories.map(item => item.category).filter(Boolean);
|
const categoryList = categories.map(item => item.category).filter(Boolean);
|
||||||
res.json(categoryList);
|
res.json(categoryList);
|
||||||
@@ -326,14 +356,14 @@ router.get('/low-stock', async (req, res) => {
|
|||||||
status: 'active',
|
status: 'active',
|
||||||
[Op.and]: [
|
[Op.and]: [
|
||||||
sequelize.where(sequelize.col('currentStock'), {
|
sequelize.where(sequelize.col('currentStock'), {
|
||||||
[Op.lte]: sequelize.col('minStock')
|
[Op.lte]: sequelize.col('minStock'),
|
||||||
}),
|
}),
|
||||||
sequelize.where(sequelize.col('minStock'), {
|
sequelize.where(sequelize.col('minStock'), {
|
||||||
[Op.gt]: 0
|
[Op.gt]: 0,
|
||||||
})
|
}),
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
order: [['currentStock', 'ASC']]
|
order: [['currentStock', 'ASC']],
|
||||||
});
|
});
|
||||||
res.json(consumables);
|
res.json(consumables);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -344,7 +374,7 @@ router.get('/low-stock', async (req, res) => {
|
|||||||
router.get('/statistics/summary', async (req, res) => {
|
router.get('/statistics/summary', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const consumables = await Consumable.findAll({
|
const consumables = await Consumable.findAll({
|
||||||
attributes: ['currentStock', 'unitPrice', 'category', 'minStock', 'status']
|
attributes: ['currentStock', 'unitPrice', 'category', 'minStock', 'status'],
|
||||||
});
|
});
|
||||||
|
|
||||||
let total = 0;
|
let total = 0;
|
||||||
@@ -353,7 +383,9 @@ router.get('/statistics/summary', async (req, res) => {
|
|||||||
const categoryMap = {};
|
const categoryMap = {};
|
||||||
|
|
||||||
consumables.forEach(item => {
|
consumables.forEach(item => {
|
||||||
if (item.status === 'inactive') return;
|
if (item.status === 'inactive') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
total++;
|
total++;
|
||||||
const currentStock = parseFloat(item.currentStock) || 0;
|
const currentStock = parseFloat(item.currentStock) || 0;
|
||||||
@@ -378,14 +410,14 @@ router.get('/statistics/summary', async (req, res) => {
|
|||||||
const byCategory = Object.entries(categoryMap).map(([category, data]) => ({
|
const byCategory = Object.entries(categoryMap).map(([category, data]) => ({
|
||||||
category,
|
category,
|
||||||
count: data.count,
|
count: data.count,
|
||||||
totalQuantity: data.totalQuantity
|
totalQuantity: data.totalQuantity,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
total,
|
total,
|
||||||
lowStock,
|
lowStock,
|
||||||
totalValue: totalValue.toFixed(2),
|
totalValue: totalValue.toFixed(2),
|
||||||
byCategory
|
byCategory,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -401,18 +433,20 @@ router.get('/inout/records', async (req, res) => {
|
|||||||
offset,
|
offset,
|
||||||
limit: parseInt(pageSize),
|
limit: parseInt(pageSize),
|
||||||
order: [['createdAt', 'DESC']],
|
order: [['createdAt', 'DESC']],
|
||||||
include: [{
|
include: [
|
||||||
|
{
|
||||||
model: Consumable,
|
model: Consumable,
|
||||||
as: 'consumable',
|
as: 'consumable',
|
||||||
attributes: ['consumableId', 'name', 'category']
|
attributes: ['consumableId', 'name', 'category'],
|
||||||
}]
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
total: count,
|
total: count,
|
||||||
records: rows,
|
records: rows,
|
||||||
page: parseInt(page),
|
page: parseInt(page),
|
||||||
pageSize: parseInt(pageSize)
|
pageSize: parseInt(pageSize),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -435,9 +469,9 @@ router.post('/quick-inout', async (req, res) => {
|
|||||||
|
|
||||||
const previousStock = parseFloat(consumable.currentStock);
|
const previousStock = parseFloat(consumable.currentStock);
|
||||||
let newStock;
|
let newStock;
|
||||||
let currentSnList = consumable.snList || [];
|
const currentSnList = consumable.snList || [];
|
||||||
let updatedSnList = [...currentSnList];
|
let updatedSnList = [...currentSnList];
|
||||||
let operationSnList = snList || [];
|
const operationSnList = snList || [];
|
||||||
|
|
||||||
if (type === 'in') {
|
if (type === 'in') {
|
||||||
newStock = previousStock + parseFloat(quantity);
|
newStock = previousStock + parseFloat(quantity);
|
||||||
@@ -472,14 +506,14 @@ router.post('/quick-inout', async (req, res) => {
|
|||||||
{
|
{
|
||||||
currentStock: newStock,
|
currentStock: newStock,
|
||||||
snList: updatedSnList,
|
snList: updatedSnList,
|
||||||
version: sequelize.literal('version + 1')
|
version: sequelize.literal('version + 1'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
where: {
|
where: {
|
||||||
consumableId,
|
consumableId,
|
||||||
version: consumable.version
|
version: consumable.version,
|
||||||
},
|
},
|
||||||
transaction
|
transaction,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -492,7 +526,8 @@ router.post('/quick-inout', async (req, res) => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const record = await ConsumableRecord.create({
|
const record = await ConsumableRecord.create(
|
||||||
|
{
|
||||||
consumableId,
|
consumableId,
|
||||||
type,
|
type,
|
||||||
quantity,
|
quantity,
|
||||||
@@ -501,10 +536,13 @@ router.post('/quick-inout', async (req, res) => {
|
|||||||
operator,
|
operator,
|
||||||
reason,
|
reason,
|
||||||
notes,
|
notes,
|
||||||
snList: operationSnList
|
snList: operationSnList,
|
||||||
}, { transaction });
|
},
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
|
||||||
await ConsumableLog.create({
|
await ConsumableLog.create(
|
||||||
|
{
|
||||||
consumableId,
|
consumableId,
|
||||||
consumableName: consumable.name,
|
consumableName: consumable.name,
|
||||||
operationType: type,
|
operationType: type,
|
||||||
@@ -521,16 +559,18 @@ router.post('/quick-inout', async (req, res) => {
|
|||||||
unit: consumable.unit,
|
unit: consumable.unit,
|
||||||
unitPrice: consumable.unitPrice,
|
unitPrice: consumable.unitPrice,
|
||||||
supplier: consumable.supplier,
|
supplier: consumable.supplier,
|
||||||
location: consumable.location
|
location: consumable.location,
|
||||||
}
|
},
|
||||||
}, { transaction });
|
},
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: '操作成功',
|
message: '操作成功',
|
||||||
record,
|
record,
|
||||||
consumable: await Consumable.findByPk(consumableId)
|
consumable: await Consumable.findByPk(consumableId),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -559,9 +599,9 @@ router.post('/inout', async (req, res) => {
|
|||||||
|
|
||||||
const previousStock = parseFloat(consumable.currentStock);
|
const previousStock = parseFloat(consumable.currentStock);
|
||||||
let newStock;
|
let newStock;
|
||||||
let currentSnList = consumable.snList || [];
|
const currentSnList = consumable.snList || [];
|
||||||
let updatedSnList = [...currentSnList];
|
let updatedSnList = [...currentSnList];
|
||||||
let operationSnList = snList || [];
|
const operationSnList = snList || [];
|
||||||
|
|
||||||
if (type === 'in') {
|
if (type === 'in') {
|
||||||
newStock = previousStock + parseFloat(quantity);
|
newStock = previousStock + parseFloat(quantity);
|
||||||
@@ -593,14 +633,14 @@ router.post('/inout', async (req, res) => {
|
|||||||
{
|
{
|
||||||
currentStock: newStock,
|
currentStock: newStock,
|
||||||
snList: updatedSnList,
|
snList: updatedSnList,
|
||||||
version: sequelize.literal('version + 1')
|
version: sequelize.literal('version + 1'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
where: {
|
where: {
|
||||||
consumableId,
|
consumableId,
|
||||||
version: consumable.version
|
version: consumable.version,
|
||||||
},
|
},
|
||||||
transaction
|
transaction,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -613,7 +653,8 @@ router.post('/inout', async (req, res) => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const record = await ConsumableRecord.create({
|
const record = await ConsumableRecord.create(
|
||||||
|
{
|
||||||
consumableId,
|
consumableId,
|
||||||
type,
|
type,
|
||||||
quantity,
|
quantity,
|
||||||
@@ -623,10 +664,13 @@ router.post('/inout', async (req, res) => {
|
|||||||
reason,
|
reason,
|
||||||
recipient,
|
recipient,
|
||||||
notes,
|
notes,
|
||||||
snList: operationSnList
|
snList: operationSnList,
|
||||||
}, { transaction });
|
},
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
|
||||||
await ConsumableLog.create({
|
await ConsumableLog.create(
|
||||||
|
{
|
||||||
consumableId,
|
consumableId,
|
||||||
consumableName: consumable.name,
|
consumableName: consumable.name,
|
||||||
operationType: type,
|
operationType: type,
|
||||||
@@ -643,16 +687,18 @@ router.post('/inout', async (req, res) => {
|
|||||||
unit: consumable.unit,
|
unit: consumable.unit,
|
||||||
unitPrice: consumable.unitPrice,
|
unitPrice: consumable.unitPrice,
|
||||||
supplier: consumable.supplier,
|
supplier: consumable.supplier,
|
||||||
location: consumable.location
|
location: consumable.location,
|
||||||
}
|
},
|
||||||
}, { transaction });
|
},
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: '操作成功',
|
message: '操作成功',
|
||||||
record,
|
record,
|
||||||
consumable: await Consumable.findByPk(consumableId)
|
consumable: await Consumable.findByPk(consumableId),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -704,14 +750,14 @@ router.post('/adjust', async (req, res) => {
|
|||||||
const [affectedRows] = await Consumable.update(
|
const [affectedRows] = await Consumable.update(
|
||||||
{
|
{
|
||||||
currentStock: newStock,
|
currentStock: newStock,
|
||||||
version: sequelize.literal('version + 1')
|
version: sequelize.literal('version + 1'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
where: {
|
where: {
|
||||||
consumableId,
|
consumableId,
|
||||||
version: consumable.version
|
version: consumable.version,
|
||||||
},
|
},
|
||||||
transaction
|
transaction,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -726,7 +772,8 @@ router.post('/adjust', async (req, res) => {
|
|||||||
|
|
||||||
const changeQuantity = newStock - previousStock;
|
const changeQuantity = newStock - previousStock;
|
||||||
|
|
||||||
await ConsumableLog.create({
|
await ConsumableLog.create(
|
||||||
|
{
|
||||||
consumableId,
|
consumableId,
|
||||||
consumableName: consumable.name,
|
consumableName: consumable.name,
|
||||||
operationType: 'adjust',
|
operationType: 'adjust',
|
||||||
@@ -742,15 +789,17 @@ router.post('/adjust', async (req, res) => {
|
|||||||
unit: consumable.unit,
|
unit: consumable.unit,
|
||||||
unitPrice: consumable.unitPrice,
|
unitPrice: consumable.unitPrice,
|
||||||
supplier: consumable.supplier,
|
supplier: consumable.supplier,
|
||||||
location: consumable.location
|
location: consumable.location,
|
||||||
}
|
},
|
||||||
}, { transaction });
|
},
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: '调整成功',
|
message: '调整成功',
|
||||||
consumable: await Consumable.findByPk(consumableId)
|
consumable: await Consumable.findByPk(consumableId),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -775,7 +824,10 @@ router.get('/logs', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (operationType) {
|
if (operationType) {
|
||||||
const types = operationType.split(',').map(t => t.trim()).filter(t => t);
|
const types = operationType
|
||||||
|
.split(',')
|
||||||
|
.map(t => t.trim())
|
||||||
|
.filter(t => t);
|
||||||
if (types.length === 1) {
|
if (types.length === 1) {
|
||||||
where.operationType = types[0];
|
where.operationType = types[0];
|
||||||
} else if (types.length > 1) {
|
} else if (types.length > 1) {
|
||||||
@@ -785,7 +837,7 @@ router.get('/logs', async (req, res) => {
|
|||||||
|
|
||||||
if (startDate && endDate) {
|
if (startDate && endDate) {
|
||||||
where.createdAt = {
|
where.createdAt = {
|
||||||
[Op.between]: [new Date(startDate), new Date(endDate)]
|
[Op.between]: [new Date(startDate), new Date(endDate)],
|
||||||
};
|
};
|
||||||
} else if (startDate) {
|
} else if (startDate) {
|
||||||
where.createdAt = { [Op.gte]: new Date(startDate) };
|
where.createdAt = { [Op.gte]: new Date(startDate) };
|
||||||
@@ -797,14 +849,14 @@ router.get('/logs', async (req, res) => {
|
|||||||
where,
|
where,
|
||||||
offset,
|
offset,
|
||||||
limit: parseInt(pageSize),
|
limit: parseInt(pageSize),
|
||||||
order: [['createdAt', 'DESC']]
|
order: [['createdAt', 'DESC']],
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
total: count,
|
total: count,
|
||||||
logs: rows,
|
logs: rows,
|
||||||
page: parseInt(page),
|
page: parseInt(page),
|
||||||
pageSize: parseInt(pageSize)
|
pageSize: parseInt(pageSize),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -827,7 +879,7 @@ router.get('/logs/export', async (req, res) => {
|
|||||||
|
|
||||||
if (startDate && endDate) {
|
if (startDate && endDate) {
|
||||||
where.createdAt = {
|
where.createdAt = {
|
||||||
[Op.between]: [new Date(startDate), new Date(endDate)]
|
[Op.between]: [new Date(startDate), new Date(endDate)],
|
||||||
};
|
};
|
||||||
} else if (startDate) {
|
} else if (startDate) {
|
||||||
where.createdAt = { [Op.gte]: new Date(startDate) };
|
where.createdAt = { [Op.gte]: new Date(startDate) };
|
||||||
@@ -837,19 +889,21 @@ router.get('/logs/export', async (req, res) => {
|
|||||||
|
|
||||||
const logs = await ConsumableLog.findAll({
|
const logs = await ConsumableLog.findAll({
|
||||||
where,
|
where,
|
||||||
order: [['createdAt', 'DESC']]
|
order: [['createdAt', 'DESC']],
|
||||||
});
|
});
|
||||||
|
|
||||||
const csvHeader = 'ID,耗材ID,耗材名称,操作类型,变动数量,操作前库存,操作后库存,操作人,原因,备注,耗材状态,分类,单位,单价,创建时间,更新时间\n';
|
const csvHeader =
|
||||||
const csvRows = logs.map(log => {
|
'ID,耗材ID,耗材名称,操作类型,变动数量,操作前库存,操作后库存,操作人,原因,备注,耗材状态,分类,单位,单价,创建时间,更新时间\n';
|
||||||
|
const csvRows = logs
|
||||||
|
.map(log => {
|
||||||
const operationTypeMap = {
|
const operationTypeMap = {
|
||||||
'in': '入库',
|
in: '入库',
|
||||||
'out': '出库',
|
out: '出库',
|
||||||
'create': '创建',
|
create: '创建',
|
||||||
'update': '更新',
|
update: '更新',
|
||||||
'delete': '删除',
|
delete: '删除',
|
||||||
'adjust': '调整',
|
adjust: '调整',
|
||||||
'import': '导入'
|
import: '导入',
|
||||||
};
|
};
|
||||||
const snapshot = log.consumableSnapshot || {};
|
const snapshot = log.consumableSnapshot || {};
|
||||||
return [
|
return [
|
||||||
@@ -868,14 +922,20 @@ router.get('/logs/export', async (req, res) => {
|
|||||||
snapshot.unit || '',
|
snapshot.unit || '',
|
||||||
snapshot.unitPrice || '',
|
snapshot.unitPrice || '',
|
||||||
dayjs(log.createdAt).format('YYYY-MM-DD HH:mm:ss'),
|
dayjs(log.createdAt).format('YYYY-MM-DD HH:mm:ss'),
|
||||||
dayjs(log.updatedAt).format('YYYY-MM-DD HH:mm:ss')
|
dayjs(log.updatedAt).format('YYYY-MM-DD HH:mm:ss'),
|
||||||
].map(v => `"${String(v).replace(/"/g, '""')}"`).join(',');
|
]
|
||||||
}).join('\n');
|
.map(v => `"${String(v).replace(/"/g, '""')}"`)
|
||||||
|
.join(',');
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
const csv = csvHeader + csvRows;
|
const csv = csvHeader + csvRows;
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'text/csv;charset=utf-8');
|
res.setHeader('Content-Type', 'text/csv;charset=utf-8');
|
||||||
res.setHeader('Content-Disposition', `attachment; filename=consumable_logs_${dayjs().format('YYYYMMDD_HHmmss')}.csv`);
|
res.setHeader(
|
||||||
|
'Content-Disposition',
|
||||||
|
`attachment; filename=consumable_logs_${dayjs().format('YYYYMMDD_HHmmss')}.csv`
|
||||||
|
);
|
||||||
res.send(csv);
|
res.send(csv);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -895,17 +955,17 @@ router.post('/logs/import', async (req, res) => {
|
|||||||
const results = {
|
const results = {
|
||||||
success: 0,
|
success: 0,
|
||||||
failed: 0,
|
failed: 0,
|
||||||
errors: []
|
errors: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const operationTypeMap = {
|
const operationTypeMap = {
|
||||||
'入库': 'in',
|
入库: 'in',
|
||||||
'出库': 'out',
|
出库: 'out',
|
||||||
'创建': 'create',
|
创建: 'create',
|
||||||
'更新': 'update',
|
更新: 'update',
|
||||||
'删除': 'delete',
|
删除: 'delete',
|
||||||
'调整': 'adjust',
|
调整: 'adjust',
|
||||||
'导入': 'import'
|
导入: 'import',
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let i = 0; i < logItems.length; i++) {
|
for (let i = 0; i < logItems.length; i++) {
|
||||||
@@ -913,7 +973,10 @@ router.post('/logs/import', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const consumableId = item.耗材ID || item.consumableId || item['consumableId'];
|
const consumableId = item.耗材ID || item.consumableId || item['consumableId'];
|
||||||
const consumableName = item.耗材名称 || item.consumableName || item['consumableName'];
|
const consumableName = item.耗材名称 || item.consumableName || item['consumableName'];
|
||||||
const operationType = operationTypeMap[item.操作类型 || item.operationType] || item.operationType || item['operationType'];
|
const operationType =
|
||||||
|
operationTypeMap[item.操作类型 || item.operationType] ||
|
||||||
|
item.operationType ||
|
||||||
|
item['operationType'];
|
||||||
|
|
||||||
if (!consumableId || !operationType) {
|
if (!consumableId || !operationType) {
|
||||||
results.failed++;
|
results.failed++;
|
||||||
@@ -921,17 +984,22 @@ router.post('/logs/import', async (req, res) => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ConsumableLog.create({
|
await ConsumableLog.create(
|
||||||
|
{
|
||||||
consumableId,
|
consumableId,
|
||||||
consumableName: consumableName || '',
|
consumableName: consumableName || '',
|
||||||
operationType,
|
operationType,
|
||||||
quantity: parseFloat(item.变动数量 || item.quantity || item['quantity']) || 0,
|
quantity: parseFloat(item.变动数量 || item.quantity || item['quantity']) || 0,
|
||||||
previousStock: parseFloat(item.操作前库存 || item.previousStock || item['previousStock']) || 0,
|
previousStock:
|
||||||
currentStock: parseFloat(item.操作后库存 || item.currentStock || item['currentStock']) || 0,
|
parseFloat(item.操作前库存 || item.previousStock || item['previousStock']) || 0,
|
||||||
|
currentStock:
|
||||||
|
parseFloat(item.操作后库存 || item.currentStock || item['currentStock']) || 0,
|
||||||
operator: item.操作人 || item.operator || operator,
|
operator: item.操作人 || item.operator || operator,
|
||||||
reason: item.原因 || item.reason || '',
|
reason: item.原因 || item.reason || '',
|
||||||
notes: item.备注 || item.notes || ''
|
notes: item.备注 || item.notes || '',
|
||||||
}, { transaction });
|
},
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
|
||||||
results.success++;
|
results.success++;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -980,7 +1048,8 @@ router.put('/:id', async (req, res) => {
|
|||||||
}
|
}
|
||||||
await consumable.update(updateData, { transaction });
|
await consumable.update(updateData, { transaction });
|
||||||
|
|
||||||
await ConsumableLog.create({
|
await ConsumableLog.create(
|
||||||
|
{
|
||||||
consumableId: consumable.consumableId,
|
consumableId: consumable.consumableId,
|
||||||
consumableName: consumable.name,
|
consumableName: consumable.name,
|
||||||
operationType: 'update',
|
operationType: 'update',
|
||||||
@@ -995,9 +1064,11 @@ router.put('/:id', async (req, res) => {
|
|||||||
unit: consumable.unit,
|
unit: consumable.unit,
|
||||||
unitPrice: consumable.unitPrice,
|
unitPrice: consumable.unitPrice,
|
||||||
supplier: consumable.supplier,
|
supplier: consumable.supplier,
|
||||||
location: consumable.location
|
location: consumable.location,
|
||||||
}
|
},
|
||||||
}, { transaction });
|
},
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
res.json(consumable);
|
res.json(consumable);
|
||||||
@@ -1030,14 +1101,14 @@ router.delete('/:id', async (req, res) => {
|
|||||||
description: consumable.description,
|
description: consumable.description,
|
||||||
minStock: consumable.minStock,
|
minStock: consumable.minStock,
|
||||||
maxStock: consumable.maxStock,
|
maxStock: consumable.maxStock,
|
||||||
status: consumable.status
|
status: consumable.status,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 查询该耗材的所有操作日志
|
// 查询该耗材的所有操作日志
|
||||||
const logs = await ConsumableLog.findAll({
|
const logs = await ConsumableLog.findAll({
|
||||||
where: { consumableId },
|
where: { consumableId },
|
||||||
order: [['createdAt', 'ASC']],
|
order: [['createdAt', 'ASC']],
|
||||||
transaction
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 计算统计数据
|
// 计算统计数据
|
||||||
@@ -1051,7 +1122,8 @@ router.delete('/:id', async (req, res) => {
|
|||||||
|
|
||||||
// 创建归档记录
|
// 创建归档记录
|
||||||
const archiveId = `ARC${Date.now()}`;
|
const archiveId = `ARC${Date.now()}`;
|
||||||
await ConsumableLogArchive.create({
|
await ConsumableLogArchive.create(
|
||||||
|
{
|
||||||
archiveId,
|
archiveId,
|
||||||
consumableId,
|
consumableId,
|
||||||
consumableName,
|
consumableName,
|
||||||
@@ -1064,11 +1136,14 @@ router.delete('/:id', async (req, res) => {
|
|||||||
finalStock: currentStock,
|
finalStock: currentStock,
|
||||||
deletedBy: operator,
|
deletedBy: operator,
|
||||||
deletedAt: new Date(),
|
deletedAt: new Date(),
|
||||||
deleteReason: req.body.reason || '删除耗材'
|
deleteReason: req.body.reason || '删除耗材',
|
||||||
}, { transaction });
|
},
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
|
||||||
// 创建一条汇总日志(用于在日志列表中显示)
|
// 创建一条汇总日志(用于在日志列表中显示)
|
||||||
await ConsumableLog.create({
|
await ConsumableLog.create(
|
||||||
|
{
|
||||||
consumableId,
|
consumableId,
|
||||||
consumableName,
|
consumableName,
|
||||||
operationType: 'delete',
|
operationType: 'delete',
|
||||||
@@ -1081,18 +1156,20 @@ router.delete('/:id', async (req, res) => {
|
|||||||
isEditable: false,
|
isEditable: false,
|
||||||
isConsumableDeleted: true,
|
isConsumableDeleted: true,
|
||||||
consumableSnapshot,
|
consumableSnapshot,
|
||||||
relatedId: archiveId // 关联归档ID
|
relatedId: archiveId, // 关联归档ID
|
||||||
}, { transaction });
|
},
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
|
||||||
// 删除原日志记录(已归档)
|
// 删除原日志记录(已归档)
|
||||||
await ConsumableLog.destroy({
|
await ConsumableLog.destroy({
|
||||||
where: { consumableId },
|
where: { consumableId },
|
||||||
transaction
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
await ConsumableRecord.destroy({
|
await ConsumableRecord.destroy({
|
||||||
where: { consumableId },
|
where: { consumableId },
|
||||||
transaction
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
await consumable.destroy({ transaction });
|
await consumable.destroy({ transaction });
|
||||||
@@ -1101,7 +1178,7 @@ router.delete('/:id', async (req, res) => {
|
|||||||
res.json({
|
res.json({
|
||||||
message: '删除成功',
|
message: '删除成功',
|
||||||
archiveId,
|
archiveId,
|
||||||
archivedLogs: totalOperations
|
archivedLogs: totalOperations,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
@@ -1121,7 +1198,7 @@ router.get('/archives', async (req, res) => {
|
|||||||
where[Op.or] = [
|
where[Op.or] = [
|
||||||
{ consumableId: { [Op.like]: `%${keyword}%` } },
|
{ consumableId: { [Op.like]: `%${keyword}%` } },
|
||||||
{ consumableName: { [Op.like]: `%${keyword}%` } },
|
{ consumableName: { [Op.like]: `%${keyword}%` } },
|
||||||
{ archiveId: { [Op.like]: `%${keyword}%` } }
|
{ archiveId: { [Op.like]: `%${keyword}%` } },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1129,14 +1206,14 @@ router.get('/archives', async (req, res) => {
|
|||||||
where,
|
where,
|
||||||
offset,
|
offset,
|
||||||
limit: parseInt(pageSize),
|
limit: parseInt(pageSize),
|
||||||
order: [['deletedAt', 'DESC']]
|
order: [['deletedAt', 'DESC']],
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
total: count,
|
total: count,
|
||||||
archives: rows,
|
archives: rows,
|
||||||
page: parseInt(page),
|
page: parseInt(page),
|
||||||
pageSize: parseInt(pageSize)
|
pageSize: parseInt(pageSize),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -1149,7 +1226,7 @@ router.get('/archives/:archiveId', async (req, res) => {
|
|||||||
const { archiveId } = req.params;
|
const { archiveId } = req.params;
|
||||||
|
|
||||||
const archive = await ConsumableLogArchive.findOne({
|
const archive = await ConsumableLogArchive.findOne({
|
||||||
where: { archiveId }
|
where: { archiveId },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!archive) {
|
if (!archive) {
|
||||||
@@ -1185,19 +1262,22 @@ router.put('/logs/:id', async (req, res) => {
|
|||||||
const originalLogId = log.originalLogId || log.id;
|
const originalLogId = log.originalLogId || log.id;
|
||||||
|
|
||||||
// 更新当前记录,并标记为已修改
|
// 更新当前记录,并标记为已修改
|
||||||
await log.update({
|
await log.update(
|
||||||
|
{
|
||||||
reason: reason !== undefined ? reason : log.reason,
|
reason: reason !== undefined ? reason : log.reason,
|
||||||
notes: notes !== undefined ? notes : log.notes,
|
notes: notes !== undefined ? notes : log.notes,
|
||||||
modifiedBy: operator || '系统',
|
modifiedBy: operator || '系统',
|
||||||
modifiedAt: new Date(),
|
modifiedAt: new Date(),
|
||||||
modificationReason: modificationReason || '用户修改'
|
modificationReason: modificationReason || '用户修改',
|
||||||
}, { transaction });
|
},
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: '日志修改成功',
|
message: '日志修改成功',
|
||||||
log: await ConsumableLog.findByPk(id)
|
log: await ConsumableLog.findByPk(id),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
@@ -1220,17 +1300,14 @@ router.get('/logs/:id/history', async (req, res) => {
|
|||||||
|
|
||||||
const history = await ConsumableLog.findAll({
|
const history = await ConsumableLog.findAll({
|
||||||
where: {
|
where: {
|
||||||
[Op.or]: [
|
[Op.or]: [{ id: originalLogId }, { originalLogId: originalLogId }],
|
||||||
{ id: originalLogId },
|
|
||||||
{ originalLogId: originalLogId }
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
order: [['createdAt', 'ASC']]
|
order: [['createdAt', 'ASC']],
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
current: log,
|
current: log,
|
||||||
history: history
|
history: history,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { logDangerousOperation, getDangerousOperationsLogs, cleanOldLogs, DANGEROUS_OPERATION_TYPES, RISK_LEVELS, calculateRiskLevel } = require('../utils/dangerousOperationLogger');
|
const {
|
||||||
|
logDangerousOperation,
|
||||||
|
getDangerousOperationsLogs,
|
||||||
|
cleanOldLogs,
|
||||||
|
DANGEROUS_OPERATION_TYPES,
|
||||||
|
RISK_LEVELS,
|
||||||
|
calculateRiskLevel,
|
||||||
|
} = require('../utils/dangerousOperationLogger');
|
||||||
|
|
||||||
router.post('/log', async (req, res) => {
|
router.post('/log', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -20,7 +27,9 @@ router.post('/log', async (req, res) => {
|
|||||||
return res.status(400).json({ error: '缺少必需参数 operationType 或 operationName' });
|
return res.status(400).json({ error: '缺少必需参数 operationType 或 operationName' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const riskLevel = metadata.riskLevel || calculateRiskLevel(operationType, metadata.itemCount || 1, {
|
const riskLevel =
|
||||||
|
metadata.riskLevel ||
|
||||||
|
calculateRiskLevel(operationType, metadata.itemCount || 1, {
|
||||||
hasRelatedData: metadata.relatedDataCount > 0,
|
hasRelatedData: metadata.relatedDataCount > 0,
|
||||||
isSystemLevel: metadata.isSystemLevel,
|
isSystemLevel: metadata.isSystemLevel,
|
||||||
});
|
});
|
||||||
@@ -49,7 +58,17 @@ router.post('/log', async (req, res) => {
|
|||||||
|
|
||||||
router.get('/logs', async (req, res) => {
|
router.get('/logs', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { operationType, targetType, success, startDate, endDate, username, riskLevel, page = 1, pageSize = 50 } = req.query;
|
const {
|
||||||
|
operationType,
|
||||||
|
targetType,
|
||||||
|
success,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
username,
|
||||||
|
riskLevel,
|
||||||
|
page = 1,
|
||||||
|
pageSize = 50,
|
||||||
|
} = req.query;
|
||||||
|
|
||||||
const filters = {
|
const filters = {
|
||||||
operationType,
|
operationType,
|
||||||
@@ -121,14 +140,10 @@ router.get('/risk-assessment', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { operationType, itemCount, hasRelatedData, isSystemLevel } = req.query;
|
const { operationType, itemCount, hasRelatedData, isSystemLevel } = req.query;
|
||||||
|
|
||||||
const riskLevel = calculateRiskLevel(
|
const riskLevel = calculateRiskLevel(operationType, parseInt(itemCount) || 1, {
|
||||||
operationType,
|
|
||||||
parseInt(itemCount) || 1,
|
|
||||||
{
|
|
||||||
hasRelatedData: hasRelatedData === 'true',
|
hasRelatedData: hasRelatedData === 'true',
|
||||||
isSystemLevel: isSystemLevel === 'true',
|
isSystemLevel: isSystemLevel === 'true',
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const riskDescriptions = {
|
const riskDescriptions = {
|
||||||
[RISK_LEVELS.EXTREME]: '极高风险操作,需要输入确认关键词才能执行',
|
[RISK_LEVELS.EXTREME]: '极高风险操作,需要输入确认关键词才能执行',
|
||||||
@@ -141,7 +156,12 @@ router.get('/risk-assessment', async (req, res) => {
|
|||||||
riskLevel,
|
riskLevel,
|
||||||
description: riskDescriptions[riskLevel],
|
description: riskDescriptions[riskLevel],
|
||||||
requiresKeyword: riskLevel === RISK_LEVELS.EXTREME,
|
requiresKeyword: riskLevel === RISK_LEVELS.EXTREME,
|
||||||
confirmationLevel: riskLevel === RISK_LEVELS.EXTREME ? 'KEYWORD' : riskLevel === RISK_LEVELS.HIGH ? 'ENHANCED' : 'STANDARD',
|
confirmationLevel:
|
||||||
|
riskLevel === RISK_LEVELS.EXTREME
|
||||||
|
? 'KEYWORD'
|
||||||
|
: riskLevel === RISK_LEVELS.HIGH
|
||||||
|
? 'ENHANCED'
|
||||||
|
: 'STANDARD',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to assess risk:', error);
|
console.error('Failed to assess risk:', error);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const DeviceField = require('../models/DeviceField');
|
|||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const fields = await DeviceField.findAll({
|
const fields = await DeviceField.findAll({
|
||||||
order: [['order', 'ASC']]
|
order: [['order', 'ASC']],
|
||||||
});
|
});
|
||||||
res.json(fields);
|
res.json(fields);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -41,7 +41,7 @@ router.post('/', async (req, res) => {
|
|||||||
router.put('/:fieldId', async (req, res) => {
|
router.put('/:fieldId', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const [updated] = await DeviceField.update(req.body, {
|
const [updated] = await DeviceField.update(req.body, {
|
||||||
where: { fieldId: req.params.fieldId }
|
where: { fieldId: req.params.fieldId },
|
||||||
});
|
});
|
||||||
if (updated) {
|
if (updated) {
|
||||||
const updatedField = await DeviceField.findByPk(req.params.fieldId);
|
const updatedField = await DeviceField.findByPk(req.params.fieldId);
|
||||||
@@ -70,7 +70,7 @@ router.delete('/:fieldId', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deleted = await DeviceField.destroy({
|
const deleted = await DeviceField.destroy({
|
||||||
where: { fieldId: req.params.fieldId }
|
where: { fieldId: req.params.fieldId },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
|
|||||||
@@ -39,24 +39,24 @@ router.get('/', async (req, res) => {
|
|||||||
{
|
{
|
||||||
model: Device,
|
model: Device,
|
||||||
as: 'device',
|
as: 'device',
|
||||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
model: NetworkCard,
|
model: NetworkCard,
|
||||||
as: 'networkCard',
|
as: 'networkCard',
|
||||||
attributes: ['nicId', 'name']
|
attributes: ['nicId', 'name'],
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
offset,
|
offset,
|
||||||
limit: parseInt(pageSize),
|
limit: parseInt(pageSize),
|
||||||
order: [['createdAt', 'DESC']]
|
order: [['createdAt', 'DESC']],
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
total: count,
|
total: count,
|
||||||
ports: rows,
|
ports: rows,
|
||||||
page: parseInt(page),
|
page: parseInt(page),
|
||||||
pageSize: parseInt(pageSize)
|
pageSize: parseInt(pageSize),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取端口列表失败:', error);
|
console.error('获取端口列表失败:', error);
|
||||||
@@ -75,15 +75,15 @@ router.get('/device/:deviceId', async (req, res) => {
|
|||||||
{
|
{
|
||||||
model: Device,
|
model: Device,
|
||||||
as: 'device',
|
as: 'device',
|
||||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
model: NetworkCard,
|
model: NetworkCard,
|
||||||
as: 'networkCard',
|
as: 'networkCard',
|
||||||
attributes: ['nicId', 'name']
|
attributes: ['nicId', 'name'],
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
order: [['portName', 'ASC']]
|
order: [['portName', 'ASC']],
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json(ports);
|
res.json(ports);
|
||||||
@@ -95,14 +95,15 @@ router.get('/device/:deviceId', async (req, res) => {
|
|||||||
|
|
||||||
router.post('/', async (req, res) => {
|
router.post('/', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { portId, deviceId, nicId, portName, portType, portSpeed, status, vlanId, description } = req.body;
|
const { portId, deviceId, nicId, portName, portType, portSpeed, status, vlanId, description } =
|
||||||
|
req.body;
|
||||||
|
|
||||||
if (!deviceId || !portName) {
|
if (!deviceId || !portName) {
|
||||||
return res.status(400).json({ error: '缺少必填字段' });
|
return res.status(400).json({ error: '缺少必填字段' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingPort = await DevicePort.findOne({
|
const existingPort = await DevicePort.findOne({
|
||||||
where: { deviceId, portName }
|
where: { deviceId, portName },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingPort) {
|
if (existingPort) {
|
||||||
@@ -120,7 +121,7 @@ router.post('/', async (req, res) => {
|
|||||||
portSpeed: portSpeed || '1G',
|
portSpeed: portSpeed || '1G',
|
||||||
status: status || 'free',
|
status: status || 'free',
|
||||||
vlanId,
|
vlanId,
|
||||||
description
|
description,
|
||||||
});
|
});
|
||||||
|
|
||||||
const createdPort = await DevicePort.findByPk(port.portId, {
|
const createdPort = await DevicePort.findByPk(port.portId, {
|
||||||
@@ -128,9 +129,9 @@ router.post('/', async (req, res) => {
|
|||||||
{
|
{
|
||||||
model: Device,
|
model: Device,
|
||||||
as: 'device',
|
as: 'device',
|
||||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(201).json(createdPort);
|
res.status(201).json(createdPort);
|
||||||
@@ -154,7 +155,7 @@ router.post('/batch', async (req, res) => {
|
|||||||
failed: 0,
|
failed: 0,
|
||||||
skipped: 0,
|
skipped: 0,
|
||||||
updated: 0,
|
updated: 0,
|
||||||
errors: []
|
errors: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const transaction = await DevicePort.sequelize.transaction();
|
const transaction = await DevicePort.sequelize.transaction();
|
||||||
@@ -177,7 +178,9 @@ router.post('/batch', async (req, res) => {
|
|||||||
|
|
||||||
if (isServer) {
|
if (isServer) {
|
||||||
if (!portData.nicId && !portData.网卡名称) {
|
if (!portData.nicId && !portData.网卡名称) {
|
||||||
throw new Error(`服务器 ${portData.deviceId} 的端口必须关联网卡,请先在网卡管理中添加网卡`);
|
throw new Error(
|
||||||
|
`服务器 ${portData.deviceId} 的端口必须关联网卡,请先在网卡管理中添加网卡`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let nicId = portData.nicId;
|
let nicId = portData.nicId;
|
||||||
@@ -185,10 +188,12 @@ router.post('/batch', async (req, res) => {
|
|||||||
if (!nicId && portData.网卡名称) {
|
if (!nicId && portData.网卡名称) {
|
||||||
const networkCard = await NetworkCard.findOne({
|
const networkCard = await NetworkCard.findOne({
|
||||||
where: { deviceId: portData.deviceId, name: portData.网卡名称 },
|
where: { deviceId: portData.deviceId, name: portData.网卡名称 },
|
||||||
transaction
|
transaction,
|
||||||
});
|
});
|
||||||
if (!networkCard) {
|
if (!networkCard) {
|
||||||
throw new Error(`服务器 ${portData.deviceId} 的网卡"${portData.网卡名称}"不存在,请先在网卡管理中添加该网卡`);
|
throw new Error(
|
||||||
|
`服务器 ${portData.deviceId} 的网卡"${portData.网卡名称}"不存在,请先在网卡管理中添加该网卡`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
nicId = networkCard.nicId;
|
nicId = networkCard.nicId;
|
||||||
}
|
}
|
||||||
@@ -212,7 +217,7 @@ router.post('/batch', async (req, res) => {
|
|||||||
|
|
||||||
const existingPort = await DevicePort.findOne({
|
const existingPort = await DevicePort.findOne({
|
||||||
where: { deviceId: portData.deviceId, portName: portData.portName },
|
where: { deviceId: portData.deviceId, portName: portData.portName },
|
||||||
transaction
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingPort) {
|
if (existingPort) {
|
||||||
@@ -221,17 +226,23 @@ router.post('/batch', async (req, res) => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (updateExisting) {
|
if (updateExisting) {
|
||||||
await DevicePort.update({
|
await DevicePort.update(
|
||||||
|
{
|
||||||
portType: portData.portType || existingPort.portType,
|
portType: portData.portType || existingPort.portType,
|
||||||
portSpeed: portData.portSpeed || existingPort.portSpeed,
|
portSpeed: portData.portSpeed || existingPort.portSpeed,
|
||||||
status: portData.status || existingPort.status,
|
status: portData.status || existingPort.status,
|
||||||
vlanId: portData.vlanId !== undefined ? portData.vlanId : existingPort.vlanId,
|
vlanId: portData.vlanId !== undefined ? portData.vlanId : existingPort.vlanId,
|
||||||
description: portData.description !== undefined ? portData.description : existingPort.description,
|
description:
|
||||||
nicId: portData.nicId !== undefined ? portData.nicId : existingPort.nicId
|
portData.description !== undefined
|
||||||
}, {
|
? portData.description
|
||||||
|
: existingPort.description,
|
||||||
|
nicId: portData.nicId !== undefined ? portData.nicId : existingPort.nicId,
|
||||||
|
},
|
||||||
|
{
|
||||||
where: { portId: existingPort.portId },
|
where: { portId: existingPort.portId },
|
||||||
transaction
|
transaction,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
results.updated++;
|
results.updated++;
|
||||||
results.success++;
|
results.success++;
|
||||||
continue;
|
continue;
|
||||||
@@ -239,7 +250,8 @@ router.post('/batch', async (req, res) => {
|
|||||||
throw new Error('该设备的端口名称已存在');
|
throw new Error('该设备的端口名称已存在');
|
||||||
}
|
}
|
||||||
|
|
||||||
await DevicePort.create({
|
await DevicePort.create(
|
||||||
|
{
|
||||||
portId: portData.portId,
|
portId: portData.portId,
|
||||||
deviceId: portData.deviceId,
|
deviceId: portData.deviceId,
|
||||||
nicId: portData.nicId || null,
|
nicId: portData.nicId || null,
|
||||||
@@ -248,8 +260,10 @@ router.post('/batch', async (req, res) => {
|
|||||||
portSpeed: portData.portSpeed || '1G',
|
portSpeed: portData.portSpeed || '1G',
|
||||||
status: portData.status || 'free',
|
status: portData.status || 'free',
|
||||||
vlanId: portData.vlanId,
|
vlanId: portData.vlanId,
|
||||||
description: portData.description
|
description: portData.description,
|
||||||
}, { transaction });
|
},
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
|
||||||
results.success++;
|
results.success++;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -259,7 +273,7 @@ router.post('/batch', async (req, res) => {
|
|||||||
portId: portData.portId,
|
portId: portData.portId,
|
||||||
deviceId: portData.deviceId,
|
deviceId: portData.deviceId,
|
||||||
portName: portData.portName,
|
portName: portData.portName,
|
||||||
error: error.message
|
error: error.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -279,7 +293,7 @@ router.post('/batch', async (req, res) => {
|
|||||||
router.put('/:portId', async (req, res) => {
|
router.put('/:portId', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const [updated] = await DevicePort.update(req.body, {
|
const [updated] = await DevicePort.update(req.body, {
|
||||||
where: { portId: req.params.portId }
|
where: { portId: req.params.portId },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (updated) {
|
if (updated) {
|
||||||
@@ -288,9 +302,9 @@ router.put('/:portId', async (req, res) => {
|
|||||||
{
|
{
|
||||||
model: Device,
|
model: Device,
|
||||||
as: 'device',
|
as: 'device',
|
||||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
res.json(port);
|
res.json(port);
|
||||||
} else {
|
} else {
|
||||||
@@ -313,9 +327,9 @@ router.delete('/:portId', async (req, res) => {
|
|||||||
where: {
|
where: {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ sourceDeviceId: port.deviceId, sourcePort: port.portName },
|
{ sourceDeviceId: port.deviceId, sourcePort: port.portName },
|
||||||
{ targetDeviceId: port.deviceId, targetPort: port.portName }
|
{ targetDeviceId: port.deviceId, targetPort: port.portName },
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (relatedCables.length > 0) {
|
if (relatedCables.length > 0) {
|
||||||
@@ -326,13 +340,13 @@ router.delete('/:portId', async (req, res) => {
|
|||||||
sourceDeviceId: c.sourceDeviceId,
|
sourceDeviceId: c.sourceDeviceId,
|
||||||
sourcePort: c.sourcePort,
|
sourcePort: c.sourcePort,
|
||||||
targetDeviceId: c.targetDeviceId,
|
targetDeviceId: c.targetDeviceId,
|
||||||
targetPort: c.targetPort
|
targetPort: c.targetPort,
|
||||||
}))
|
})),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await DevicePort.destroy({
|
await DevicePort.destroy({
|
||||||
where: { portId: req.params.portId }
|
where: { portId: req.params.portId },
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(204).json();
|
res.status(204).json();
|
||||||
@@ -351,12 +365,12 @@ router.delete('/batch', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deletedCount = await DevicePort.destroy({
|
const deletedCount = await DevicePort.destroy({
|
||||||
where: { portId: { [Op.in]: portIds } }
|
where: { portId: { [Op.in]: portIds } },
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: `批量删除成功,已删除 ${deletedCount} 个端口`,
|
message: `批量删除成功,已删除 ${deletedCount} 个端口`,
|
||||||
deletedCount
|
deletedCount,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('批量删除端口失败:', error);
|
console.error('批量删除端口失败:', error);
|
||||||
@@ -373,12 +387,12 @@ router.post('/batch-delete', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deletedCount = await DevicePort.destroy({
|
const deletedCount = await DevicePort.destroy({
|
||||||
where: { portId: { [Op.in]: portIds } }
|
where: { portId: { [Op.in]: portIds } },
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: `批量删除成功,已删除 ${deletedCount} 个端口`,
|
message: `批量删除成功,已删除 ${deletedCount} 个端口`,
|
||||||
deletedCount
|
deletedCount,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('批量删除端口失败:', error);
|
console.error('批量删除端口失败:', error);
|
||||||
@@ -393,9 +407,9 @@ router.get('/:portId', async (req, res) => {
|
|||||||
{
|
{
|
||||||
model: Device,
|
model: Device,
|
||||||
as: 'device',
|
as: 'device',
|
||||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!port) {
|
if (!port) {
|
||||||
@@ -457,24 +471,24 @@ router.get('/export/all', async (req, res) => {
|
|||||||
{
|
{
|
||||||
model: require('../models/Room'),
|
model: require('../models/Room'),
|
||||||
as: 'room',
|
as: 'room',
|
||||||
attributes: ['roomId', 'name']
|
attributes: ['roomId', 'name'],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
model: NetworkCard,
|
model: NetworkCard,
|
||||||
as: 'networkCard',
|
as: 'networkCard',
|
||||||
attributes: ['nicId', 'name']
|
attributes: ['nicId', 'name'],
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
order: [['createdAt', 'DESC']],
|
order: [['createdAt', 'DESC']],
|
||||||
limit: parsedPageSize,
|
limit: parsedPageSize,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
subQuery: false
|
subQuery: false,
|
||||||
}),
|
}),
|
||||||
timeoutPromise
|
timeoutPromise,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const ports = countResult;
|
const ports = countResult;
|
||||||
@@ -482,7 +496,7 @@ router.get('/export/all', async (req, res) => {
|
|||||||
const statusMap = {
|
const statusMap = {
|
||||||
free: '空闲',
|
free: '空闲',
|
||||||
occupied: '占用',
|
occupied: '占用',
|
||||||
fault: '故障'
|
fault: '故障',
|
||||||
};
|
};
|
||||||
|
|
||||||
const exportData = ports.map(port => ({
|
const exportData = ports.map(port => ({
|
||||||
@@ -499,13 +513,14 @@ router.get('/export/all', async (req, res) => {
|
|||||||
状态: statusMap[port.status] || port.status,
|
状态: statusMap[port.status] || port.status,
|
||||||
VLAN_ID: port.vlanId || '-',
|
VLAN_ID: port.vlanId || '-',
|
||||||
描述: port.description || '-',
|
描述: port.description || '-',
|
||||||
创建时间: port.createdAt ? new Date(port.createdAt).toLocaleString('zh-CN') : '-'
|
创建时间: port.createdAt ? new Date(port.createdAt).toLocaleString('zh-CN') : '-',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let filteredExportData = exportData;
|
let filteredExportData = exportData;
|
||||||
if (keyword) {
|
if (keyword) {
|
||||||
const searchLower = keyword.toLowerCase();
|
const searchLower = keyword.toLowerCase();
|
||||||
filteredExportData = exportData.filter(item =>
|
filteredExportData = exportData.filter(
|
||||||
|
item =>
|
||||||
item.端口名称?.toLowerCase().includes(searchLower) ||
|
item.端口名称?.toLowerCase().includes(searchLower) ||
|
||||||
item.端口类型?.toLowerCase().includes(searchLower) ||
|
item.端口类型?.toLowerCase().includes(searchLower) ||
|
||||||
item.设备名称?.toLowerCase().includes(searchLower) ||
|
item.设备名称?.toLowerCase().includes(searchLower) ||
|
||||||
@@ -517,7 +532,7 @@ router.get('/export/all', async (req, res) => {
|
|||||||
page: parsedPage,
|
page: parsedPage,
|
||||||
pageSize: parsedPageSize,
|
pageSize: parsedPageSize,
|
||||||
total: filteredExportData.length,
|
total: filteredExportData.length,
|
||||||
ports: filteredExportData
|
ports: filteredExportData,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('导出端口失败:', error);
|
console.error('导出端口失败:', error);
|
||||||
|
|||||||
+431
-242
File diff suppressed because it is too large
Load Diff
+286
-129
@@ -4,15 +4,19 @@ const { Op } = require('sequelize');
|
|||||||
const Device = require('../models/Device');
|
const Device = require('../models/Device');
|
||||||
const Rack = require('../models/Rack');
|
const Rack = require('../models/Rack');
|
||||||
const Room = require('../models/Room');
|
const Room = require('../models/Room');
|
||||||
const { logDeviceOperation, generateDeviceDescription, buildDeviceMetadata } = require('../utils/operationLogger');
|
const {
|
||||||
|
logDeviceOperation,
|
||||||
|
generateDeviceDescription,
|
||||||
|
buildDeviceMetadata,
|
||||||
|
} = require('../utils/operationLogger');
|
||||||
|
|
||||||
async function generateIdleDeviceId() {
|
async function generateIdleDeviceId() {
|
||||||
const devices = await Device.findAll({
|
const devices = await Device.findAll({
|
||||||
where: {
|
where: {
|
||||||
deviceId: {
|
deviceId: {
|
||||||
[Op.like]: 'DEV%'
|
[Op.like]: 'DEV%',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let maxNumber = 0;
|
let maxNumber = 0;
|
||||||
@@ -41,7 +45,7 @@ router.get('/', async (req, res) => {
|
|||||||
where[Op.or] = [
|
where[Op.or] = [
|
||||||
{ deviceId: { [Op.like]: `%${keyword}%` } },
|
{ deviceId: { [Op.like]: `%${keyword}%` } },
|
||||||
{ name: { [Op.like]: `%${keyword}%` } },
|
{ name: { [Op.like]: `%${keyword}%` } },
|
||||||
{ serialNumber: { [Op.like]: `%${keyword}%` } }
|
{ serialNumber: { [Op.like]: `%${keyword}%` } },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,22 +63,24 @@ router.get('/', async (req, res) => {
|
|||||||
{
|
{
|
||||||
model: Rack,
|
model: Rack,
|
||||||
attributes: ['rackId', 'name', 'roomId'],
|
attributes: ['rackId', 'name', 'roomId'],
|
||||||
include: [{
|
include: [
|
||||||
|
{
|
||||||
model: Room,
|
model: Room,
|
||||||
attributes: ['roomId', 'name']
|
attributes: ['roomId', 'name'],
|
||||||
}]
|
},
|
||||||
}
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
offset: parseInt(offset),
|
offset: parseInt(offset),
|
||||||
limit: parseInt(pageSize),
|
limit: parseInt(pageSize),
|
||||||
order: [['idleDate', 'DESC']]
|
order: [['idleDate', 'DESC']],
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
total: count,
|
total: count,
|
||||||
idleDevices: rows,
|
idleDevices: rows,
|
||||||
page: parseInt(page),
|
page: parseInt(page),
|
||||||
pageSize: parseInt(pageSize)
|
pageSize: parseInt(pageSize),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取空闲设备列表失败:', error);
|
console.error('获取空闲设备列表失败:', error);
|
||||||
@@ -90,12 +96,14 @@ router.get('/:deviceId', async (req, res) => {
|
|||||||
{
|
{
|
||||||
model: Rack,
|
model: Rack,
|
||||||
attributes: ['rackId', 'name', 'roomId'],
|
attributes: ['rackId', 'name', 'roomId'],
|
||||||
include: [{
|
include: [
|
||||||
|
{
|
||||||
model: Room,
|
model: Room,
|
||||||
attributes: ['roomId', 'name']
|
attributes: ['roomId', 'name'],
|
||||||
}]
|
},
|
||||||
}
|
],
|
||||||
]
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!device) {
|
if (!device) {
|
||||||
@@ -110,7 +118,18 @@ router.get('/:deviceId', async (req, res) => {
|
|||||||
|
|
||||||
router.post('/', async (req, res) => {
|
router.post('/', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { name, type, model, serialNumber, powerConsumption, idleReason, warehouseId, description, rackId, position } = req.body;
|
const {
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
model,
|
||||||
|
serialNumber,
|
||||||
|
powerConsumption,
|
||||||
|
idleReason,
|
||||||
|
warehouseId,
|
||||||
|
description,
|
||||||
|
rackId,
|
||||||
|
position,
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
let { deviceId } = req.body;
|
let { deviceId } = req.body;
|
||||||
|
|
||||||
@@ -144,24 +163,35 @@ router.post('/', async (req, res) => {
|
|||||||
warehouseId: warehouseId || null,
|
warehouseId: warehouseId || null,
|
||||||
rackId: rackId || null,
|
rackId: rackId || null,
|
||||||
position: position || null,
|
position: position || null,
|
||||||
sourceType: warehouseId ? 'warehouse' : (rackId ? 'rack' : 'rack'),
|
sourceType: warehouseId ? 'warehouse' : rackId ? 'rack' : 'rack',
|
||||||
description: description || ''
|
description: description || '',
|
||||||
});
|
});
|
||||||
|
|
||||||
await logDeviceOperation('create', generateDeviceDescription('新增空闲设备', {
|
await logDeviceOperation(
|
||||||
|
'create',
|
||||||
|
generateDeviceDescription(
|
||||||
|
'新增空闲设备',
|
||||||
|
{
|
||||||
deviceId: device.deviceId,
|
deviceId: device.deviceId,
|
||||||
name: device.name || deviceId,
|
name: device.name || deviceId,
|
||||||
type: device.type,
|
type: device.type,
|
||||||
model: device.model,
|
model: device.model,
|
||||||
serialNumber: device.serialNumber,
|
serialNumber: device.serialNumber,
|
||||||
ipAddress: device.ipAddress
|
ipAddress: device.ipAddress,
|
||||||
}, { includeRack: false }), {
|
},
|
||||||
|
{ includeRack: false }
|
||||||
|
),
|
||||||
|
{
|
||||||
targetId: device.deviceId,
|
targetId: device.deviceId,
|
||||||
targetName: device.name || deviceId,
|
targetName: device.name || deviceId,
|
||||||
afterState: device.toJSON(),
|
afterState: device.toJSON(),
|
||||||
req,
|
req,
|
||||||
metadata: buildDeviceMetadata(device.toJSON(), { sourceType: device.sourceType, type: 'idle_device_create' })
|
metadata: buildDeviceMetadata(device.toJSON(), {
|
||||||
});
|
sourceType: device.sourceType,
|
||||||
|
type: 'idle_device_create',
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
res.status(201).json(device);
|
res.status(201).json(device);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -186,13 +216,16 @@ router.post('/from-device/:deviceId', async (req, res) => {
|
|||||||
return res.status(400).json({ error: '设备已经标记为空闲设备' });
|
return res.status(400).json({ error: '设备已经标记为空闲设备' });
|
||||||
}
|
}
|
||||||
|
|
||||||
await device.update({
|
await device.update(
|
||||||
|
{
|
||||||
isIdle: true,
|
isIdle: true,
|
||||||
status: 'idle',
|
status: 'idle',
|
||||||
idleDate: new Date(),
|
idleDate: new Date(),
|
||||||
idleReason: idleReason || `从设备管理转入`,
|
idleReason: idleReason || `从设备管理转入`,
|
||||||
sourceType: 'rack'
|
sourceType: 'rack',
|
||||||
}, { transaction: t });
|
},
|
||||||
|
{ transaction: t }
|
||||||
|
);
|
||||||
|
|
||||||
await t.commit();
|
await t.commit();
|
||||||
|
|
||||||
@@ -203,12 +236,12 @@ router.post('/from-device/:deviceId', async (req, res) => {
|
|||||||
beforeState: { ...deviceData, isIdle: false },
|
beforeState: { ...deviceData, isIdle: false },
|
||||||
afterState: { ...deviceData, isIdle: true },
|
afterState: { ...deviceData, isIdle: true },
|
||||||
req,
|
req,
|
||||||
metadata: buildDeviceMetadata(deviceData, { idleReason, type: 'device_to_idle' })
|
metadata: buildDeviceMetadata(deviceData, { idleReason, type: 'device_to_idle' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: '设备已转入空闲设备',
|
message: '设备已转入空闲设备',
|
||||||
device: device.toJSON()
|
device: device.toJSON(),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await t.rollback();
|
await t.rollback();
|
||||||
@@ -229,7 +262,7 @@ router.post('/batch-from-devices', async (req, res) => {
|
|||||||
|
|
||||||
const devices = await Device.findAll({
|
const devices = await Device.findAll({
|
||||||
where: { deviceId: { [Op.in]: deviceIds } },
|
where: { deviceId: { [Op.in]: deviceIds } },
|
||||||
transaction: t
|
transaction: t,
|
||||||
});
|
});
|
||||||
|
|
||||||
const notIdleDevices = devices.filter(d => !d.isIdle);
|
const notIdleDevices = devices.filter(d => !d.isIdle);
|
||||||
@@ -241,11 +274,11 @@ router.post('/batch-from-devices', async (req, res) => {
|
|||||||
isIdle: true,
|
isIdle: true,
|
||||||
status: 'idle',
|
status: 'idle',
|
||||||
idleDate: new Date(),
|
idleDate: new Date(),
|
||||||
idleReason: idleReason || `批量转入`
|
idleReason: idleReason || `批量转入`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
where: { deviceId: { [Op.in]: notIdleDevices.map(d => d.deviceId) } },
|
where: { deviceId: { [Op.in]: notIdleDevices.map(d => d.deviceId) } },
|
||||||
transaction: t
|
transaction: t,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -253,22 +286,29 @@ router.post('/batch-from-devices', async (req, res) => {
|
|||||||
await t.commit();
|
await t.commit();
|
||||||
|
|
||||||
const deviceDetails = notIdleDevices.map(d => d.toJSON());
|
const deviceDetails = notIdleDevices.map(d => d.toJSON());
|
||||||
const deviceSummary = deviceDetails.map(d =>
|
const deviceSummary = deviceDetails
|
||||||
|
.map(
|
||||||
|
d =>
|
||||||
`${d.name}(编号:${d.deviceId}${d.type ? `,类型:${d.type}` : ''}${d.serialNumber ? `,序列号:${d.serialNumber}` : ''})`
|
`${d.name}(编号:${d.deviceId}${d.type ? `,类型:${d.type}` : ''}${d.serialNumber ? `,序列号:${d.serialNumber}` : ''})`
|
||||||
).join('、');
|
)
|
||||||
|
.join('、');
|
||||||
|
|
||||||
await logDeviceOperation('batch_to_idle', `批量将 ${notIdleDevices.length} 台设备转入空闲设备:${deviceSummary}`, {
|
await logDeviceOperation(
|
||||||
|
'batch_to_idle',
|
||||||
|
`批量将 ${notIdleDevices.length} 台设备转入空闲设备:${deviceSummary}`,
|
||||||
|
{
|
||||||
targetId: deviceIds.join(','),
|
targetId: deviceIds.join(','),
|
||||||
targetName: `${notIdleDevices.length}台设备`,
|
targetName: `${notIdleDevices.length}台设备`,
|
||||||
req,
|
req,
|
||||||
metadata: { idleReason, type: 'batch_device_to_idle', devices: deviceDetails }
|
metadata: { idleReason, type: 'batch_device_to_idle', devices: deviceDetails },
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: `成功将 ${notIdleDevices.length} 台设备转入空闲设备`,
|
message: `成功将 ${notIdleDevices.length} 台设备转入空闲设备`,
|
||||||
total: devices.length,
|
total: devices.length,
|
||||||
updated: notIdleDevices.length,
|
updated: notIdleDevices.length,
|
||||||
skipped: alreadyIdleDevices.length
|
skipped: alreadyIdleDevices.length,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await t.rollback();
|
await t.rollback();
|
||||||
@@ -305,23 +345,29 @@ router.put('/batch-restore', async (req, res) => {
|
|||||||
|
|
||||||
const idleDevices = await Device.findAll({
|
const idleDevices = await Device.findAll({
|
||||||
where: { deviceId: { [Op.in]: deviceIds }, isIdle: true },
|
where: { deviceId: { [Op.in]: deviceIds }, isIdle: true },
|
||||||
transaction: t
|
transaction: t,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('查询到的空闲设备数量:', idleDevices.length);
|
console.log('查询到的空闲设备数量:', idleDevices.length);
|
||||||
if (idleDevices.length > 0) {
|
if (idleDevices.length > 0) {
|
||||||
console.log('查询到的设备ID:', idleDevices.map(d => d.deviceId));
|
console.log(
|
||||||
|
'查询到的设备ID:',
|
||||||
|
idleDevices.map(d => d.deviceId)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (idleDevices.length === 0) {
|
if (idleDevices.length === 0) {
|
||||||
console.log('没有找到空闲设备,检查设备是否存在:');
|
console.log('没有找到空闲设备,检查设备是否存在:');
|
||||||
const allDevices = await Device.findAll({
|
const allDevices = await Device.findAll({
|
||||||
where: { deviceId: { [Op.in]: deviceIds } },
|
where: { deviceId: { [Op.in]: deviceIds } },
|
||||||
transaction: t
|
transaction: t,
|
||||||
});
|
});
|
||||||
console.log('设备表中存在的设备数量:', allDevices.length);
|
console.log('设备表中存在的设备数量:', allDevices.length);
|
||||||
if (allDevices.length > 0) {
|
if (allDevices.length > 0) {
|
||||||
console.log('存在的设备及其 isIdle 状态:', allDevices.map(d => ({ deviceId: d.deviceId, isIdle: d.isIdle })));
|
console.log(
|
||||||
|
'存在的设备及其 isIdle 状态:',
|
||||||
|
allDevices.map(d => ({ deviceId: d.deviceId, isIdle: d.isIdle }))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await t.rollback();
|
await t.rollback();
|
||||||
@@ -333,7 +379,9 @@ router.put('/batch-restore', async (req, res) => {
|
|||||||
|
|
||||||
for (const device of idleDevices) {
|
for (const device of idleDevices) {
|
||||||
const deviceConfig = devices.find(d => d.deviceId === device.deviceId);
|
const deviceConfig = devices.find(d => d.deviceId === device.deviceId);
|
||||||
if (!deviceConfig) continue;
|
if (!deviceConfig) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const targetRackId = deviceConfig.targetRackId;
|
const targetRackId = deviceConfig.targetRackId;
|
||||||
const targetPosition = deviceConfig.targetPosition;
|
const targetPosition = deviceConfig.targetPosition;
|
||||||
@@ -343,7 +391,7 @@ router.put('/batch-restore', async (req, res) => {
|
|||||||
deviceId: device.deviceId,
|
deviceId: device.deviceId,
|
||||||
name: device.name,
|
name: device.name,
|
||||||
status: 'skipped',
|
status: 'skipped',
|
||||||
reason: '未指定目标机柜'
|
reason: '未指定目标机柜',
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -354,7 +402,7 @@ router.put('/batch-restore', async (req, res) => {
|
|||||||
deviceId: device.deviceId,
|
deviceId: device.deviceId,
|
||||||
name: device.name,
|
name: device.name,
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
reason: '目标机柜不存在'
|
reason: '目标机柜不存在',
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -368,12 +416,13 @@ router.put('/batch-restore', async (req, res) => {
|
|||||||
deviceId: device.deviceId,
|
deviceId: device.deviceId,
|
||||||
name: device.name,
|
name: device.name,
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
reason: `U位${position}已被占用`
|
reason: `U位${position}已被占用`,
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await device.update({
|
await device.update(
|
||||||
|
{
|
||||||
isIdle: false,
|
isIdle: false,
|
||||||
idleDate: null,
|
idleDate: null,
|
||||||
idleReason: null,
|
idleReason: null,
|
||||||
@@ -381,12 +430,17 @@ router.put('/batch-restore', async (req, res) => {
|
|||||||
position: position,
|
position: position,
|
||||||
warehouseId: null,
|
warehouseId: null,
|
||||||
sourceType: 'rack',
|
sourceType: 'rack',
|
||||||
status: 'offline'
|
status: 'offline',
|
||||||
}, { transaction: t });
|
},
|
||||||
|
{ transaction: t }
|
||||||
|
);
|
||||||
|
|
||||||
await targetRack.update({
|
await targetRack.update(
|
||||||
currentPower: targetRack.currentPower + (device.powerConsumption || 0)
|
{
|
||||||
}, { transaction: t });
|
currentPower: targetRack.currentPower + (device.powerConsumption || 0),
|
||||||
|
},
|
||||||
|
{ transaction: t }
|
||||||
|
);
|
||||||
|
|
||||||
restoredCount++;
|
restoredCount++;
|
||||||
results.push({
|
results.push({
|
||||||
@@ -394,7 +448,7 @@ router.put('/batch-restore', async (req, res) => {
|
|||||||
name: device.name,
|
name: device.name,
|
||||||
status: 'success',
|
status: 'success',
|
||||||
targetRack: targetRack.name,
|
targetRack: targetRack.name,
|
||||||
targetPosition: position
|
targetPosition: position,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,17 +458,30 @@ router.put('/batch-restore', async (req, res) => {
|
|||||||
const failedCount = results.filter(r => r.status === 'failed').length;
|
const failedCount = results.filter(r => r.status === 'failed').length;
|
||||||
const skippedCount = results.filter(r => r.status === 'skipped').length;
|
const skippedCount = results.filter(r => r.status === 'skipped').length;
|
||||||
|
|
||||||
const successDevices = idleDevices.filter(d => results.some(r => r.deviceId === d.deviceId && r.status === 'success'));
|
const successDevices = idleDevices.filter(d =>
|
||||||
const deviceSummary = successDevices.map(d =>
|
results.some(r => r.deviceId === d.deviceId && r.status === 'success')
|
||||||
|
);
|
||||||
|
const deviceSummary = successDevices
|
||||||
|
.map(
|
||||||
|
d =>
|
||||||
`${d.name}(编号:${d.deviceId}${d.type ? `,类型:${d.type}` : ''}${d.ipAddress ? `,IP:${d.ipAddress}` : ''})`
|
`${d.name}(编号:${d.deviceId}${d.type ? `,类型:${d.type}` : ''}${d.ipAddress ? `,IP:${d.ipAddress}` : ''})`
|
||||||
).join('、');
|
)
|
||||||
|
.join('、');
|
||||||
|
|
||||||
await logDeviceOperation('batch_restore', `批量上架 ${successCount} 台空闲设备:${deviceSummary}`, {
|
await logDeviceOperation(
|
||||||
|
'batch_restore',
|
||||||
|
`批量上架 ${successCount} 台空闲设备:${deviceSummary}`,
|
||||||
|
{
|
||||||
targetId: deviceIds.join(','),
|
targetId: deviceIds.join(','),
|
||||||
targetName: `${successCount}台设备`,
|
targetName: `${successCount}台设备`,
|
||||||
req,
|
req,
|
||||||
metadata: { results, type: 'batch_idle_device_restore', devices: successDevices.map(d => d.toJSON()) }
|
metadata: {
|
||||||
});
|
results,
|
||||||
|
type: 'batch_idle_device_restore',
|
||||||
|
devices: successDevices.map(d => d.toJSON()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: `成功上架 ${successCount} 台设备`,
|
message: `成功上架 ${successCount} 台设备`,
|
||||||
@@ -422,7 +489,7 @@ router.put('/batch-restore', async (req, res) => {
|
|||||||
restored: successCount,
|
restored: successCount,
|
||||||
failed: failedCount,
|
failed: failedCount,
|
||||||
skipped: skippedCount,
|
skipped: skippedCount,
|
||||||
details: results
|
details: results,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await t.rollback();
|
await t.rollback();
|
||||||
@@ -435,11 +502,21 @@ router.put('/:deviceId/shelve', async (req, res) => {
|
|||||||
const t = await require('../db').sequelize.transaction();
|
const t = await require('../db').sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
const { deviceId } = req.params;
|
const { deviceId } = req.params;
|
||||||
const { name, type, model, serialNumber, height, powerConsumption, rackId, position, description } = req.body;
|
const {
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
model,
|
||||||
|
serialNumber,
|
||||||
|
height,
|
||||||
|
powerConsumption,
|
||||||
|
rackId,
|
||||||
|
position,
|
||||||
|
description,
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
const device = await Device.findOne({
|
const device = await Device.findOne({
|
||||||
where: { deviceId, isIdle: true },
|
where: { deviceId, isIdle: true },
|
||||||
transaction: t
|
transaction: t,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!device) {
|
if (!device) {
|
||||||
@@ -467,7 +544,8 @@ router.put('/:deviceId/shelve', async (req, res) => {
|
|||||||
|
|
||||||
const beforeState = device.toJSON();
|
const beforeState = device.toJSON();
|
||||||
|
|
||||||
await device.update({
|
await device.update(
|
||||||
|
{
|
||||||
name: name || device.name,
|
name: name || device.name,
|
||||||
type: type || device.type,
|
type: type || device.type,
|
||||||
model: model || device.model,
|
model: model || device.model,
|
||||||
@@ -482,39 +560,47 @@ router.put('/:deviceId/shelve', async (req, res) => {
|
|||||||
idleReason: null,
|
idleReason: null,
|
||||||
warehouseId: null,
|
warehouseId: null,
|
||||||
sourceType: 'rack',
|
sourceType: 'rack',
|
||||||
status: 'running'
|
status: 'running',
|
||||||
}, { transaction: t });
|
},
|
||||||
|
{ transaction: t }
|
||||||
|
);
|
||||||
|
|
||||||
await targetRack.update({
|
await targetRack.update(
|
||||||
currentPower: targetRack.currentPower + (powerConsumption || device.powerConsumption || 0)
|
{
|
||||||
}, { transaction: t });
|
currentPower: targetRack.currentPower + (powerConsumption || device.powerConsumption || 0),
|
||||||
|
},
|
||||||
|
{ transaction: t }
|
||||||
|
);
|
||||||
|
|
||||||
await t.commit();
|
await t.commit();
|
||||||
|
|
||||||
const updatedDevice = await Device.findByPk(deviceId, {
|
const updatedDevice = await Device.findByPk(deviceId, {
|
||||||
include: [
|
include: [{ model: Rack, include: [Room] }],
|
||||||
{ model: Rack, include: [Room] }
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const deviceData = {
|
const deviceData = {
|
||||||
...updatedDevice.toJSON(),
|
...updatedDevice.toJSON(),
|
||||||
rackName: targetRack.name,
|
rackName: targetRack.name,
|
||||||
roomName: updatedDevice.Rack?.Room?.name
|
roomName: updatedDevice.Rack?.Room?.name,
|
||||||
};
|
};
|
||||||
|
|
||||||
await logDeviceOperation('shelve', generateDeviceDescription('空闲设备上架', deviceData, { includePosition: false }) + `到机柜【${targetRack.name}】U${position}`, {
|
await logDeviceOperation(
|
||||||
|
'shelve',
|
||||||
|
generateDeviceDescription('空闲设备上架', deviceData, { includePosition: false }) +
|
||||||
|
`到机柜【${targetRack.name}】U${position}`,
|
||||||
|
{
|
||||||
targetId: device.deviceId,
|
targetId: device.deviceId,
|
||||||
targetName: device.name,
|
targetName: device.name,
|
||||||
beforeState: { ...beforeState, isIdle: true },
|
beforeState: { ...beforeState, isIdle: true },
|
||||||
afterState: deviceData,
|
afterState: deviceData,
|
||||||
req,
|
req,
|
||||||
metadata: buildDeviceMetadata(deviceData, { rackId, position, type: 'idle_device_shelve' })
|
metadata: buildDeviceMetadata(deviceData, { rackId, position, type: 'idle_device_shelve' }),
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: '设备上架成功',
|
message: '设备上架成功',
|
||||||
device: updatedDevice
|
device: updatedDevice,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await t.rollback();
|
await t.rollback();
|
||||||
@@ -526,7 +612,7 @@ router.put('/:deviceId/shelve', async (req, res) => {
|
|||||||
router.put('/:deviceId', async (req, res) => {
|
router.put('/:deviceId', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const device = await Device.findOne({
|
const device = await Device.findOne({
|
||||||
where: { deviceId: req.params.deviceId, isIdle: true }
|
where: { deviceId: req.params.deviceId, isIdle: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!device) {
|
if (!device) {
|
||||||
@@ -534,7 +620,14 @@ router.put('/:deviceId', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const beforeState = device.toJSON();
|
const beforeState = device.toJSON();
|
||||||
const allowedFields = ['name', 'type', 'model', 'idleReason', 'description', 'powerConsumption'];
|
const allowedFields = [
|
||||||
|
'name',
|
||||||
|
'type',
|
||||||
|
'model',
|
||||||
|
'idleReason',
|
||||||
|
'description',
|
||||||
|
'powerConsumption',
|
||||||
|
];
|
||||||
|
|
||||||
allowedFields.forEach(field => {
|
allowedFields.forEach(field => {
|
||||||
if (req.body[field] !== undefined) {
|
if (req.body[field] !== undefined) {
|
||||||
@@ -566,14 +659,18 @@ router.put('/:deviceId', async (req, res) => {
|
|||||||
|
|
||||||
await device.save();
|
await device.save();
|
||||||
|
|
||||||
await logDeviceOperation('update', generateDeviceDescription('更新空闲设备', device.toJSON(), { includeRack: false }), {
|
await logDeviceOperation(
|
||||||
|
'update',
|
||||||
|
generateDeviceDescription('更新空闲设备', device.toJSON(), { includeRack: false }),
|
||||||
|
{
|
||||||
targetId: device.deviceId,
|
targetId: device.deviceId,
|
||||||
targetName: device.name,
|
targetName: device.name,
|
||||||
beforeState,
|
beforeState,
|
||||||
afterState: device.toJSON(),
|
afterState: device.toJSON(),
|
||||||
req,
|
req,
|
||||||
metadata: buildDeviceMetadata(device.toJSON(), { type: 'idle_device_update' })
|
metadata: buildDeviceMetadata(device.toJSON(), { type: 'idle_device_update' }),
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
res.json(device);
|
res.json(device);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -589,7 +686,7 @@ router.put('/:deviceId/restore', async (req, res) => {
|
|||||||
|
|
||||||
const device = await Device.findOne({
|
const device = await Device.findOne({
|
||||||
where: { deviceId, isIdle: true },
|
where: { deviceId, isIdle: true },
|
||||||
transaction: t
|
transaction: t,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!device) {
|
if (!device) {
|
||||||
@@ -608,13 +705,20 @@ router.put('/:deviceId/restore', async (req, res) => {
|
|||||||
return res.status(404).json({ error: '目标机柜不存在' });
|
return res.status(404).json({ error: '目标机柜不存在' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const positionCheck = await checkPositionAvailable(targetRackId, targetPosition, device.height || 1, deviceId, t);
|
const positionCheck = await checkPositionAvailable(
|
||||||
|
targetRackId,
|
||||||
|
targetPosition,
|
||||||
|
device.height || 1,
|
||||||
|
deviceId,
|
||||||
|
t
|
||||||
|
);
|
||||||
if (!positionCheck.available) {
|
if (!positionCheck.available) {
|
||||||
await t.rollback();
|
await t.rollback();
|
||||||
return res.status(400).json({ error: positionCheck.reason });
|
return res.status(400).json({ error: positionCheck.reason });
|
||||||
}
|
}
|
||||||
|
|
||||||
await device.update({
|
await device.update(
|
||||||
|
{
|
||||||
isIdle: false,
|
isIdle: false,
|
||||||
idleDate: null,
|
idleDate: null,
|
||||||
idleReason: null,
|
idleReason: null,
|
||||||
@@ -622,39 +726,51 @@ router.put('/:deviceId/restore', async (req, res) => {
|
|||||||
position: targetPosition,
|
position: targetPosition,
|
||||||
warehouseId: null,
|
warehouseId: null,
|
||||||
sourceType: 'rack',
|
sourceType: 'rack',
|
||||||
status: 'offline'
|
status: 'offline',
|
||||||
}, { transaction: t });
|
},
|
||||||
|
{ transaction: t }
|
||||||
|
);
|
||||||
|
|
||||||
await targetRack.update({
|
await targetRack.update(
|
||||||
currentPower: targetRack.currentPower + (device.powerConsumption || 0)
|
{
|
||||||
}, { transaction: t });
|
currentPower: targetRack.currentPower + (device.powerConsumption || 0),
|
||||||
|
},
|
||||||
|
{ transaction: t }
|
||||||
|
);
|
||||||
|
|
||||||
await t.commit();
|
await t.commit();
|
||||||
|
|
||||||
const updatedDevice = await Device.findByPk(deviceId, {
|
const updatedDevice = await Device.findByPk(deviceId, {
|
||||||
include: [
|
include: [{ model: Rack, include: [Room] }],
|
||||||
{ model: Rack, include: [Room] }
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const deviceData = {
|
const deviceData = {
|
||||||
...updatedDevice.toJSON(),
|
...updatedDevice.toJSON(),
|
||||||
rackName: targetRack.name,
|
rackName: targetRack.name,
|
||||||
roomName: updatedDevice.Rack?.Room?.name
|
roomName: updatedDevice.Rack?.Room?.name,
|
||||||
};
|
};
|
||||||
|
|
||||||
await logDeviceOperation('restore', generateDeviceDescription('空闲设备恢复', deviceData, { includePosition: false }) + `到机柜【${targetRack.name}】U${targetPosition}`, {
|
await logDeviceOperation(
|
||||||
|
'restore',
|
||||||
|
generateDeviceDescription('空闲设备恢复', deviceData, { includePosition: false }) +
|
||||||
|
`到机柜【${targetRack.name}】U${targetPosition}`,
|
||||||
|
{
|
||||||
targetId: device.deviceId,
|
targetId: device.deviceId,
|
||||||
targetName: device.name,
|
targetName: device.name,
|
||||||
beforeState: { ...device.toJSON(), isIdle: true },
|
beforeState: { ...device.toJSON(), isIdle: true },
|
||||||
afterState: deviceData,
|
afterState: deviceData,
|
||||||
req,
|
req,
|
||||||
metadata: buildDeviceMetadata(deviceData, { targetRackId, targetPosition, type: 'idle_device_restore' })
|
metadata: buildDeviceMetadata(deviceData, {
|
||||||
});
|
targetRackId,
|
||||||
|
targetPosition,
|
||||||
|
type: 'idle_device_restore',
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: '设备已恢复到设备管理',
|
message: '设备已恢复到设备管理',
|
||||||
device: updatedDevice
|
device: updatedDevice,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await t.rollback();
|
await t.rollback();
|
||||||
@@ -692,23 +808,29 @@ router.put('/batch-restore', async (req, res) => {
|
|||||||
|
|
||||||
const idleDevices = await Device.findAll({
|
const idleDevices = await Device.findAll({
|
||||||
where: { deviceId: { [Op.in]: deviceIds }, isIdle: true },
|
where: { deviceId: { [Op.in]: deviceIds }, isIdle: true },
|
||||||
transaction: t
|
transaction: t,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('查询到的空闲设备数量:', idleDevices.length);
|
console.log('查询到的空闲设备数量:', idleDevices.length);
|
||||||
if (idleDevices.length > 0) {
|
if (idleDevices.length > 0) {
|
||||||
console.log('查询到的设备ID:', idleDevices.map(d => d.deviceId));
|
console.log(
|
||||||
|
'查询到的设备ID:',
|
||||||
|
idleDevices.map(d => d.deviceId)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (idleDevices.length === 0) {
|
if (idleDevices.length === 0) {
|
||||||
console.log('没有找到空闲设备,检查设备是否存在:');
|
console.log('没有找到空闲设备,检查设备是否存在:');
|
||||||
const allDevices = await Device.findAll({
|
const allDevices = await Device.findAll({
|
||||||
where: { deviceId: { [Op.in]: deviceIds } },
|
where: { deviceId: { [Op.in]: deviceIds } },
|
||||||
transaction: t
|
transaction: t,
|
||||||
});
|
});
|
||||||
console.log('设备表中存在的设备数量:', allDevices.length);
|
console.log('设备表中存在的设备数量:', allDevices.length);
|
||||||
if (allDevices.length > 0) {
|
if (allDevices.length > 0) {
|
||||||
console.log('存在的设备及其 isIdle 状态:', allDevices.map(d => ({ deviceId: d.deviceId, isIdle: d.isIdle })));
|
console.log(
|
||||||
|
'存在的设备及其 isIdle 状态:',
|
||||||
|
allDevices.map(d => ({ deviceId: d.deviceId, isIdle: d.isIdle }))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await t.rollback();
|
await t.rollback();
|
||||||
@@ -720,7 +842,9 @@ router.put('/batch-restore', async (req, res) => {
|
|||||||
|
|
||||||
for (const device of idleDevices) {
|
for (const device of idleDevices) {
|
||||||
const deviceConfig = devices.find(d => d.deviceId === device.deviceId);
|
const deviceConfig = devices.find(d => d.deviceId === device.deviceId);
|
||||||
if (!deviceConfig) continue;
|
if (!deviceConfig) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const targetRackId = deviceConfig.targetRackId;
|
const targetRackId = deviceConfig.targetRackId;
|
||||||
const targetPosition = deviceConfig.targetPosition;
|
const targetPosition = deviceConfig.targetPosition;
|
||||||
@@ -730,7 +854,7 @@ router.put('/batch-restore', async (req, res) => {
|
|||||||
deviceId: device.deviceId,
|
deviceId: device.deviceId,
|
||||||
name: device.name,
|
name: device.name,
|
||||||
status: 'skipped',
|
status: 'skipped',
|
||||||
reason: '未指定目标机柜'
|
reason: '未指定目标机柜',
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -741,7 +865,7 @@ router.put('/batch-restore', async (req, res) => {
|
|||||||
deviceId: device.deviceId,
|
deviceId: device.deviceId,
|
||||||
name: device.name,
|
name: device.name,
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
reason: '目标机柜不存在'
|
reason: '目标机柜不存在',
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -755,12 +879,13 @@ router.put('/batch-restore', async (req, res) => {
|
|||||||
deviceId: device.deviceId,
|
deviceId: device.deviceId,
|
||||||
name: device.name,
|
name: device.name,
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
reason: `U位${position}已被占用`
|
reason: `U位${position}已被占用`,
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await device.update({
|
await device.update(
|
||||||
|
{
|
||||||
isIdle: false,
|
isIdle: false,
|
||||||
idleDate: null,
|
idleDate: null,
|
||||||
idleReason: null,
|
idleReason: null,
|
||||||
@@ -768,12 +893,17 @@ router.put('/batch-restore', async (req, res) => {
|
|||||||
position: position,
|
position: position,
|
||||||
warehouseId: null,
|
warehouseId: null,
|
||||||
sourceType: 'rack',
|
sourceType: 'rack',
|
||||||
status: 'offline'
|
status: 'offline',
|
||||||
}, { transaction: t });
|
},
|
||||||
|
{ transaction: t }
|
||||||
|
);
|
||||||
|
|
||||||
await targetRack.update({
|
await targetRack.update(
|
||||||
currentPower: targetRack.currentPower + (device.powerConsumption || 0)
|
{
|
||||||
}, { transaction: t });
|
currentPower: targetRack.currentPower + (device.powerConsumption || 0),
|
||||||
|
},
|
||||||
|
{ transaction: t }
|
||||||
|
);
|
||||||
|
|
||||||
restoredCount++;
|
restoredCount++;
|
||||||
results.push({
|
results.push({
|
||||||
@@ -781,7 +911,7 @@ router.put('/batch-restore', async (req, res) => {
|
|||||||
name: device.name,
|
name: device.name,
|
||||||
status: 'success',
|
status: 'success',
|
||||||
targetRack: targetRack.name,
|
targetRack: targetRack.name,
|
||||||
targetPosition: position
|
targetPosition: position,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -791,17 +921,30 @@ router.put('/batch-restore', async (req, res) => {
|
|||||||
const failedCount = results.filter(r => r.status === 'failed').length;
|
const failedCount = results.filter(r => r.status === 'failed').length;
|
||||||
const skippedCount = results.filter(r => r.status === 'skipped').length;
|
const skippedCount = results.filter(r => r.status === 'skipped').length;
|
||||||
|
|
||||||
const successDevices = idleDevices.filter(d => results.some(r => r.deviceId === d.deviceId && r.status === 'success'));
|
const successDevices = idleDevices.filter(d =>
|
||||||
const deviceSummary = successDevices.map(d =>
|
results.some(r => r.deviceId === d.deviceId && r.status === 'success')
|
||||||
|
);
|
||||||
|
const deviceSummary = successDevices
|
||||||
|
.map(
|
||||||
|
d =>
|
||||||
`${d.name}(编号:${d.deviceId}${d.type ? `,类型:${d.type}` : ''}${d.ipAddress ? `,IP:${d.ipAddress}` : ''})`
|
`${d.name}(编号:${d.deviceId}${d.type ? `,类型:${d.type}` : ''}${d.ipAddress ? `,IP:${d.ipAddress}` : ''})`
|
||||||
).join('、');
|
)
|
||||||
|
.join('、');
|
||||||
|
|
||||||
await logDeviceOperation('batch_restore', `批量上架 ${successCount} 台空闲设备:${deviceSummary}`, {
|
await logDeviceOperation(
|
||||||
|
'batch_restore',
|
||||||
|
`批量上架 ${successCount} 台空闲设备:${deviceSummary}`,
|
||||||
|
{
|
||||||
targetId: deviceIds.join(','),
|
targetId: deviceIds.join(','),
|
||||||
targetName: `${successCount}台设备`,
|
targetName: `${successCount}台设备`,
|
||||||
req,
|
req,
|
||||||
metadata: { results, type: 'batch_idle_device_restore', devices: successDevices.map(d => d.toJSON()) }
|
metadata: {
|
||||||
});
|
results,
|
||||||
|
type: 'batch_idle_device_restore',
|
||||||
|
devices: successDevices.map(d => d.toJSON()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: `成功上架 ${successCount} 台设备`,
|
message: `成功上架 ${successCount} 台设备`,
|
||||||
@@ -809,7 +952,7 @@ router.put('/batch-restore', async (req, res) => {
|
|||||||
restored: successCount,
|
restored: successCount,
|
||||||
failed: failedCount,
|
failed: failedCount,
|
||||||
skipped: skippedCount,
|
skipped: skippedCount,
|
||||||
details: results
|
details: results,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await t.rollback();
|
await t.rollback();
|
||||||
@@ -823,7 +966,7 @@ router.delete('/:deviceId', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const device = await Device.findOne({
|
const device = await Device.findOne({
|
||||||
where: { deviceId: req.params.deviceId, isIdle: true },
|
where: { deviceId: req.params.deviceId, isIdle: true },
|
||||||
transaction: t
|
transaction: t,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!device) {
|
if (!device) {
|
||||||
@@ -837,16 +980,24 @@ router.delete('/:deviceId', async (req, res) => {
|
|||||||
|
|
||||||
await t.commit();
|
await t.commit();
|
||||||
|
|
||||||
await logDeviceOperation('delete', generateDeviceDescription('删除空闲设备', {
|
await logDeviceOperation(
|
||||||
|
'delete',
|
||||||
|
generateDeviceDescription(
|
||||||
|
'删除空闲设备',
|
||||||
|
{
|
||||||
...device.toJSON(),
|
...device.toJSON(),
|
||||||
name: device.name || device.deviceId
|
name: device.name || device.deviceId,
|
||||||
}, { includeRack: false }), {
|
},
|
||||||
|
{ includeRack: false }
|
||||||
|
),
|
||||||
|
{
|
||||||
targetId: device.deviceId,
|
targetId: device.deviceId,
|
||||||
targetName: device.name,
|
targetName: device.name,
|
||||||
beforeState,
|
beforeState,
|
||||||
req,
|
req,
|
||||||
metadata: buildDeviceMetadata(device.toJSON(), { type: 'idle_device_delete' })
|
metadata: buildDeviceMetadata(device.toJSON(), { type: 'idle_device_delete' }),
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
res.json({ message: '空闲设备删除成功' });
|
res.json({ message: '空闲设备删除成功' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -856,7 +1007,13 @@ router.delete('/:deviceId', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function checkPositionAvailable(rackId, position, height, excludeDeviceId = null, transaction = null) {
|
async function checkPositionAvailable(
|
||||||
|
rackId,
|
||||||
|
position,
|
||||||
|
height,
|
||||||
|
excludeDeviceId = null,
|
||||||
|
transaction = null
|
||||||
|
) {
|
||||||
if (!position || position <= 0) {
|
if (!position || position <= 0) {
|
||||||
return { available: true, reason: null };
|
return { available: true, reason: null };
|
||||||
}
|
}
|
||||||
@@ -869,9 +1026,9 @@ async function checkPositionAvailable(rackId, position, height, excludeDeviceId
|
|||||||
where: {
|
where: {
|
||||||
rackId: rackId,
|
rackId: rackId,
|
||||||
position: { [Op.ne]: null },
|
position: { [Op.ne]: null },
|
||||||
isIdle: false
|
isIdle: false,
|
||||||
},
|
},
|
||||||
attributes: ['deviceId', 'position', 'height']
|
attributes: ['deviceId', 'position', 'height'],
|
||||||
};
|
};
|
||||||
|
|
||||||
if (transaction) {
|
if (transaction) {
|
||||||
@@ -891,7 +1048,7 @@ async function checkPositionAvailable(rackId, position, height, excludeDeviceId
|
|||||||
if (!(endU < existStart || startU > existEnd)) {
|
if (!(endU < existStart || startU > existEnd)) {
|
||||||
return {
|
return {
|
||||||
available: false,
|
available: false,
|
||||||
reason: `U位冲突:机柜中已有设备 ${d.deviceId} 占用 U${existStart}${existEnd !== existStart ? '-' + existEnd : ''}`
|
reason: `U位冲突:机柜中已有设备 ${d.deviceId} 占用 U${existStart}${existEnd !== existStart ? '-' + existEnd : ''}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+187
-78
@@ -54,25 +54,29 @@ router.get('/plans', async (req, res) => {
|
|||||||
if (keyword) {
|
if (keyword) {
|
||||||
where[Op.or] = [
|
where[Op.or] = [
|
||||||
{ name: { [Op.like]: `%${keyword}%` } },
|
{ name: { [Op.like]: `%${keyword}%` } },
|
||||||
{ description: { [Op.like]: `%${keyword}%` } }
|
{ description: { [Op.like]: `%${keyword}%` } },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
const { count, rows } = await InventoryPlan.findAndCountAll({
|
const { count, rows } = await InventoryPlan.findAndCountAll({
|
||||||
where,
|
where,
|
||||||
include: [
|
include: [
|
||||||
{ model: require('../models/User'), as: 'Creator', attributes: ['userId', 'username', 'realName'] }
|
{
|
||||||
|
model: require('../models/User'),
|
||||||
|
as: 'Creator',
|
||||||
|
attributes: ['userId', 'username', 'realName'],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
order: [['createdAt', 'DESC']],
|
order: [['createdAt', 'DESC']],
|
||||||
limit: parseInt(pageSize),
|
limit: parseInt(pageSize),
|
||||||
offset: parseInt(offset)
|
offset: parseInt(offset),
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
plans: rows,
|
plans: rows,
|
||||||
total: count,
|
total: count,
|
||||||
page: parseInt(page),
|
page: parseInt(page),
|
||||||
pageSize: parseInt(pageSize)
|
pageSize: parseInt(pageSize),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -83,8 +87,12 @@ router.get('/plans/:planId', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const plan = await InventoryPlan.findByPk(req.params.planId, {
|
const plan = await InventoryPlan.findByPk(req.params.planId, {
|
||||||
include: [
|
include: [
|
||||||
{ model: require('../models/User'), as: 'Creator', attributes: ['userId', 'username', 'realName'] }
|
{
|
||||||
]
|
model: require('../models/User'),
|
||||||
|
as: 'Creator',
|
||||||
|
attributes: ['userId', 'username', 'realName'],
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!plan) {
|
if (!plan) {
|
||||||
@@ -94,9 +102,13 @@ router.get('/plans/:planId', async (req, res) => {
|
|||||||
const tasks = await InventoryTask.findAll({
|
const tasks = await InventoryTask.findAll({
|
||||||
where: { planId: plan.planId },
|
where: { planId: plan.planId },
|
||||||
include: [
|
include: [
|
||||||
{ model: require('../models/User'), as: 'Assignee', attributes: ['userId', 'username', 'realName'] }
|
{
|
||||||
|
model: require('../models/User'),
|
||||||
|
as: 'Assignee',
|
||||||
|
attributes: ['userId', 'username', 'realName'],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
order: [['createdAt', 'ASC']]
|
order: [['createdAt', 'ASC']],
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({ plan, tasks });
|
res.json({ plan, tasks });
|
||||||
@@ -118,7 +130,7 @@ router.post('/plans', async (req, res) => {
|
|||||||
targetRooms: targetRooms || [],
|
targetRooms: targetRooms || [],
|
||||||
targetRacks: targetRacks || [],
|
targetRacks: targetRacks || [],
|
||||||
status: 'draft',
|
status: 'draft',
|
||||||
createdBy: req.user?.userId
|
createdBy: req.user?.userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(201).json(plan);
|
res.status(201).json(plan);
|
||||||
@@ -143,7 +155,7 @@ router.put('/plans/:planId', async (req, res) => {
|
|||||||
scheduledDate: scheduledDate ? new Date(scheduledDate) : plan.scheduledDate,
|
scheduledDate: scheduledDate ? new Date(scheduledDate) : plan.scheduledDate,
|
||||||
targetRooms: targetRooms || plan.targetRooms,
|
targetRooms: targetRooms || plan.targetRooms,
|
||||||
targetRacks: targetRacks || plan.targetRacks,
|
targetRacks: targetRacks || plan.targetRacks,
|
||||||
status: status || plan.status
|
status: status || plan.status,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json(plan);
|
res.json(plan);
|
||||||
@@ -187,16 +199,16 @@ router.post('/plans/:planId/start', async (req, res) => {
|
|||||||
|
|
||||||
if (targetRacks.length > 0) {
|
if (targetRacks.length > 0) {
|
||||||
allDevices = await Device.findAll({
|
allDevices = await Device.findAll({
|
||||||
where: { rackId: { [Op.in]: targetRacks } }
|
where: { rackId: { [Op.in]: targetRacks } },
|
||||||
});
|
});
|
||||||
} else if (targetRooms.length > 0) {
|
} else if (targetRooms.length > 0) {
|
||||||
const racksInRooms = await Rack.findAll({
|
const racksInRooms = await Rack.findAll({
|
||||||
where: { roomId: { [Op.in]: targetRooms } },
|
where: { roomId: { [Op.in]: targetRooms } },
|
||||||
attributes: ['rackId']
|
attributes: ['rackId'],
|
||||||
});
|
});
|
||||||
const rackIds = racksInRooms.map(r => r.rackId);
|
const rackIds = racksInRooms.map(r => r.rackId);
|
||||||
allDevices = await Device.findAll({
|
allDevices = await Device.findAll({
|
||||||
where: { rackId: { [Op.in]: rackIds } }
|
where: { rackId: { [Op.in]: rackIds } },
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
allDevices = await Device.findAll();
|
allDevices = await Device.findAll();
|
||||||
@@ -214,7 +226,7 @@ router.post('/plans/:planId/start', async (req, res) => {
|
|||||||
targetId: 'all',
|
targetId: 'all',
|
||||||
targetName: '全部设备',
|
targetName: '全部设备',
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
totalDevices: allDevices.length
|
totalDevices: allDevices.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let i = 0; i < allDevices.length; i++) {
|
for (let i = 0; i < allDevices.length; i++) {
|
||||||
@@ -229,7 +241,7 @@ router.post('/plans/:planId/start', async (req, res) => {
|
|||||||
serialNumber: device.serialNumber,
|
serialNumber: device.serialNumber,
|
||||||
rackId: device.rackId,
|
rackId: device.rackId,
|
||||||
position: device.position,
|
position: device.position,
|
||||||
status: 'pending'
|
status: 'pending',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,12 +250,16 @@ router.post('/plans/:planId/start', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (recordsToCreate.length > 0) {
|
if (recordsToCreate.length > 0) {
|
||||||
const now = dbDialect === 'mysql'
|
const now =
|
||||||
|
dbDialect === 'mysql'
|
||||||
? new Date().toISOString().replace('T', ' ').replace('Z', '')
|
? new Date().toISOString().replace('T', ' ').replace('Z', '')
|
||||||
: new Date().toISOString();
|
: new Date().toISOString();
|
||||||
const placeholders = recordsToCreate.map(r =>
|
const placeholders = recordsToCreate
|
||||||
|
.map(
|
||||||
|
r =>
|
||||||
`('${r.recordId}', '${r.taskId}', '${r.planId}', '${r.deviceId}', '${r.deviceName}', '${r.deviceType}', '${r.serialNumber || ''}', '${r.rackId}', ${r.position}, 'pending', '${now}', '${now}')`
|
`('${r.recordId}', '${r.taskId}', '${r.planId}', '${r.deviceId}', '${r.deviceName}', '${r.deviceType}', '${r.serialNumber || ''}', '${r.rackId}', ${r.position}, 'pending', '${now}', '${now}')`
|
||||||
).join(',');
|
)
|
||||||
|
.join(',');
|
||||||
|
|
||||||
if (placeholders) {
|
if (placeholders) {
|
||||||
await sequelize.query(`
|
await sequelize.query(`
|
||||||
@@ -259,10 +275,14 @@ router.post('/plans/:planId/start', async (req, res) => {
|
|||||||
checkedDevices: 0,
|
checkedDevices: 0,
|
||||||
normalDevices: 0,
|
normalDevices: 0,
|
||||||
abnormalDevices: 0,
|
abnormalDevices: 0,
|
||||||
missedDevices: allDevices.length
|
missedDevices: allDevices.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({ message: '盘点任务已启动', taskCount: tasksToCreate.length, deviceCount: allDevices.length });
|
res.json({
|
||||||
|
message: '盘点任务已启动',
|
||||||
|
taskCount: tasksToCreate.length,
|
||||||
|
deviceCount: allDevices.length,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('启动盘点错误:', error);
|
console.error('启动盘点错误:', error);
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -274,8 +294,12 @@ router.get('/tasks/:taskId', async (req, res) => {
|
|||||||
const task = await InventoryTask.findByPk(req.params.taskId, {
|
const task = await InventoryTask.findByPk(req.params.taskId, {
|
||||||
include: [
|
include: [
|
||||||
{ model: InventoryPlan, as: 'Plan', attributes: ['planId', 'name'] },
|
{ model: InventoryPlan, as: 'Plan', attributes: ['planId', 'name'] },
|
||||||
{ model: require('../models/User'), as: 'Assignee', attributes: ['userId', 'username', 'realName'] }
|
{
|
||||||
]
|
model: require('../models/User'),
|
||||||
|
as: 'Assignee',
|
||||||
|
attributes: ['userId', 'username', 'realName'],
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!task) {
|
if (!task) {
|
||||||
@@ -285,15 +309,23 @@ router.get('/tasks/:taskId', async (req, res) => {
|
|||||||
const records = await InventoryRecord.findAll({
|
const records = await InventoryRecord.findAll({
|
||||||
where: { taskId: task.taskId },
|
where: { taskId: task.taskId },
|
||||||
include: [
|
include: [
|
||||||
{ model: Device, as: 'Device', attributes: ['deviceId', 'name', 'type', 'serialNumber', 'rackId', 'position'] },
|
{
|
||||||
{ model: require('../models/User'), as: 'Checker', attributes: ['userId', 'username', 'realName'] }
|
model: Device,
|
||||||
]
|
as: 'Device',
|
||||||
|
attributes: ['deviceId', 'name', 'type', 'serialNumber', 'rackId', 'position'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: require('../models/User'),
|
||||||
|
as: 'Checker',
|
||||||
|
attributes: ['userId', 'username', 'realName'],
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const rackIds = [...new Set(records.map(r => r.rackId).filter(Boolean))];
|
const rackIds = [...new Set(records.map(r => r.rackId).filter(Boolean))];
|
||||||
const racks = await Rack.findAll({
|
const racks = await Rack.findAll({
|
||||||
where: { rackId: rackIds },
|
where: { rackId: rackIds },
|
||||||
include: [{ model: Room, as: 'Room' }]
|
include: [{ model: Room, as: 'Room' }],
|
||||||
});
|
});
|
||||||
const rackMap = {};
|
const rackMap = {};
|
||||||
racks.forEach(r => {
|
racks.forEach(r => {
|
||||||
@@ -308,7 +340,9 @@ router.get('/tasks/:taskId', async (req, res) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...record.toJSON(),
|
...record.toJSON(),
|
||||||
displayLocation: roomName ? `${roomName} - ${rackName} - U${position}` : `${rackName} - U${position}`
|
displayLocation: roomName
|
||||||
|
? `${roomName} - ${rackName} - U${position}`
|
||||||
|
: `${rackName} - U${position}`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -331,14 +365,14 @@ router.put('/tasks/:taskId', async (req, res) => {
|
|||||||
if (assignedTo !== undefined) {
|
if (assignedTo !== undefined) {
|
||||||
await task.update({
|
await task.update({
|
||||||
assignedTo,
|
assignedTo,
|
||||||
assignedAt: assignedTo ? new Date() : task.assignedAt
|
assignedAt: assignedTo ? new Date() : task.assignedAt,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status) {
|
if (status) {
|
||||||
await task.update({
|
await task.update({
|
||||||
status,
|
status,
|
||||||
completedAt: status === 'completed' ? new Date() : null
|
completedAt: status === 'completed' ? new Date() : null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,7 +385,7 @@ router.put('/tasks/:taskId', async (req, res) => {
|
|||||||
router.post('/records/:recordId/check', async (req, res) => {
|
router.post('/records/:recordId/check', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const record = await InventoryRecord.findByPk(req.params.recordId, {
|
const record = await InventoryRecord.findByPk(req.params.recordId, {
|
||||||
include: [{ model: Device, as: 'Device' }]
|
include: [{ model: Device, as: 'Device' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!record) {
|
if (!record) {
|
||||||
@@ -380,7 +414,7 @@ router.post('/records/:recordId/check', async (req, res) => {
|
|||||||
checkedBy: req.user?.userId,
|
checkedBy: req.user?.userId,
|
||||||
checkedAt: new Date(),
|
checkedAt: new Date(),
|
||||||
remark: remark || null,
|
remark: remark || null,
|
||||||
photoUrl: photoUrl || null
|
photoUrl: photoUrl || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const task = await InventoryTask.findByPk(record.taskId);
|
const task = await InventoryTask.findByPk(record.taskId);
|
||||||
@@ -391,7 +425,7 @@ router.post('/records/:recordId/check', async (req, res) => {
|
|||||||
totalDevices: taskRecords.length,
|
totalDevices: taskRecords.length,
|
||||||
checkedDevices: taskRecords.filter(r => r.status !== 'pending').length,
|
checkedDevices: taskRecords.filter(r => r.status !== 'pending').length,
|
||||||
normalDevices: taskRecords.filter(r => r.status === 'normal').length,
|
normalDevices: taskRecords.filter(r => r.status === 'normal').length,
|
||||||
abnormalDevices: taskRecords.filter(r => r.status === 'abnormal').length
|
abnormalDevices: taskRecords.filter(r => r.status === 'abnormal').length,
|
||||||
};
|
};
|
||||||
|
|
||||||
await task.update(taskStats);
|
await task.update(taskStats);
|
||||||
@@ -402,7 +436,7 @@ router.post('/records/:recordId/check', async (req, res) => {
|
|||||||
checkedDevices: planRecords.filter(r => r.status !== 'pending').length,
|
checkedDevices: planRecords.filter(r => r.status !== 'pending').length,
|
||||||
normalDevices: planRecords.filter(r => r.status === 'normal').length,
|
normalDevices: planRecords.filter(r => r.status === 'normal').length,
|
||||||
abnormalDevices: planRecords.filter(r => r.status === 'abnormal').length,
|
abnormalDevices: planRecords.filter(r => r.status === 'abnormal').length,
|
||||||
missedDevices: planRecords.filter(r => r.status === 'pending').length
|
missedDevices: planRecords.filter(r => r.status === 'pending').length,
|
||||||
};
|
};
|
||||||
|
|
||||||
await plan.update(planStats);
|
await plan.update(planStats);
|
||||||
@@ -419,26 +453,43 @@ router.get('/records', async (req, res) => {
|
|||||||
const offset = (page - 1) * pageSize;
|
const offset = (page - 1) * pageSize;
|
||||||
const where = {};
|
const where = {};
|
||||||
|
|
||||||
if (planId) where.planId = planId;
|
if (planId) {
|
||||||
if (taskId) where.taskId = taskId;
|
where.planId = planId;
|
||||||
if (status) where.status = status;
|
}
|
||||||
|
if (taskId) {
|
||||||
|
where.taskId = taskId;
|
||||||
|
}
|
||||||
|
if (status) {
|
||||||
|
where.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
const { count, rows } = await InventoryRecord.findAndCountAll({
|
const { count, rows } = await InventoryRecord.findAndCountAll({
|
||||||
where,
|
where,
|
||||||
include: [
|
include: [
|
||||||
{ model: Device, as: 'Device', attributes: ['deviceId', 'name', 'type', 'serialNumber', 'rackId', 'position'] },
|
{
|
||||||
{ model: require('../models/User'), as: 'Checker', attributes: ['userId', 'username', 'realName'] }
|
model: Device,
|
||||||
|
as: 'Device',
|
||||||
|
attributes: ['deviceId', 'name', 'type', 'serialNumber', 'rackId', 'position'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: require('../models/User'),
|
||||||
|
as: 'Checker',
|
||||||
|
attributes: ['userId', 'username', 'realName'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
order: [
|
||||||
|
['checkedAt', 'DESC'],
|
||||||
|
['createdAt', 'DESC'],
|
||||||
],
|
],
|
||||||
order: [['checkedAt', 'DESC'], ['createdAt', 'DESC']],
|
|
||||||
limit: parseInt(pageSize),
|
limit: parseInt(pageSize),
|
||||||
offset: parseInt(offset)
|
offset: parseInt(offset),
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
records: rows,
|
records: rows,
|
||||||
total: count,
|
total: count,
|
||||||
page: parseInt(page),
|
page: parseInt(page),
|
||||||
pageSize: parseInt(pageSize)
|
pageSize: parseInt(pageSize),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -466,7 +517,7 @@ router.post('/plans/:planId/complete', async (req, res) => {
|
|||||||
checkedDevices: finalRecords.filter(r => r.status !== 'pending').length,
|
checkedDevices: finalRecords.filter(r => r.status !== 'pending').length,
|
||||||
normalDevices: finalRecords.filter(r => r.status === 'normal').length,
|
normalDevices: finalRecords.filter(r => r.status === 'normal').length,
|
||||||
abnormalDevices: finalRecords.filter(r => r.status === 'abnormal').length,
|
abnormalDevices: finalRecords.filter(r => r.status === 'abnormal').length,
|
||||||
missedDevices: finalRecords.filter(r => r.status === 'missed').length
|
missedDevices: finalRecords.filter(r => r.status === 'missed').length,
|
||||||
});
|
});
|
||||||
|
|
||||||
await InventoryTask.update(
|
await InventoryTask.update(
|
||||||
@@ -495,8 +546,12 @@ router.get('/stats/dashboard', async (req, res) => {
|
|||||||
limit: 5,
|
limit: 5,
|
||||||
order: [['createdAt', 'DESC']],
|
order: [['createdAt', 'DESC']],
|
||||||
include: [
|
include: [
|
||||||
{ model: require('../models/User'), as: 'Creator', attributes: ['userId', 'username', 'realName'] }
|
{
|
||||||
]
|
model: require('../models/User'),
|
||||||
|
as: 'Creator',
|
||||||
|
attributes: ['userId', 'username', 'realName'],
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -507,9 +562,12 @@ router.get('/stats/dashboard', async (req, res) => {
|
|||||||
normalRecords,
|
normalRecords,
|
||||||
abnormalRecords,
|
abnormalRecords,
|
||||||
pendingRecords,
|
pendingRecords,
|
||||||
completionRate: totalRecords > 0 ? ((normalRecords + abnormalRecords) / totalRecords * 100).toFixed(1) : 0,
|
completionRate:
|
||||||
abnormalRate: totalRecords > 0 ? (abnormalRecords / totalRecords * 100).toFixed(1) : 0,
|
totalRecords > 0
|
||||||
recentPlans
|
? (((normalRecords + abnormalRecords) / totalRecords) * 100).toFixed(1)
|
||||||
|
: 0,
|
||||||
|
abnormalRate: totalRecords > 0 ? ((abnormalRecords / totalRecords) * 100).toFixed(1) : 0,
|
||||||
|
recentPlans,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -558,20 +616,25 @@ router.post('/quick-add-device', async (req, res) => {
|
|||||||
|
|
||||||
const existingDevice = await Device.findOne({ where: { serialNumber: finalSerialNumber } });
|
const existingDevice = await Device.findOne({ where: { serialNumber: finalSerialNumber } });
|
||||||
if (existingDevice) {
|
if (existingDevice) {
|
||||||
return res.status(400).json({ error: '该序列号的设备已存在于设备管理中', deviceId: existingDevice.deviceId });
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ error: '该序列号的设备已存在于设备管理中', deviceId: existingDevice.deviceId });
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingPending = await PendingDevice.findOne({
|
const existingPending = await PendingDevice.findOne({
|
||||||
where: { serialNumber: finalSerialNumber, status: 'pending' }
|
where: { serialNumber: finalSerialNumber, status: 'pending' },
|
||||||
});
|
});
|
||||||
if (existingPending) {
|
if (existingPending) {
|
||||||
return res.status(400).json({ error: '该序列号的设备已在暂存列表中', pendingId: existingPending.pendingId });
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ error: '该序列号的设备已在暂存列表中', pendingId: existingPending.pendingId });
|
||||||
}
|
}
|
||||||
|
|
||||||
const pendingId = `PEND${Date.now().toString(36).toUpperCase()}${Math.random().toString(36).substr(2, 4).toUpperCase()}`;
|
const pendingId = `PEND${Date.now().toString(36).toUpperCase()}${Math.random().toString(36).substr(2, 4).toUpperCase()}`;
|
||||||
|
|
||||||
// 只有当用户没有填写设备名称时,才使用默认名称
|
// 只有当用户没有填写设备名称时,才使用默认名称
|
||||||
const finalName = finalDeviceName && finalDeviceName.trim() !== ''
|
const finalName =
|
||||||
|
finalDeviceName && finalDeviceName.trim() !== ''
|
||||||
? finalDeviceName.trim()
|
? finalDeviceName.trim()
|
||||||
: `新设备-${finalSerialNumber.slice(-6)}`;
|
: `新设备-${finalSerialNumber.slice(-6)}`;
|
||||||
|
|
||||||
@@ -596,12 +659,12 @@ router.post('/quick-add-device', async (req, res) => {
|
|||||||
taskId: taskId || null,
|
taskId: taskId || null,
|
||||||
createdBy: req.user?.userId,
|
createdBy: req.user?.userId,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
remark: remark || '盘点时快速添加'
|
remark: remark || '盘点时快速添加',
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
message: '设备已暂存,请前往暂存设备页面完善信息后同步',
|
message: '设备已暂存,请前往暂存设备页面完善信息后同步',
|
||||||
pendingDevice
|
pendingDevice,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('快速添加设备错误:', error);
|
console.error('快速添加设备错误:', error);
|
||||||
@@ -611,7 +674,14 @@ router.post('/quick-add-device', async (req, res) => {
|
|||||||
|
|
||||||
router.get('/pending-devices', async (req, res) => {
|
router.get('/pending-devices', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { status, planId, roomId, keyword, page = 1, pageSize = PAGINATION.DEFAULT_PAGE_SIZE } = req.query;
|
const {
|
||||||
|
status,
|
||||||
|
planId,
|
||||||
|
roomId,
|
||||||
|
keyword,
|
||||||
|
page = 1,
|
||||||
|
pageSize = PAGINATION.DEFAULT_PAGE_SIZE,
|
||||||
|
} = req.query;
|
||||||
const offset = (page - 1) * pageSize;
|
const offset = (page - 1) * pageSize;
|
||||||
const where = {};
|
const where = {};
|
||||||
|
|
||||||
@@ -627,7 +697,7 @@ router.get('/pending-devices', async (req, res) => {
|
|||||||
if (keyword) {
|
if (keyword) {
|
||||||
where[Op.or] = [
|
where[Op.or] = [
|
||||||
{ serialNumber: { [Op.like]: `%${keyword}%` } },
|
{ serialNumber: { [Op.like]: `%${keyword}%` } },
|
||||||
{ deviceName: { [Op.like]: `%${keyword}%` } }
|
{ deviceName: { [Op.like]: `%${keyword}%` } },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -638,18 +708,18 @@ router.get('/pending-devices', async (req, res) => {
|
|||||||
{ model: User, as: 'Syncer', attributes: ['userId', 'username', 'realName'] },
|
{ model: User, as: 'Syncer', attributes: ['userId', 'username', 'realName'] },
|
||||||
{ model: InventoryPlan, as: 'Plan', attributes: ['planId', 'name'] },
|
{ model: InventoryPlan, as: 'Plan', attributes: ['planId', 'name'] },
|
||||||
{ model: Room, as: 'Room', attributes: ['roomId', 'name'] },
|
{ model: Room, as: 'Room', attributes: ['roomId', 'name'] },
|
||||||
{ model: Rack, as: 'Rack', attributes: ['rackId', 'name'] }
|
{ model: Rack, as: 'Rack', attributes: ['rackId', 'name'] },
|
||||||
],
|
],
|
||||||
order: [['createdAt', 'DESC']],
|
order: [['createdAt', 'DESC']],
|
||||||
limit: parseInt(pageSize),
|
limit: parseInt(pageSize),
|
||||||
offset: parseInt(offset)
|
offset: parseInt(offset),
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
pendingDevices: rows,
|
pendingDevices: rows,
|
||||||
total: count,
|
total: count,
|
||||||
page: parseInt(page),
|
page: parseInt(page),
|
||||||
pageSize: parseInt(pageSize)
|
pageSize: parseInt(pageSize),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取暂存设备列表错误:', error);
|
console.error('获取暂存设备列表错误:', error);
|
||||||
@@ -677,8 +747,8 @@ router.get('/pending-devices/:pendingId', async (req, res) => {
|
|||||||
{ model: User, as: 'Syncer', attributes: ['userId', 'username', 'realName'] },
|
{ model: User, as: 'Syncer', attributes: ['userId', 'username', 'realName'] },
|
||||||
{ model: InventoryPlan, as: 'Plan', attributes: ['planId', 'name'] },
|
{ model: InventoryPlan, as: 'Plan', attributes: ['planId', 'name'] },
|
||||||
{ model: Room, as: 'Room', attributes: ['roomId', 'name'] },
|
{ model: Room, as: 'Room', attributes: ['roomId', 'name'] },
|
||||||
{ model: Rack, as: 'Rack', attributes: ['rackId', 'name'] }
|
{ model: Rack, as: 'Rack', attributes: ['rackId', 'name'] },
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!pendingDevice) {
|
if (!pendingDevice) {
|
||||||
@@ -702,7 +772,23 @@ router.put('/pending-devices/:pendingId', async (req, res) => {
|
|||||||
return res.status(400).json({ error: '已同步的设备无法修改' });
|
return res.status(400).json({ error: '已同步的设备无法修改' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { deviceName, deviceType, roomId, rackId, position, model, brand, height, powerConsumption, ipAddress, purchaseDate, warrantyExpiry, description, remark, ...restFields } = req.body;
|
const {
|
||||||
|
deviceName,
|
||||||
|
deviceType,
|
||||||
|
roomId,
|
||||||
|
rackId,
|
||||||
|
position,
|
||||||
|
model,
|
||||||
|
brand,
|
||||||
|
height,
|
||||||
|
powerConsumption,
|
||||||
|
ipAddress,
|
||||||
|
purchaseDate,
|
||||||
|
warrantyExpiry,
|
||||||
|
description,
|
||||||
|
remark,
|
||||||
|
...restFields
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
const updateData = {
|
const updateData = {
|
||||||
deviceName: deviceName !== undefined ? deviceName : pendingDevice.deviceName,
|
deviceName: deviceName !== undefined ? deviceName : pendingDevice.deviceName,
|
||||||
@@ -713,12 +799,23 @@ router.put('/pending-devices/:pendingId', async (req, res) => {
|
|||||||
model: model !== undefined ? model : pendingDevice.model,
|
model: model !== undefined ? model : pendingDevice.model,
|
||||||
brand: brand !== undefined ? brand : pendingDevice.brand,
|
brand: brand !== undefined ? brand : pendingDevice.brand,
|
||||||
height: height !== undefined ? height : pendingDevice.height,
|
height: height !== undefined ? height : pendingDevice.height,
|
||||||
powerConsumption: powerConsumption !== undefined ? powerConsumption : pendingDevice.powerConsumption,
|
powerConsumption:
|
||||||
|
powerConsumption !== undefined ? powerConsumption : pendingDevice.powerConsumption,
|
||||||
ipAddress: ipAddress !== undefined ? ipAddress : pendingDevice.ipAddress,
|
ipAddress: ipAddress !== undefined ? ipAddress : pendingDevice.ipAddress,
|
||||||
purchaseDate: purchaseDate !== undefined ? (purchaseDate ? new Date(purchaseDate) : null) : pendingDevice.purchaseDate,
|
purchaseDate:
|
||||||
warrantyExpiry: warrantyExpiry !== undefined ? (warrantyExpiry ? new Date(warrantyExpiry) : null) : pendingDevice.warrantyExpiry,
|
purchaseDate !== undefined
|
||||||
|
? purchaseDate
|
||||||
|
? new Date(purchaseDate)
|
||||||
|
: null
|
||||||
|
: pendingDevice.purchaseDate,
|
||||||
|
warrantyExpiry:
|
||||||
|
warrantyExpiry !== undefined
|
||||||
|
? warrantyExpiry
|
||||||
|
? new Date(warrantyExpiry)
|
||||||
|
: null
|
||||||
|
: pendingDevice.warrantyExpiry,
|
||||||
description: description !== undefined ? description : pendingDevice.description,
|
description: description !== undefined ? description : pendingDevice.description,
|
||||||
remark: remark !== undefined ? remark : pendingDevice.remark
|
remark: remark !== undefined ? remark : pendingDevice.remark,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (Object.keys(restFields).length > 0) {
|
if (Object.keys(restFields).length > 0) {
|
||||||
@@ -759,7 +856,9 @@ router.post('/pending-devices/:pendingId/sync', async (req, res) => {
|
|||||||
return res.status(400).json({ error: '该设备已同步' });
|
return res.status(400).json({ error: '该设备已同步' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingDevice = await Device.findOne({ where: { serialNumber: pendingDevice.serialNumber } });
|
const existingDevice = await Device.findOne({
|
||||||
|
where: { serialNumber: pendingDevice.serialNumber },
|
||||||
|
});
|
||||||
if (existingDevice) {
|
if (existingDevice) {
|
||||||
return res.status(400).json({ error: '该序列号的设备已存在于设备管理中' });
|
return res.status(400).json({ error: '该序列号的设备已存在于设备管理中' });
|
||||||
}
|
}
|
||||||
@@ -770,7 +869,9 @@ router.post('/pending-devices/:pendingId/sync', async (req, res) => {
|
|||||||
const match = device.deviceId.match(/^DEV(\d+)$/);
|
const match = device.deviceId.match(/^DEV(\d+)$/);
|
||||||
if (match) {
|
if (match) {
|
||||||
const num = parseInt(match[1], 10);
|
const num = parseInt(match[1], 10);
|
||||||
if (num > maxNumber) maxNumber = num;
|
if (num > maxNumber) {
|
||||||
|
maxNumber = num;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const deviceId = `DEV${String(maxNumber + 1).padStart(3, '0')}`;
|
const deviceId = `DEV${String(maxNumber + 1).padStart(3, '0')}`;
|
||||||
@@ -790,20 +891,20 @@ router.post('/pending-devices/:pendingId/sync', async (req, res) => {
|
|||||||
purchaseDate: pendingDevice.purchaseDate,
|
purchaseDate: pendingDevice.purchaseDate,
|
||||||
warrantyExpiry: pendingDevice.warrantyExpiry,
|
warrantyExpiry: pendingDevice.warrantyExpiry,
|
||||||
customFields: pendingDevice.customFields,
|
customFields: pendingDevice.customFields,
|
||||||
status: 'running'
|
status: 'running',
|
||||||
});
|
});
|
||||||
|
|
||||||
await pendingDevice.update({
|
await pendingDevice.update({
|
||||||
status: 'synced',
|
status: 'synced',
|
||||||
syncedAt: new Date(),
|
syncedAt: new Date(),
|
||||||
syncedBy: req.user?.userId,
|
syncedBy: req.user?.userId,
|
||||||
syncedDeviceId: newDevice.deviceId
|
syncedDeviceId: newDevice.deviceId,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: '同步成功',
|
message: '同步成功',
|
||||||
device: newDevice,
|
device: newDevice,
|
||||||
pendingDevice
|
pendingDevice,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('同步设备错误:', error);
|
console.error('同步设备错误:', error);
|
||||||
@@ -821,8 +922,8 @@ router.post('/pending-devices/batch-sync', async (req, res) => {
|
|||||||
const pendingDevices = await PendingDevice.findAll({
|
const pendingDevices = await PendingDevice.findAll({
|
||||||
where: {
|
where: {
|
||||||
pendingId: { [Op.in]: pendingIds },
|
pendingId: { [Op.in]: pendingIds },
|
||||||
status: 'pending'
|
status: 'pending',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (pendingDevices.length === 0) {
|
if (pendingDevices.length === 0) {
|
||||||
@@ -835,7 +936,9 @@ router.post('/pending-devices/batch-sync', async (req, res) => {
|
|||||||
const match = device.deviceId.match(/^DEV(\d+)$/);
|
const match = device.deviceId.match(/^DEV(\d+)$/);
|
||||||
if (match) {
|
if (match) {
|
||||||
const num = parseInt(match[1], 10);
|
const num = parseInt(match[1], 10);
|
||||||
if (num > maxNumber) maxNumber = num;
|
if (num > maxNumber) {
|
||||||
|
maxNumber = num;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -844,9 +947,15 @@ router.post('/pending-devices/batch-sync', async (req, res) => {
|
|||||||
|
|
||||||
for (const pending of pendingDevices) {
|
for (const pending of pendingDevices) {
|
||||||
try {
|
try {
|
||||||
const existingDevice = await Device.findOne({ where: { serialNumber: pending.serialNumber } });
|
const existingDevice = await Device.findOne({
|
||||||
|
where: { serialNumber: pending.serialNumber },
|
||||||
|
});
|
||||||
if (existingDevice) {
|
if (existingDevice) {
|
||||||
errors.push({ pendingId: pending.pendingId, serialNumber: pending.serialNumber, error: '序列号已存在' });
|
errors.push({
|
||||||
|
pendingId: pending.pendingId,
|
||||||
|
serialNumber: pending.serialNumber,
|
||||||
|
error: '序列号已存在',
|
||||||
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -868,14 +977,14 @@ router.post('/pending-devices/batch-sync', async (req, res) => {
|
|||||||
purchaseDate: pending.purchaseDate,
|
purchaseDate: pending.purchaseDate,
|
||||||
warrantyExpiry: pending.warrantyExpiry,
|
warrantyExpiry: pending.warrantyExpiry,
|
||||||
customFields: pending.customFields,
|
customFields: pending.customFields,
|
||||||
status: 'running'
|
status: 'running',
|
||||||
});
|
});
|
||||||
|
|
||||||
await pending.update({
|
await pending.update({
|
||||||
status: 'synced',
|
status: 'synced',
|
||||||
syncedAt: new Date(),
|
syncedAt: new Date(),
|
||||||
syncedBy: req.user?.userId,
|
syncedBy: req.user?.userId,
|
||||||
syncedDeviceId: newDevice.deviceId
|
syncedDeviceId: newDevice.deviceId,
|
||||||
});
|
});
|
||||||
|
|
||||||
results.push({ pendingId: pending.pendingId, deviceId: newDevice.deviceId });
|
results.push({ pendingId: pending.pendingId, deviceId: newDevice.deviceId });
|
||||||
@@ -889,7 +998,7 @@ router.post('/pending-devices/batch-sync', async (req, res) => {
|
|||||||
successCount: results.length,
|
successCount: results.length,
|
||||||
errorCount: errors.length,
|
errorCount: errors.length,
|
||||||
results,
|
results,
|
||||||
errors
|
errors,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('批量同步设备错误:', error);
|
console.error('批量同步设备错误:', error);
|
||||||
|
|||||||
@@ -24,10 +24,13 @@ router.get('/', async (req, res) => {
|
|||||||
{
|
{
|
||||||
model: Device,
|
model: Device,
|
||||||
as: 'device',
|
as: 'device',
|
||||||
attributes: ['deviceId', 'name', 'type']
|
attributes: ['deviceId', 'name', 'type'],
|
||||||
}
|
},
|
||||||
|
],
|
||||||
|
order: [
|
||||||
|
['slotNumber', 'ASC'],
|
||||||
|
['name', 'ASC'],
|
||||||
],
|
],
|
||||||
order: [['slotNumber', 'ASC'], ['name', 'ASC']]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json(networkCards);
|
res.json(networkCards);
|
||||||
@@ -47,10 +50,13 @@ router.get('/device/:deviceId', async (req, res) => {
|
|||||||
{
|
{
|
||||||
model: Device,
|
model: Device,
|
||||||
as: 'device',
|
as: 'device',
|
||||||
attributes: ['deviceId', 'name', 'type']
|
attributes: ['deviceId', 'name', 'type'],
|
||||||
}
|
},
|
||||||
|
],
|
||||||
|
order: [
|
||||||
|
['slotNumber', 'ASC'],
|
||||||
|
['name', 'ASC'],
|
||||||
],
|
],
|
||||||
order: [['slotNumber', 'ASC'], ['name', 'ASC']]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json(networkCards);
|
res.json(networkCards);
|
||||||
@@ -66,14 +72,17 @@ router.get('/device/:deviceId/with-ports', async (req, res) => {
|
|||||||
|
|
||||||
const networkCards = await NetworkCard.findAll({
|
const networkCards = await NetworkCard.findAll({
|
||||||
where: { deviceId },
|
where: { deviceId },
|
||||||
order: [['slotNumber', 'ASC'], ['name', 'ASC']]
|
order: [
|
||||||
|
['slotNumber', 'ASC'],
|
||||||
|
['name', 'ASC'],
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const cardsWithPorts = await Promise.all(
|
const cardsWithPorts = await Promise.all(
|
||||||
networkCards.map(async (card) => {
|
networkCards.map(async card => {
|
||||||
const ports = await DevicePort.findAll({
|
const ports = await DevicePort.findAll({
|
||||||
where: { nicId: card.nicId },
|
where: { nicId: card.nicId },
|
||||||
order: [['portName', 'ASC']]
|
order: [['portName', 'ASC']],
|
||||||
});
|
});
|
||||||
|
|
||||||
const freeCount = ports.filter(p => p.status === 'free').length;
|
const freeCount = ports.filter(p => p.status === 'free').length;
|
||||||
@@ -87,15 +96,15 @@ router.get('/device/:deviceId/with-ports', async (req, res) => {
|
|||||||
total: ports.length,
|
total: ports.length,
|
||||||
free: freeCount,
|
free: freeCount,
|
||||||
occupied: occupiedCount,
|
occupied: occupiedCount,
|
||||||
fault: faultCount
|
fault: faultCount,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const ungroupedPorts = await DevicePort.findAll({
|
const ungroupedPorts = await DevicePort.findAll({
|
||||||
where: { deviceId, nicId: null },
|
where: { deviceId, nicId: null },
|
||||||
order: [['portName', 'ASC']]
|
order: [['portName', 'ASC']],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (ungroupedPorts.length > 0) {
|
if (ungroupedPorts.length > 0) {
|
||||||
@@ -110,8 +119,8 @@ router.get('/device/:deviceId/with-ports', async (req, res) => {
|
|||||||
total: ungroupedPorts.length,
|
total: ungroupedPorts.length,
|
||||||
free: ungroupedPorts.filter(p => p.status === 'free').length,
|
free: ungroupedPorts.filter(p => p.status === 'free').length,
|
||||||
occupied: ungroupedPorts.filter(p => p.status === 'occupied').length,
|
occupied: ungroupedPorts.filter(p => p.status === 'occupied').length,
|
||||||
fault: ungroupedPorts.filter(p => p.status === 'fault').length
|
fault: ungroupedPorts.filter(p => p.status === 'fault').length,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,9 +138,9 @@ router.get('/:nicId', async (req, res) => {
|
|||||||
{
|
{
|
||||||
model: Device,
|
model: Device,
|
||||||
as: 'device',
|
as: 'device',
|
||||||
attributes: ['deviceId', 'name', 'type']
|
attributes: ['deviceId', 'name', 'type'],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!networkCard) {
|
if (!networkCard) {
|
||||||
@@ -151,7 +160,7 @@ router.get('/:nicId/ports', async (req, res) => {
|
|||||||
|
|
||||||
const ports = await DevicePort.findAll({
|
const ports = await DevicePort.findAll({
|
||||||
where: { nicId },
|
where: { nicId },
|
||||||
order: [['portName', 'ASC']]
|
order: [['portName', 'ASC']],
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json(ports);
|
res.json(ports);
|
||||||
@@ -170,7 +179,7 @@ router.get('/find', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const networkCard = await NetworkCard.findOne({
|
const networkCard = await NetworkCard.findOne({
|
||||||
where: { deviceId, name }
|
where: { deviceId, name },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!networkCard) {
|
if (!networkCard) {
|
||||||
@@ -186,14 +195,15 @@ router.get('/find', async (req, res) => {
|
|||||||
|
|
||||||
router.post('/', async (req, res) => {
|
router.post('/', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { nicId, deviceId, name, description, slotNumber, model, manufacturer, status } = req.body;
|
const { nicId, deviceId, name, description, slotNumber, model, manufacturer, status } =
|
||||||
|
req.body;
|
||||||
|
|
||||||
if (!deviceId || !name) {
|
if (!deviceId || !name) {
|
||||||
return res.status(400).json({ error: '缺少必填字段' });
|
return res.status(400).json({ error: '缺少必填字段' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingCard = await NetworkCard.findOne({
|
const existingCard = await NetworkCard.findOne({
|
||||||
where: { deviceId, name }
|
where: { deviceId, name },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingCard) {
|
if (existingCard) {
|
||||||
@@ -211,7 +221,7 @@ router.post('/', async (req, res) => {
|
|||||||
model,
|
model,
|
||||||
manufacturer,
|
manufacturer,
|
||||||
status: status || 'normal',
|
status: status || 'normal',
|
||||||
portCount: 0
|
portCount: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const createdCard = await NetworkCard.findByPk(networkCard.nicId, {
|
const createdCard = await NetworkCard.findByPk(networkCard.nicId, {
|
||||||
@@ -219,9 +229,9 @@ router.post('/', async (req, res) => {
|
|||||||
{
|
{
|
||||||
model: Device,
|
model: Device,
|
||||||
as: 'device',
|
as: 'device',
|
||||||
attributes: ['deviceId', 'name', 'type']
|
attributes: ['deviceId', 'name', 'type'],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(201).json(createdCard);
|
res.status(201).json(createdCard);
|
||||||
@@ -245,7 +255,7 @@ router.post('/batch', async (req, res) => {
|
|||||||
failed: 0,
|
failed: 0,
|
||||||
skipped: 0,
|
skipped: 0,
|
||||||
updated: 0,
|
updated: 0,
|
||||||
errors: []
|
errors: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const transaction = await NetworkCard.sequelize.transaction();
|
const transaction = await NetworkCard.sequelize.transaction();
|
||||||
@@ -272,7 +282,7 @@ router.post('/batch', async (req, res) => {
|
|||||||
|
|
||||||
const existingCard = await NetworkCard.findOne({
|
const existingCard = await NetworkCard.findOne({
|
||||||
where: { deviceId: cardData.deviceId, name: cardData.name },
|
where: { deviceId: cardData.deviceId, name: cardData.name },
|
||||||
transaction
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingCard) {
|
if (existingCard) {
|
||||||
@@ -281,16 +291,28 @@ router.post('/batch', async (req, res) => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (updateExisting) {
|
if (updateExisting) {
|
||||||
await NetworkCard.update({
|
await NetworkCard.update(
|
||||||
slotNumber: cardData.slotNumber !== undefined ? cardData.slotNumber : existingCard.slotNumber,
|
{
|
||||||
|
slotNumber:
|
||||||
|
cardData.slotNumber !== undefined
|
||||||
|
? cardData.slotNumber
|
||||||
|
: existingCard.slotNumber,
|
||||||
model: cardData.model !== undefined ? cardData.model : existingCard.model,
|
model: cardData.model !== undefined ? cardData.model : existingCard.model,
|
||||||
manufacturer: cardData.manufacturer !== undefined ? cardData.manufacturer : existingCard.manufacturer,
|
manufacturer:
|
||||||
description: cardData.description !== undefined ? cardData.description : existingCard.description,
|
cardData.manufacturer !== undefined
|
||||||
status: cardData.status || existingCard.status
|
? cardData.manufacturer
|
||||||
}, {
|
: existingCard.manufacturer,
|
||||||
|
description:
|
||||||
|
cardData.description !== undefined
|
||||||
|
? cardData.description
|
||||||
|
: existingCard.description,
|
||||||
|
status: cardData.status || existingCard.status,
|
||||||
|
},
|
||||||
|
{
|
||||||
where: { nicId: existingCard.nicId },
|
where: { nicId: existingCard.nicId },
|
||||||
transaction
|
transaction,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
results.updated++;
|
results.updated++;
|
||||||
results.success++;
|
results.success++;
|
||||||
continue;
|
continue;
|
||||||
@@ -298,9 +320,11 @@ router.post('/batch', async (req, res) => {
|
|||||||
throw new Error('该设备已存在同名网卡');
|
throw new Error('该设备已存在同名网卡');
|
||||||
}
|
}
|
||||||
|
|
||||||
const autoNicId = cardData.nicId || `NIC-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
|
const autoNicId =
|
||||||
|
cardData.nicId || `NIC-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
|
||||||
|
|
||||||
await NetworkCard.create({
|
await NetworkCard.create(
|
||||||
|
{
|
||||||
nicId: autoNicId,
|
nicId: autoNicId,
|
||||||
deviceId: cardData.deviceId,
|
deviceId: cardData.deviceId,
|
||||||
name: cardData.name,
|
name: cardData.name,
|
||||||
@@ -309,8 +333,10 @@ router.post('/batch', async (req, res) => {
|
|||||||
manufacturer: cardData.manufacturer,
|
manufacturer: cardData.manufacturer,
|
||||||
description: cardData.description,
|
description: cardData.description,
|
||||||
status: cardData.status || 'normal',
|
status: cardData.status || 'normal',
|
||||||
portCount: 0
|
portCount: 0,
|
||||||
}, { transaction });
|
},
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
|
||||||
results.success++;
|
results.success++;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -319,7 +345,7 @@ router.post('/batch', async (req, res) => {
|
|||||||
index: i + 1,
|
index: i + 1,
|
||||||
deviceId: cardData.deviceId,
|
deviceId: cardData.deviceId,
|
||||||
name: cardData.name,
|
name: cardData.name,
|
||||||
error: error.message
|
error: error.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -339,7 +365,7 @@ router.post('/batch', async (req, res) => {
|
|||||||
router.put('/:nicId', async (req, res) => {
|
router.put('/:nicId', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const [updated] = await NetworkCard.update(req.body, {
|
const [updated] = await NetworkCard.update(req.body, {
|
||||||
where: { nicId: req.params.nicId }
|
where: { nicId: req.params.nicId },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (updated) {
|
if (updated) {
|
||||||
@@ -348,9 +374,9 @@ router.put('/:nicId', async (req, res) => {
|
|||||||
{
|
{
|
||||||
model: Device,
|
model: Device,
|
||||||
as: 'device',
|
as: 'device',
|
||||||
attributes: ['deviceId', 'name', 'type']
|
attributes: ['deviceId', 'name', 'type'],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
res.json(networkCard);
|
res.json(networkCard);
|
||||||
} else {
|
} else {
|
||||||
@@ -369,12 +395,12 @@ router.delete('/:nicId', async (req, res) => {
|
|||||||
const portCount = await DevicePort.count({ where: { nicId } });
|
const portCount = await DevicePort.count({ where: { nicId } });
|
||||||
if (portCount > 0) {
|
if (portCount > 0) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
error: `该网卡下还有 ${portCount} 个端口,请先删除或转移端口后再删除网卡`
|
error: `该网卡下还有 ${portCount} 个端口,请先删除或转移端口后再删除网卡`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleted = await NetworkCard.destroy({
|
const deleted = await NetworkCard.destroy({
|
||||||
where: { nicId }
|
where: { nicId },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ router.get('/', authMiddleware, async (req, res) => {
|
|||||||
keyword,
|
keyword,
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
result
|
result,
|
||||||
} = req.query;
|
} = req.query;
|
||||||
|
|
||||||
const offset = (parseInt(page) - 1) * parseInt(pageSize);
|
const offset = (parseInt(page) - 1) * parseInt(pageSize);
|
||||||
@@ -46,7 +46,7 @@ router.get('/', authMiddleware, async (req, res) => {
|
|||||||
where[Op.or] = [
|
where[Op.or] = [
|
||||||
{ operationDescription: { [Op.like]: `%${keyword}%` } },
|
{ operationDescription: { [Op.like]: `%${keyword}%` } },
|
||||||
{ targetName: { [Op.like]: `%${keyword}%` } },
|
{ targetName: { [Op.like]: `%${keyword}%` } },
|
||||||
{ operatorName: { [Op.like]: `%${keyword}%` } }
|
{ operatorName: { [Op.like]: `%${keyword}%` } },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ router.get('/', authMiddleware, async (req, res) => {
|
|||||||
where,
|
where,
|
||||||
order: [['createdAt', 'DESC']],
|
order: [['createdAt', 'DESC']],
|
||||||
offset,
|
offset,
|
||||||
limit
|
limit,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -79,14 +79,14 @@ router.get('/', authMiddleware, async (req, res) => {
|
|||||||
total: count,
|
total: count,
|
||||||
page: parseInt(page),
|
page: parseInt(page),
|
||||||
pageSize: parseInt(pageSize),
|
pageSize: parseInt(pageSize),
|
||||||
logs
|
logs,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取操作日志失败:', error);
|
console.error('获取操作日志失败:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '获取操作日志失败'
|
message: '获取操作日志失败',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -95,23 +95,23 @@ router.get('/modules', authMiddleware, async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const modules = await OperationLog.findAll({
|
const modules = await OperationLog.findAll({
|
||||||
attributes: ['module'],
|
attributes: ['module'],
|
||||||
group: ['module']
|
group: ['module'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const moduleList = modules.map(m => ({
|
const moduleList = modules.map(m => ({
|
||||||
value: m.module,
|
value: m.module,
|
||||||
label: getModuleName(m.module)
|
label: getModuleName(m.module),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: moduleList
|
data: moduleList,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取模块列表失败:', error);
|
console.error('获取模块列表失败:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '获取模块列表失败'
|
message: '获取模块列表失败',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -128,23 +128,23 @@ router.get('/types', authMiddleware, async (req, res) => {
|
|||||||
const types = await OperationLog.findAll({
|
const types = await OperationLog.findAll({
|
||||||
where,
|
where,
|
||||||
attributes: ['operationType'],
|
attributes: ['operationType'],
|
||||||
group: ['operationType']
|
group: ['operationType'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const typeList = types.map(t => ({
|
const typeList = types.map(t => ({
|
||||||
value: t.operationType,
|
value: t.operationType,
|
||||||
label: getOperationTypeName(t.operationType)
|
label: getOperationTypeName(t.operationType),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: typeList
|
data: typeList,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取操作类型列表失败:', error);
|
console.error('获取操作类型列表失败:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '获取操作类型列表失败'
|
message: '获取操作类型列表失败',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -170,23 +170,26 @@ router.get('/statistics', authMiddleware, async (req, res) => {
|
|||||||
OperationLog.findAll({
|
OperationLog.findAll({
|
||||||
where,
|
where,
|
||||||
attributes: ['module', [sequelize.fn('COUNT', sequelize.col('module')), 'count']],
|
attributes: ['module', [sequelize.fn('COUNT', sequelize.col('module')), 'count']],
|
||||||
group: ['module']
|
group: ['module'],
|
||||||
}),
|
}),
|
||||||
OperationLog.findAll({
|
OperationLog.findAll({
|
||||||
where,
|
where,
|
||||||
attributes: ['operationType', [sequelize.fn('COUNT', sequelize.col('operationType')), 'count']],
|
attributes: [
|
||||||
group: ['operationType']
|
'operationType',
|
||||||
|
[sequelize.fn('COUNT', sequelize.col('operationType')), 'count'],
|
||||||
|
],
|
||||||
|
group: ['operationType'],
|
||||||
}),
|
}),
|
||||||
OperationLog.findAll({
|
OperationLog.findAll({
|
||||||
where,
|
where,
|
||||||
attributes: [
|
attributes: [
|
||||||
[sequelize.fn('DATE', sequelize.col('createdAt')), 'date'],
|
[sequelize.fn('DATE', sequelize.col('createdAt')), 'date'],
|
||||||
[sequelize.fn('COUNT', '*'), 'count']
|
[sequelize.fn('COUNT', '*'), 'count'],
|
||||||
],
|
],
|
||||||
group: [sequelize.fn('DATE', sequelize.col('createdAt'))],
|
group: [sequelize.fn('DATE', sequelize.col('createdAt'))],
|
||||||
order: [[sequelize.fn('DATE', sequelize.col('createdAt')), 'DESC']],
|
order: [[sequelize.fn('DATE', sequelize.col('createdAt')), 'DESC']],
|
||||||
limit: 30
|
limit: 30,
|
||||||
})
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -194,14 +197,14 @@ router.get('/statistics', authMiddleware, async (req, res) => {
|
|||||||
data: {
|
data: {
|
||||||
byModule: moduleStats.map(s => ({ module: s.module, count: s.get('count') })),
|
byModule: moduleStats.map(s => ({ module: s.module, count: s.get('count') })),
|
||||||
byType: typeStats.map(s => ({ type: s.operationType, count: s.get('count') })),
|
byType: typeStats.map(s => ({ type: s.operationType, count: s.get('count') })),
|
||||||
byDay: dailyStats.map(s => ({ date: s.get('date'), count: s.get('count') }))
|
byDay: dailyStats.map(s => ({ date: s.get('date'), count: s.get('count') })),
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取操作日志统计失败:', error);
|
console.error('获取操作日志统计失败:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '获取操作日志统计失败'
|
message: '获取操作日志统计失败',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -213,19 +216,19 @@ router.get('/:recordId', authMiddleware, async (req, res) => {
|
|||||||
if (!log) {
|
if (!log) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '日志记录不存在'
|
message: '日志记录不存在',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: log
|
data: log,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取操作日志详情失败:', error);
|
console.error('获取操作日志详情失败:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '获取操作日志详情失败'
|
message: '获取操作日志详情失败',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -239,7 +242,7 @@ function getModuleName(module) {
|
|||||||
rack: '机柜管理',
|
rack: '机柜管理',
|
||||||
room: '机房管理',
|
room: '机房管理',
|
||||||
ticket: '工单管理',
|
ticket: '工单管理',
|
||||||
backup: '备份管理'
|
backup: '备份管理',
|
||||||
};
|
};
|
||||||
return moduleNames[module] || module;
|
return moduleNames[module] || module;
|
||||||
}
|
}
|
||||||
@@ -255,7 +258,7 @@ function getOperationTypeName(type) {
|
|||||||
move: '移动',
|
move: '移动',
|
||||||
permission_change: '权限变更',
|
permission_change: '权限变更',
|
||||||
import: '导入',
|
import: '导入',
|
||||||
export: '导出'
|
export: '导出',
|
||||||
};
|
};
|
||||||
return typeNames[type] || type;
|
return typeNames[type] || type;
|
||||||
}
|
}
|
||||||
|
|||||||
+96
-79
@@ -32,7 +32,7 @@ router.get('/', async (req, res) => {
|
|||||||
if (keyword) {
|
if (keyword) {
|
||||||
where[require('sequelize').Op.or] = [
|
where[require('sequelize').Op.or] = [
|
||||||
{ rackId: { [require('sequelize').Op.like]: `%${keyword}%` } },
|
{ rackId: { [require('sequelize').Op.like]: `%${keyword}%` } },
|
||||||
{ name: { [require('sequelize').Op.like]: `%${keyword}%` } }
|
{ name: { [require('sequelize').Op.like]: `%${keyword}%` } },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,18 +42,16 @@ router.get('/', async (req, res) => {
|
|||||||
// 获取分页数据 - 先查询机柜基本信息
|
// 获取分页数据 - 先查询机柜基本信息
|
||||||
const racks = await Rack.findAll({
|
const racks = await Rack.findAll({
|
||||||
where,
|
where,
|
||||||
include: [
|
include: [{ model: Room, separate: false }],
|
||||||
{ model: Room, separate: false }
|
|
||||||
],
|
|
||||||
limit: pageSize,
|
limit: pageSize,
|
||||||
offset: offset
|
offset: offset,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 单独查询每个机柜的设备信息(避免 JOIN 导致的数据重复问题)
|
// 单独查询每个机柜的设备信息(避免 JOIN 导致的数据重复问题)
|
||||||
const rackIds = racks.map(r => r.rackId);
|
const rackIds = racks.map(r => r.rackId);
|
||||||
const devices = await Device.findAll({
|
const devices = await Device.findAll({
|
||||||
where: { rackId: rackIds },
|
where: { rackId: rackIds },
|
||||||
attributes: ['deviceId', 'rackId', 'name', 'powerConsumption', 'height']
|
attributes: ['deviceId', 'rackId', 'name', 'powerConsumption', 'height'],
|
||||||
});
|
});
|
||||||
|
|
||||||
// 将设备信息关联到对应的机柜
|
// 将设备信息关联到对应的机柜
|
||||||
@@ -72,7 +70,7 @@ router.get('/', async (req, res) => {
|
|||||||
// 返回带分页信息的响应
|
// 返回带分页信息的响应
|
||||||
res.json({
|
res.json({
|
||||||
racks,
|
racks,
|
||||||
total
|
total,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -95,20 +93,20 @@ router.get('/all', async (req, res) => {
|
|||||||
if (keyword) {
|
if (keyword) {
|
||||||
where[require('sequelize').Op.or] = [
|
where[require('sequelize').Op.or] = [
|
||||||
{ rackId: { [require('sequelize').Op.like]: `%${keyword}%` } },
|
{ rackId: { [require('sequelize').Op.like]: `%${keyword}%` } },
|
||||||
{ name: { [require('sequelize').Op.like]: `%${keyword}%` } }
|
{ name: { [require('sequelize').Op.like]: `%${keyword}%` } },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
const racks = await Rack.findAll({
|
const racks = await Rack.findAll({
|
||||||
where,
|
where,
|
||||||
include: [{ model: Room, separate: false }],
|
include: [{ model: Room, separate: false }],
|
||||||
limit: MAX_EXPORT_SIZE
|
limit: MAX_EXPORT_SIZE,
|
||||||
});
|
});
|
||||||
|
|
||||||
const rackIds = racks.map(r => r.rackId);
|
const rackIds = racks.map(r => r.rackId);
|
||||||
const devices = await Device.findAll({
|
const devices = await Device.findAll({
|
||||||
where: { rackId: rackIds },
|
where: { rackId: rackIds },
|
||||||
attributes: ['deviceId', 'rackId', 'name', 'powerConsumption', 'height']
|
attributes: ['deviceId', 'rackId', 'name', 'powerConsumption', 'height'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const deviceMap = {};
|
const deviceMap = {};
|
||||||
@@ -125,7 +123,7 @@ router.get('/all', async (req, res) => {
|
|||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
racks,
|
racks,
|
||||||
total: racks.length
|
total: racks.length,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -139,20 +137,20 @@ router.get('/import-template', async (req, res) => {
|
|||||||
const templateData = [
|
const templateData = [
|
||||||
{
|
{
|
||||||
'机柜ID(留空自动生成)': '',
|
'机柜ID(留空自动生成)': '',
|
||||||
'机柜名称': '测试机柜1',
|
机柜名称: '测试机柜1',
|
||||||
'所属机房名称': '测试机房1',
|
所属机房名称: '测试机房1',
|
||||||
'高度(U)': 42,
|
'高度(U)': 42,
|
||||||
'最大功率(W)': 5000,
|
'最大功率(W)': 5000,
|
||||||
'状态': 'active'
|
状态: 'active',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'机柜ID(留空自动生成)': 'RACK001',
|
'机柜ID(留空自动生成)': 'RACK001',
|
||||||
'机柜名称': '测试机柜2',
|
机柜名称: '测试机柜2',
|
||||||
'所属机房名称': '测试机房1',
|
所属机房名称: '测试机房1',
|
||||||
'高度(U)': 42,
|
'高度(U)': 42,
|
||||||
'最大功率(W)': 3000,
|
'最大功率(W)': 3000,
|
||||||
'状态': 'maintenance'
|
状态: 'maintenance',
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// 使用xlsx创建工作簿
|
// 使用xlsx创建工作簿
|
||||||
@@ -162,14 +160,7 @@ router.get('/import-template', async (req, res) => {
|
|||||||
const ws = XLSX.utils.json_to_sheet(templateData);
|
const ws = XLSX.utils.json_to_sheet(templateData);
|
||||||
|
|
||||||
// 设置列宽
|
// 设置列宽
|
||||||
ws['!cols'] = [
|
ws['!cols'] = [{ wch: 15 }, { wch: 20 }, { wch: 15 }, { wch: 10 }, { wch: 15 }, { wch: 15 }];
|
||||||
{ wch: 15 },
|
|
||||||
{ wch: 20 },
|
|
||||||
{ wch: 15 },
|
|
||||||
{ wch: 10 },
|
|
||||||
{ wch: 15 },
|
|
||||||
{ wch: 15 }
|
|
||||||
];
|
|
||||||
|
|
||||||
// 添加工作表到工作簿
|
// 添加工作表到工作簿
|
||||||
XLSX.utils.book_append_sheet(wb, ws, '机柜模板');
|
XLSX.utils.book_append_sheet(wb, ws, '机柜模板');
|
||||||
@@ -178,12 +169,17 @@ router.get('/import-template', async (req, res) => {
|
|||||||
const excelBuffer = XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' });
|
const excelBuffer = XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' });
|
||||||
|
|
||||||
// 设置响应头
|
// 设置响应头
|
||||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
res.setHeader(
|
||||||
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent('机柜导入模板.xlsx')}`);
|
'Content-Type',
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||||
|
);
|
||||||
|
res.setHeader(
|
||||||
|
'Content-Disposition',
|
||||||
|
`attachment; filename*=UTF-8''${encodeURIComponent('机柜导入模板.xlsx')}`
|
||||||
|
);
|
||||||
|
|
||||||
// 发送文件
|
// 发送文件
|
||||||
res.send(excelBuffer);
|
res.send(excelBuffer);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('生成导入模板失败:', error);
|
console.error('生成导入模板失败:', error);
|
||||||
res.status(500).json({ error: '生成导入模板失败' });
|
res.status(500).json({ error: '生成导入模板失败' });
|
||||||
@@ -197,27 +193,29 @@ router.get('/export', async (req, res) => {
|
|||||||
const racks = await Rack.findAll({
|
const racks = await Rack.findAll({
|
||||||
include: [
|
include: [
|
||||||
{ model: Room, attributes: ['name'] },
|
{ model: Room, attributes: ['name'] },
|
||||||
{ model: Device, attributes: ['deviceId', 'name', 'powerConsumption'] }
|
{ model: Device, attributes: ['deviceId', 'name', 'powerConsumption'] },
|
||||||
],
|
],
|
||||||
order: [['rackId', 'ASC']]
|
order: [['rackId', 'ASC']],
|
||||||
});
|
});
|
||||||
|
|
||||||
// 准备导出数据
|
// 准备导出数据
|
||||||
const exportData = racks.map(rack => {
|
const exportData = racks.map(rack => {
|
||||||
const deviceCount = rack.Devices ? rack.Devices.length : 0;
|
const deviceCount = rack.Devices ? rack.Devices.length : 0;
|
||||||
const totalPower = rack.Devices ? rack.Devices.reduce((sum, d) => sum + (d.powerConsumption || 0), 0) : 0;
|
const totalPower = rack.Devices
|
||||||
|
? rack.Devices.reduce((sum, d) => sum + (d.powerConsumption || 0), 0)
|
||||||
|
: 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'机柜ID': rack.rackId,
|
机柜ID: rack.rackId,
|
||||||
'机柜名称': rack.name,
|
机柜名称: rack.name,
|
||||||
'所属机房': rack.Room ? rack.Room.name : '',
|
所属机房: rack.Room ? rack.Room.name : '',
|
||||||
'机柜高度(U)': rack.height,
|
'机柜高度(U)': rack.height,
|
||||||
'最大功耗(W)': rack.maxPower,
|
'最大功耗(W)': rack.maxPower,
|
||||||
'当前功耗(W)': rack.currentPower || 0,
|
'当前功耗(W)': rack.currentPower || 0,
|
||||||
'设备数量': deviceCount,
|
设备数量: deviceCount,
|
||||||
'设备总功耗(W)': totalPower,
|
'设备总功耗(W)': totalPower,
|
||||||
'状态': rack.status === 'active' ? '启用' : rack.status === 'maintenance' ? '维护中' : '停用',
|
状态: rack.status === 'active' ? '启用' : rack.status === 'maintenance' ? '维护中' : '停用',
|
||||||
'创建时间': rack.createdAt ? new Date(rack.createdAt).toLocaleString() : ''
|
创建时间: rack.createdAt ? new Date(rack.createdAt).toLocaleString() : '',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -236,7 +234,7 @@ router.get('/export', async (req, res) => {
|
|||||||
{ wch: 12 }, // 设备数量
|
{ wch: 12 }, // 设备数量
|
||||||
{ wch: 15 }, // 设备总功耗
|
{ wch: 15 }, // 设备总功耗
|
||||||
{ wch: 10 }, // 状态
|
{ wch: 10 }, // 状态
|
||||||
{ wch: 20 } // 创建时间
|
{ wch: 20 }, // 创建时间
|
||||||
];
|
];
|
||||||
|
|
||||||
XLSX.utils.book_append_sheet(wb, ws, '机柜列表');
|
XLSX.utils.book_append_sheet(wb, ws, '机柜列表');
|
||||||
@@ -257,8 +255,14 @@ router.get('/export', async (req, res) => {
|
|||||||
XLSX.writeFile(wb, filePath);
|
XLSX.writeFile(wb, filePath);
|
||||||
|
|
||||||
// 发送文件
|
// 发送文件
|
||||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
res.setHeader(
|
||||||
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(fileName)}`);
|
'Content-Type',
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||||
|
);
|
||||||
|
res.setHeader(
|
||||||
|
'Content-Disposition',
|
||||||
|
`attachment; filename*=UTF-8''${encodeURIComponent(fileName)}`
|
||||||
|
);
|
||||||
|
|
||||||
const fileStream = fs.createReadStream(filePath);
|
const fileStream = fs.createReadStream(filePath);
|
||||||
fileStream.pipe(res);
|
fileStream.pipe(res);
|
||||||
@@ -270,19 +274,18 @@ router.get('/export', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
fileStream.on('error', (err) => {
|
fileStream.on('error', err => {
|
||||||
console.error('文件流错误:', err);
|
console.error('文件流错误:', err);
|
||||||
if (fs.existsSync(filePath)) {
|
if (fs.existsSync(filePath)) {
|
||||||
fs.unlinkSync(filePath);
|
fs.unlinkSync(filePath);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('导出租机柜数据失败:', error);
|
console.error('导出租机柜数据失败:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '导出失败',
|
message: '导出失败',
|
||||||
error: error.message
|
error: error.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -293,9 +296,9 @@ router.get('/:rackId', async (req, res) => {
|
|||||||
const rack = await Rack.findByPk(req.params.rackId, {
|
const rack = await Rack.findByPk(req.params.rackId, {
|
||||||
include: [
|
include: [
|
||||||
{ model: Room, separate: false },
|
{ model: Room, separate: false },
|
||||||
{ model: Device, separate: false }
|
{ model: Device, separate: false },
|
||||||
],
|
],
|
||||||
subQuery: false // 避免子查询导致的性能问题
|
subQuery: false, // 避免子查询导致的性能问题
|
||||||
});
|
});
|
||||||
if (!rack) {
|
if (!rack) {
|
||||||
return res.status(404).json({ error: '机柜不存在' });
|
return res.status(404).json({ error: '机柜不存在' });
|
||||||
@@ -312,9 +315,9 @@ async function generateRackId() {
|
|||||||
const racks = await Rack.findAll({
|
const racks = await Rack.findAll({
|
||||||
where: {
|
where: {
|
||||||
rackId: {
|
rackId: {
|
||||||
[require('sequelize').Op.like]: 'RACK%'
|
[require('sequelize').Op.like]: 'RACK%',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let maxNumber = 0;
|
let maxNumber = 0;
|
||||||
@@ -354,15 +357,15 @@ router.post('/', validateBody(createRackSchema), async (req, res) => {
|
|||||||
router.put('/:rackId', validateBody(updateRackSchema), async (req, res) => {
|
router.put('/:rackId', validateBody(updateRackSchema), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const [updated] = await Rack.update(req.body, {
|
const [updated] = await Rack.update(req.body, {
|
||||||
where: { rackId: req.params.rackId }
|
where: { rackId: req.params.rackId },
|
||||||
});
|
});
|
||||||
if (updated) {
|
if (updated) {
|
||||||
const updatedRack = await Rack.findByPk(req.params.rackId, {
|
const updatedRack = await Rack.findByPk(req.params.rackId, {
|
||||||
include: [
|
include: [
|
||||||
{ model: Room, separate: false },
|
{ model: Room, separate: false },
|
||||||
{ model: Device, separate: false }
|
{ model: Device, separate: false },
|
||||||
],
|
],
|
||||||
subQuery: false // 避免子查询导致的性能问题
|
subQuery: false, // 避免子查询导致的性能问题
|
||||||
});
|
});
|
||||||
res.json(updatedRack);
|
res.json(updatedRack);
|
||||||
} else {
|
} else {
|
||||||
@@ -383,7 +386,7 @@ router.delete('/:rackId', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deleted = await Rack.destroy({
|
const deleted = await Rack.destroy({
|
||||||
where: { rackId: req.params.rackId }
|
where: { rackId: req.params.rackId },
|
||||||
});
|
});
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
res.status(204).json();
|
res.status(204).json();
|
||||||
@@ -406,7 +409,7 @@ router.post('/import', async (req, res) => {
|
|||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '没有上传文件',
|
message: '没有上传文件',
|
||||||
error: '没有找到有效的上传文件,请选择一个Excel文件后重试'
|
error: '没有找到有效的上传文件,请选择一个Excel文件后重试',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,7 +430,7 @@ router.post('/import', async (req, res) => {
|
|||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '文件保存失败',
|
message: '文件保存失败',
|
||||||
error: `无法保存上传的文件: ${saveError.message}`
|
error: `无法保存上传的文件: ${saveError.message}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,7 +444,7 @@ router.post('/import', async (req, res) => {
|
|||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '文件解析失败',
|
message: '文件解析失败',
|
||||||
error: `无法解析Excel文件: ${readError.message}`
|
error: `无法解析Excel文件: ${readError.message}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,14 +454,15 @@ router.post('/import', async (req, res) => {
|
|||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '文件格式错误',
|
message: '文件格式错误',
|
||||||
error: 'Excel文件中没有找到工作表'
|
error: 'Excel文件中没有找到工作表',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
|
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
|
||||||
|
|
||||||
// 读取第一行作为列头
|
// 读取第一行作为列头
|
||||||
const headerRow = XLSX.utils.sheet_to_json(worksheet, { header: 1, range: 0, limit: 1 })[0] || [];
|
const headerRow =
|
||||||
|
XLSX.utils.sheet_to_json(worksheet, { header: 1, range: 0, limit: 1 })[0] || [];
|
||||||
|
|
||||||
// 定义列名映射(支持导入模板格式和导出文件格式)
|
// 定义列名映射(支持导入模板格式和导出文件格式)
|
||||||
const columnMapping = {
|
const columnMapping = {
|
||||||
@@ -467,7 +471,7 @@ router.post('/import', async (req, res) => {
|
|||||||
roomName: ['所属机房名称', '所属机房'],
|
roomName: ['所属机房名称', '所属机房'],
|
||||||
height: ['高度(U)', '机柜高度(U)'],
|
height: ['高度(U)', '机柜高度(U)'],
|
||||||
maxPower: ['最大功率(W)', '最大功耗(W)'],
|
maxPower: ['最大功率(W)', '最大功耗(W)'],
|
||||||
status: ['状态']
|
status: ['状态'],
|
||||||
};
|
};
|
||||||
|
|
||||||
// 根据列头自动检测列索引映射
|
// 根据列头自动检测列索引映射
|
||||||
@@ -488,7 +492,7 @@ router.post('/import', async (req, res) => {
|
|||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Excel列名格式不正确',
|
message: 'Excel列名格式不正确',
|
||||||
error: `缺少必需的列: ${missingColumns.join(', ')},请使用系统导出的文件或下载导入模板`
|
error: `缺少必需的列: ${missingColumns.join(', ')},请使用系统导出的文件或下载导入模板`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,7 +500,7 @@ router.post('/import', async (req, res) => {
|
|||||||
const rawData = XLSX.utils.sheet_to_json(worksheet, {
|
const rawData = XLSX.utils.sheet_to_json(worksheet, {
|
||||||
header: headerRow.map((h, i) => `col_${i}`),
|
header: headerRow.map((h, i) => `col_${i}`),
|
||||||
range: 1,
|
range: 1,
|
||||||
blankrows: false
|
blankrows: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const jsonData = rawData.map(row => {
|
const jsonData = rawData.map(row => {
|
||||||
@@ -513,15 +517,20 @@ router.post('/import', async (req, res) => {
|
|||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '没有找到有效数据',
|
message: '没有找到有效数据',
|
||||||
error: 'Excel文件中没有找到可导入的数据行'
|
error: 'Excel文件中没有找到可导入的数据行',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 状态值转换映射
|
// 状态值转换映射
|
||||||
const statusMapping = {
|
const statusMapping = {
|
||||||
'启用': 'active', '在用': 'active', '停用': 'inactive',
|
启用: 'active',
|
||||||
'禁用': 'inactive', '维护中': 'maintenance',
|
在用: 'active',
|
||||||
'active': 'active', 'inactive': 'inactive', 'maintenance': 'maintenance'
|
停用: 'inactive',
|
||||||
|
禁用: 'inactive',
|
||||||
|
维护中: 'maintenance',
|
||||||
|
active: 'active',
|
||||||
|
inactive: 'inactive',
|
||||||
|
maintenance: 'maintenance',
|
||||||
};
|
};
|
||||||
|
|
||||||
const validStatuses = ['active', 'maintenance', 'inactive'];
|
const validStatuses = ['active', 'maintenance', 'inactive'];
|
||||||
@@ -534,13 +543,21 @@ router.post('/import', async (req, res) => {
|
|||||||
|
|
||||||
// 【优化2】批量查询现有最大机柜ID(单次查询)
|
// 【优化2】批量查询现有最大机柜ID(单次查询)
|
||||||
const maxRackResult = await Rack.findOne({
|
const maxRackResult = await Rack.findOne({
|
||||||
attributes: [[sequelize.fn('MAX', sequelize.cast(sequelize.fn('SUBSTR', sequelize.col('rackId'), 5), 'INTEGER')), 'maxNum']],
|
attributes: [
|
||||||
|
[
|
||||||
|
sequelize.fn(
|
||||||
|
'MAX',
|
||||||
|
sequelize.cast(sequelize.fn('SUBSTR', sequelize.col('rackId'), 5), 'INTEGER')
|
||||||
|
),
|
||||||
|
'maxNum',
|
||||||
|
],
|
||||||
|
],
|
||||||
where: {
|
where: {
|
||||||
rackId: {
|
rackId: {
|
||||||
[require('sequelize').Op.like]: 'RACK%'
|
[require('sequelize').Op.like]: 'RACK%',
|
||||||
}
|
|
||||||
},
|
},
|
||||||
transaction: t
|
},
|
||||||
|
transaction: t,
|
||||||
});
|
});
|
||||||
let maxNumber = maxRackResult?.get('maxNum') || 0;
|
let maxNumber = maxRackResult?.get('maxNum') || 0;
|
||||||
|
|
||||||
@@ -557,7 +574,7 @@ router.post('/import', async (req, res) => {
|
|||||||
...item,
|
...item,
|
||||||
rackId: `RACK${String(maxNumber).padStart(3, '0')}`,
|
rackId: `RACK${String(maxNumber).padStart(3, '0')}`,
|
||||||
status: normalizedStatus,
|
status: normalizedStatus,
|
||||||
rowNumber
|
rowNumber,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,12 +582,12 @@ router.post('/import', async (req, res) => {
|
|||||||
...item,
|
...item,
|
||||||
rackId: rawRackId,
|
rackId: rawRackId,
|
||||||
status: normalizedStatus,
|
status: normalizedStatus,
|
||||||
rowNumber
|
rowNumber,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// 验证数据
|
// 验证数据
|
||||||
processedData.forEach((item) => {
|
processedData.forEach(item => {
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
|
||||||
if (!/^[a-zA-Z0-9_-]+$/.test(item.rackId)) {
|
if (!/^[a-zA-Z0-9_-]+$/.test(item.rackId)) {
|
||||||
@@ -606,16 +623,16 @@ router.post('/import', async (req, res) => {
|
|||||||
success: false,
|
success: false,
|
||||||
message: '数据验证失败',
|
message: '数据验证失败',
|
||||||
error: `${validationResults.length} 行数据格式错误`,
|
error: `${validationResults.length} 行数据格式错误`,
|
||||||
details: validationResults
|
details: validationResults,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 【优化3】批量查询已存在的机柜ID(单次查询)
|
// 【优化3】批量查询已存在的机柜ID(单次查询)
|
||||||
const existingRacks = await Rack.findAll({
|
const existingRacks = await Rack.findAll({
|
||||||
where: {
|
where: {
|
||||||
rackId: processedData.map(item => item.rackId)
|
rackId: processedData.map(item => item.rackId),
|
||||||
},
|
},
|
||||||
transaction: t
|
transaction: t,
|
||||||
});
|
});
|
||||||
|
|
||||||
const existingIds = new Set(existingRacks.map(rack => rack.rackId));
|
const existingIds = new Set(existingRacks.map(rack => rack.rackId));
|
||||||
@@ -632,12 +649,12 @@ router.post('/import', async (req, res) => {
|
|||||||
maxPower: item.maxPower,
|
maxPower: item.maxPower,
|
||||||
status: item.status,
|
status: item.status,
|
||||||
roomId: roomNameToIdMap.get(item.roomName.trim()),
|
roomId: roomNameToIdMap.get(item.roomName.trim()),
|
||||||
currentPower: 0
|
currentPower: 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const result = await Rack.bulkCreate(dataWithRoomId, {
|
const result = await Rack.bulkCreate(dataWithRoomId, {
|
||||||
transaction: t,
|
transaction: t,
|
||||||
ignoreDuplicates: true
|
ignoreDuplicates: true,
|
||||||
});
|
});
|
||||||
createdCount = result.length;
|
createdCount = result.length;
|
||||||
}
|
}
|
||||||
@@ -655,7 +672,7 @@ router.post('/import', async (req, res) => {
|
|||||||
duplicates: duplicateCount,
|
duplicates: duplicateCount,
|
||||||
total: jsonData.length,
|
total: jsonData.length,
|
||||||
createdRacks,
|
createdRacks,
|
||||||
skippedRacks
|
skippedRacks,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
// 删除临时文件
|
// 删除临时文件
|
||||||
@@ -668,7 +685,7 @@ router.post('/import', async (req, res) => {
|
|||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '服务器内部错误',
|
message: '服务器内部错误',
|
||||||
error: `导入过程中发生未知错误: ${error.message}`
|
error: `导入过程中发生未知错误: ${error.message}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+93
-51
@@ -32,7 +32,10 @@ router.get('/', authMiddleware, async (req, res) => {
|
|||||||
where,
|
where,
|
||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
order: [['sort', 'ASC'], ['createdAt', 'DESC']]
|
order: [
|
||||||
|
['sort', 'ASC'],
|
||||||
|
['createdAt', 'DESC'],
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -41,14 +44,14 @@ router.get('/', authMiddleware, async (req, res) => {
|
|||||||
total: count,
|
total: count,
|
||||||
page: parseInt(page),
|
page: parseInt(page),
|
||||||
pageSize: parseInt(pageSize),
|
pageSize: parseInt(pageSize),
|
||||||
roles
|
roles,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取角色列表错误:', error);
|
console.error('获取角色列表错误:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '获取角色列表失败'
|
message: '获取角色列表失败',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -57,18 +60,18 @@ router.get('/all', authMiddleware, async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const roles = await Role.findAll({
|
const roles = await Role.findAll({
|
||||||
where: { status: 'active' },
|
where: { status: 'active' },
|
||||||
order: [['sort', 'ASC']]
|
order: [['sort', 'ASC']],
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: roles
|
data: roles,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取所有角色错误:', error);
|
console.error('获取所有角色错误:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '获取角色列表失败'
|
message: '获取角色列表失败',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -80,13 +83,13 @@ router.get('/:roleId', authMiddleware, async (req, res) => {
|
|||||||
if (!role) {
|
if (!role) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '角色不存在'
|
message: '角色不存在',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const permissions = await Permission.findAll({
|
const permissions = await Permission.findAll({
|
||||||
where: { status: 'active' },
|
where: { status: 'active' },
|
||||||
order: [['sort', 'ASC']]
|
order: [['sort', 'ASC']],
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -94,14 +97,14 @@ router.get('/:roleId', authMiddleware, async (req, res) => {
|
|||||||
data: {
|
data: {
|
||||||
role,
|
role,
|
||||||
permissions,
|
permissions,
|
||||||
rolePermissions: role.permissions || []
|
rolePermissions: role.permissions || [],
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取角色详情错误:', error);
|
console.error('获取角色详情错误:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '获取角色详情失败'
|
message: '获取角色详情失败',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -113,7 +116,7 @@ router.post('/', authMiddleware, async (req, res) => {
|
|||||||
if (!roleName || !roleCode) {
|
if (!roleName || !roleCode) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '角色名称和角色编码不能为空'
|
message: '角色名称和角色编码不能为空',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +124,7 @@ router.post('/', authMiddleware, async (req, res) => {
|
|||||||
if (existingRole) {
|
if (existingRole) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '角色编码已存在'
|
message: '角色编码已存在',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,31 +135,33 @@ router.post('/', authMiddleware, async (req, res) => {
|
|||||||
description,
|
description,
|
||||||
permissions: permissions || [],
|
permissions: permissions || [],
|
||||||
status: status || 'active',
|
status: status || 'active',
|
||||||
sort: sort || 0
|
sort: sort || 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const permissionNames = permissions && permissions.length > 0
|
const permissionNames = permissions && permissions.length > 0 ? permissions.join('、') : '无';
|
||||||
? permissions.join('、')
|
|
||||||
: '无';
|
|
||||||
|
|
||||||
await logRoleOperation('create', `创建角色【${roleName}】(编码:${roleCode},权限:${permissionNames})`, {
|
await logRoleOperation(
|
||||||
|
'create',
|
||||||
|
`创建角色【${roleName}】(编码:${roleCode},权限:${permissionNames})`,
|
||||||
|
{
|
||||||
targetId: role.roleId,
|
targetId: role.roleId,
|
||||||
targetName: roleName,
|
targetName: roleName,
|
||||||
afterState: role.toJSON(),
|
afterState: role.toJSON(),
|
||||||
req,
|
req,
|
||||||
metadata: { roleCode, permissions, permissionNames }
|
metadata: { roleCode, permissions, permissionNames },
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
success: true,
|
success: true,
|
||||||
message: '创建成功',
|
message: '创建成功',
|
||||||
data: role
|
data: role,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('创建角色错误:', error);
|
console.error('创建角色错误:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '创建角色失败'
|
message: '创建角色失败',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -169,17 +174,27 @@ router.put('/:roleId', authMiddleware, async (req, res) => {
|
|||||||
if (!role) {
|
if (!role) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '角色不存在'
|
message: '角色不存在',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const beforeState = role.toJSON();
|
const beforeState = role.toJSON();
|
||||||
|
|
||||||
if (roleName !== undefined) role.roleName = roleName;
|
if (roleName !== undefined) {
|
||||||
if (description !== undefined) role.description = description;
|
role.roleName = roleName;
|
||||||
if (permissions !== undefined) role.permissions = permissions;
|
}
|
||||||
if (status !== undefined) role.status = status;
|
if (description !== undefined) {
|
||||||
if (sort !== undefined) role.sort = sort;
|
role.description = description;
|
||||||
|
}
|
||||||
|
if (permissions !== undefined) {
|
||||||
|
role.permissions = permissions;
|
||||||
|
}
|
||||||
|
if (status !== undefined) {
|
||||||
|
role.status = status;
|
||||||
|
}
|
||||||
|
if (sort !== undefined) {
|
||||||
|
role.sort = sort;
|
||||||
|
}
|
||||||
|
|
||||||
await role.save();
|
await role.save();
|
||||||
|
|
||||||
@@ -201,11 +216,22 @@ router.put('/:roleId', authMiddleware, async (req, res) => {
|
|||||||
}
|
}
|
||||||
if (status !== undefined && beforeState.status !== status) {
|
if (status !== undefined && beforeState.status !== status) {
|
||||||
const statusText = { active: '启用', inactive: '禁用' };
|
const statusText = { active: '启用', inactive: '禁用' };
|
||||||
changedFields.status = { from: beforeState.status, to: status, fromText: statusText[beforeState.status], toText: statusText[status] };
|
changedFields.status = {
|
||||||
|
from: beforeState.status,
|
||||||
|
to: status,
|
||||||
|
fromText: statusText[beforeState.status],
|
||||||
|
toText: statusText[status],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const changeDetails = Object.entries(changedFields).map(([field, values]) => {
|
const changeDetails = Object.entries(changedFields)
|
||||||
const fieldNames = { roleName: '角色名称', description: '描述', permissions: '权限', status: '状态' };
|
.map(([field, values]) => {
|
||||||
|
const fieldNames = {
|
||||||
|
roleName: '角色名称',
|
||||||
|
description: '描述',
|
||||||
|
permissions: '权限',
|
||||||
|
status: '状态',
|
||||||
|
};
|
||||||
const displayName = fieldNames[field] || field;
|
const displayName = fieldNames[field] || field;
|
||||||
|
|
||||||
if (field === 'permissions') {
|
if (field === 'permissions') {
|
||||||
@@ -215,7 +241,8 @@ router.put('/:roleId', authMiddleware, async (req, res) => {
|
|||||||
return `状态: ${values.fromText} → ${values.toText}`;
|
return `状态: ${values.fromText} → ${values.toText}`;
|
||||||
}
|
}
|
||||||
return `${displayName}: ${values.from ?? '空'} → ${values.to ?? '空'}`;
|
return `${displayName}: ${values.from ?? '空'} → ${values.to ?? '空'}`;
|
||||||
}).join(';');
|
})
|
||||||
|
.join(';');
|
||||||
|
|
||||||
const updateDesc = changeDetails
|
const updateDesc = changeDetails
|
||||||
? `更新角色【${role.roleName}】:${changeDetails}`
|
? `更新角色【${role.roleName}】:${changeDetails}`
|
||||||
@@ -227,19 +254,23 @@ router.put('/:roleId', authMiddleware, async (req, res) => {
|
|||||||
beforeState,
|
beforeState,
|
||||||
afterState,
|
afterState,
|
||||||
req,
|
req,
|
||||||
metadata: { changedFields, oldRoleName: beforeState.roleName, oldPermissions: beforeState.permissions }
|
metadata: {
|
||||||
|
changedFields,
|
||||||
|
oldRoleName: beforeState.roleName,
|
||||||
|
oldPermissions: beforeState.permissions,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: '更新成功',
|
message: '更新成功',
|
||||||
data: role
|
data: role,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('更新角色错误:', error);
|
console.error('更新角色错误:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '更新角色失败'
|
message: '更新角色失败',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -251,14 +282,14 @@ router.delete('/:roleId', authMiddleware, async (req, res) => {
|
|||||||
if (!role) {
|
if (!role) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '角色不存在'
|
message: '角色不存在',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role.roleCode === 'admin') {
|
if (role.roleCode === 'admin') {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '不能删除管理员角色'
|
message: '不能删除管理员角色',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,7 +297,7 @@ router.delete('/:roleId', authMiddleware, async (req, res) => {
|
|||||||
if (userCount > 0) {
|
if (userCount > 0) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '该角色下有用户,不能删除'
|
message: '该角色下有用户,不能删除',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,23 +307,27 @@ router.delete('/:roleId', authMiddleware, async (req, res) => {
|
|||||||
|
|
||||||
await role.destroy();
|
await role.destroy();
|
||||||
|
|
||||||
await logRoleOperation('delete', `删除角色【${roleName}】(编码:${roleCode},权限:${(role.permissions || []).join('、') || '无'})`, {
|
await logRoleOperation(
|
||||||
|
'delete',
|
||||||
|
`删除角色【${roleName}】(编码:${roleCode},权限:${(role.permissions || []).join('、') || '无'})`,
|
||||||
|
{
|
||||||
targetId: req.params.roleId,
|
targetId: req.params.roleId,
|
||||||
targetName: roleName,
|
targetName: roleName,
|
||||||
beforeState,
|
beforeState,
|
||||||
req,
|
req,
|
||||||
metadata: { roleCode, userCount, permissions: role.permissions }
|
metadata: { roleCode, userCount, permissions: role.permissions },
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: '删除成功'
|
message: '删除成功',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('删除角色错误:', error);
|
console.error('删除角色错误:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '删除角色失败'
|
message: '删除角色失败',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -307,16 +342,23 @@ router.post('/init-roles', async (req, res) => {
|
|||||||
description: '系统管理员,拥有所有权限',
|
description: '系统管理员,拥有所有权限',
|
||||||
permissions: ['*'],
|
permissions: ['*'],
|
||||||
status: 'active',
|
status: 'active',
|
||||||
sort: 1
|
sort: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
roleId: 'role_operator',
|
roleId: 'role_operator',
|
||||||
roleName: '运维人员',
|
roleName: '运维人员',
|
||||||
roleCode: 'operator',
|
roleCode: 'operator',
|
||||||
description: '负责日常运维操作',
|
description: '负责日常运维操作',
|
||||||
permissions: ['devices:read', 'devices:write', 'racks:read', 'rooms:read', 'consumables:read', 'consumables:write'],
|
permissions: [
|
||||||
|
'devices:read',
|
||||||
|
'devices:write',
|
||||||
|
'racks:read',
|
||||||
|
'rooms:read',
|
||||||
|
'consumables:read',
|
||||||
|
'consumables:write',
|
||||||
|
],
|
||||||
status: 'active',
|
status: 'active',
|
||||||
sort: 2
|
sort: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
roleId: 'role_viewer',
|
roleId: 'role_viewer',
|
||||||
@@ -325,8 +367,8 @@ router.post('/init-roles', async (req, res) => {
|
|||||||
description: '仅能查看数据',
|
description: '仅能查看数据',
|
||||||
permissions: ['devices:read', 'racks:read', 'rooms:read', 'consumables:read'],
|
permissions: ['devices:read', 'racks:read', 'rooms:read', 'consumables:read'],
|
||||||
status: 'active',
|
status: 'active',
|
||||||
sort: 3
|
sort: 3,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const roleData of defaultRoles) {
|
for (const roleData of defaultRoles) {
|
||||||
@@ -335,13 +377,13 @@ router.post('/init-roles', async (req, res) => {
|
|||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: '初始化角色成功'
|
message: '初始化角色成功',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('初始化角色错误:', error);
|
console.error('初始化角色错误:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '初始化角色失败'
|
message: '初始化角色失败',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ router.get('/', async (req, res) => {
|
|||||||
const { count, rows } = await Room.findAndCountAll({
|
const { count, rows } = await Room.findAndCountAll({
|
||||||
include: [{ model: Rack, attributes: ['rackId', 'name'] }],
|
include: [{ model: Rack, attributes: ['rackId', 'name'] }],
|
||||||
offset: offset,
|
offset: offset,
|
||||||
limit: pageSize
|
limit: pageSize,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
rooms: rows,
|
rooms: rows,
|
||||||
total: count
|
total: count,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -31,7 +31,7 @@ router.get('/', async (req, res) => {
|
|||||||
router.get('/:roomId', async (req, res) => {
|
router.get('/:roomId', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const room = await Room.findByPk(req.params.roomId, {
|
const room = await Room.findByPk(req.params.roomId, {
|
||||||
include: Rack
|
include: Rack,
|
||||||
});
|
});
|
||||||
if (!room) {
|
if (!room) {
|
||||||
return res.status(404).json({ error: '机房不存在' });
|
return res.status(404).json({ error: '机房不存在' });
|
||||||
@@ -56,7 +56,7 @@ router.post('/', async (req, res) => {
|
|||||||
router.put('/:roomId', validateBody(updateRoomSchema), async (req, res) => {
|
router.put('/:roomId', validateBody(updateRoomSchema), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const [updated] = await Room.update(req.body, {
|
const [updated] = await Room.update(req.body, {
|
||||||
where: { roomId: req.params.roomId }
|
where: { roomId: req.params.roomId },
|
||||||
});
|
});
|
||||||
if (updated) {
|
if (updated) {
|
||||||
const updatedRoom = await Room.findByPk(req.params.roomId);
|
const updatedRoom = await Room.findByPk(req.params.roomId);
|
||||||
@@ -79,7 +79,7 @@ router.delete('/:roomId', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deleted = await Room.destroy({
|
const deleted = await Room.destroy({
|
||||||
where: { roomId: req.params.roomId }
|
where: { roomId: req.params.roomId },
|
||||||
});
|
});
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
res.status(204).json();
|
res.status(204).json();
|
||||||
|
|||||||
@@ -32,26 +32,37 @@ router.get('/', async (req, res) => {
|
|||||||
where: {
|
where: {
|
||||||
createdAt: {
|
createdAt: {
|
||||||
[Op.gte]: dayStart,
|
[Op.gte]: dayStart,
|
||||||
[Op.lt]: dayEnd
|
[Op.lt]: dayEnd,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const dayResults = await Promise.all(dayQueries.map(d => d.query));
|
const dayResults = await Promise.all(dayQueries.map(d => d.query));
|
||||||
const deviceTrendData = dayQueries.map((d, index) => ({
|
const deviceTrendData = dayQueries.map((d, index) => ({
|
||||||
label: d.label,
|
label: d.label,
|
||||||
value: dayResults[index]
|
value: dayResults[index],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const [
|
const [
|
||||||
totalDevices, faultDevices, totalRacks, rooms, totalUsers, activeTickets,
|
totalDevices,
|
||||||
newDevicesThisWeek, newDevicesLastWeek,
|
faultDevices,
|
||||||
faultDevicesThisWeek, faultDevicesLastWeek,
|
totalRacks,
|
||||||
newUsersThisWeek, newUsersLastWeek,
|
rooms,
|
||||||
newTicketsThisWeek, newTicketsLastWeek,
|
totalUsers,
|
||||||
runningDevices, maintenanceDevices, offlineDevices
|
activeTickets,
|
||||||
|
newDevicesThisWeek,
|
||||||
|
newDevicesLastWeek,
|
||||||
|
faultDevicesThisWeek,
|
||||||
|
faultDevicesLastWeek,
|
||||||
|
newUsersThisWeek,
|
||||||
|
newUsersLastWeek,
|
||||||
|
newTicketsThisWeek,
|
||||||
|
newTicketsLastWeek,
|
||||||
|
runningDevices,
|
||||||
|
maintenanceDevices,
|
||||||
|
offlineDevices,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
Device.count(),
|
Device.count(),
|
||||||
Device.count({ where: { status: 'fault' } }),
|
Device.count({ where: { status: 'fault' } }),
|
||||||
@@ -62,7 +73,9 @@ router.get('/', async (req, res) => {
|
|||||||
Device.count({ where: { createdAt: { [Op.gte]: oneWeekAgo } } }),
|
Device.count({ where: { createdAt: { [Op.gte]: oneWeekAgo } } }),
|
||||||
Device.count({ where: { createdAt: { [Op.gte]: twoWeeksAgo, [Op.lt]: oneWeekAgo } } }),
|
Device.count({ where: { createdAt: { [Op.gte]: twoWeeksAgo, [Op.lt]: oneWeekAgo } } }),
|
||||||
Device.count({ where: { status: 'fault', updatedAt: { [Op.gte]: oneWeekAgo } } }),
|
Device.count({ where: { status: 'fault', updatedAt: { [Op.gte]: oneWeekAgo } } }),
|
||||||
Device.count({ where: { status: 'fault', updatedAt: { [Op.gte]: twoWeeksAgo, [Op.lt]: oneWeekAgo } } }),
|
Device.count({
|
||||||
|
where: { status: 'fault', updatedAt: { [Op.gte]: twoWeeksAgo, [Op.lt]: oneWeekAgo } },
|
||||||
|
}),
|
||||||
User.count({ where: { createdAt: { [Op.gte]: oneWeekAgo } } }),
|
User.count({ where: { createdAt: { [Op.gte]: oneWeekAgo } } }),
|
||||||
User.count({ where: { createdAt: { [Op.gte]: twoWeeksAgo, [Op.lt]: oneWeekAgo } } }),
|
User.count({ where: { createdAt: { [Op.gte]: twoWeeksAgo, [Op.lt]: oneWeekAgo } } }),
|
||||||
Ticket.count({ where: { createdAt: { [Op.gte]: oneWeekAgo } } }),
|
Ticket.count({ where: { createdAt: { [Op.gte]: oneWeekAgo } } }),
|
||||||
@@ -74,7 +87,9 @@ router.get('/', async (req, res) => {
|
|||||||
|
|
||||||
let deviceGrowth = 0;
|
let deviceGrowth = 0;
|
||||||
if (newDevicesLastWeek > 0) {
|
if (newDevicesLastWeek > 0) {
|
||||||
deviceGrowth = parseFloat(((newDevicesThisWeek - newDevicesLastWeek) / newDevicesLastWeek * 100).toFixed(1));
|
deviceGrowth = parseFloat(
|
||||||
|
(((newDevicesThisWeek - newDevicesLastWeek) / newDevicesLastWeek) * 100).toFixed(1)
|
||||||
|
);
|
||||||
} else if (newDevicesThisWeek > 0) {
|
} else if (newDevicesThisWeek > 0) {
|
||||||
deviceGrowth = 100;
|
deviceGrowth = 100;
|
||||||
} else {
|
} else {
|
||||||
@@ -83,7 +98,9 @@ router.get('/', async (req, res) => {
|
|||||||
|
|
||||||
let faultTrend = 0;
|
let faultTrend = 0;
|
||||||
if (faultDevicesLastWeek > 0) {
|
if (faultDevicesLastWeek > 0) {
|
||||||
faultTrend = parseFloat(((faultDevicesThisWeek - faultDevicesLastWeek) / faultDevicesLastWeek * 100).toFixed(1));
|
faultTrend = parseFloat(
|
||||||
|
(((faultDevicesThisWeek - faultDevicesLastWeek) / faultDevicesLastWeek) * 100).toFixed(1)
|
||||||
|
);
|
||||||
} else if (faultDevicesThisWeek > 0) {
|
} else if (faultDevicesThisWeek > 0) {
|
||||||
faultTrend = 100;
|
faultTrend = 100;
|
||||||
} else {
|
} else {
|
||||||
@@ -92,7 +109,9 @@ router.get('/', async (req, res) => {
|
|||||||
|
|
||||||
let userGrowth = 0;
|
let userGrowth = 0;
|
||||||
if (newUsersLastWeek > 0) {
|
if (newUsersLastWeek > 0) {
|
||||||
userGrowth = parseFloat(((newUsersThisWeek - newUsersLastWeek) / newUsersLastWeek * 100).toFixed(1));
|
userGrowth = parseFloat(
|
||||||
|
(((newUsersThisWeek - newUsersLastWeek) / newUsersLastWeek) * 100).toFixed(1)
|
||||||
|
);
|
||||||
} else if (newUsersThisWeek > 0) {
|
} else if (newUsersThisWeek > 0) {
|
||||||
userGrowth = 100;
|
userGrowth = 100;
|
||||||
} else {
|
} else {
|
||||||
@@ -101,16 +120,17 @@ router.get('/', async (req, res) => {
|
|||||||
|
|
||||||
let ticketTrend = 0;
|
let ticketTrend = 0;
|
||||||
if (newTicketsLastWeek > 0) {
|
if (newTicketsLastWeek > 0) {
|
||||||
ticketTrend = parseFloat(((newTicketsThisWeek - newTicketsLastWeek) / newTicketsLastWeek * 100).toFixed(1));
|
ticketTrend = parseFloat(
|
||||||
|
(((newTicketsThisWeek - newTicketsLastWeek) / newTicketsLastWeek) * 100).toFixed(1)
|
||||||
|
);
|
||||||
} else if (newTicketsThisWeek > 0) {
|
} else if (newTicketsThisWeek > 0) {
|
||||||
ticketTrend = 100;
|
ticketTrend = 100;
|
||||||
} else {
|
} else {
|
||||||
ticketTrend = 0;
|
ticketTrend = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onlineRate = totalDevices > 0
|
const onlineRate =
|
||||||
? (((totalDevices - faultDevices) / totalDevices) * 100).toFixed(1)
|
totalDevices > 0 ? (((totalDevices - faultDevices) / totalDevices) * 100).toFixed(1) : 100;
|
||||||
: 100;
|
|
||||||
|
|
||||||
const totalRooms = rooms.length;
|
const totalRooms = rooms.length;
|
||||||
|
|
||||||
|
|||||||
@@ -10,33 +10,194 @@ const { FRONTEND } = require('../config');
|
|||||||
const initDefaultSettings = async () => {
|
const initDefaultSettings = async () => {
|
||||||
const defaultSettings = [
|
const defaultSettings = [
|
||||||
// 全局配置
|
// 全局配置
|
||||||
{ settingKey: 'site_name', settingValue: JSON.stringify('机柜管理系统'), settingType: 'string', category: 'general', description: '网站名称', isEditable: true },
|
{
|
||||||
{ settingKey: 'site_logo', settingValue: JSON.stringify(''), settingType: 'string', category: 'general', description: '网站Logo URL', isEditable: true },
|
settingKey: 'site_name',
|
||||||
{ settingKey: 'timezone', settingValue: JSON.stringify('Asia/Shanghai'), settingType: 'string', category: 'general', description: '时区设置', isEditable: true },
|
settingValue: JSON.stringify('机柜管理系统'),
|
||||||
{ settingKey: 'date_format', settingValue: JSON.stringify('YYYY-MM-DD'), settingType: 'string', category: 'general', description: '日期格式', isEditable: true },
|
settingType: 'string',
|
||||||
{ settingKey: 'session_timeout', settingValue: JSON.stringify(30), settingType: 'number', category: 'general', description: '登录有效期(分钟)', isEditable: true },
|
category: 'general',
|
||||||
{ settingKey: 'idle_timeout', settingValue: JSON.stringify(30), settingType: 'number', category: 'general', description: '用户空闲超时时间(分钟)', isEditable: true },
|
description: '网站名称',
|
||||||
{ settingKey: 'idle_warning_time', settingValue: JSON.stringify(60), settingType: 'number', category: 'general', description: '空闲超时前警告时间(秒)', isEditable: false },
|
isEditable: true,
|
||||||
{ settingKey: 'max_login_attempts', settingValue: JSON.stringify(5), settingType: 'number', category: 'general', description: '最大登录尝试次数', isEditable: true },
|
},
|
||||||
{ settingKey: 'maintenance_mode', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'general', description: '维护模式', isEditable: true },
|
{
|
||||||
|
settingKey: 'site_logo',
|
||||||
|
settingValue: JSON.stringify(''),
|
||||||
|
settingType: 'string',
|
||||||
|
category: 'general',
|
||||||
|
description: '网站Logo URL',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'timezone',
|
||||||
|
settingValue: JSON.stringify('Asia/Shanghai'),
|
||||||
|
settingType: 'string',
|
||||||
|
category: 'general',
|
||||||
|
description: '时区设置',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'date_format',
|
||||||
|
settingValue: JSON.stringify('YYYY-MM-DD'),
|
||||||
|
settingType: 'string',
|
||||||
|
category: 'general',
|
||||||
|
description: '日期格式',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'session_timeout',
|
||||||
|
settingValue: JSON.stringify(30),
|
||||||
|
settingType: 'number',
|
||||||
|
category: 'general',
|
||||||
|
description: '登录有效期(分钟)',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'idle_timeout',
|
||||||
|
settingValue: JSON.stringify(30),
|
||||||
|
settingType: 'number',
|
||||||
|
category: 'general',
|
||||||
|
description: '用户空闲超时时间(分钟)',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'idle_warning_time',
|
||||||
|
settingValue: JSON.stringify(60),
|
||||||
|
settingType: 'number',
|
||||||
|
category: 'general',
|
||||||
|
description: '空闲超时前警告时间(秒)',
|
||||||
|
isEditable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'max_login_attempts',
|
||||||
|
settingValue: JSON.stringify(5),
|
||||||
|
settingType: 'number',
|
||||||
|
category: 'general',
|
||||||
|
description: '最大登录尝试次数',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'maintenance_mode',
|
||||||
|
settingValue: JSON.stringify(false),
|
||||||
|
settingType: 'boolean',
|
||||||
|
category: 'general',
|
||||||
|
description: '维护模式',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
|
||||||
// 外观设置
|
// 外观设置
|
||||||
{ settingKey: 'primary_color', settingValue: JSON.stringify('#667eea'), settingType: 'string', category: 'appearance', description: '主题主色调', isEditable: true },
|
{
|
||||||
{ settingKey: 'secondary_color', settingValue: JSON.stringify('#764ba2'), settingType: 'string', category: 'appearance', description: '主题辅助色调', isEditable: true },
|
settingKey: 'primary_color',
|
||||||
{ settingKey: 'compact_mode', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'appearance', description: '紧凑模式', isEditable: true },
|
settingValue: JSON.stringify('#667eea'),
|
||||||
{ settingKey: 'sidebar_collapsed', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'appearance', description: '侧边栏默认折叠', isEditable: true },
|
settingType: 'string',
|
||||||
{ settingKey: 'table_row_height', settingValue: JSON.stringify('default'), settingType: 'string', category: 'appearance', description: '表格行高: small/default/middle/large', isEditable: true },
|
category: 'appearance',
|
||||||
{ settingKey: 'animation_enabled', settingValue: JSON.stringify(true), settingType: 'boolean', category: 'appearance', description: '启用动画效果', isEditable: true },
|
description: '主题主色调',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'secondary_color',
|
||||||
|
settingValue: JSON.stringify('#764ba2'),
|
||||||
|
settingType: 'string',
|
||||||
|
category: 'appearance',
|
||||||
|
description: '主题辅助色调',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'compact_mode',
|
||||||
|
settingValue: JSON.stringify(false),
|
||||||
|
settingType: 'boolean',
|
||||||
|
category: 'appearance',
|
||||||
|
description: '紧凑模式',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'sidebar_collapsed',
|
||||||
|
settingValue: JSON.stringify(false),
|
||||||
|
settingType: 'boolean',
|
||||||
|
category: 'appearance',
|
||||||
|
description: '侧边栏默认折叠',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'table_row_height',
|
||||||
|
settingValue: JSON.stringify('default'),
|
||||||
|
settingType: 'string',
|
||||||
|
category: 'appearance',
|
||||||
|
description: '表格行高: small/default/middle/large',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'animation_enabled',
|
||||||
|
settingValue: JSON.stringify(true),
|
||||||
|
settingType: 'boolean',
|
||||||
|
category: 'appearance',
|
||||||
|
description: '启用动画效果',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
|
||||||
// 关于页面
|
// 关于页面
|
||||||
{ settingKey: 'app_version', settingValue: JSON.stringify('1.0.0'), settingType: 'string', category: 'about', description: '应用版本', isEditable: false },
|
{
|
||||||
{ settingKey: 'company_name', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '公司/组织名称', isEditable: true },
|
settingKey: 'app_version',
|
||||||
{ settingKey: 'contact_email', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '联系邮箱', isEditable: true },
|
settingValue: JSON.stringify('1.0.0'),
|
||||||
{ settingKey: 'contact_phone', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '联系电话', isEditable: true },
|
settingType: 'string',
|
||||||
{ settingKey: 'company_address', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '公司地址', isEditable: true },
|
category: 'about',
|
||||||
{ settingKey: 'system_description', settingValue: JSON.stringify('机柜管理系统 - 专业的数据中心设备管理解决方案'), settingType: 'string', category: 'about', description: '系统描述', isEditable: true },
|
description: '应用版本',
|
||||||
{ settingKey: 'privacy_policy', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '隐私政策URL', isEditable: true },
|
isEditable: false,
|
||||||
{ settingKey: 'terms_of_service', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '服务条款URL', isEditable: true },
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'company_name',
|
||||||
|
settingValue: JSON.stringify(''),
|
||||||
|
settingType: 'string',
|
||||||
|
category: 'about',
|
||||||
|
description: '公司/组织名称',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'contact_email',
|
||||||
|
settingValue: JSON.stringify(''),
|
||||||
|
settingType: 'string',
|
||||||
|
category: 'about',
|
||||||
|
description: '联系邮箱',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'contact_phone',
|
||||||
|
settingValue: JSON.stringify(''),
|
||||||
|
settingType: 'string',
|
||||||
|
category: 'about',
|
||||||
|
description: '联系电话',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'company_address',
|
||||||
|
settingValue: JSON.stringify(''),
|
||||||
|
settingType: 'string',
|
||||||
|
category: 'about',
|
||||||
|
description: '公司地址',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'system_description',
|
||||||
|
settingValue: JSON.stringify('机柜管理系统 - 专业的数据中心设备管理解决方案'),
|
||||||
|
settingType: 'string',
|
||||||
|
category: 'about',
|
||||||
|
description: '系统描述',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'privacy_policy',
|
||||||
|
settingValue: JSON.stringify(''),
|
||||||
|
settingType: 'string',
|
||||||
|
category: 'about',
|
||||||
|
description: '隐私政策URL',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'terms_of_service',
|
||||||
|
settingValue: JSON.stringify(''),
|
||||||
|
settingType: 'string',
|
||||||
|
category: 'about',
|
||||||
|
description: '服务条款URL',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
let createdCount = 0;
|
let createdCount = 0;
|
||||||
@@ -60,7 +221,9 @@ const initDefaultSettings = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`系统设置初始化结果: 创建 ${createdCount} 个, 更新 ${updatedCount} 个, 失败 ${errorCount} 个`);
|
console.log(
|
||||||
|
`系统设置初始化结果: 创建 ${createdCount} 个, 更新 ${updatedCount} 个, 失败 ${errorCount} 个`
|
||||||
|
);
|
||||||
return { createdCount, updatedCount, errorCount };
|
return { createdCount, updatedCount, errorCount };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -78,7 +241,10 @@ router.get('/', async (req, res) => {
|
|||||||
|
|
||||||
const settings = await SystemSetting.findAll({
|
const settings = await SystemSetting.findAll({
|
||||||
where,
|
where,
|
||||||
order: [['category', 'ASC'], ['settingKey', 'ASC']]
|
order: [
|
||||||
|
['category', 'ASC'],
|
||||||
|
['settingKey', 'ASC'],
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
// 格式化返回数据
|
// 格式化返回数据
|
||||||
@@ -90,7 +256,7 @@ router.get('/', async (req, res) => {
|
|||||||
category: setting.category,
|
category: setting.category,
|
||||||
description: setting.description,
|
description: setting.description,
|
||||||
isEditable: setting.isEditable,
|
isEditable: setting.isEditable,
|
||||||
updatedAt: setting.updatedAt
|
updatedAt: setting.updatedAt,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -116,7 +282,7 @@ router.get('/idle-timeout', async (req, res) => {
|
|||||||
timeout: timeout * 60 * 1000, // 转换为毫秒
|
timeout: timeout * 60 * 1000, // 转换为毫秒
|
||||||
warningTime: fixedWarningTime * 1000, // 固定10秒(转换为毫秒)
|
warningTime: fixedWarningTime * 1000, // 固定10秒(转换为毫秒)
|
||||||
timeoutMinutes: timeout,
|
timeoutMinutes: timeout,
|
||||||
warningTimeSeconds: fixedWarningTime
|
warningTimeSeconds: fixedWarningTime,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -140,7 +306,7 @@ router.get('/:key', async (req, res) => {
|
|||||||
category: setting.category,
|
category: setting.category,
|
||||||
description: setting.description,
|
description: setting.description,
|
||||||
isEditable: setting.isEditable,
|
isEditable: setting.isEditable,
|
||||||
updatedAt: setting.updatedAt
|
updatedAt: setting.updatedAt,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -175,7 +341,7 @@ router.put('/:key', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await setting.update({
|
await setting.update({
|
||||||
settingValue: JSON.stringify(parsedValue)
|
settingValue: JSON.stringify(parsedValue),
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -183,8 +349,8 @@ router.put('/:key', async (req, res) => {
|
|||||||
setting: {
|
setting: {
|
||||||
key: setting.settingKey,
|
key: setting.settingKey,
|
||||||
value: parsedValue,
|
value: parsedValue,
|
||||||
updatedAt: setting.updatedAt
|
updatedAt: setting.updatedAt,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -229,7 +395,7 @@ router.put('/', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await setting.update({
|
await setting.update({
|
||||||
settingValue: JSON.stringify(parsedValue)
|
settingValue: JSON.stringify(parsedValue),
|
||||||
});
|
});
|
||||||
|
|
||||||
updatedSettings.push({ key, value: parsedValue });
|
updatedSettings.push({ key, value: parsedValue });
|
||||||
@@ -241,7 +407,7 @@ router.put('/', async (req, res) => {
|
|||||||
res.json({
|
res.json({
|
||||||
message: `成功更新 ${updatedSettings.length} 个设置`,
|
message: `成功更新 ${updatedSettings.length} 个设置`,
|
||||||
updatedSettings,
|
updatedSettings,
|
||||||
errors: errors.length > 0 ? errors : undefined
|
errors: errors.length > 0 ? errors : undefined,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -281,7 +447,7 @@ router.post('/reset/:key', async (req, res) => {
|
|||||||
company_address: '',
|
company_address: '',
|
||||||
system_description: '机柜管理系统 - 专业的数据中心设备管理解决方案',
|
system_description: '机柜管理系统 - 专业的数据中心设备管理解决方案',
|
||||||
privacy_policy: '',
|
privacy_policy: '',
|
||||||
terms_of_service: ''
|
terms_of_service: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultValue = defaultValues[key];
|
const defaultValue = defaultValues[key];
|
||||||
@@ -290,13 +456,13 @@ router.post('/reset/:key', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await setting.update({
|
await setting.update({
|
||||||
settingValue: JSON.stringify(defaultValue)
|
settingValue: JSON.stringify(defaultValue),
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: '设置已重置为默认值',
|
message: '设置已重置为默认值',
|
||||||
key,
|
key,
|
||||||
value: defaultValue
|
value: defaultValue,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -346,9 +512,9 @@ router.post('/backup', async (req, res) => {
|
|||||||
// 不包含敏感用户信息
|
// 不包含敏感用户信息
|
||||||
users: await User.findAll({
|
users: await User.findAll({
|
||||||
attributes: ['userId', 'username', 'role', 'createdAt', 'updatedAt'],
|
attributes: ['userId', 'username', 'role', 'createdAt', 'updatedAt'],
|
||||||
raw: true
|
raw: true,
|
||||||
})
|
}),
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 写入备份文件
|
// 写入备份文件
|
||||||
@@ -358,7 +524,7 @@ router.post('/backup', async (req, res) => {
|
|||||||
const lastBackupSetting = await SystemSetting.findByPk('last_backup_time');
|
const lastBackupSetting = await SystemSetting.findByPk('last_backup_time');
|
||||||
if (lastBackupSetting) {
|
if (lastBackupSetting) {
|
||||||
await lastBackupSetting.update({
|
await lastBackupSetting.update({
|
||||||
settingValue: JSON.stringify(new Date().toISOString())
|
settingValue: JSON.stringify(new Date().toISOString()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,7 +533,7 @@ router.post('/backup', async (req, res) => {
|
|||||||
const countSetting = await SystemSetting.findByPk('backup_count');
|
const countSetting = await SystemSetting.findByPk('backup_count');
|
||||||
if (countSetting) {
|
if (countSetting) {
|
||||||
await countSetting.update({
|
await countSetting.update({
|
||||||
settingValue: JSON.stringify(backupFiles.length)
|
settingValue: JSON.stringify(backupFiles.length),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,7 +541,7 @@ router.post('/backup', async (req, res) => {
|
|||||||
message: '备份成功',
|
message: '备份成功',
|
||||||
backupFile: `${backupPath}/backup_${timestamp}.json`,
|
backupFile: `${backupPath}/backup_${timestamp}.json`,
|
||||||
fileSize: fs.statSync(backupFile).size,
|
fileSize: fs.statSync(backupFile).size,
|
||||||
backupCount: backupFiles.length
|
backupCount: backupFiles.length,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('备份失败:', error);
|
console.error('备份失败:', error);
|
||||||
@@ -409,7 +575,8 @@ router.get('/backup/list', async (req, res) => {
|
|||||||
return res.json({ backups: [] });
|
return res.json({ backups: [] });
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = fs.readdirSync(backupDir)
|
const files = fs
|
||||||
|
.readdirSync(backupDir)
|
||||||
.filter(f => f.startsWith('backup_') && f.endsWith('.json'))
|
.filter(f => f.startsWith('backup_') && f.endsWith('.json'))
|
||||||
.map(f => {
|
.map(f => {
|
||||||
const filePath = path.join(backupDir, f);
|
const filePath = path.join(backupDir, f);
|
||||||
@@ -419,7 +586,7 @@ router.get('/backup/list', async (req, res) => {
|
|||||||
path: `${path.basename(backupDir)}/${f}`,
|
path: `${path.basename(backupDir)}/${f}`,
|
||||||
size: stats.size,
|
size: stats.size,
|
||||||
createdAt: stats.birthtime,
|
createdAt: stats.birthtime,
|
||||||
modifiedAt: stats.mtime
|
modifiedAt: stats.mtime,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
||||||
@@ -477,7 +644,7 @@ router.post('/backup/restore', async (req, res) => {
|
|||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: '恢复成功',
|
message: '恢复成功',
|
||||||
restoredAt: new Date().toISOString()
|
restoredAt: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('恢复备份失败:', error);
|
console.error('恢复备份失败:', error);
|
||||||
@@ -533,7 +700,7 @@ router.get('/system/info', async (req, res) => {
|
|||||||
Device.count(),
|
Device.count(),
|
||||||
Rack.count(),
|
Rack.count(),
|
||||||
Room.count(),
|
Room.count(),
|
||||||
User.count()
|
User.count(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -545,15 +712,15 @@ router.get('/system/info', async (req, res) => {
|
|||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
arch: process.arch,
|
arch: process.arch,
|
||||||
memoryUsage: process.memoryUsage(),
|
memoryUsage: process.memoryUsage(),
|
||||||
pid: process.pid
|
pid: process.pid,
|
||||||
},
|
},
|
||||||
statistics: {
|
statistics: {
|
||||||
devices: deviceCount,
|
devices: deviceCount,
|
||||||
racks: rackCount,
|
racks: rackCount,
|
||||||
rooms: roomCount,
|
rooms: roomCount,
|
||||||
users: userCount
|
users: userCount,
|
||||||
},
|
},
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -590,7 +757,7 @@ router.post('/frontend/port/sync', async (req, res) => {
|
|||||||
message: '前端端口配置已同步',
|
message: '前端端口配置已同步',
|
||||||
port,
|
port,
|
||||||
configPath: '.frontend-port',
|
configPath: '.frontend-port',
|
||||||
notice: '配置已更新,请重启前端服务以应用新端口'
|
notice: '配置已更新,请重启前端服务以应用新端口',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -614,8 +781,8 @@ router.post('/frontend/restart', async (req, res) => {
|
|||||||
after: {
|
after: {
|
||||||
pid: result.pid,
|
pid: result.pid,
|
||||||
port: result.port,
|
port: result.port,
|
||||||
url: `http://localhost:${result.port}`
|
url: `http://localhost:${result.port}`,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ router.get('/', async (req, res) => {
|
|||||||
|
|
||||||
const categories = await FaultCategory.findAll({
|
const categories = await FaultCategory.findAll({
|
||||||
where,
|
where,
|
||||||
order: [['priority', 'ASC'], ['name', 'ASC']]
|
order: [
|
||||||
|
['priority', 'ASC'],
|
||||||
|
['name', 'ASC'],
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json(categories);
|
res.json(categories);
|
||||||
@@ -31,8 +34,12 @@ router.get('/stats', async (req, res) => {
|
|||||||
const where = {};
|
const where = {};
|
||||||
if (startDate || endDate) {
|
if (startDate || endDate) {
|
||||||
where.createdAt = {};
|
where.createdAt = {};
|
||||||
if (startDate) where.createdAt[Op.gte] = new Date(startDate);
|
if (startDate) {
|
||||||
if (endDate) where.createdAt[Op.lte] = new Date(endDate + ' 23:59:59');
|
where.createdAt[Op.gte] = new Date(startDate);
|
||||||
|
}
|
||||||
|
if (endDate) {
|
||||||
|
where.createdAt[Op.lte] = new Date(endDate + ' 23:59:59');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const stats = await Ticket.findAll({
|
const stats = await Ticket.findAll({
|
||||||
@@ -40,16 +47,32 @@ router.get('/stats', async (req, res) => {
|
|||||||
attributes: [
|
attributes: [
|
||||||
'faultCategory',
|
'faultCategory',
|
||||||
[require('sequelize').fn('COUNT', '*'), 'totalCount'],
|
[require('sequelize').fn('COUNT', '*'), 'totalCount'],
|
||||||
[require('sequelize').sum(require('sequelize').case({
|
[
|
||||||
|
require('sequelize').sum(
|
||||||
|
require('sequelize').case(
|
||||||
|
{
|
||||||
when: { status: 'completed' },
|
when: { status: 'completed' },
|
||||||
then: 1
|
then: 1,
|
||||||
}, 0)), 'completedCount'],
|
},
|
||||||
[require('sequelize').sum(require('sequelize').case({
|
0
|
||||||
when: { status: { [Op.ne]: 'completed' } },
|
)
|
||||||
then: 1
|
),
|
||||||
}, 0)), 'pendingCount']
|
'completedCount',
|
||||||
],
|
],
|
||||||
group: ['faultCategory']
|
[
|
||||||
|
require('sequelize').sum(
|
||||||
|
require('sequelize').case(
|
||||||
|
{
|
||||||
|
when: { status: { [Op.ne]: 'completed' } },
|
||||||
|
then: 1,
|
||||||
|
},
|
||||||
|
0
|
||||||
|
)
|
||||||
|
),
|
||||||
|
'pendingCount',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
group: ['faultCategory'],
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json(stats);
|
res.json(stats);
|
||||||
@@ -72,15 +95,8 @@ router.get('/:categoryId', async (req, res) => {
|
|||||||
|
|
||||||
router.post('/', async (req, res) => {
|
router.post('/', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const {
|
const { name, description, priority, defaultPriority, expectedDuration, solutions, isActive } =
|
||||||
name,
|
req.body;
|
||||||
description,
|
|
||||||
priority,
|
|
||||||
defaultPriority,
|
|
||||||
expectedDuration,
|
|
||||||
solutions,
|
|
||||||
isActive
|
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
const existing = await FaultCategory.findOne({ where: { name } });
|
const existing = await FaultCategory.findOne({ where: { name } });
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -98,7 +114,7 @@ router.post('/', async (req, res) => {
|
|||||||
expectedDuration: expectedDuration ? parseInt(expectedDuration) : null,
|
expectedDuration: expectedDuration ? parseInt(expectedDuration) : null,
|
||||||
solutions: solutions || [],
|
solutions: solutions || [],
|
||||||
isSystem: false,
|
isSystem: false,
|
||||||
isActive: isActive !== false
|
isActive: isActive !== false,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(201).json(category);
|
res.status(201).json(category);
|
||||||
@@ -110,16 +126,66 @@ router.post('/', async (req, res) => {
|
|||||||
router.post('/init', async (req, res) => {
|
router.post('/init', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const defaultCategories = [
|
const defaultCategories = [
|
||||||
{ name: '系统故障', description: '操作系统、应用程序等系统软件的故障问题', priority: 1, defaultPriority: 'high' },
|
{
|
||||||
{ name: '硬件故障', description: '物理设备、服务器、存储等硬件设备的故障问题', priority: 2, defaultPriority: 'high' },
|
name: '系统故障',
|
||||||
{ name: '网络故障', description: '网络连接、交换机、路由器等网络相关故障', priority: 3, defaultPriority: 'high' },
|
description: '操作系统、应用程序等系统软件的故障问题',
|
||||||
{ name: '软件故障', description: '应用程序错误、软件兼容性等问题', priority: 4, defaultPriority: 'medium' },
|
priority: 1,
|
||||||
{ name: '安全事件', description: '安全漏洞、入侵检测、权限异常等安全问题', priority: 5, defaultPriority: 'urgent' },
|
defaultPriority: 'high',
|
||||||
{ name: '性能问题', description: '系统响应慢、资源利用率高等性能问题', priority: 6, defaultPriority: 'medium' },
|
},
|
||||||
{ name: '配置变更', description: '系统配置、软件配置等变更需求', priority: 7, defaultPriority: 'low' },
|
{
|
||||||
{ name: '例行维护', description: '定期维护、巡检、更新等计划性工作', priority: 8, defaultPriority: 'low' },
|
name: '硬件故障',
|
||||||
{ name: '数据问题', description: '数据错误、数据丢失、数据同步等数据相关问题', priority: 9, defaultPriority: 'high' },
|
description: '物理设备、服务器、存储等硬件设备的故障问题',
|
||||||
{ name: '其他问题', description: '无法归类的其他问题', priority: 99, defaultPriority: 'medium' }
|
priority: 2,
|
||||||
|
defaultPriority: 'high',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '网络故障',
|
||||||
|
description: '网络连接、交换机、路由器等网络相关故障',
|
||||||
|
priority: 3,
|
||||||
|
defaultPriority: 'high',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '软件故障',
|
||||||
|
description: '应用程序错误、软件兼容性等问题',
|
||||||
|
priority: 4,
|
||||||
|
defaultPriority: 'medium',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '安全事件',
|
||||||
|
description: '安全漏洞、入侵检测、权限异常等安全问题',
|
||||||
|
priority: 5,
|
||||||
|
defaultPriority: 'urgent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '性能问题',
|
||||||
|
description: '系统响应慢、资源利用率高等性能问题',
|
||||||
|
priority: 6,
|
||||||
|
defaultPriority: 'medium',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '配置变更',
|
||||||
|
description: '系统配置、软件配置等变更需求',
|
||||||
|
priority: 7,
|
||||||
|
defaultPriority: 'low',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '例行维护',
|
||||||
|
description: '定期维护、巡检、更新等计划性工作',
|
||||||
|
priority: 8,
|
||||||
|
defaultPriority: 'low',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '数据问题',
|
||||||
|
description: '数据错误、数据丢失、数据同步等数据相关问题',
|
||||||
|
priority: 9,
|
||||||
|
defaultPriority: 'high',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '其他问题',
|
||||||
|
description: '无法归类的其他问题',
|
||||||
|
priority: 99,
|
||||||
|
defaultPriority: 'medium',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const cat of defaultCategories) {
|
for (const cat of defaultCategories) {
|
||||||
@@ -132,7 +198,7 @@ router.post('/init', async (req, res) => {
|
|||||||
expectedDuration: 120,
|
expectedDuration: 120,
|
||||||
solutions: [],
|
solutions: [],
|
||||||
isSystem: true,
|
isSystem: true,
|
||||||
isActive: true
|
isActive: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const TicketField = require('../models/TicketField');
|
|||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const fields = await TicketField.findAll({
|
const fields = await TicketField.findAll({
|
||||||
order: [['order', 'ASC']]
|
order: [['order', 'ASC']],
|
||||||
});
|
});
|
||||||
res.json(fields);
|
res.json(fields);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -37,7 +37,7 @@ router.post('/', async (req, res) => {
|
|||||||
router.put('/:fieldId', async (req, res) => {
|
router.put('/:fieldId', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const [updated] = await TicketField.update(req.body, {
|
const [updated] = await TicketField.update(req.body, {
|
||||||
where: { fieldId: req.params.fieldId }
|
where: { fieldId: req.params.fieldId },
|
||||||
});
|
});
|
||||||
if (updated) {
|
if (updated) {
|
||||||
const updatedField = await TicketField.findByPk(req.params.fieldId);
|
const updatedField = await TicketField.findByPk(req.params.fieldId);
|
||||||
@@ -53,7 +53,7 @@ router.put('/:fieldId', async (req, res) => {
|
|||||||
router.delete('/:fieldId', async (req, res) => {
|
router.delete('/:fieldId', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const deleted = await TicketField.destroy({
|
const deleted = await TicketField.destroy({
|
||||||
where: { fieldId: req.params.fieldId }
|
where: { fieldId: req.params.fieldId },
|
||||||
});
|
});
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
res.status(204).json();
|
res.status(204).json();
|
||||||
|
|||||||
+84
-56
@@ -17,41 +17,56 @@ router.get('/stats', async (req, res) => {
|
|||||||
const where = {};
|
const where = {};
|
||||||
if (startDate || endDate) {
|
if (startDate || endDate) {
|
||||||
where.createdAt = {};
|
where.createdAt = {};
|
||||||
if (startDate) where.createdAt[Op.gte] = new Date(startDate);
|
if (startDate) {
|
||||||
if (endDate) where.createdAt[Op.lte] = new Date(endDate + ' 23:59:59');
|
where.createdAt[Op.gte] = new Date(startDate);
|
||||||
|
}
|
||||||
|
if (endDate) {
|
||||||
|
where.createdAt[Op.lte] = new Date(endDate + ' 23:59:59');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const Sequelize = require('sequelize');
|
const Sequelize = require('sequelize');
|
||||||
|
|
||||||
const [total, statusStats, priorityStats, categoryStats, monthlyStats, deviceStats, dailyCreatedStats, dailyCompletedStats] = await Promise.all([
|
const [
|
||||||
|
total,
|
||||||
|
statusStats,
|
||||||
|
priorityStats,
|
||||||
|
categoryStats,
|
||||||
|
monthlyStats,
|
||||||
|
deviceStats,
|
||||||
|
dailyCreatedStats,
|
||||||
|
dailyCompletedStats,
|
||||||
|
] = await Promise.all([
|
||||||
Ticket.count({ where }),
|
Ticket.count({ where }),
|
||||||
Ticket.findAll({
|
Ticket.findAll({
|
||||||
where,
|
where,
|
||||||
attributes: ['status', [Sequelize.fn('COUNT', '*'), 'count']],
|
attributes: ['status', [Sequelize.fn('COUNT', '*'), 'count']],
|
||||||
group: ['status']
|
group: ['status'],
|
||||||
}),
|
}),
|
||||||
Ticket.findAll({
|
Ticket.findAll({
|
||||||
where,
|
where,
|
||||||
attributes: ['priority', [Sequelize.fn('COUNT', '*'), 'count']],
|
attributes: ['priority', [Sequelize.fn('COUNT', '*'), 'count']],
|
||||||
group: ['priority']
|
group: ['priority'],
|
||||||
}),
|
}),
|
||||||
Ticket.findAll({
|
Ticket.findAll({
|
||||||
where,
|
where,
|
||||||
attributes: ['faultCategory', [Sequelize.fn('COUNT', '*'), 'count']],
|
attributes: ['faultCategory', [Sequelize.fn('COUNT', '*'), 'count']],
|
||||||
group: ['faultCategory']
|
group: ['faultCategory'],
|
||||||
}),
|
}),
|
||||||
Ticket.findAll({
|
Ticket.findAll({
|
||||||
where,
|
where,
|
||||||
attributes: [
|
attributes: [
|
||||||
[dbDialect === 'mysql'
|
[
|
||||||
|
dbDialect === 'mysql'
|
||||||
? Sequelize.fn('DATE_FORMAT', Sequelize.col('createdAt'), '%Y-%m')
|
? Sequelize.fn('DATE_FORMAT', Sequelize.col('createdAt'), '%Y-%m')
|
||||||
: Sequelize.fn('strftime', '%Y-%m', Sequelize.col('createdAt')),
|
: Sequelize.fn('strftime', '%Y-%m', Sequelize.col('createdAt')),
|
||||||
'month'],
|
'month',
|
||||||
[Sequelize.fn('COUNT', '*'), 'count']
|
],
|
||||||
|
[Sequelize.fn('COUNT', '*'), 'count'],
|
||||||
],
|
],
|
||||||
group: ['month'],
|
group: ['month'],
|
||||||
order: [['month', 'DESC']],
|
order: [['month', 'DESC']],
|
||||||
limit: 12
|
limit: 12,
|
||||||
}),
|
}),
|
||||||
Ticket.findAll({
|
Ticket.findAll({
|
||||||
where,
|
where,
|
||||||
@@ -59,39 +74,43 @@ router.get('/stats', async (req, res) => {
|
|||||||
'deviceId',
|
'deviceId',
|
||||||
'deviceName',
|
'deviceName',
|
||||||
[Sequelize.fn('COUNT', '*'), 'count'],
|
[Sequelize.fn('COUNT', '*'), 'count'],
|
||||||
[Sequelize.fn('MAX', Sequelize.col('createdAt')), 'lastFaultTime']
|
[Sequelize.fn('MAX', Sequelize.col('createdAt')), 'lastFaultTime'],
|
||||||
],
|
],
|
||||||
group: ['deviceId', 'deviceName'],
|
group: ['deviceId', 'deviceName'],
|
||||||
order: [[Sequelize.fn('COUNT', '*'), 'DESC']],
|
order: [[Sequelize.fn('COUNT', '*'), 'DESC']],
|
||||||
limit: 10
|
limit: 10,
|
||||||
}),
|
}),
|
||||||
Ticket.findAll({
|
Ticket.findAll({
|
||||||
where,
|
where,
|
||||||
attributes: [
|
attributes: [
|
||||||
[dbDialect === 'mysql'
|
[
|
||||||
|
dbDialect === 'mysql'
|
||||||
? Sequelize.fn('DATE_FORMAT', Sequelize.col('createdAt'), '%Y-%m-%d')
|
? Sequelize.fn('DATE_FORMAT', Sequelize.col('createdAt'), '%Y-%m-%d')
|
||||||
: Sequelize.fn('date', Sequelize.col('createdAt')),
|
: Sequelize.fn('date', Sequelize.col('createdAt')),
|
||||||
'date'],
|
'date',
|
||||||
[Sequelize.fn('COUNT', '*'), 'created']
|
],
|
||||||
|
[Sequelize.fn('COUNT', '*'), 'created'],
|
||||||
],
|
],
|
||||||
group: ['date'],
|
group: ['date'],
|
||||||
order: [['date', 'ASC']]
|
order: [['date', 'ASC']],
|
||||||
}),
|
}),
|
||||||
Ticket.findAll({
|
Ticket.findAll({
|
||||||
where: {
|
where: {
|
||||||
...where,
|
...where,
|
||||||
status: 'completed'
|
status: 'completed',
|
||||||
},
|
},
|
||||||
attributes: [
|
attributes: [
|
||||||
[dbDialect === 'mysql'
|
[
|
||||||
|
dbDialect === 'mysql'
|
||||||
? Sequelize.fn('DATE_FORMAT', Sequelize.col('updatedAt'), '%Y-%m-%d')
|
? Sequelize.fn('DATE_FORMAT', Sequelize.col('updatedAt'), '%Y-%m-%d')
|
||||||
: Sequelize.fn('date', Sequelize.col('updatedAt')),
|
: Sequelize.fn('date', Sequelize.col('updatedAt')),
|
||||||
'date'],
|
'date',
|
||||||
[Sequelize.fn('COUNT', '*'), 'completed']
|
],
|
||||||
|
[Sequelize.fn('COUNT', '*'), 'completed'],
|
||||||
],
|
],
|
||||||
group: ['date'],
|
group: ['date'],
|
||||||
order: [['date', 'ASC']]
|
order: [['date', 'ASC']],
|
||||||
})
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const statusData = statusStats.map(s => s.dataValues);
|
const statusData = statusStats.map(s => s.dataValues);
|
||||||
@@ -103,21 +122,21 @@ router.get('/stats', async (req, res) => {
|
|||||||
const byStatus = statusData.map(item => ({
|
const byStatus = statusData.map(item => ({
|
||||||
status: item.status,
|
status: item.status,
|
||||||
count: item.count,
|
count: item.count,
|
||||||
percentage: total > 0 ? (item.count / total * 100) : 0
|
percentage: total > 0 ? (item.count / total) * 100 : 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const byPriority = priorityStats.map(p => ({
|
const byPriority = priorityStats.map(p => ({
|
||||||
priority: p.dataValues.priority,
|
priority: p.dataValues.priority,
|
||||||
count: p.dataValues.count,
|
count: p.dataValues.count,
|
||||||
completed: 0,
|
completed: 0,
|
||||||
avgTime: 0
|
avgTime: 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const byCategory = categoryStats.map(c => ({
|
const byCategory = categoryStats.map(c => ({
|
||||||
category: c.dataValues.faultCategory,
|
category: c.dataValues.faultCategory,
|
||||||
count: c.dataValues.count,
|
count: c.dataValues.count,
|
||||||
completed: 0,
|
completed: 0,
|
||||||
avgTime: 0
|
avgTime: 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const byDevice = deviceStats.map(d => ({
|
const byDevice = deviceStats.map(d => ({
|
||||||
@@ -125,7 +144,7 @@ router.get('/stats', async (req, res) => {
|
|||||||
deviceName: d.deviceName,
|
deviceName: d.deviceName,
|
||||||
count: d.dataValues.count,
|
count: d.dataValues.count,
|
||||||
lastFaultTime: d.dataValues.lastFaultTime,
|
lastFaultTime: d.dataValues.lastFaultTime,
|
||||||
deviceType: ''
|
deviceType: '',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const createdMap = {};
|
const createdMap = {};
|
||||||
@@ -137,22 +156,24 @@ router.get('/stats', async (req, res) => {
|
|||||||
completedMap[d.dataValues.date] = d.dataValues.completed;
|
completedMap[d.dataValues.date] = d.dataValues.completed;
|
||||||
});
|
});
|
||||||
|
|
||||||
const allDates = [...new Set([...Object.keys(createdMap), ...Object.keys(completedMap)])].sort();
|
const allDates = [
|
||||||
|
...new Set([...Object.keys(createdMap), ...Object.keys(completedMap)]),
|
||||||
|
].sort();
|
||||||
const trend = allDates.map(date => ({
|
const trend = allDates.map(date => ({
|
||||||
date,
|
date,
|
||||||
created: createdMap[date] || 0,
|
created: createdMap[date] || 0,
|
||||||
completed: completedMap[date] || 0,
|
completed: completedMap[date] || 0,
|
||||||
closed: 0,
|
closed: 0,
|
||||||
inProgress: 0,
|
inProgress: 0,
|
||||||
pending: 0
|
pending: 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const completedTickets = await Ticket.findAll({
|
const completedTickets = await Ticket.findAll({
|
||||||
where: {
|
where: {
|
||||||
...where,
|
...where,
|
||||||
status: 'completed'
|
status: 'completed',
|
||||||
},
|
},
|
||||||
attributes: ['createdAt', 'updatedAt']
|
attributes: ['createdAt', 'updatedAt'],
|
||||||
});
|
});
|
||||||
|
|
||||||
let avgProcessingTime = 0;
|
let avgProcessingTime = 0;
|
||||||
@@ -162,7 +183,11 @@ router.get('/stats', async (req, res) => {
|
|||||||
const updated = new Date(ticket.updatedAt);
|
const updated = new Date(ticket.updatedAt);
|
||||||
return sum + (updated - created);
|
return sum + (updated - created);
|
||||||
}, 0);
|
}, 0);
|
||||||
avgProcessingTime = (totalProcessingTime / completedTickets.length / (1000 * 60 * 60)).toFixed(1);
|
avgProcessingTime = (
|
||||||
|
totalProcessingTime /
|
||||||
|
completedTickets.length /
|
||||||
|
(1000 * 60 * 60)
|
||||||
|
).toFixed(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -177,7 +202,10 @@ router.get('/stats', async (req, res) => {
|
|||||||
byCategory,
|
byCategory,
|
||||||
byDevice,
|
byDevice,
|
||||||
trend,
|
trend,
|
||||||
monthlyStats: monthlyStats.map(m => ({ month: m.dataValues.month, count: m.dataValues.count }))
|
monthlyStats: monthlyStats.map(m => ({
|
||||||
|
month: m.dataValues.month,
|
||||||
|
count: m.dataValues.count,
|
||||||
|
})),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -203,7 +231,7 @@ router.get('/', async (req, res) => {
|
|||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
page = 1,
|
page = 1,
|
||||||
pageSize = 10
|
pageSize = 10,
|
||||||
} = req.query;
|
} = req.query;
|
||||||
|
|
||||||
const offset = (page - 1) * pageSize;
|
const offset = (page - 1) * pageSize;
|
||||||
@@ -216,7 +244,7 @@ router.get('/', async (req, res) => {
|
|||||||
{ title: { [Op.like]: `%${keyword}%` } },
|
{ title: { [Op.like]: `%${keyword}%` } },
|
||||||
{ deviceName: { [Op.like]: `%${keyword}%` } },
|
{ deviceName: { [Op.like]: `%${keyword}%` } },
|
||||||
{ serialNumber: { [Op.like]: `%${keyword}%` } },
|
{ serialNumber: { [Op.like]: `%${keyword}%` } },
|
||||||
{ description: { [Op.like]: `%${keyword}%` } }
|
{ description: { [Op.like]: `%${keyword}%` } },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,18 +288,18 @@ router.get('/', async (req, res) => {
|
|||||||
where,
|
where,
|
||||||
include: [
|
include: [
|
||||||
{ model: User, as: 'reporter', attributes: ['userId', 'username'] },
|
{ model: User, as: 'reporter', attributes: ['userId', 'username'] },
|
||||||
{ model: Device, attributes: ['deviceId', 'name', 'type', 'model'] }
|
{ model: Device, attributes: ['deviceId', 'name', 'type', 'model'] },
|
||||||
],
|
],
|
||||||
order: [['createdAt', 'DESC']],
|
order: [['createdAt', 'DESC']],
|
||||||
offset,
|
offset,
|
||||||
limit: parseInt(pageSize)
|
limit: parseInt(pageSize),
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
total: count,
|
total: count,
|
||||||
tickets: rows,
|
tickets: rows,
|
||||||
page: parseInt(page),
|
page: parseInt(page),
|
||||||
pageSize: parseInt(pageSize)
|
pageSize: parseInt(pageSize),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -293,14 +321,14 @@ router.get('/:ticketId', async (req, res) => {
|
|||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: Room,
|
model: Room,
|
||||||
attributes: ['roomId', 'name']
|
attributes: ['roomId', 'name'],
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{ model: TicketOperationRecord, as: 'operationRecords', order: [['createdAt', 'DESC']] }
|
],
|
||||||
]
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ model: TicketOperationRecord, as: 'operationRecords', order: [['createdAt', 'DESC']] },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!ticket) {
|
if (!ticket) {
|
||||||
@@ -334,7 +362,7 @@ router.post('/', async (req, res) => {
|
|||||||
expectedCompletionDate,
|
expectedCompletionDate,
|
||||||
title,
|
title,
|
||||||
attachments,
|
attachments,
|
||||||
tags
|
tags,
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
let device = null;
|
let device = null;
|
||||||
@@ -347,7 +375,7 @@ router.post('/', async (req, res) => {
|
|||||||
if (deviceId) {
|
if (deviceId) {
|
||||||
// 从设备列表选择
|
// 从设备列表选择
|
||||||
device = await Device.findByPk(deviceId, {
|
device = await Device.findByPk(deviceId, {
|
||||||
include: [{ model: require('../models/Rack') }]
|
include: [{ model: require('../models/Rack') }],
|
||||||
});
|
});
|
||||||
if (!device) {
|
if (!device) {
|
||||||
return res.status(404).json({ error: '设备不存在' });
|
return res.status(404).json({ error: '设备不存在' });
|
||||||
@@ -385,7 +413,7 @@ router.post('/', async (req, res) => {
|
|||||||
location: ticketLocation,
|
location: ticketLocation,
|
||||||
attachments: attachments || [],
|
attachments: attachments || [],
|
||||||
tags: tags || [],
|
tags: tags || [],
|
||||||
status: 'pending'
|
status: 'pending',
|
||||||
});
|
});
|
||||||
|
|
||||||
// 创建操作记录
|
// 创建操作记录
|
||||||
@@ -397,7 +425,7 @@ router.post('/', async (req, res) => {
|
|||||||
operatorId: ticket.reporterId,
|
operatorId: ticket.reporterId,
|
||||||
operatorName: ticket.reporterName,
|
operatorName: ticket.reporterName,
|
||||||
operatorRole: 'user',
|
operatorRole: 'user',
|
||||||
afterState: ticket.toJSON()
|
afterState: ticket.toJSON(),
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(201).json(ticket);
|
res.status(201).json(ticket);
|
||||||
@@ -428,7 +456,7 @@ router.put('/:ticketId', async (req, res) => {
|
|||||||
operatorName: operatorName || ticket.reporterName,
|
operatorName: operatorName || ticket.reporterName,
|
||||||
operatorRole: operatorRole || 'user',
|
operatorRole: operatorRole || 'user',
|
||||||
beforeState,
|
beforeState,
|
||||||
afterState: ticket.toJSON()
|
afterState: ticket.toJSON(),
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json(ticket);
|
res.json(ticket);
|
||||||
@@ -468,7 +496,7 @@ router.put('/:ticketId/status', async (req, res) => {
|
|||||||
operatorName: operatorName || ticket.reporterName,
|
operatorName: operatorName || ticket.reporterName,
|
||||||
operatorRole: operatorRole || 'user',
|
operatorRole: operatorRole || 'user',
|
||||||
beforeState,
|
beforeState,
|
||||||
afterState: ticket.toJSON()
|
afterState: ticket.toJSON(),
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json(ticket);
|
res.json(ticket);
|
||||||
@@ -490,7 +518,7 @@ router.put('/:ticketId/process', async (req, res) => {
|
|||||||
const beforeState = ticket.toJSON();
|
const beforeState = ticket.toJSON();
|
||||||
const updateData = {
|
const updateData = {
|
||||||
status: 'in_progress',
|
status: 'in_progress',
|
||||||
resolution: solution
|
resolution: solution,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (result === 'resolved') {
|
if (result === 'resolved') {
|
||||||
@@ -513,7 +541,7 @@ router.put('/:ticketId/process', async (req, res) => {
|
|||||||
operatorName: operatorName || ticket.reporterName,
|
operatorName: operatorName || ticket.reporterName,
|
||||||
operatorRole: operatorRole || 'user',
|
operatorRole: operatorRole || 'user',
|
||||||
beforeState,
|
beforeState,
|
||||||
afterState: ticket.toJSON()
|
afterState: ticket.toJSON(),
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json(ticket);
|
res.json(ticket);
|
||||||
@@ -535,7 +563,7 @@ router.post('/:ticketId/operations', async (req, res) => {
|
|||||||
notes,
|
notes,
|
||||||
operatorId,
|
operatorId,
|
||||||
operatorName,
|
operatorName,
|
||||||
operatorRole
|
operatorRole,
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
const ticket = await Ticket.findByPk(req.params.ticketId);
|
const ticket = await Ticket.findByPk(req.params.ticketId);
|
||||||
@@ -555,7 +583,7 @@ router.post('/:ticketId/operations', async (req, res) => {
|
|||||||
notes,
|
notes,
|
||||||
operatorId,
|
operatorId,
|
||||||
operatorName,
|
operatorName,
|
||||||
operatorRole
|
operatorRole,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(201).json(record);
|
res.status(201).json(record);
|
||||||
@@ -569,7 +597,7 @@ router.get('/:ticketId/operations', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const records = await TicketOperationRecord.findAll({
|
const records = await TicketOperationRecord.findAll({
|
||||||
where: { ticketId: req.params.ticketId },
|
where: { ticketId: req.params.ticketId },
|
||||||
order: [['createdAt', 'DESC']]
|
order: [['createdAt', 'DESC']],
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json(records);
|
res.json(records);
|
||||||
@@ -613,7 +641,7 @@ router.post('/:ticketId/evaluate', async (req, res) => {
|
|||||||
operatorId,
|
operatorId,
|
||||||
operatorName,
|
operatorName,
|
||||||
operatorRole: 'user',
|
operatorRole: 'user',
|
||||||
notes: `评价: ${evaluation}, 星级: ${evaluationRating}`
|
notes: `评价: ${evaluation}, 星级: ${evaluationRating}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json(ticket);
|
res.json(ticket);
|
||||||
|
|||||||
+127
-90
@@ -15,7 +15,7 @@ const generateId = () => {
|
|||||||
return 'user_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 9);
|
return 'user_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 9);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getWhereClause = (query) => {
|
const getWhereClause = query => {
|
||||||
const where = {};
|
const where = {};
|
||||||
|
|
||||||
if (query.username) {
|
if (query.username) {
|
||||||
@@ -33,10 +33,10 @@ const getWhereClause = (query) => {
|
|||||||
return where;
|
return where;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserRoleIds = async (userId) => {
|
const getUserRoleIds = async userId => {
|
||||||
const userRoles = await UserRole.findAll({
|
const userRoles = await UserRole.findAll({
|
||||||
where: { UserId: userId },
|
where: { UserId: userId },
|
||||||
attributes: ['RoleId']
|
attributes: ['RoleId'],
|
||||||
});
|
});
|
||||||
return userRoles.map(ur => ur.RoleId);
|
return userRoles.map(ur => ur.RoleId);
|
||||||
};
|
};
|
||||||
@@ -45,7 +45,13 @@ const { Op } = require('sequelize');
|
|||||||
|
|
||||||
router.get('/', authMiddleware, async (req, res) => {
|
router.get('/', authMiddleware, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { page = 1, pageSize = PAGINATION.DEFAULT_PAGE_SIZE, username, status, realName } = req.query;
|
const {
|
||||||
|
page = 1,
|
||||||
|
pageSize = PAGINATION.DEFAULT_PAGE_SIZE,
|
||||||
|
username,
|
||||||
|
status,
|
||||||
|
realName,
|
||||||
|
} = req.query;
|
||||||
const offset = (parseInt(page) - 1) * parseInt(pageSize);
|
const offset = (parseInt(page) - 1) * parseInt(pageSize);
|
||||||
const limit = Math.min(parseInt(pageSize), PAGINATION.MAX_PAGE_SIZE);
|
const limit = Math.min(parseInt(pageSize), PAGINATION.MAX_PAGE_SIZE);
|
||||||
|
|
||||||
@@ -56,20 +62,22 @@ router.get('/', authMiddleware, async (req, res) => {
|
|||||||
attributes: { exclude: ['password'] },
|
attributes: { exclude: ['password'] },
|
||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
order: [['createdAt', 'DESC']]
|
order: [['createdAt', 'DESC']],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (users.length > 0) {
|
if (users.length > 0) {
|
||||||
const userIds = users.map(u => u.userId);
|
const userIds = users.map(u => u.userId);
|
||||||
const allUserRoles = await UserRole.findAll({
|
const allUserRoles = await UserRole.findAll({
|
||||||
include: [{
|
include: [
|
||||||
|
{
|
||||||
model: Role,
|
model: Role,
|
||||||
where: { status: 'active' },
|
where: { status: 'active' },
|
||||||
attributes: ['roleId', 'roleName', 'roleCode']
|
attributes: ['roleId', 'roleName', 'roleCode'],
|
||||||
}],
|
},
|
||||||
|
],
|
||||||
where: {
|
where: {
|
||||||
UserId: { [Op.in]: userIds }
|
UserId: { [Op.in]: userIds },
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const userRolesMap = {};
|
const userRolesMap = {};
|
||||||
@@ -80,7 +88,7 @@ router.get('/', authMiddleware, async (req, res) => {
|
|||||||
userRolesMap[ur.UserId].push({
|
userRolesMap[ur.UserId].push({
|
||||||
roleId: ur.Role.roleId,
|
roleId: ur.Role.roleId,
|
||||||
roleName: ur.Role.roleName,
|
roleName: ur.Role.roleName,
|
||||||
roleCode: ur.Role.roleCode
|
roleCode: ur.Role.roleCode,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -99,14 +107,14 @@ router.get('/', authMiddleware, async (req, res) => {
|
|||||||
total: count,
|
total: count,
|
||||||
page: parseInt(page),
|
page: parseInt(page),
|
||||||
pageSize: parseInt(pageSize),
|
pageSize: parseInt(pageSize),
|
||||||
users
|
users,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取用户列表错误:', error);
|
console.error('获取用户列表错误:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '获取用户列表失败'
|
message: '获取用户列表失败',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -116,18 +124,18 @@ router.get('/all', authMiddleware, async (req, res) => {
|
|||||||
const users = await User.findAll({
|
const users = await User.findAll({
|
||||||
where: { status: 'active' },
|
where: { status: 'active' },
|
||||||
attributes: ['userId', 'username', 'realName', 'email'],
|
attributes: ['userId', 'username', 'realName', 'email'],
|
||||||
order: [['realName', 'ASC']]
|
order: [['realName', 'ASC']],
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: users
|
data: users,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取所有用户错误:', error);
|
console.error('获取所有用户错误:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '获取用户列表失败'
|
message: '获取用户列表失败',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -135,39 +143,41 @@ router.get('/all', authMiddleware, async (req, res) => {
|
|||||||
router.get('/:userId', authMiddleware, async (req, res) => {
|
router.get('/:userId', authMiddleware, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const user = await User.findByPk(req.params.userId, {
|
const user = await User.findByPk(req.params.userId, {
|
||||||
attributes: { exclude: ['password'] }
|
attributes: { exclude: ['password'] },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '用户不存在'
|
message: '用户不存在',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const userRoles = await UserRole.findAll({
|
const userRoles = await UserRole.findAll({
|
||||||
include: [{
|
include: [
|
||||||
|
{
|
||||||
model: Role,
|
model: Role,
|
||||||
where: { status: 'active' }
|
where: { status: 'active' },
|
||||||
}],
|
},
|
||||||
where: { UserId: user.userId }
|
],
|
||||||
|
where: { UserId: user.userId },
|
||||||
});
|
});
|
||||||
|
|
||||||
user.dataValues.roles = userRoles.map(ur => ({
|
user.dataValues.roles = userRoles.map(ur => ({
|
||||||
roleId: ur.Role.roleId,
|
roleId: ur.Role.roleId,
|
||||||
roleName: ur.Role.roleName,
|
roleName: ur.Role.roleName,
|
||||||
roleCode: ur.Role.roleCode
|
roleCode: ur.Role.roleCode,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: user
|
data: user,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取用户详情错误:', error);
|
console.error('获取用户详情错误:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '获取用户详情失败'
|
message: '获取用户详情失败',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -179,7 +189,7 @@ router.post('/', authMiddleware, async (req, res) => {
|
|||||||
if (!username || !password) {
|
if (!username || !password) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '用户名和密码不能为空'
|
message: '用户名和密码不能为空',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +197,7 @@ router.post('/', authMiddleware, async (req, res) => {
|
|||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '用户名已存在'
|
message: '用户名已存在',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,34 +211,41 @@ router.post('/', authMiddleware, async (req, res) => {
|
|||||||
phone,
|
phone,
|
||||||
realName: realName || username,
|
realName: realName || username,
|
||||||
status: status || 'active',
|
status: status || 'active',
|
||||||
remark
|
remark,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (roleIds && roleIds.length > 0) {
|
if (roleIds && roleIds.length > 0) {
|
||||||
for (const roleId of roleIds) {
|
for (const roleId of roleIds) {
|
||||||
await UserRole.create({
|
await UserRole.create({
|
||||||
UserId: user.userId,
|
UserId: user.userId,
|
||||||
RoleId: roleId
|
RoleId: roleId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const roleNames = roleIds && roleIds.length > 0
|
const roleNames =
|
||||||
? (await Role.findAll({ where: { roleId: { [Op.in]: roleIds } } })).map(r => r.roleName).join('、')
|
roleIds && roleIds.length > 0
|
||||||
|
? (await Role.findAll({ where: { roleId: { [Op.in]: roleIds } } }))
|
||||||
|
.map(r => r.roleName)
|
||||||
|
.join('、')
|
||||||
: '未分配角色';
|
: '未分配角色';
|
||||||
|
|
||||||
await logUserOperation('create', `创建用户【${username}】(姓名:${realName || '未填写'},邮箱:${email || '未填写'},角色:${roleNames})`, {
|
await logUserOperation(
|
||||||
|
'create',
|
||||||
|
`创建用户【${username}】(姓名:${realName || '未填写'},邮箱:${email || '未填写'},角色:${roleNames})`,
|
||||||
|
{
|
||||||
targetId: user.userId,
|
targetId: user.userId,
|
||||||
targetName: username,
|
targetName: username,
|
||||||
afterState: {
|
afterState: {
|
||||||
username: user.username,
|
username: user.username,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
realName: user.realName,
|
realName: user.realName,
|
||||||
status: user.status
|
status: user.status,
|
||||||
},
|
},
|
||||||
req,
|
req,
|
||||||
metadata: { roleIds, roleNames }
|
metadata: { roleIds, roleNames },
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -238,14 +255,14 @@ router.post('/', authMiddleware, async (req, res) => {
|
|||||||
username: user.username,
|
username: user.username,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
realName: user.realName,
|
realName: user.realName,
|
||||||
status: user.status
|
status: user.status,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('创建用户错误:', error);
|
console.error('创建用户错误:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '创建用户失败'
|
message: '创建用户失败',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -258,7 +275,7 @@ router.put('/:userId', authMiddleware, async (req, res) => {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '用户不存在'
|
message: '用户不存在',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,29 +285,39 @@ router.put('/:userId', authMiddleware, async (req, res) => {
|
|||||||
phone: user.phone,
|
phone: user.phone,
|
||||||
realName: user.realName,
|
realName: user.realName,
|
||||||
status: user.status,
|
status: user.status,
|
||||||
remark: user.remark
|
remark: user.remark,
|
||||||
};
|
};
|
||||||
|
|
||||||
const oldRoleIds = roleIds !== undefined ? null : await getUserRoleIds(user.userId);
|
const oldRoleIds = roleIds !== undefined ? null : await getUserRoleIds(user.userId);
|
||||||
|
|
||||||
if (username !== undefined && username !== user.username) {
|
if (username !== undefined && username !== user.username) {
|
||||||
const existingUser = await User.findOne({
|
const existingUser = await User.findOne({
|
||||||
where: { username, userId: { [Op.ne]: user.userId } }
|
where: { username, userId: { [Op.ne]: user.userId } },
|
||||||
});
|
});
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '用户名已存在'
|
message: '用户名已存在',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
user.username = username;
|
user.username = username;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (email !== undefined) user.email = email;
|
if (email !== undefined) {
|
||||||
if (phone !== undefined) user.phone = phone;
|
user.email = email;
|
||||||
if (realName !== undefined) user.realName = realName;
|
}
|
||||||
if (status !== undefined) user.status = status;
|
if (phone !== undefined) {
|
||||||
if (remark !== undefined) user.remark = remark;
|
user.phone = phone;
|
||||||
|
}
|
||||||
|
if (realName !== undefined) {
|
||||||
|
user.realName = realName;
|
||||||
|
}
|
||||||
|
if (status !== undefined) {
|
||||||
|
user.status = status;
|
||||||
|
}
|
||||||
|
if (remark !== undefined) {
|
||||||
|
user.remark = remark;
|
||||||
|
}
|
||||||
|
|
||||||
if (newPassword && newPassword.length >= PASSWORD_MIN_LENGTH) {
|
if (newPassword && newPassword.length >= PASSWORD_MIN_LENGTH) {
|
||||||
user.password = await bcrypt.hash(newPassword, SALT_ROUNDS);
|
user.password = await bcrypt.hash(newPassword, SALT_ROUNDS);
|
||||||
@@ -311,7 +338,7 @@ router.put('/:userId', authMiddleware, async (req, res) => {
|
|||||||
for (const roleId of roleIds) {
|
for (const roleId of roleIds) {
|
||||||
await UserRole.create({
|
await UserRole.create({
|
||||||
UserId: user.userId,
|
UserId: user.userId,
|
||||||
RoleId: roleId
|
RoleId: roleId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,7 +348,7 @@ router.put('/:userId', authMiddleware, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updatedUser = await User.findByPk(req.params.userId, {
|
const updatedUser = await User.findByPk(req.params.userId, {
|
||||||
attributes: { exclude: ['password'] }
|
attributes: { exclude: ['password'] },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (permissionChanged) {
|
if (permissionChanged) {
|
||||||
@@ -332,7 +359,7 @@ router.put('/:userId', authMiddleware, async (req, res) => {
|
|||||||
beforeState: { ...beforeState, roleIds: oldRoleIds, roleNames: oldRoleNames },
|
beforeState: { ...beforeState, roleIds: oldRoleIds, roleNames: oldRoleNames },
|
||||||
afterState: { ...beforeState, roleIds, roleNames: newRoleNames },
|
afterState: { ...beforeState, roleIds, roleNames: newRoleNames },
|
||||||
req,
|
req,
|
||||||
metadata: { oldRoleIds, newRoleIds: roleIds, oldRoleNames, newRoleNames }
|
metadata: { oldRoleIds, newRoleIds: roleIds, oldRoleNames, newRoleNames },
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const afterState = {
|
const afterState = {
|
||||||
@@ -341,7 +368,7 @@ router.put('/:userId', authMiddleware, async (req, res) => {
|
|||||||
phone: updatedUser.phone,
|
phone: updatedUser.phone,
|
||||||
realName: updatedUser.realName,
|
realName: updatedUser.realName,
|
||||||
status: updatedUser.status,
|
status: updatedUser.status,
|
||||||
remark: updatedUser.remark
|
remark: updatedUser.remark,
|
||||||
};
|
};
|
||||||
|
|
||||||
const changedFields = {};
|
const changedFields = {};
|
||||||
@@ -351,14 +378,20 @@ router.put('/:userId', authMiddleware, async (req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const changeDetails = Object.entries(changedFields).map(([field, values]) => {
|
const changeDetails = Object.entries(changedFields)
|
||||||
|
.map(([field, values]) => {
|
||||||
const fieldNames = {
|
const fieldNames = {
|
||||||
username: '用户名', email: '邮箱', phone: '电话', realName: '姓名',
|
username: '用户名',
|
||||||
status: '状态', remark: '备注'
|
email: '邮箱',
|
||||||
|
phone: '电话',
|
||||||
|
realName: '姓名',
|
||||||
|
status: '状态',
|
||||||
|
remark: '备注',
|
||||||
};
|
};
|
||||||
const displayName = fieldNames[field] || field;
|
const displayName = fieldNames[field] || field;
|
||||||
return `${displayName}: ${values.from ?? '空'} → ${values.to ?? '空'}`;
|
return `${displayName}: ${values.from ?? '空'} → ${values.to ?? '空'}`;
|
||||||
}).join(';');
|
})
|
||||||
|
.join(';');
|
||||||
|
|
||||||
const updateDesc = changeDetails
|
const updateDesc = changeDetails
|
||||||
? `更新用户【${updatedUser.username}】:${changeDetails}`
|
? `更新用户【${updatedUser.username}】:${changeDetails}`
|
||||||
@@ -370,20 +403,20 @@ router.put('/:userId', authMiddleware, async (req, res) => {
|
|||||||
beforeState,
|
beforeState,
|
||||||
afterState,
|
afterState,
|
||||||
req,
|
req,
|
||||||
metadata: { changedFields }
|
metadata: { changedFields },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: '更新成功',
|
message: '更新成功',
|
||||||
data: updatedUser
|
data: updatedUser,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('更新用户错误:', error);
|
console.error('更新用户错误:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '更新用户失败'
|
message: '更新用户失败',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -396,14 +429,14 @@ router.put('/:userId/password', authMiddleware, async (req, res) => {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '用户不存在'
|
message: '用户不存在',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newPassword || newPassword.length < PASSWORD_MIN_LENGTH) {
|
if (!newPassword || newPassword.length < PASSWORD_MIN_LENGTH) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: `密码长度不能少于${PASSWORD_MIN_LENGTH}个字符`
|
message: `密码长度不能少于${PASSWORD_MIN_LENGTH}个字符`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,13 +445,13 @@ router.put('/:userId/password', authMiddleware, async (req, res) => {
|
|||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: '密码重置成功'
|
message: '密码重置成功',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('重置密码错误:', error);
|
console.error('重置密码错误:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '重置密码失败'
|
message: '重置密码失败',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -430,14 +463,14 @@ router.delete('/:userId', authMiddleware, async (req, res) => {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '用户不存在'
|
message: '用户不存在',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.userId === req.user.userId) {
|
if (user.userId === req.user.userId) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '不能删除当前登录用户'
|
message: '不能删除当前登录用户',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,29 +481,33 @@ router.delete('/:userId', authMiddleware, async (req, res) => {
|
|||||||
username: user.username,
|
username: user.username,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
realName: user.realName,
|
realName: user.realName,
|
||||||
status: user.status
|
status: user.status,
|
||||||
};
|
};
|
||||||
|
|
||||||
await UserRole.destroy({ where: { UserId: user.userId } });
|
await UserRole.destroy({ where: { UserId: user.userId } });
|
||||||
await user.destroy();
|
await user.destroy();
|
||||||
|
|
||||||
await logUserOperation('delete', `删除用户【${userName}】(姓名:${userRealName || '未填写'},邮箱:${userEmail || '未填写'})`, {
|
await logUserOperation(
|
||||||
|
'delete',
|
||||||
|
`删除用户【${userName}】(姓名:${userRealName || '未填写'},邮箱:${userEmail || '未填写'})`,
|
||||||
|
{
|
||||||
targetId: req.params.userId,
|
targetId: req.params.userId,
|
||||||
targetName: userName,
|
targetName: userName,
|
||||||
beforeState,
|
beforeState,
|
||||||
req,
|
req,
|
||||||
metadata: { deletedUsername: userName, realName: userRealName, email: userEmail }
|
metadata: { deletedUsername: userName, realName: userRealName, email: userEmail },
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: '删除成功'
|
message: '删除成功',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('删除用户错误:', error);
|
console.error('删除用户错误:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '删除用户失败'
|
message: '删除用户失败',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -482,14 +519,14 @@ router.post('/:userId/avatar', authMiddleware, async (req, res) => {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '用户不存在'
|
message: '用户不存在',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!req.files || !req.files.avatar) {
|
if (!req.files || !req.files.avatar) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '请选择要上传的头像文件'
|
message: '请选择要上传的头像文件',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -499,14 +536,14 @@ router.post('/:userId/avatar', authMiddleware, async (req, res) => {
|
|||||||
if (!allowedTypes.includes(avatarFile.mimetype)) {
|
if (!allowedTypes.includes(avatarFile.mimetype)) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '只支持 JPG、PNG、GIF 和 WebP 格式的图片'
|
message: '只支持 JPG、PNG、GIF 和 WebP 格式的图片',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (avatarFile.size > FILE_UPLOAD.MAX_AVATAR_SIZE) {
|
if (avatarFile.size > FILE_UPLOAD.MAX_AVATAR_SIZE) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: `图片大小不能超过 ${FILE_UPLOAD.MAX_AVATAR_SIZE / 1024 / 1024}MB`
|
message: `图片大小不能超过 ${FILE_UPLOAD.MAX_AVATAR_SIZE / 1024 / 1024}MB`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -535,13 +572,13 @@ router.post('/:userId/avatar', authMiddleware, async (req, res) => {
|
|||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: '头像上传成功',
|
message: '头像上传成功',
|
||||||
data: { avatar: avatarUrl }
|
data: { avatar: avatarUrl },
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('上传头像错误:', error);
|
console.error('上传头像错误:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '上传头像失败'
|
message: '上传头像失败',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -553,7 +590,7 @@ router.delete('/:userId/avatar', authMiddleware, async (req, res) => {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '用户不存在'
|
message: '用户不存在',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -569,13 +606,13 @@ router.delete('/:userId/avatar', authMiddleware, async (req, res) => {
|
|||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: '头像删除成功'
|
message: '头像删除成功',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('删除头像错误:', error);
|
console.error('删除头像错误:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '删除头像失败'
|
message: '删除头像失败',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -587,14 +624,14 @@ router.put('/:userId/approve', authMiddleware, async (req, res) => {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '用户不存在'
|
message: '用户不存在',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.status !== 'pending') {
|
if (user.status !== 'pending') {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '该用户不在待审核状态'
|
message: '该用户不在待审核状态',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -607,14 +644,14 @@ router.put('/:userId/approve', authMiddleware, async (req, res) => {
|
|||||||
data: {
|
data: {
|
||||||
userId: user.userId,
|
userId: user.userId,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
status: user.status
|
status: user.status,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('审核用户错误:', error);
|
console.error('审核用户错误:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '审核用户失败'
|
message: '审核用户失败',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -626,14 +663,14 @@ router.put('/:userId/reject', authMiddleware, async (req, res) => {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '用户不存在'
|
message: '用户不存在',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.status !== 'pending') {
|
if (user.status !== 'pending') {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '该用户不在待审核状态'
|
message: '该用户不在待审核状态',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -646,14 +683,14 @@ router.put('/:userId/reject', authMiddleware, async (req, res) => {
|
|||||||
data: {
|
data: {
|
||||||
userId: user.userId,
|
userId: user.userId,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
status: user.status
|
status: user.status,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('拒绝用户错误:', error);
|
console.error('拒绝用户错误:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '操作失败'
|
message: '操作失败',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ const { logDeviceOperation } = require('../utils/operationLogger');
|
|||||||
async function generateWarehouseId() {
|
async function generateWarehouseId() {
|
||||||
const warehouses = await Warehouse.findAll({
|
const warehouses = await Warehouse.findAll({
|
||||||
where: {
|
where: {
|
||||||
warehouseId: { [Op.like]: 'WH%' }
|
warehouseId: { [Op.like]: 'WH%' },
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let maxNumber = 0;
|
let maxNumber = 0;
|
||||||
@@ -38,7 +38,7 @@ router.get('/', async (req, res) => {
|
|||||||
where[Op.or] = [
|
where[Op.or] = [
|
||||||
{ warehouseId: { [Op.like]: `%${keyword}%` } },
|
{ warehouseId: { [Op.like]: `%${keyword}%` } },
|
||||||
{ name: { [Op.like]: `%${keyword}%` } },
|
{ name: { [Op.like]: `%${keyword}%` } },
|
||||||
{ location: { [Op.like]: `%${keyword}%` } }
|
{ location: { [Op.like]: `%${keyword}%` } },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,17 +50,17 @@ router.get('/', async (req, res) => {
|
|||||||
where,
|
where,
|
||||||
offset: parseInt(offset),
|
offset: parseInt(offset),
|
||||||
limit: parseInt(pageSize),
|
limit: parseInt(pageSize),
|
||||||
order: [['createdAt', 'DESC']]
|
order: [['createdAt', 'DESC']],
|
||||||
});
|
});
|
||||||
|
|
||||||
const warehousesWithCount = await Promise.all(
|
const warehousesWithCount = await Promise.all(
|
||||||
rows.map(async (warehouse) => {
|
rows.map(async warehouse => {
|
||||||
const deviceCount = await Device.count({
|
const deviceCount = await Device.count({
|
||||||
where: { warehouseId: warehouse.warehouseId, isIdle: true }
|
where: { warehouseId: warehouse.warehouseId, isIdle: true },
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
...warehouse.toJSON(),
|
...warehouse.toJSON(),
|
||||||
deviceCount
|
deviceCount,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -69,7 +69,7 @@ router.get('/', async (req, res) => {
|
|||||||
total: count,
|
total: count,
|
||||||
warehouses: warehousesWithCount,
|
warehouses: warehousesWithCount,
|
||||||
page: parseInt(page),
|
page: parseInt(page),
|
||||||
pageSize: parseInt(pageSize)
|
pageSize: parseInt(pageSize),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取库房列表失败:', error);
|
console.error('获取库房列表失败:', error);
|
||||||
@@ -85,12 +85,12 @@ router.get('/:warehouseId', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deviceCount = await Device.count({
|
const deviceCount = await Device.count({
|
||||||
where: { warehouseId: warehouse.warehouseId, isIdle: true }
|
where: { warehouseId: warehouse.warehouseId, isIdle: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
...warehouse.toJSON(),
|
...warehouse.toJSON(),
|
||||||
deviceCount
|
deviceCount,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -111,14 +111,14 @@ router.get('/:warehouseId/devices', async (req, res) => {
|
|||||||
where: { warehouseId: req.params.warehouseId, isIdle: true },
|
where: { warehouseId: req.params.warehouseId, isIdle: true },
|
||||||
offset: parseInt(offset),
|
offset: parseInt(offset),
|
||||||
limit: parseInt(pageSize),
|
limit: parseInt(pageSize),
|
||||||
order: [['idleDate', 'DESC']]
|
order: [['idleDate', 'DESC']],
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
total: count,
|
total: count,
|
||||||
devices: rows,
|
devices: rows,
|
||||||
page: parseInt(page),
|
page: parseInt(page),
|
||||||
pageSize: parseInt(pageSize)
|
pageSize: parseInt(pageSize),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取库房设备失败:', error);
|
console.error('获取库房设备失败:', error);
|
||||||
@@ -142,7 +142,7 @@ router.post('/', async (req, res) => {
|
|||||||
location: location || '',
|
location: location || '',
|
||||||
capacity: capacity || 100,
|
capacity: capacity || 100,
|
||||||
status: 'active',
|
status: 'active',
|
||||||
description: description || ''
|
description: description || '',
|
||||||
});
|
});
|
||||||
|
|
||||||
await logDeviceOperation('create', `创建库房【${name}】`, {
|
await logDeviceOperation('create', `创建库房【${name}】`, {
|
||||||
@@ -150,7 +150,7 @@ router.post('/', async (req, res) => {
|
|||||||
targetName: name,
|
targetName: name,
|
||||||
afterState: warehouse.toJSON(),
|
afterState: warehouse.toJSON(),
|
||||||
req,
|
req,
|
||||||
metadata: { type: 'warehouse_create' }
|
metadata: { type: 'warehouse_create' },
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(201).json(warehouse);
|
res.status(201).json(warehouse);
|
||||||
@@ -169,11 +169,21 @@ router.put('/:warehouseId', async (req, res) => {
|
|||||||
const beforeState = warehouse.toJSON();
|
const beforeState = warehouse.toJSON();
|
||||||
const { name, location, capacity, status, description } = req.body;
|
const { name, location, capacity, status, description } = req.body;
|
||||||
|
|
||||||
if (name) warehouse.name = name;
|
if (name) {
|
||||||
if (location !== undefined) warehouse.location = location;
|
warehouse.name = name;
|
||||||
if (capacity !== undefined) warehouse.capacity = capacity;
|
}
|
||||||
if (status) warehouse.status = status;
|
if (location !== undefined) {
|
||||||
if (description !== undefined) warehouse.description = description;
|
warehouse.location = location;
|
||||||
|
}
|
||||||
|
if (capacity !== undefined) {
|
||||||
|
warehouse.capacity = capacity;
|
||||||
|
}
|
||||||
|
if (status) {
|
||||||
|
warehouse.status = status;
|
||||||
|
}
|
||||||
|
if (description !== undefined) {
|
||||||
|
warehouse.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
await warehouse.save();
|
await warehouse.save();
|
||||||
|
|
||||||
@@ -183,7 +193,7 @@ router.put('/:warehouseId', async (req, res) => {
|
|||||||
beforeState,
|
beforeState,
|
||||||
afterState: warehouse.toJSON(),
|
afterState: warehouse.toJSON(),
|
||||||
req,
|
req,
|
||||||
metadata: { type: 'warehouse_update' }
|
metadata: { type: 'warehouse_update' },
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json(warehouse);
|
res.json(warehouse);
|
||||||
@@ -200,12 +210,12 @@ router.delete('/:warehouseId', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const idleDeviceCount = await Device.count({
|
const idleDeviceCount = await Device.count({
|
||||||
where: { warehouseId: req.params.warehouseId, isIdle: true }
|
where: { warehouseId: req.params.warehouseId, isIdle: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (idleDeviceCount > 0) {
|
if (idleDeviceCount > 0) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
error: `库房中还有 ${idleDeviceCount} 台空闲设备,请先处理后再删除`
|
error: `库房中还有 ${idleDeviceCount} 台空闲设备,请先处理后再删除`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,7 +226,7 @@ router.delete('/:warehouseId', async (req, res) => {
|
|||||||
targetId: req.params.warehouseId,
|
targetId: req.params.warehouseId,
|
||||||
targetName: warehouseName,
|
targetName: warehouseName,
|
||||||
req,
|
req,
|
||||||
metadata: { type: 'warehouse_delete' }
|
metadata: { type: 'warehouse_delete' },
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({ message: '库房删除成功' });
|
res.json({ message: '库房删除成功' });
|
||||||
|
|||||||
@@ -5,10 +5,9 @@ async function addIsSystemColumn() {
|
|||||||
console.log('开始添加 isSystem 列到 deviceFields 表...');
|
console.log('开始添加 isSystem 列到 deviceFields 表...');
|
||||||
|
|
||||||
// 检查列是否已存在
|
// 检查列是否已存在
|
||||||
const tableInfo = await sequelize.query(
|
const tableInfo = await sequelize.query('PRAGMA table_info(deviceFields)', {
|
||||||
"PRAGMA table_info(deviceFields)",
|
type: sequelize.QueryTypes.SELECT,
|
||||||
{ type: sequelize.QueryTypes.SELECT }
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const hasIsSystemColumn = tableInfo.some(col => col.name === 'isSystem');
|
const hasIsSystemColumn = tableInfo.some(col => col.name === 'isSystem');
|
||||||
|
|
||||||
@@ -16,19 +15,28 @@ async function addIsSystemColumn() {
|
|||||||
console.log('isSystem 列已存在,跳过添加');
|
console.log('isSystem 列已存在,跳过添加');
|
||||||
} else {
|
} else {
|
||||||
// 添加 isSystem 列
|
// 添加 isSystem 列
|
||||||
await sequelize.query(
|
await sequelize.query('ALTER TABLE deviceFields ADD COLUMN isSystem BOOLEAN DEFAULT 0', {
|
||||||
"ALTER TABLE deviceFields ADD COLUMN isSystem BOOLEAN DEFAULT 0",
|
type: sequelize.QueryTypes.RAW,
|
||||||
{ type: sequelize.QueryTypes.RAW }
|
});
|
||||||
);
|
|
||||||
console.log('isSystem 列添加成功');
|
console.log('isSystem 列添加成功');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新现有数据:将核心字段标记为系统字段
|
// 更新现有数据:将核心字段标记为系统字段
|
||||||
const systemFields = [
|
const systemFields = [
|
||||||
'deviceId', 'name', 'type', 'model', 'serialNumber',
|
'deviceId',
|
||||||
'rackId', 'position', 'height', 'powerConsumption',
|
'name',
|
||||||
'status', 'purchaseDate', 'warrantyExpiry',
|
'type',
|
||||||
'ipAddress', 'description'
|
'model',
|
||||||
|
'serialNumber',
|
||||||
|
'rackId',
|
||||||
|
'position',
|
||||||
|
'height',
|
||||||
|
'powerConsumption',
|
||||||
|
'status',
|
||||||
|
'purchaseDate',
|
||||||
|
'warrantyExpiry',
|
||||||
|
'ipAddress',
|
||||||
|
'description',
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const fieldName of systemFields) {
|
for (const fieldName of systemFields) {
|
||||||
|
|||||||
@@ -16,61 +16,61 @@ async function migrate() {
|
|||||||
userId: {
|
userId: {
|
||||||
type: sequelize.Sequelize.STRING,
|
type: sequelize.Sequelize.STRING,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
username: {
|
username: {
|
||||||
type: sequelize.Sequelize.STRING,
|
type: sequelize.Sequelize.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true
|
unique: true,
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
type: sequelize.Sequelize.STRING,
|
type: sequelize.Sequelize.STRING,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
type: sequelize.Sequelize.STRING,
|
type: sequelize.Sequelize.STRING,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
phone: {
|
phone: {
|
||||||
type: sequelize.Sequelize.STRING,
|
type: sequelize.Sequelize.STRING,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
realName: {
|
realName: {
|
||||||
type: sequelize.Sequelize.STRING,
|
type: sequelize.Sequelize.STRING,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
avatar: {
|
avatar: {
|
||||||
type: sequelize.Sequelize.STRING,
|
type: sequelize.Sequelize.STRING,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: sequelize.Sequelize.ENUM('active', 'inactive', 'locked', 'pending'),
|
type: sequelize.Sequelize.ENUM('active', 'inactive', 'locked', 'pending'),
|
||||||
defaultValue: 'active'
|
defaultValue: 'active',
|
||||||
},
|
},
|
||||||
lastLoginTime: {
|
lastLoginTime: {
|
||||||
type: sequelize.Sequelize.DATE,
|
type: sequelize.Sequelize.DATE,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
lastLoginIp: {
|
lastLoginIp: {
|
||||||
type: sequelize.Sequelize.STRING,
|
type: sequelize.Sequelize.STRING,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
loginCount: {
|
loginCount: {
|
||||||
type: sequelize.Sequelize.INTEGER,
|
type: sequelize.Sequelize.INTEGER,
|
||||||
defaultValue: 0
|
defaultValue: 0,
|
||||||
},
|
},
|
||||||
remark: {
|
remark: {
|
||||||
type: sequelize.Sequelize.TEXT,
|
type: sequelize.Sequelize.TEXT,
|
||||||
allowNull: true
|
allowNull: true,
|
||||||
},
|
},
|
||||||
createdAt: {
|
createdAt: {
|
||||||
type: sequelize.Sequelize.DATE,
|
type: sequelize.Sequelize.DATE,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
updatedAt: {
|
updatedAt: {
|
||||||
type: sequelize.Sequelize.DATE,
|
type: sequelize.Sequelize.DATE,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. 复制数据
|
// 2. 复制数据
|
||||||
|
|||||||
@@ -104,14 +104,12 @@ async function migrateSQLite() {
|
|||||||
{ name: 'idx_archive_consumable_id', fields: 'consumableId' },
|
{ name: 'idx_archive_consumable_id', fields: 'consumableId' },
|
||||||
{ name: 'idx_archive_archive_id', fields: 'archiveId' },
|
{ name: 'idx_archive_archive_id', fields: 'archiveId' },
|
||||||
{ name: 'idx_archive_deleted_at', fields: 'deletedAt' },
|
{ name: 'idx_archive_deleted_at', fields: 'deletedAt' },
|
||||||
{ name: 'idx_archive_consumable_deleted', fields: 'consumableId, deletedAt' }
|
{ name: 'idx_archive_consumable_deleted', fields: 'consumableId, deletedAt' },
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const idx of indexes) {
|
for (const idx of indexes) {
|
||||||
try {
|
try {
|
||||||
await sequelize.query(
|
await sequelize.query(`CREATE INDEX ${idx.name} ON consumable_log_archives(${idx.fields})`);
|
||||||
`CREATE INDEX ${idx.name} ON consumable_log_archives(${idx.fields})`
|
|
||||||
);
|
|
||||||
console.log(`✓ 创建索引: ${idx.name}`);
|
console.log(`✓ 创建索引: ${idx.name}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(`! 创建索引失败: ${idx.name}`, err.message);
|
console.log(`! 创建索引失败: ${idx.name}`, err.message);
|
||||||
|
|||||||
@@ -120,7 +120,17 @@ async function updateExistingLogs() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const consumables = await Consumable.findAll({
|
const consumables = await Consumable.findAll({
|
||||||
attributes: ['consumableId', 'category', 'unit', 'unitPrice', 'supplier', 'location', 'minStock', 'maxStock', 'status']
|
attributes: [
|
||||||
|
'consumableId',
|
||||||
|
'category',
|
||||||
|
'unit',
|
||||||
|
'unitPrice',
|
||||||
|
'supplier',
|
||||||
|
'location',
|
||||||
|
'minStock',
|
||||||
|
'maxStock',
|
||||||
|
'status',
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const consumableMap = new Map();
|
const consumableMap = new Map();
|
||||||
@@ -133,14 +143,14 @@ async function updateExistingLogs() {
|
|||||||
location: c.location,
|
location: c.location,
|
||||||
minStock: c.minStock,
|
minStock: c.minStock,
|
||||||
maxStock: c.maxStock,
|
maxStock: c.maxStock,
|
||||||
status: c.status
|
status: c.status,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const logs = await ConsumableLog.findAll({
|
const logs = await ConsumableLog.findAll({
|
||||||
where: {
|
where: {
|
||||||
consumableSnapshot: null
|
consumableSnapshot: null,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let updatedCount = 0;
|
let updatedCount = 0;
|
||||||
@@ -155,10 +165,9 @@ async function updateExistingLogs() {
|
|||||||
console.log(`✓ 更新了 ${updatedCount} 条日志的快照信息`);
|
console.log(`✓ 更新了 ${updatedCount} 条日志的快照信息`);
|
||||||
|
|
||||||
const deletedLogsCount = await ConsumableLog.count({
|
const deletedLogsCount = await ConsumableLog.count({
|
||||||
where: { operationType: 'delete' }
|
where: { operationType: 'delete' },
|
||||||
});
|
});
|
||||||
console.log(`✓ 当前有 ${deletedLogsCount} 条删除类型日志`);
|
console.log(`✓ 当前有 ${deletedLogsCount} 条删除类型日志`);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('! 更新现有日志数据时出错:', error.message);
|
console.log('! 更新现有日志数据时出错:', error.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,18 +16,15 @@ async function migrate() {
|
|||||||
|
|
||||||
if (actualDbType === 'sqlite') {
|
if (actualDbType === 'sqlite') {
|
||||||
// SQLite: 检查字段是否存在
|
// SQLite: 检查字段是否存在
|
||||||
const tableInfo = await sequelize.query(
|
const tableInfo = await sequelize.query('PRAGMA table_info(consumables)', {
|
||||||
"PRAGMA table_info(consumables)",
|
type: sequelize.QueryTypes.SELECT,
|
||||||
{ type: sequelize.QueryTypes.SELECT }
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const hasVersion = tableInfo.some(col => col.name === 'version');
|
const hasVersion = tableInfo.some(col => col.name === 'version');
|
||||||
|
|
||||||
if (!hasVersion) {
|
if (!hasVersion) {
|
||||||
console.log('添加 version 字段...');
|
console.log('添加 version 字段...');
|
||||||
await sequelize.query(
|
await sequelize.query('ALTER TABLE consumables ADD COLUMN version INTEGER DEFAULT 0');
|
||||||
"ALTER TABLE consumables ADD COLUMN version INTEGER DEFAULT 0"
|
|
||||||
);
|
|
||||||
console.log('version 字段添加成功');
|
console.log('version 字段添加成功');
|
||||||
} else {
|
} else {
|
||||||
console.log('version 字段已存在,跳过');
|
console.log('version 字段已存在,跳过');
|
||||||
@@ -43,14 +40,11 @@ async function migrate() {
|
|||||||
|
|
||||||
if (!hasUpdatedAtIndex) {
|
if (!hasUpdatedAtIndex) {
|
||||||
console.log('添加 updatedAt 索引...');
|
console.log('添加 updatedAt 索引...');
|
||||||
await sequelize.query(
|
await sequelize.query('CREATE INDEX consumables_updatedAt ON consumables(updatedAt)');
|
||||||
"CREATE INDEX consumables_updatedAt ON consumables(updatedAt)"
|
|
||||||
);
|
|
||||||
console.log('updatedAt 索引添加成功');
|
console.log('updatedAt 索引添加成功');
|
||||||
} else {
|
} else {
|
||||||
console.log('updatedAt 索引已存在,跳过');
|
console.log('updatedAt 索引已存在,跳过');
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (actualDbType === 'mysql') {
|
} else if (actualDbType === 'mysql') {
|
||||||
// MySQL: 检查并添加字段
|
// MySQL: 检查并添加字段
|
||||||
try {
|
try {
|
||||||
@@ -70,9 +64,7 @@ async function migrate() {
|
|||||||
// 添加索引
|
// 添加索引
|
||||||
try {
|
try {
|
||||||
console.log('添加 updatedAt 索引...');
|
console.log('添加 updatedAt 索引...');
|
||||||
await sequelize.query(
|
await sequelize.query('CREATE INDEX idx_consumables_updatedAt ON consumables(updatedAt)');
|
||||||
"CREATE INDEX idx_consumables_updatedAt ON consumables(updatedAt)"
|
|
||||||
);
|
|
||||||
console.log('updatedAt 索引添加成功');
|
console.log('updatedAt 索引添加成功');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.message.includes('Duplicate key')) {
|
if (err.message.includes('Duplicate key')) {
|
||||||
@@ -85,9 +77,7 @@ async function migrate() {
|
|||||||
|
|
||||||
// 初始化现有数据的 version 值
|
// 初始化现有数据的 version 值
|
||||||
console.log('初始化现有数据的 version 值...');
|
console.log('初始化现有数据的 version 值...');
|
||||||
await sequelize.query(
|
await sequelize.query('UPDATE consumables SET version = 0 WHERE version IS NULL');
|
||||||
"UPDATE consumables SET version = 0 WHERE version IS NULL"
|
|
||||||
);
|
|
||||||
console.log('version 值初始化完成');
|
console.log('version 值初始化完成');
|
||||||
|
|
||||||
console.log('迁移完成!');
|
console.log('迁移完成!');
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ async function migrate() {
|
|||||||
console.log('');
|
console.log('');
|
||||||
console.log('📊 验证迁移结果...');
|
console.log('📊 验证迁移结果...');
|
||||||
|
|
||||||
const [tables] = await sequelize.query("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name");
|
const [tables] = await sequelize.query(
|
||||||
|
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
|
||||||
|
);
|
||||||
console.log(` 数据库表: ${tables.map(t => t.name).join(', ')}`);
|
console.log(` 数据库表: ${tables.map(t => t.name).join(', ')}`);
|
||||||
|
|
||||||
const portCount = await DevicePort.count();
|
const portCount = await DevicePort.count();
|
||||||
@@ -47,7 +49,6 @@ async function migrate() {
|
|||||||
console.log('========================================');
|
console.log('========================================');
|
||||||
console.log(' 迁移成功完成!🎉');
|
console.log(' 迁移成功完成!🎉');
|
||||||
console.log('========================================');
|
console.log('========================================');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error('❌ 迁移失败:', error.message);
|
console.error('❌ 迁移失败:', error.message);
|
||||||
@@ -88,7 +89,6 @@ async function migrateSQLite() {
|
|||||||
await NetworkCard.sync({ force: false });
|
await NetworkCard.sync({ force: false });
|
||||||
|
|
||||||
await sequelize.query('PRAGMA foreign_keys = ON');
|
await sequelize.query('PRAGMA foreign_keys = ON');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await sequelize.query('PRAGMA foreign_keys = ON');
|
await sequelize.query('PRAGMA foreign_keys = ON');
|
||||||
throw error;
|
throw error;
|
||||||
@@ -124,10 +124,9 @@ async function migrateMySQL() {
|
|||||||
await sequelize.query(createTableSQL);
|
await sequelize.query(createTableSQL);
|
||||||
|
|
||||||
console.log(' → 检查 nic_id 字段是否存在...');
|
console.log(' → 检查 nic_id 字段是否存在...');
|
||||||
const [columns] = await sequelize.query(
|
const [columns] = await sequelize.query("SHOW COLUMNS FROM `device_ports` LIKE 'nic_id'", {
|
||||||
"SHOW COLUMNS FROM `device_ports` LIKE 'nic_id'",
|
type: sequelize.QueryTypes.SELECT,
|
||||||
{ type: sequelize.QueryTypes.SELECT }
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (columns.length === 0) {
|
if (columns.length === 0) {
|
||||||
console.log(' → 添加 nic_id 字段...');
|
console.log(' → 添加 nic_id 字段...');
|
||||||
|
|||||||
@@ -55,10 +55,9 @@ async function removeSQLiteForeignKey() {
|
|||||||
console.log('SQLite 不支持直接删除外键,需要重建表');
|
console.log('SQLite 不支持直接删除外键,需要重建表');
|
||||||
|
|
||||||
// 检查外键是否存在
|
// 检查外键是否存在
|
||||||
const fks = await sequelize.query(
|
const fks = await sequelize.query(`PRAGMA foreign_key_list(consumable_logs);`, {
|
||||||
`PRAGMA foreign_key_list(consumable_logs);`,
|
type: sequelize.QueryTypes.SELECT,
|
||||||
{ type: sequelize.QueryTypes.SELECT }
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (!fks || fks.length === 0) {
|
if (!fks || fks.length === 0) {
|
||||||
console.log('✓ 没有外键约束需要移除');
|
console.log('✓ 没有外键约束需要移除');
|
||||||
@@ -68,17 +67,21 @@ async function removeSQLiteForeignKey() {
|
|||||||
console.log('发现外键约束:', fks);
|
console.log('发现外键约束:', fks);
|
||||||
|
|
||||||
// SQLite 不支持 ALTER TABLE DROP FOREIGN KEY,需要重建表
|
// SQLite 不支持 ALTER TABLE DROP FOREIGN KEY,需要重建表
|
||||||
await sequelize.transaction(async (transaction) => {
|
await sequelize.transaction(async transaction => {
|
||||||
// 获取表结构
|
// 获取表结构
|
||||||
const columns = await sequelize.query(
|
const columns = await sequelize.query(`PRAGMA table_info(consumable_logs);`, {
|
||||||
`PRAGMA table_info(consumable_logs);`,
|
type: sequelize.QueryTypes.SELECT,
|
||||||
{ type: sequelize.QueryTypes.SELECT, transaction }
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'\n当前表列:',
|
||||||
|
columns.map(c => c.name)
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('\n当前表列:', columns.map(c => c.name));
|
|
||||||
|
|
||||||
// 创建新表(不包含外键约束)
|
// 创建新表(不包含外键约束)
|
||||||
await sequelize.query(`
|
await sequelize.query(
|
||||||
|
`
|
||||||
CREATE TABLE consumable_logs_new (
|
CREATE TABLE consumable_logs_new (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
consumableId VARCHAR(255) NOT NULL,
|
consumableId VARCHAR(255) NOT NULL,
|
||||||
@@ -101,20 +104,25 @@ async function removeSQLiteForeignKey() {
|
|||||||
isConsumableDeleted BOOLEAN DEFAULT 0,
|
isConsumableDeleted BOOLEAN DEFAULT 0,
|
||||||
consumableSnapshot TEXT
|
consumableSnapshot TEXT
|
||||||
)
|
)
|
||||||
`, { transaction });
|
`,
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
|
||||||
console.log('✓ 创建新表成功');
|
console.log('✓ 创建新表成功');
|
||||||
|
|
||||||
// 复制数据
|
// 复制数据
|
||||||
await sequelize.query(`
|
await sequelize.query(
|
||||||
|
`
|
||||||
INSERT INTO consumable_logs_new
|
INSERT INTO consumable_logs_new
|
||||||
SELECT * FROM consumable_logs
|
SELECT * FROM consumable_logs
|
||||||
`, { transaction });
|
`,
|
||||||
|
{ transaction }
|
||||||
const countResult = await sequelize.query(
|
|
||||||
`SELECT COUNT(*) as count FROM consumable_logs_new`,
|
|
||||||
{ type: sequelize.QueryTypes.SELECT, transaction }
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const countResult = await sequelize.query(`SELECT COUNT(*) as count FROM consumable_logs_new`, {
|
||||||
|
type: sequelize.QueryTypes.SELECT,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
console.log(`✓ 复制了 ${countResult[0].count} 条数据`);
|
console.log(`✓ 复制了 ${countResult[0].count} 条数据`);
|
||||||
|
|
||||||
// 删除旧表
|
// 删除旧表
|
||||||
@@ -122,7 +130,9 @@ async function removeSQLiteForeignKey() {
|
|||||||
console.log('✓ 删除旧表成功');
|
console.log('✓ 删除旧表成功');
|
||||||
|
|
||||||
// 重命名新表
|
// 重命名新表
|
||||||
await sequelize.query(`ALTER TABLE consumable_logs_new RENAME TO consumable_logs`, { transaction });
|
await sequelize.query(`ALTER TABLE consumable_logs_new RENAME TO consumable_logs`, {
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
console.log('✓ 重命名新表成功');
|
console.log('✓ 重命名新表成功');
|
||||||
|
|
||||||
// 创建索引
|
// 创建索引
|
||||||
@@ -133,7 +143,7 @@ async function removeSQLiteForeignKey() {
|
|||||||
{ name: 'consumable_logs_consumable_id_created_at', fields: ['consumableId', 'createdAt'] },
|
{ name: 'consumable_logs_consumable_id_created_at', fields: ['consumableId', 'createdAt'] },
|
||||||
{ name: 'consumable_logs_original_log_id', fields: ['originalLogId'] },
|
{ name: 'consumable_logs_original_log_id', fields: ['originalLogId'] },
|
||||||
{ name: 'consumable_logs_is_editable', fields: ['isEditable'] },
|
{ name: 'consumable_logs_is_editable', fields: ['isEditable'] },
|
||||||
{ name: 'idx_consumable_logs_is_deleted', fields: ['isConsumableDeleted'] }
|
{ name: 'idx_consumable_logs_is_deleted', fields: ['isConsumableDeleted'] },
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const idx of indexes) {
|
for (const idx of indexes) {
|
||||||
|
|||||||
@@ -8,21 +8,22 @@ async function ensureSchema() {
|
|||||||
|
|
||||||
if (dbType === 'sqlite') {
|
if (dbType === 'sqlite') {
|
||||||
// Check if nicId column exists in device_ports
|
// Check if nicId column exists in device_ports
|
||||||
const [columns] = await sequelize.query("PRAGMA table_info(device_ports)");
|
const [columns] = await sequelize.query('PRAGMA table_info(device_ports)');
|
||||||
const hasNicId = columns.some(col => col.name === 'nicId');
|
const hasNicId = columns.some(col => col.name === 'nicId');
|
||||||
|
|
||||||
if (!hasNicId) {
|
if (!hasNicId) {
|
||||||
console.log('Adding nicId column to device_ports...');
|
console.log('Adding nicId column to device_ports...');
|
||||||
await sequelize.query('ALTER TABLE device_ports ADD COLUMN nicId VARCHAR(255) NULL REFERENCES network_cards(nicId)');
|
await sequelize.query(
|
||||||
|
'ALTER TABLE device_ports ADD COLUMN nicId VARCHAR(255) NULL REFERENCES network_cards(nicId)'
|
||||||
|
);
|
||||||
console.log('Added nicId column.');
|
console.log('Added nicId column.');
|
||||||
} else {
|
} else {
|
||||||
console.log('nicId column already exists in device_ports.');
|
console.log('nicId column already exists in device_ports.');
|
||||||
}
|
}
|
||||||
} else if (dbType === 'mysql') {
|
} else if (dbType === 'mysql') {
|
||||||
const [columns] = await sequelize.query(
|
const [columns] = await sequelize.query("SHOW COLUMNS FROM `device_ports` LIKE 'nicId'", {
|
||||||
"SHOW COLUMNS FROM `device_ports` LIKE 'nicId'",
|
type: QueryTypes.SELECT,
|
||||||
{ type: QueryTypes.SELECT }
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (columns.length === 0) {
|
if (columns.length === 0) {
|
||||||
console.log('Adding nicId column to device_ports...');
|
console.log('Adding nicId column to device_ports...');
|
||||||
|
|||||||
@@ -4,98 +4,91 @@ const path = require('path');
|
|||||||
const templateData = [
|
const templateData = [
|
||||||
{
|
{
|
||||||
'机柜ID(留空自动生成)': '',
|
'机柜ID(留空自动生成)': '',
|
||||||
'机柜名称': 'IDC2-SERVER-01',
|
机柜名称: 'IDC2-SERVER-01',
|
||||||
'所属机房名称': 'IDC2',
|
所属机房名称: 'IDC2',
|
||||||
'高度(U)': 42,
|
'高度(U)': 42,
|
||||||
'最大功率(W)': 5000,
|
'最大功率(W)': 5000,
|
||||||
'状态': 'active'
|
状态: 'active',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'机柜ID(留空自动生成)': '',
|
'机柜ID(留空自动生成)': '',
|
||||||
'机柜名称': 'IDC2-SERVER-02',
|
机柜名称: 'IDC2-SERVER-02',
|
||||||
'所属机房名称': 'IDC2',
|
所属机房名称: 'IDC2',
|
||||||
'高度(U)': 42,
|
'高度(U)': 42,
|
||||||
'最大功率(W)': 5000,
|
'最大功率(W)': 5000,
|
||||||
'状态': 'active'
|
状态: 'active',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'机柜ID(留空自动生成)': '',
|
'机柜ID(留空自动生成)': '',
|
||||||
'机柜名称': 'IDC2-SERVER-03',
|
机柜名称: 'IDC2-SERVER-03',
|
||||||
'所属机房名称': 'IDC2',
|
所属机房名称: 'IDC2',
|
||||||
'高度(U)': 42,
|
'高度(U)': 42,
|
||||||
'最大功率(W)': 5000,
|
'最大功率(W)': 5000,
|
||||||
'状态': 'active'
|
状态: 'active',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'机柜ID(留空自动生成)': '',
|
'机柜ID(留空自动生成)': '',
|
||||||
'机柜名称': 'IDC4-NETWORK-01',
|
机柜名称: 'IDC4-NETWORK-01',
|
||||||
'所属机房名称': 'IDC4',
|
所属机房名称: 'IDC4',
|
||||||
'高度(U)': 48,
|
'高度(U)': 48,
|
||||||
'最大功率(W)': 8000,
|
'最大功率(W)': 8000,
|
||||||
'状态': 'active'
|
状态: 'active',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'机柜ID(留空自动生成)': '',
|
'机柜ID(留空自动生成)': '',
|
||||||
'机柜名称': 'IDC4-NETWORK-02',
|
机柜名称: 'IDC4-NETWORK-02',
|
||||||
'所属机房名称': 'IDC4',
|
所属机房名称: 'IDC4',
|
||||||
'高度(U)': 48,
|
'高度(U)': 48,
|
||||||
'最大功率(W)': 8000,
|
'最大功率(W)': 8000,
|
||||||
'状态': 'maintenance'
|
状态: 'maintenance',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'机柜ID(留空自动生成)': '',
|
'机柜ID(留空自动生成)': '',
|
||||||
'机柜名称': 'IDC5-STORAGE-01',
|
机柜名称: 'IDC5-STORAGE-01',
|
||||||
'所属机房名称': 'IDC5',
|
所属机房名称: 'IDC5',
|
||||||
'高度(U)': 42,
|
'高度(U)': 42,
|
||||||
'最大功率(W)': 10000,
|
'最大功率(W)': 10000,
|
||||||
'状态': 'active'
|
状态: 'active',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'机柜ID(留空自动生成)': '',
|
'机柜ID(留空自动生成)': '',
|
||||||
'机柜名称': 'IDC5-STORAGE-02',
|
机柜名称: 'IDC5-STORAGE-02',
|
||||||
'所属机房名称': 'IDC5',
|
所属机房名称: 'IDC5',
|
||||||
'高度(U)': 42,
|
'高度(U)': 42,
|
||||||
'最大功率(W)': 10000,
|
'最大功率(W)': 10000,
|
||||||
'状态': 'inactive'
|
状态: 'inactive',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'机柜ID(留空自动生成)': '',
|
'机柜ID(留空自动生成)': '',
|
||||||
'机柜名称': 'IDC7-SERVER-01',
|
机柜名称: 'IDC7-SERVER-01',
|
||||||
'所属机房名称': 'IDC7',
|
所属机房名称: 'IDC7',
|
||||||
'高度(U)': 36,
|
'高度(U)': 36,
|
||||||
'最大功率(W)': 6000,
|
'最大功率(W)': 6000,
|
||||||
'状态': 'active'
|
状态: 'active',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'机柜ID(留空自动生成)': '',
|
'机柜ID(留空自动生成)': '',
|
||||||
'机柜名称': '古荡-机柜-01',
|
机柜名称: '古荡-机柜-01',
|
||||||
'所属机房名称': '古荡机房1-1',
|
所属机房名称: '古荡机房1-1',
|
||||||
'高度(U)': 42,
|
'高度(U)': 42,
|
||||||
'最大功率(W)': 5000,
|
'最大功率(W)': 5000,
|
||||||
'状态': 'active'
|
状态: 'active',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'机柜ID(留空自动生成)': '',
|
'机柜ID(留空自动生成)': '',
|
||||||
'机柜名称': '古荡-机柜-02',
|
机柜名称: '古荡-机柜-02',
|
||||||
'所属机房名称': '古荡机房1-1',
|
所属机房名称: '古荡机房1-1',
|
||||||
'高度(U)': 42,
|
'高度(U)': 42,
|
||||||
'最大功率(W)': 5000,
|
'最大功率(W)': 5000,
|
||||||
'状态': 'active'
|
状态: 'active',
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const wb = XLSX.utils.book_new();
|
const wb = XLSX.utils.book_new();
|
||||||
|
|
||||||
const ws = XLSX.utils.json_to_sheet(templateData);
|
const ws = XLSX.utils.json_to_sheet(templateData);
|
||||||
|
|
||||||
ws['!cols'] = [
|
ws['!cols'] = [{ wch: 20 }, { wch: 20 }, { wch: 15 }, { wch: 10 }, { wch: 15 }, { wch: 15 }];
|
||||||
{ wch: 20 },
|
|
||||||
{ wch: 20 },
|
|
||||||
{ wch: 15 },
|
|
||||||
{ wch: 10 },
|
|
||||||
{ wch: 15 },
|
|
||||||
{ wch: 15 }
|
|
||||||
];
|
|
||||||
|
|
||||||
XLSX.utils.book_append_sheet(wb, ws, '机柜导入模板');
|
XLSX.utils.book_append_sheet(wb, ws, '机柜导入模板');
|
||||||
|
|
||||||
@@ -107,7 +100,9 @@ console.log(`共 ${templateData.length} 条测试数据`);
|
|||||||
console.log('\n字段说明:');
|
console.log('\n字段说明:');
|
||||||
console.log('- 机柜ID(留空自动生成): 留空则系统自动生成唯一ID');
|
console.log('- 机柜ID(留空自动生成): 留空则系统自动生成唯一ID');
|
||||||
console.log('- 机柜名称: 必填,机柜的唯一标识名称');
|
console.log('- 机柜名称: 必填,机柜的唯一标识名称');
|
||||||
console.log('- 所属机房名称: 必填,系统现有机房: IDC2, IDC4, IDC5, IDC7, 古荡机房1-1, 三墩机房1-1, 地市机房, IDCT');
|
console.log(
|
||||||
|
'- 所属机房名称: 必填,系统现有机房: IDC2, IDC4, IDC5, IDC7, 古荡机房1-1, 三墩机房1-1, 地市机房, IDCT'
|
||||||
|
);
|
||||||
console.log('- 高度(U): 必填,机柜的标准高度(1-50U)');
|
console.log('- 高度(U): 必填,机柜的标准高度(1-50U)');
|
||||||
console.log('- 最大功率(W): 必填,机柜的最大承载功率');
|
console.log('- 最大功率(W): 必填,机柜的最大承载功率');
|
||||||
console.log('- 状态: active(在用)/maintenance(维护中)/inactive(停用)');
|
console.log('- 状态: active(在用)/maintenance(维护中)/inactive(停用)');
|
||||||
@@ -26,33 +26,194 @@ const InventoryRecord = require('../models/InventoryRecord');
|
|||||||
// 默认系统设置(包含中文描述)
|
// 默认系统设置(包含中文描述)
|
||||||
const defaultSettings = [
|
const defaultSettings = [
|
||||||
// 全局配置
|
// 全局配置
|
||||||
{ settingKey: 'site_name', settingValue: JSON.stringify('机柜管理系统'), settingType: 'string', category: 'general', description: '网站名称', isEditable: true },
|
{
|
||||||
{ settingKey: 'site_logo', settingValue: JSON.stringify(''), settingType: 'string', category: 'general', description: '网站Logo URL', isEditable: true },
|
settingKey: 'site_name',
|
||||||
{ settingKey: 'timezone', settingValue: JSON.stringify('Asia/Shanghai'), settingType: 'string', category: 'general', description: '时区设置', isEditable: true },
|
settingValue: JSON.stringify('机柜管理系统'),
|
||||||
{ settingKey: 'date_format', settingValue: JSON.stringify('YYYY-MM-DD'), settingType: 'string', category: 'general', description: '日期格式', isEditable: true },
|
settingType: 'string',
|
||||||
{ settingKey: 'session_timeout', settingValue: JSON.stringify(30), settingType: 'number', category: 'general', description: '登录有效期(分钟)', isEditable: true },
|
category: 'general',
|
||||||
{ settingKey: 'idle_timeout', settingValue: JSON.stringify(10), settingType: 'number', category: 'general', description: '用户空闲超时时间(分钟)', isEditable: true },
|
description: '网站名称',
|
||||||
{ settingKey: 'idle_warning_time', settingValue: JSON.stringify(10), settingType: 'number', category: 'general', description: '空闲超时前警告时间(秒)', isEditable: false },
|
isEditable: true,
|
||||||
{ settingKey: 'max_login_attempts', settingValue: JSON.stringify(5), settingType: 'number', category: 'general', description: '最大登录尝试次数', isEditable: true },
|
},
|
||||||
{ settingKey: 'maintenance_mode', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'general', description: '维护模式', isEditable: true },
|
{
|
||||||
|
settingKey: 'site_logo',
|
||||||
|
settingValue: JSON.stringify(''),
|
||||||
|
settingType: 'string',
|
||||||
|
category: 'general',
|
||||||
|
description: '网站Logo URL',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'timezone',
|
||||||
|
settingValue: JSON.stringify('Asia/Shanghai'),
|
||||||
|
settingType: 'string',
|
||||||
|
category: 'general',
|
||||||
|
description: '时区设置',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'date_format',
|
||||||
|
settingValue: JSON.stringify('YYYY-MM-DD'),
|
||||||
|
settingType: 'string',
|
||||||
|
category: 'general',
|
||||||
|
description: '日期格式',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'session_timeout',
|
||||||
|
settingValue: JSON.stringify(30),
|
||||||
|
settingType: 'number',
|
||||||
|
category: 'general',
|
||||||
|
description: '登录有效期(分钟)',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'idle_timeout',
|
||||||
|
settingValue: JSON.stringify(10),
|
||||||
|
settingType: 'number',
|
||||||
|
category: 'general',
|
||||||
|
description: '用户空闲超时时间(分钟)',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'idle_warning_time',
|
||||||
|
settingValue: JSON.stringify(10),
|
||||||
|
settingType: 'number',
|
||||||
|
category: 'general',
|
||||||
|
description: '空闲超时前警告时间(秒)',
|
||||||
|
isEditable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'max_login_attempts',
|
||||||
|
settingValue: JSON.stringify(5),
|
||||||
|
settingType: 'number',
|
||||||
|
category: 'general',
|
||||||
|
description: '最大登录尝试次数',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'maintenance_mode',
|
||||||
|
settingValue: JSON.stringify(false),
|
||||||
|
settingType: 'boolean',
|
||||||
|
category: 'general',
|
||||||
|
description: '维护模式',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
|
||||||
// 外观设置
|
// 外观设置
|
||||||
{ settingKey: 'primary_color', settingValue: JSON.stringify('#667eea'), settingType: 'string', category: 'appearance', description: '主题主色调', isEditable: true },
|
{
|
||||||
{ settingKey: 'secondary_color', settingValue: JSON.stringify('#764ba2'), settingType: 'string', category: 'appearance', description: '主题辅助色调', isEditable: true },
|
settingKey: 'primary_color',
|
||||||
{ settingKey: 'compact_mode', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'appearance', description: '紧凑模式', isEditable: true },
|
settingValue: JSON.stringify('#667eea'),
|
||||||
{ settingKey: 'sidebar_collapsed', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'appearance', description: '侧边栏默认折叠', isEditable: true },
|
settingType: 'string',
|
||||||
{ settingKey: 'table_row_height', settingValue: JSON.stringify('default'), settingType: 'string', category: 'appearance', description: '表格行高: small/default/middle/large', isEditable: true },
|
category: 'appearance',
|
||||||
{ settingKey: 'animation_enabled', settingValue: JSON.stringify(true), settingType: 'boolean', category: 'appearance', description: '启用动画效果', isEditable: true },
|
description: '主题主色调',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'secondary_color',
|
||||||
|
settingValue: JSON.stringify('#764ba2'),
|
||||||
|
settingType: 'string',
|
||||||
|
category: 'appearance',
|
||||||
|
description: '主题辅助色调',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'compact_mode',
|
||||||
|
settingValue: JSON.stringify(false),
|
||||||
|
settingType: 'boolean',
|
||||||
|
category: 'appearance',
|
||||||
|
description: '紧凑模式',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'sidebar_collapsed',
|
||||||
|
settingValue: JSON.stringify(false),
|
||||||
|
settingType: 'boolean',
|
||||||
|
category: 'appearance',
|
||||||
|
description: '侧边栏默认折叠',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'table_row_height',
|
||||||
|
settingValue: JSON.stringify('default'),
|
||||||
|
settingType: 'string',
|
||||||
|
category: 'appearance',
|
||||||
|
description: '表格行高: small/default/middle/large',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'animation_enabled',
|
||||||
|
settingValue: JSON.stringify(true),
|
||||||
|
settingType: 'boolean',
|
||||||
|
category: 'appearance',
|
||||||
|
description: '启用动画效果',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
|
||||||
// 关于页面
|
// 关于页面
|
||||||
{ settingKey: 'app_version', settingValue: JSON.stringify('1.0.0'), settingType: 'string', category: 'about', description: '应用版本', isEditable: false },
|
{
|
||||||
{ settingKey: 'company_name', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '公司/组织名称', isEditable: true },
|
settingKey: 'app_version',
|
||||||
{ settingKey: 'contact_email', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '联系邮箱', isEditable: true },
|
settingValue: JSON.stringify('1.0.0'),
|
||||||
{ settingKey: 'contact_phone', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '联系电话', isEditable: true },
|
settingType: 'string',
|
||||||
{ settingKey: 'company_address', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '公司地址', isEditable: true },
|
category: 'about',
|
||||||
{ settingKey: 'system_description', settingValue: JSON.stringify('机柜管理系统 - 专业的数据中心设备管理解决方案'), settingType: 'string', category: 'about', description: '系统描述', isEditable: true },
|
description: '应用版本',
|
||||||
{ settingKey: 'privacy_policy', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '隐私政策URL', isEditable: true },
|
isEditable: false,
|
||||||
{ settingKey: 'terms_of_service', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '服务条款URL', isEditable: true },
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'company_name',
|
||||||
|
settingValue: JSON.stringify(''),
|
||||||
|
settingType: 'string',
|
||||||
|
category: 'about',
|
||||||
|
description: '公司/组织名称',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'contact_email',
|
||||||
|
settingValue: JSON.stringify(''),
|
||||||
|
settingType: 'string',
|
||||||
|
category: 'about',
|
||||||
|
description: '联系邮箱',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'contact_phone',
|
||||||
|
settingValue: JSON.stringify(''),
|
||||||
|
settingType: 'string',
|
||||||
|
category: 'about',
|
||||||
|
description: '联系电话',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'company_address',
|
||||||
|
settingValue: JSON.stringify(''),
|
||||||
|
settingType: 'string',
|
||||||
|
category: 'about',
|
||||||
|
description: '公司地址',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'system_description',
|
||||||
|
settingValue: JSON.stringify('机柜管理系统 - 专业的数据中心设备管理解决方案'),
|
||||||
|
settingType: 'string',
|
||||||
|
category: 'about',
|
||||||
|
description: '系统描述',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'privacy_policy',
|
||||||
|
settingValue: JSON.stringify(''),
|
||||||
|
settingType: 'string',
|
||||||
|
category: 'about',
|
||||||
|
description: '隐私政策URL',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingKey: 'terms_of_service',
|
||||||
|
settingValue: JSON.stringify(''),
|
||||||
|
settingType: 'string',
|
||||||
|
category: 'about',
|
||||||
|
description: '服务条款URL',
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
async function initDatabase() {
|
async function initDatabase() {
|
||||||
|
|||||||
+113
-92
@@ -28,73 +28,73 @@ const migrations = [
|
|||||||
{
|
{
|
||||||
name: 'v2.0 - 网卡和端口表',
|
name: 'v2.0 - 网卡和端口表',
|
||||||
description: '创建 network_cards 表,为 device_ports 添加 nic_id 字段',
|
description: '创建 network_cards 表,为 device_ports 添加 nic_id 字段',
|
||||||
migrate: migrateV2
|
migrate: migrateV2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '用户表 pending 状态',
|
name: '用户表 pending 状态',
|
||||||
description: '为用户表添加 pending 状态支持',
|
description: '为用户表添加 pending 状态支持',
|
||||||
migrate: migratePendingStatus
|
migrate: migratePendingStatus,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '耗材乐观锁',
|
name: '耗材乐观锁',
|
||||||
description: '为 consumables 表添加 version 字段',
|
description: '为 consumables 表添加 version 字段',
|
||||||
migrate: migrateConsumableVersion
|
migrate: migrateConsumableVersion,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '耗材操作日志表结构',
|
name: '耗材操作日志表结构',
|
||||||
description: '添加 isEditable、originalLogId 等修改记录字段',
|
description: '添加 isEditable、originalLogId 等修改记录字段',
|
||||||
migrate: migrateConsumableLogs
|
migrate: migrateConsumableLogs,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '耗材日志解耦',
|
name: '耗材日志解耦',
|
||||||
description: '添加 isConsumableDeleted 和 consumableSnapshot 字段',
|
description: '添加 isConsumableDeleted 和 consumableSnapshot 字段',
|
||||||
migrate: migrateConsumableLogDecouple
|
migrate: migrateConsumableLogDecouple,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '移除日志外键约束',
|
name: '移除日志外键约束',
|
||||||
description: '移除 consumable_logs 表的外键约束,防止级联删除',
|
description: '移除 consumable_logs 表的外键约束,防止级联删除',
|
||||||
migrate: removeConsumableLogFK
|
migrate: removeConsumableLogFK,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '耗材日志归档表',
|
name: '耗材日志归档表',
|
||||||
description: '创建 consumable_log_archives 归档表',
|
description: '创建 consumable_log_archives 归档表',
|
||||||
migrate: migrateConsumableLogArchive
|
migrate: migrateConsumableLogArchive,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '耗材SN序列号字段',
|
name: '耗材SN序列号字段',
|
||||||
description: '为 consumables、consumable_records、consumable_logs 添加 snList 字段',
|
description: '为 consumables、consumable_records、consumable_logs 添加 snList 字段',
|
||||||
migrate: migrateSnList
|
migrate: migrateSnList,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '设备型号字段可空',
|
name: '设备型号字段可空',
|
||||||
description: '将 devices 表 model 字段改为可空,支持非必填',
|
description: '将 devices 表 model 字段改为可空,支持非必填',
|
||||||
migrate: migrateDeviceModelField
|
migrate: migrateDeviceModelField,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '设备字段配置同步',
|
name: '设备字段配置同步',
|
||||||
description: '同步前后端字段必填配置',
|
description: '同步前后端字段必填配置',
|
||||||
migrate: migrateDeviceFieldsConfig
|
migrate: migrateDeviceFieldsConfig,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '设备表字段可空',
|
name: '设备表字段可空',
|
||||||
description: '将设备表所有字段改为可空,由应用层验证控制',
|
description: '将设备表所有字段改为可空,由应用层验证控制',
|
||||||
migrate: migrateDeviceFieldsNullable
|
migrate: migrateDeviceFieldsNullable,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '暂存设备自定义字段',
|
name: '暂存设备自定义字段',
|
||||||
description: '为 pending_devices 表添加 customFields 字段,支持自定义字段存储',
|
description: '为 pending_devices 表添加 customFields 字段,支持自定义字段存储',
|
||||||
migrate: migratePendingDeviceCustomFields
|
migrate: migratePendingDeviceCustomFields,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '空闲设备与业务关联',
|
name: '空闲设备与业务关联',
|
||||||
description: '创建 businesses、warehouses、device_business 表,为 devices 添加空闲设备字段',
|
description: '创建 businesses、warehouses、device_business 表,为 devices 添加空闲设备字段',
|
||||||
migrate: migrateIdleDeviceAndBusiness
|
migrate: migrateIdleDeviceAndBusiness,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '设备字段系统标记',
|
name: '设备字段系统标记',
|
||||||
description: '为 deviceFields 表添加 isSystem 字段,标记系统字段不可删除',
|
description: '为 deviceFields 表添加 isSystem 字段,标记系统字段不可删除',
|
||||||
migrate: migrateDeviceFieldsIsSystem
|
migrate: migrateDeviceFieldsIsSystem,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
async function runMigrations() {
|
async function runMigrations() {
|
||||||
@@ -151,16 +151,14 @@ async function getTableColumns(tableName) {
|
|||||||
const dialect = sequelize.getDialect();
|
const dialect = sequelize.getDialect();
|
||||||
|
|
||||||
if (dialect === 'sqlite') {
|
if (dialect === 'sqlite') {
|
||||||
const tableInfo = await sequelize.query(
|
const tableInfo = await sequelize.query(`PRAGMA table_info(${tableName})`, {
|
||||||
`PRAGMA table_info(${tableName})`,
|
type: sequelize.QueryTypes.SELECT,
|
||||||
{ type: sequelize.QueryTypes.SELECT }
|
});
|
||||||
);
|
|
||||||
return tableInfo.map(col => col.name);
|
return tableInfo.map(col => col.name);
|
||||||
} else {
|
} else {
|
||||||
const tableInfo = await sequelize.query(
|
const tableInfo = await sequelize.query(`SHOW COLUMNS FROM ${tableName}`, {
|
||||||
`SHOW COLUMNS FROM ${tableName}`,
|
type: sequelize.QueryTypes.SELECT,
|
||||||
{ type: sequelize.QueryTypes.SELECT }
|
});
|
||||||
);
|
|
||||||
return tableInfo.map(col => col.Field);
|
return tableInfo.map(col => col.Field);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,10 +173,10 @@ async function tableExists(tableName) {
|
|||||||
);
|
);
|
||||||
return tables.length > 0;
|
return tables.length > 0;
|
||||||
} else {
|
} else {
|
||||||
const tables = await sequelize.query(
|
const tables = await sequelize.query('SHOW TABLES LIKE ?', {
|
||||||
"SHOW TABLES LIKE ?",
|
replacements: [tableName],
|
||||||
{ replacements: [tableName], type: sequelize.QueryTypes.SELECT }
|
type: sequelize.QueryTypes.SELECT,
|
||||||
);
|
});
|
||||||
return tables.length > 0;
|
return tables.length > 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,7 +186,8 @@ async function addColumnIfNotExists(tableName, columnName, columnDef) {
|
|||||||
|
|
||||||
if (!columns.includes(columnName)) {
|
if (!columns.includes(columnName)) {
|
||||||
const dialect = sequelize.getDialect();
|
const dialect = sequelize.getDialect();
|
||||||
const sql = dialect === 'sqlite'
|
const sql =
|
||||||
|
dialect === 'sqlite'
|
||||||
? `ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnDef}`
|
? `ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnDef}`
|
||||||
: `ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnDef}`;
|
: `ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnDef}`;
|
||||||
await sequelize.query(sql);
|
await sequelize.query(sql);
|
||||||
@@ -210,32 +209,32 @@ async function migrateV2() {
|
|||||||
id: {
|
id: {
|
||||||
type: sequelize.Sequelize.INTEGER,
|
type: sequelize.Sequelize.INTEGER,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
autoIncrement: true
|
autoIncrement: true,
|
||||||
},
|
},
|
||||||
nicId: {
|
nicId: {
|
||||||
type: sequelize.Sequelize.STRING,
|
type: sequelize.Sequelize.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true
|
unique: true,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
type: sequelize.Sequelize.STRING,
|
type: sequelize.Sequelize.STRING,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
macAddress: {
|
macAddress: {
|
||||||
type: sequelize.Sequelize.STRING
|
type: sequelize.Sequelize.STRING,
|
||||||
},
|
},
|
||||||
ipAddress: {
|
ipAddress: {
|
||||||
type: sequelize.Sequelize.STRING
|
type: sequelize.Sequelize.STRING,
|
||||||
},
|
},
|
||||||
deviceId: {
|
deviceId: {
|
||||||
type: sequelize.Sequelize.STRING
|
type: sequelize.Sequelize.STRING,
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: sequelize.Sequelize.STRING,
|
type: sequelize.Sequelize.STRING,
|
||||||
defaultValue: 'active'
|
defaultValue: 'active',
|
||||||
},
|
},
|
||||||
createdAt: sequelize.Sequelize.DATE,
|
createdAt: sequelize.Sequelize.DATE,
|
||||||
updatedAt: sequelize.Sequelize.DATE
|
updatedAt: sequelize.Sequelize.DATE,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,10 +277,9 @@ async function removeConsumableLogFK() {
|
|||||||
const dialect = sequelize.getDialect();
|
const dialect = sequelize.getDialect();
|
||||||
|
|
||||||
if (dialect === 'sqlite') {
|
if (dialect === 'sqlite') {
|
||||||
const fks = await sequelize.query(
|
const fks = await sequelize.query(`PRAGMA foreign_key_list(consumable_logs);`, {
|
||||||
`PRAGMA foreign_key_list(consumable_logs);`,
|
type: sequelize.QueryTypes.SELECT,
|
||||||
{ type: sequelize.QueryTypes.SELECT }
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (!fks || fks.length === 0) {
|
if (!fks || fks.length === 0) {
|
||||||
return;
|
return;
|
||||||
@@ -327,7 +325,9 @@ async function removeConsumableLogFK() {
|
|||||||
await sequelize.query(`CREATE INDEX idx_logs_consumable_id ON consumable_logs(consumableId)`);
|
await sequelize.query(`CREATE INDEX idx_logs_consumable_id ON consumable_logs(consumableId)`);
|
||||||
await sequelize.query(`CREATE INDEX idx_logs_operation_type ON consumable_logs(operationType)`);
|
await sequelize.query(`CREATE INDEX idx_logs_operation_type ON consumable_logs(operationType)`);
|
||||||
await sequelize.query(`CREATE INDEX idx_logs_created_at ON consumable_logs(createdAt)`);
|
await sequelize.query(`CREATE INDEX idx_logs_created_at ON consumable_logs(createdAt)`);
|
||||||
await sequelize.query(`CREATE INDEX idx_logs_is_consumable_deleted ON consumable_logs(isConsumableDeleted)`);
|
await sequelize.query(
|
||||||
|
`CREATE INDEX idx_logs_is_consumable_deleted ON consumable_logs(isConsumableDeleted)`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,71 +341,77 @@ async function migrateConsumableLogArchive() {
|
|||||||
id: {
|
id: {
|
||||||
type: sequelize.Sequelize.INTEGER,
|
type: sequelize.Sequelize.INTEGER,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
autoIncrement: true
|
autoIncrement: true,
|
||||||
},
|
},
|
||||||
archiveId: {
|
archiveId: {
|
||||||
type: sequelize.Sequelize.STRING,
|
type: sequelize.Sequelize.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true
|
unique: true,
|
||||||
},
|
},
|
||||||
consumableId: {
|
consumableId: {
|
||||||
type: sequelize.Sequelize.STRING,
|
type: sequelize.Sequelize.STRING,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
consumableName: {
|
consumableName: {
|
||||||
type: sequelize.Sequelize.STRING,
|
type: sequelize.Sequelize.STRING,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
consumableSnapshot: {
|
consumableSnapshot: {
|
||||||
type: sequelize.Sequelize.TEXT
|
type: sequelize.Sequelize.TEXT,
|
||||||
},
|
},
|
||||||
totalOperations: {
|
totalOperations: {
|
||||||
type: sequelize.Sequelize.INTEGER,
|
type: sequelize.Sequelize.INTEGER,
|
||||||
defaultValue: 0
|
defaultValue: 0,
|
||||||
},
|
},
|
||||||
firstOperationAt: {
|
firstOperationAt: {
|
||||||
type: sequelize.Sequelize.DATE
|
type: sequelize.Sequelize.DATE,
|
||||||
},
|
},
|
||||||
lastOperationAt: {
|
lastOperationAt: {
|
||||||
type: sequelize.Sequelize.DATE
|
type: sequelize.Sequelize.DATE,
|
||||||
},
|
},
|
||||||
totalInQuantity: {
|
totalInQuantity: {
|
||||||
type: sequelize.Sequelize.INTEGER,
|
type: sequelize.Sequelize.INTEGER,
|
||||||
defaultValue: 0
|
defaultValue: 0,
|
||||||
},
|
},
|
||||||
totalOutQuantity: {
|
totalOutQuantity: {
|
||||||
type: sequelize.Sequelize.INTEGER,
|
type: sequelize.Sequelize.INTEGER,
|
||||||
defaultValue: 0
|
defaultValue: 0,
|
||||||
},
|
},
|
||||||
finalStock: {
|
finalStock: {
|
||||||
type: sequelize.Sequelize.INTEGER,
|
type: sequelize.Sequelize.INTEGER,
|
||||||
defaultValue: 0
|
defaultValue: 0,
|
||||||
},
|
},
|
||||||
deletedBy: {
|
deletedBy: {
|
||||||
type: sequelize.Sequelize.STRING
|
type: sequelize.Sequelize.STRING,
|
||||||
},
|
},
|
||||||
deletedAt: {
|
deletedAt: {
|
||||||
type: sequelize.Sequelize.DATE
|
type: sequelize.Sequelize.DATE,
|
||||||
},
|
},
|
||||||
deleteReason: {
|
deleteReason: {
|
||||||
type: sequelize.Sequelize.STRING
|
type: sequelize.Sequelize.STRING,
|
||||||
},
|
},
|
||||||
createdAt: {
|
createdAt: {
|
||||||
type: sequelize.Sequelize.DATE,
|
type: sequelize.Sequelize.DATE,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: sequelize.Sequelize.literal('CURRENT_TIMESTAMP')
|
defaultValue: sequelize.Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||||
},
|
},
|
||||||
updatedAt: {
|
updatedAt: {
|
||||||
type: sequelize.Sequelize.DATE,
|
type: sequelize.Sequelize.DATE,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: sequelize.Sequelize.literal('CURRENT_TIMESTAMP')
|
defaultValue: sequelize.Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (dbDialect === 'sqlite') {
|
if (dbDialect === 'sqlite') {
|
||||||
await sequelize.query(`CREATE INDEX idx_archive_consumable_id ON consumable_log_archives(consumableId)`);
|
await sequelize.query(
|
||||||
await sequelize.query(`CREATE INDEX idx_archive_archive_id ON consumable_log_archives(archiveId)`);
|
`CREATE INDEX idx_archive_consumable_id ON consumable_log_archives(consumableId)`
|
||||||
await sequelize.query(`CREATE INDEX idx_archive_deleted_at ON consumable_log_archives(deletedAt)`);
|
);
|
||||||
|
await sequelize.query(
|
||||||
|
`CREATE INDEX idx_archive_archive_id ON consumable_log_archives(archiveId)`
|
||||||
|
);
|
||||||
|
await sequelize.query(
|
||||||
|
`CREATE INDEX idx_archive_deleted_at ON consumable_log_archives(deletedAt)`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,7 +420,7 @@ async function migrateSnList() {
|
|||||||
|
|
||||||
for (const table of tables) {
|
for (const table of tables) {
|
||||||
if (await tableExists(table)) {
|
if (await tableExists(table)) {
|
||||||
const columnDef = dbDialect === 'sqlite' ? "TEXT DEFAULT '[]'" : "JSON";
|
const columnDef = dbDialect === 'sqlite' ? "TEXT DEFAULT '[]'" : 'JSON';
|
||||||
await addColumnIfNotExists(table, 'snList', columnDef);
|
await addColumnIfNotExists(table, 'snList', columnDef);
|
||||||
} else {
|
} else {
|
||||||
console.log(` ${table} 表不存在,跳过`);
|
console.log(` ${table} 表不存在,跳过`);
|
||||||
@@ -431,9 +437,7 @@ async function migrateDeviceModelField() {
|
|||||||
const dialect = sequelize.getDialect();
|
const dialect = sequelize.getDialect();
|
||||||
|
|
||||||
if (dialect === 'mysql') {
|
if (dialect === 'mysql') {
|
||||||
await sequelize.query(
|
await sequelize.query('ALTER TABLE devices MODIFY COLUMN model VARCHAR(255) NULL');
|
||||||
'ALTER TABLE devices MODIFY COLUMN model VARCHAR(255) NULL'
|
|
||||||
);
|
|
||||||
console.log(' devices 表 model 字段已改为可空');
|
console.log(' devices 表 model 字段已改为可空');
|
||||||
} else if (dialect === 'sqlite') {
|
} else if (dialect === 'sqlite') {
|
||||||
const columns = await getTableColumns('devices');
|
const columns = await getTableColumns('devices');
|
||||||
@@ -478,15 +482,15 @@ async function migrateDeviceFieldsNullable() {
|
|||||||
|
|
||||||
if (dialect === 'mysql') {
|
if (dialect === 'mysql') {
|
||||||
const alterCommands = [
|
const alterCommands = [
|
||||||
"ALTER TABLE devices MODIFY COLUMN name VARCHAR(255) NULL",
|
'ALTER TABLE devices MODIFY COLUMN name VARCHAR(255) NULL',
|
||||||
"ALTER TABLE devices MODIFY COLUMN type VARCHAR(255) NULL",
|
'ALTER TABLE devices MODIFY COLUMN type VARCHAR(255) NULL',
|
||||||
"ALTER TABLE devices MODIFY COLUMN model VARCHAR(255) NULL",
|
'ALTER TABLE devices MODIFY COLUMN model VARCHAR(255) NULL',
|
||||||
"ALTER TABLE devices MODIFY COLUMN serialNumber VARCHAR(255) NULL",
|
'ALTER TABLE devices MODIFY COLUMN serialNumber VARCHAR(255) NULL',
|
||||||
"ALTER TABLE devices MODIFY COLUMN rackId VARCHAR(255) NULL",
|
'ALTER TABLE devices MODIFY COLUMN rackId VARCHAR(255) NULL',
|
||||||
"ALTER TABLE devices MODIFY COLUMN position INTEGER NULL",
|
'ALTER TABLE devices MODIFY COLUMN position INTEGER NULL',
|
||||||
"ALTER TABLE devices MODIFY COLUMN height INTEGER NULL",
|
'ALTER TABLE devices MODIFY COLUMN height INTEGER NULL',
|
||||||
"ALTER TABLE devices MODIFY COLUMN powerConsumption FLOAT NULL",
|
'ALTER TABLE devices MODIFY COLUMN powerConsumption FLOAT NULL',
|
||||||
"ALTER TABLE devices MODIFY COLUMN customFields JSON NULL"
|
'ALTER TABLE devices MODIFY COLUMN customFields JSON NULL',
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const sql of alterCommands) {
|
for (const sql of alterCommands) {
|
||||||
@@ -499,7 +503,6 @@ async function migrateDeviceFieldsNullable() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(' devices 表字段已改为可空');
|
console.log(' devices 表字段已改为可空');
|
||||||
|
|
||||||
} else if (dialect === 'sqlite') {
|
} else if (dialect === 'sqlite') {
|
||||||
const columns = await getTableColumns('devices');
|
const columns = await getTableColumns('devices');
|
||||||
const hasNullableFlag = columns.includes('_nullable_migration_done');
|
const hasNullableFlag = columns.includes('_nullable_migration_done');
|
||||||
@@ -571,7 +574,7 @@ async function migratePendingDeviceCustomFields() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnDef = dbDialect === 'sqlite' ? "JSON DEFAULT '{}'" : "JSON";
|
const columnDef = dbDialect === 'sqlite' ? "JSON DEFAULT '{}'" : 'JSON';
|
||||||
await addColumnIfNotExists('pending_devices', 'customFields', columnDef);
|
await addColumnIfNotExists('pending_devices', 'customFields', columnDef);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -605,10 +608,9 @@ async function migrateDeviceFieldsIsSystem() {
|
|||||||
} else {
|
} else {
|
||||||
const columns = await getTableColumns('deviceFields');
|
const columns = await getTableColumns('deviceFields');
|
||||||
if (!columns.includes('isSystem')) {
|
if (!columns.includes('isSystem')) {
|
||||||
await sequelize.query(
|
await sequelize.query('ALTER TABLE deviceFields ADD COLUMN isSystem BOOLEAN DEFAULT 0', {
|
||||||
"ALTER TABLE deviceFields ADD COLUMN isSystem BOOLEAN DEFAULT 0",
|
type: sequelize.QueryTypes.RAW,
|
||||||
{ type: sequelize.QueryTypes.RAW }
|
});
|
||||||
);
|
|
||||||
console.log(' deviceFields 表添加 isSystem 字段成功');
|
console.log(' deviceFields 表添加 isSystem 字段成功');
|
||||||
} else {
|
} else {
|
||||||
console.log(' deviceFields 表 isSystem 字段已存在,跳过');
|
console.log(' deviceFields 表 isSystem 字段已存在,跳过');
|
||||||
@@ -616,16 +618,25 @@ async function migrateDeviceFieldsIsSystem() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const systemFields = [
|
const systemFields = [
|
||||||
'deviceId', 'name', 'type', 'model', 'serialNumber',
|
'deviceId',
|
||||||
'rackId', 'position', 'height', 'powerConsumption',
|
'name',
|
||||||
'status', 'purchaseDate', 'warrantyExpiry'
|
'type',
|
||||||
|
'model',
|
||||||
|
'serialNumber',
|
||||||
|
'rackId',
|
||||||
|
'position',
|
||||||
|
'height',
|
||||||
|
'powerConsumption',
|
||||||
|
'status',
|
||||||
|
'purchaseDate',
|
||||||
|
'warrantyExpiry',
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const fieldName of systemFields) {
|
for (const fieldName of systemFields) {
|
||||||
await sequelize.query(
|
await sequelize.query(`UPDATE deviceFields SET isSystem = 1 WHERE fieldName = ?`, {
|
||||||
`UPDATE deviceFields SET isSystem = 1 WHERE fieldName = ?`,
|
replacements: [fieldName],
|
||||||
{ replacements: [fieldName], type: sequelize.QueryTypes.RAW }
|
type: sequelize.QueryTypes.RAW,
|
||||||
);
|
});
|
||||||
console.log(` 标记系统字段: ${fieldName}`);
|
console.log(` 标记系统字段: ${fieldName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -643,14 +654,19 @@ async function migrateIdleDeviceAndBusiness() {
|
|||||||
try {
|
try {
|
||||||
if (!(await tableExists('businesses'))) {
|
if (!(await tableExists('businesses'))) {
|
||||||
await queryInterface.createTable('businesses', {
|
await queryInterface.createTable('businesses', {
|
||||||
businessId: { type: sequelize.Sequelize.STRING, primaryKey: true, allowNull: false, unique: true },
|
businessId: {
|
||||||
|
type: sequelize.Sequelize.STRING,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true,
|
||||||
|
},
|
||||||
name: { type: sequelize.Sequelize.STRING, allowNull: false },
|
name: { type: sequelize.Sequelize.STRING, allowNull: false },
|
||||||
description: { type: sequelize.Sequelize.TEXT },
|
description: { type: sequelize.Sequelize.TEXT },
|
||||||
status: { type: sequelize.Sequelize.ENUM('active', 'offline'), defaultValue: 'active' },
|
status: { type: sequelize.Sequelize.ENUM('active', 'offline'), defaultValue: 'active' },
|
||||||
offlineDate: { type: sequelize.Sequelize.DATE },
|
offlineDate: { type: sequelize.Sequelize.DATE },
|
||||||
offlineReason: { type: sequelize.Sequelize.STRING },
|
offlineReason: { type: sequelize.Sequelize.STRING },
|
||||||
createdAt: { type: sequelize.Sequelize.DATE, allowNull: false },
|
createdAt: { type: sequelize.Sequelize.DATE, allowNull: false },
|
||||||
updatedAt: { type: sequelize.Sequelize.DATE, allowNull: false }
|
updatedAt: { type: sequelize.Sequelize.DATE, allowNull: false },
|
||||||
});
|
});
|
||||||
console.log(' businesses 表创建成功');
|
console.log(' businesses 表创建成功');
|
||||||
} else {
|
} else {
|
||||||
@@ -659,14 +675,19 @@ async function migrateIdleDeviceAndBusiness() {
|
|||||||
|
|
||||||
if (!(await tableExists('warehouses'))) {
|
if (!(await tableExists('warehouses'))) {
|
||||||
await queryInterface.createTable('warehouses', {
|
await queryInterface.createTable('warehouses', {
|
||||||
warehouseId: { type: sequelize.Sequelize.STRING, primaryKey: true, allowNull: false, unique: true },
|
warehouseId: {
|
||||||
|
type: sequelize.Sequelize.STRING,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true,
|
||||||
|
},
|
||||||
name: { type: sequelize.Sequelize.STRING, allowNull: false },
|
name: { type: sequelize.Sequelize.STRING, allowNull: false },
|
||||||
location: { type: sequelize.Sequelize.STRING },
|
location: { type: sequelize.Sequelize.STRING },
|
||||||
capacity: { type: sequelize.Sequelize.INTEGER, defaultValue: 100 },
|
capacity: { type: sequelize.Sequelize.INTEGER, defaultValue: 100 },
|
||||||
status: { type: sequelize.Sequelize.ENUM('active', 'inactive'), defaultValue: 'active' },
|
status: { type: sequelize.Sequelize.ENUM('active', 'inactive'), defaultValue: 'active' },
|
||||||
description: { type: sequelize.Sequelize.TEXT },
|
description: { type: sequelize.Sequelize.TEXT },
|
||||||
createdAt: { type: sequelize.Sequelize.DATE, allowNull: false },
|
createdAt: { type: sequelize.Sequelize.DATE, allowNull: false },
|
||||||
updatedAt: { type: sequelize.Sequelize.DATE, allowNull: false }
|
updatedAt: { type: sequelize.Sequelize.DATE, allowNull: false },
|
||||||
});
|
});
|
||||||
console.log(' warehouses 表创建成功');
|
console.log(' warehouses 表创建成功');
|
||||||
} else {
|
} else {
|
||||||
@@ -680,7 +701,7 @@ async function migrateIdleDeviceAndBusiness() {
|
|||||||
businessId: { type: sequelize.Sequelize.STRING, allowNull: false },
|
businessId: { type: sequelize.Sequelize.STRING, allowNull: false },
|
||||||
isPrimary: { type: sequelize.Sequelize.BOOLEAN, defaultValue: false },
|
isPrimary: { type: sequelize.Sequelize.BOOLEAN, defaultValue: false },
|
||||||
createdAt: { type: sequelize.Sequelize.DATE, allowNull: false },
|
createdAt: { type: sequelize.Sequelize.DATE, allowNull: false },
|
||||||
updatedAt: { type: sequelize.Sequelize.DATE, allowNull: false }
|
updatedAt: { type: sequelize.Sequelize.DATE, allowNull: false },
|
||||||
});
|
});
|
||||||
console.log(' device_business 表创建成功');
|
console.log(' device_business 表创建成功');
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -131,10 +131,10 @@ async function runRestore() {
|
|||||||
skipFiles: options.skipFiles,
|
skipFiles: options.skipFiles,
|
||||||
onProgress: (tableName, status, count) => {
|
onProgress: (tableName, status, count) => {
|
||||||
const statusMap = {
|
const statusMap = {
|
||||||
'restored': '✓ 已恢复',
|
restored: '✓ 已恢复',
|
||||||
'skipped': '○ 已跳过',
|
skipped: '○ 已跳过',
|
||||||
'empty': '- 无数据',
|
empty: '- 无数据',
|
||||||
'error': '✗ 错误',
|
error: '✗ 错误',
|
||||||
};
|
};
|
||||||
const statusText = statusMap[status] || status;
|
const statusText = statusMap[status] || status;
|
||||||
const countText = count ? ` (${count} 条)` : '';
|
const countText = count ? ` (${count} 条)` : '';
|
||||||
|
|||||||
@@ -6,21 +6,27 @@ async function updateSystemFields() {
|
|||||||
|
|
||||||
// 核心系统字段(数据库必填字段,不可删除)
|
// 核心系统字段(数据库必填字段,不可删除)
|
||||||
const coreSystemFields = [
|
const coreSystemFields = [
|
||||||
'deviceId', 'name', 'type', 'model', 'serialNumber',
|
'deviceId',
|
||||||
'rackId', 'position', 'height', 'powerConsumption',
|
'name',
|
||||||
'status', 'purchaseDate', 'warrantyExpiry'
|
'type',
|
||||||
|
'model',
|
||||||
|
'serialNumber',
|
||||||
|
'rackId',
|
||||||
|
'position',
|
||||||
|
'height',
|
||||||
|
'powerConsumption',
|
||||||
|
'status',
|
||||||
|
'purchaseDate',
|
||||||
|
'warrantyExpiry',
|
||||||
];
|
];
|
||||||
|
|
||||||
// 可选字段(非系统字段,可删除)
|
// 可选字段(非系统字段,可删除)
|
||||||
const optionalFields = [
|
const optionalFields = ['ipAddress', 'description', 'owner', 'department', 'assetId', 'brand'];
|
||||||
'ipAddress', 'description', 'owner', 'department', 'assetId', 'brand'
|
|
||||||
];
|
|
||||||
|
|
||||||
// 先将所有字段设为非系统字段
|
// 先将所有字段设为非系统字段
|
||||||
await sequelize.query(
|
await sequelize.query(`UPDATE deviceFields SET isSystem = 0`, {
|
||||||
`UPDATE deviceFields SET isSystem = 0`,
|
type: sequelize.QueryTypes.RAW,
|
||||||
{ type: sequelize.QueryTypes.RAW }
|
});
|
||||||
);
|
|
||||||
console.log('已重置所有字段为非系统字段');
|
console.log('已重置所有字段为非系统字段');
|
||||||
|
|
||||||
// 标记核心系统字段
|
// 标记核心系统字段
|
||||||
|
|||||||
@@ -6,10 +6,20 @@ async function updateSystemFields() {
|
|||||||
|
|
||||||
// 系统字段列表
|
// 系统字段列表
|
||||||
const systemFields = [
|
const systemFields = [
|
||||||
'deviceId', 'name', 'type', 'model', 'serialNumber',
|
'deviceId',
|
||||||
'rackId', 'position', 'height', 'powerConsumption',
|
'name',
|
||||||
'status', 'purchaseDate', 'warrantyExpiry',
|
'type',
|
||||||
'ipAddress', 'description'
|
'model',
|
||||||
|
'serialNumber',
|
||||||
|
'rackId',
|
||||||
|
'position',
|
||||||
|
'height',
|
||||||
|
'powerConsumption',
|
||||||
|
'status',
|
||||||
|
'purchaseDate',
|
||||||
|
'warrantyExpiry',
|
||||||
|
'ipAddress',
|
||||||
|
'description',
|
||||||
];
|
];
|
||||||
|
|
||||||
// 更新系统字段标记
|
// 更新系统字段标记
|
||||||
|
|||||||
+74
-24
@@ -23,7 +23,7 @@ async function syncDatabase() {
|
|||||||
|
|
||||||
await sequelize.sync({
|
await sequelize.sync({
|
||||||
force: false,
|
force: false,
|
||||||
alter: false
|
alter: false,
|
||||||
});
|
});
|
||||||
console.log('数据库表结构同步完成');
|
console.log('数据库表结构同步完成');
|
||||||
}
|
}
|
||||||
@@ -61,7 +61,7 @@ async function syncConsumableModels() {
|
|||||||
ConsumableLog.sync(),
|
ConsumableLog.sync(),
|
||||||
ConsumableCategory.sync(),
|
ConsumableCategory.sync(),
|
||||||
ConsumableRecord.sync(),
|
ConsumableRecord.sync(),
|
||||||
ConsumableLogArchive.sync()
|
ConsumableLogArchive.sync(),
|
||||||
]);
|
]);
|
||||||
console.log('耗材模型同步完成');
|
console.log('耗材模型同步完成');
|
||||||
}
|
}
|
||||||
@@ -71,11 +71,7 @@ async function syncInventoryModels() {
|
|||||||
const InventoryTask = require('./models/InventoryTask');
|
const InventoryTask = require('./models/InventoryTask');
|
||||||
const InventoryRecord = require('./models/InventoryRecord');
|
const InventoryRecord = require('./models/InventoryRecord');
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([InventoryPlan.sync(), InventoryTask.sync(), InventoryRecord.sync()]);
|
||||||
InventoryPlan.sync(),
|
|
||||||
InventoryTask.sync(),
|
|
||||||
InventoryRecord.sync()
|
|
||||||
]);
|
|
||||||
console.log('盘点模型同步完成');
|
console.log('盘点模型同步完成');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,16 +110,66 @@ async function initFaultCategories() {
|
|||||||
const FaultCategory = require('./models/FaultCategory');
|
const FaultCategory = require('./models/FaultCategory');
|
||||||
|
|
||||||
const defaultCategories = [
|
const defaultCategories = [
|
||||||
{ name: '系统故障', description: '操作系统、应用程序等系统软件的故障问题', priority: 1, defaultPriority: 'high' },
|
{
|
||||||
{ name: '硬件故障', description: '物理设备、服务器、存储等硬件设备的故障问题', priority: 2, defaultPriority: 'high' },
|
name: '系统故障',
|
||||||
{ name: '网络故障', description: '网络连接、交换机、路由器等网络相关故障', priority: 3, defaultPriority: 'high' },
|
description: '操作系统、应用程序等系统软件的故障问题',
|
||||||
{ name: '软件故障', description: '应用程序错误、软件兼容性等问题', priority: 4, defaultPriority: 'medium' },
|
priority: 1,
|
||||||
{ name: '安全事件', description: '安全漏洞、入侵检测、权限异常等安全问题', priority: 5, defaultPriority: 'urgent' },
|
defaultPriority: 'high',
|
||||||
{ name: '性能问题', description: '系统响应慢、资源利用率高等性能问题', priority: 6, defaultPriority: 'medium' },
|
},
|
||||||
{ name: '配置变更', description: '系统配置、软件配置等变更需求', priority: 7, defaultPriority: 'low' },
|
{
|
||||||
{ name: '例行维护', description: '定期维护、巡检、更新等计划性工作', priority: 8, defaultPriority: 'low' },
|
name: '硬件故障',
|
||||||
{ name: '数据问题', description: '数据错误、数据丢失、数据同步等数据相关问题', priority: 9, defaultPriority: 'high' },
|
description: '物理设备、服务器、存储等硬件设备的故障问题',
|
||||||
{ name: '其他问题', description: '无法归类的其他问题', priority: 99, defaultPriority: 'medium' }
|
priority: 2,
|
||||||
|
defaultPriority: 'high',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '网络故障',
|
||||||
|
description: '网络连接、交换机、路由器等网络相关故障',
|
||||||
|
priority: 3,
|
||||||
|
defaultPriority: 'high',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '软件故障',
|
||||||
|
description: '应用程序错误、软件兼容性等问题',
|
||||||
|
priority: 4,
|
||||||
|
defaultPriority: 'medium',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '安全事件',
|
||||||
|
description: '安全漏洞、入侵检测、权限异常等安全问题',
|
||||||
|
priority: 5,
|
||||||
|
defaultPriority: 'urgent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '性能问题',
|
||||||
|
description: '系统响应慢、资源利用率高等性能问题',
|
||||||
|
priority: 6,
|
||||||
|
defaultPriority: 'medium',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '配置变更',
|
||||||
|
description: '系统配置、软件配置等变更需求',
|
||||||
|
priority: 7,
|
||||||
|
defaultPriority: 'low',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '例行维护',
|
||||||
|
description: '定期维护、巡检、更新等计划性工作',
|
||||||
|
priority: 8,
|
||||||
|
defaultPriority: 'low',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '数据问题',
|
||||||
|
description: '数据错误、数据丢失、数据同步等数据相关问题',
|
||||||
|
priority: 9,
|
||||||
|
defaultPriority: 'high',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '其他问题',
|
||||||
|
description: '无法归类的其他问题',
|
||||||
|
priority: 99,
|
||||||
|
defaultPriority: 'medium',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const cat of defaultCategories) {
|
for (const cat of defaultCategories) {
|
||||||
@@ -136,7 +182,7 @@ async function initFaultCategories() {
|
|||||||
expectedDuration: 120,
|
expectedDuration: 120,
|
||||||
solutions: [],
|
solutions: [],
|
||||||
isSystem: true,
|
isSystem: true,
|
||||||
isActive: true
|
isActive: true,
|
||||||
});
|
});
|
||||||
console.log(`创建故障分类: ${cat.name}`);
|
console.log(`创建故障分类: ${cat.name}`);
|
||||||
}
|
}
|
||||||
@@ -234,7 +280,10 @@ app.use('/api/dangerous-operations', dangerousOperationsRoutes);
|
|||||||
|
|
||||||
app.use('/uploads', express.static('uploads'));
|
app.use('/uploads', express.static('uploads'));
|
||||||
|
|
||||||
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs, {
|
app.use(
|
||||||
|
'/api-docs',
|
||||||
|
swaggerUi.serve,
|
||||||
|
swaggerUi.setup(specs, {
|
||||||
customCss: customCSS,
|
customCss: customCSS,
|
||||||
customSiteTitle: 'IDC设备管理系统 API文档',
|
customSiteTitle: 'IDC设备管理系统 API文档',
|
||||||
swaggerOptions: {
|
swaggerOptions: {
|
||||||
@@ -243,9 +292,10 @@ app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs, {
|
|||||||
docExpansion: 'none',
|
docExpansion: 'none',
|
||||||
deepLinking: true,
|
deepLinking: true,
|
||||||
defaultModelsExpandDepth: -1,
|
defaultModelsExpandDepth: -1,
|
||||||
defaultModelExpandDepth: 2
|
defaultModelExpandDepth: 2,
|
||||||
}
|
},
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
|
|
||||||
app.get('/api-docs', (req, res) => {
|
app.get('/api-docs', (req, res) => {
|
||||||
res.sendFile(path.join(__dirname, 'swagger_index.html'));
|
res.sendFile(path.join(__dirname, 'swagger_index.html'));
|
||||||
@@ -279,10 +329,10 @@ app.get('/api', (req, res) => {
|
|||||||
roles: '/api/roles',
|
roles: '/api/roles',
|
||||||
systemSettings: '/api/system-settings',
|
systemSettings: '/api/system-settings',
|
||||||
background: '/api/background',
|
background: '/api/background',
|
||||||
inventory: '/api/inventory'
|
inventory: '/api/inventory',
|
||||||
},
|
},
|
||||||
health: '/health',
|
health: '/health',
|
||||||
documentation: '/docs/api/README.md'
|
documentation: '/docs/api/README.md',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+15
-13
@@ -515,14 +515,14 @@ const options = {
|
|||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
description: '数据中心设备管理平台后端服务 API 文档',
|
description: '数据中心设备管理平台后端服务 API 文档',
|
||||||
contact: {
|
contact: {
|
||||||
name: 'API Support'
|
name: 'API Support',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
servers: [
|
servers: [
|
||||||
{
|
{
|
||||||
url: 'http://localhost:8000',
|
url: 'http://localhost:8000',
|
||||||
description: '开发环境服务器'
|
description: '开发环境服务器',
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
components: {
|
components: {
|
||||||
securitySchemes: {
|
securitySchemes: {
|
||||||
@@ -530,13 +530,15 @@ const options = {
|
|||||||
type: 'http',
|
type: 'http',
|
||||||
scheme: 'bearer',
|
scheme: 'bearer',
|
||||||
bearerFormat: 'JWT',
|
bearerFormat: 'JWT',
|
||||||
description: '输入 JWT token'
|
description: '输入 JWT token',
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
security: [{
|
},
|
||||||
bearerAuth: []
|
},
|
||||||
}],
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
tags: [
|
tags: [
|
||||||
{ name: 'health', description: '健康检查' },
|
{ name: 'health', description: '健康检查' },
|
||||||
{ name: 'auth', description: '认证接口' },
|
{ name: 'auth', description: '认证接口' },
|
||||||
@@ -560,10 +562,10 @@ const options = {
|
|||||||
{ name: 'inventory', description: '盘点管理' },
|
{ name: 'inventory', description: '盘点管理' },
|
||||||
{ name: 'statistics', description: '统计接口' },
|
{ name: 'statistics', description: '统计接口' },
|
||||||
{ name: 'operation-logs', description: '操作日志' },
|
{ name: 'operation-logs', description: '操作日志' },
|
||||||
{ name: 'backup', description: '备份管理' }
|
{ name: 'backup', description: '备份管理' },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
apis: ['./routes/*.js', './swagger_docs.yaml']
|
apis: ['./routes/*.js', './swagger_docs.yaml'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const specs = swaggerJsdoc(options);
|
const specs = swaggerJsdoc(options);
|
||||||
|
|||||||
@@ -28,14 +28,14 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
username: 'admin',
|
username: 'admin',
|
||||||
password: '$2a$10$test',
|
password: '$2a$10$test',
|
||||||
realName: '管理员',
|
realName: '管理员',
|
||||||
status: 'active'
|
status: 'active',
|
||||||
});
|
});
|
||||||
|
|
||||||
testRoom = await Room.create({
|
testRoom = await Room.create({
|
||||||
roomId: 'ROOM_INT_TEST',
|
roomId: 'ROOM_INT_TEST',
|
||||||
name: '测试机房',
|
name: '测试机房',
|
||||||
location: '测试位置',
|
location: '测试位置',
|
||||||
status: 'active'
|
status: 'active',
|
||||||
});
|
});
|
||||||
|
|
||||||
testRack = await Rack.create({
|
testRack = await Rack.create({
|
||||||
@@ -46,12 +46,17 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
currentPower: 0,
|
currentPower: 0,
|
||||||
totalUnits: 48,
|
totalUnits: 48,
|
||||||
usedUnits: 0,
|
usedUnits: 0,
|
||||||
status: 'active'
|
status: 'active',
|
||||||
});
|
});
|
||||||
|
|
||||||
app = createTestApp();
|
app = createTestApp();
|
||||||
authToken = jwt.sign(
|
authToken = jwt.sign(
|
||||||
{ userId: adminUser.userId, username: adminUser.username, realName: adminUser.realName, roleName: '管理员' },
|
{
|
||||||
|
userId: adminUser.userId,
|
||||||
|
username: adminUser.username,
|
||||||
|
realName: adminUser.realName,
|
||||||
|
roleName: '管理员',
|
||||||
|
},
|
||||||
JWT_SECRET,
|
JWT_SECRET,
|
||||||
{ expiresIn: '24h' }
|
{ expiresIn: '24h' }
|
||||||
);
|
);
|
||||||
@@ -101,7 +106,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
position: 1,
|
position: 1,
|
||||||
height: 2,
|
height: 2,
|
||||||
powerConsumption: 500,
|
powerConsumption: 500,
|
||||||
status: 'running'
|
status: 'running',
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
@@ -116,8 +121,8 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
where: {
|
where: {
|
||||||
module: 'device',
|
module: 'device',
|
||||||
operationType: 'create',
|
operationType: 'create',
|
||||||
targetId: response.body.deviceId
|
targetId: response.body.deviceId,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(logs.length).toBe(1);
|
expect(logs.length).toBe(1);
|
||||||
@@ -135,7 +140,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
position: 5,
|
position: 5,
|
||||||
height: 2,
|
height: 2,
|
||||||
powerConsumption: 500,
|
powerConsumption: 500,
|
||||||
status: 'offline'
|
status: 'offline',
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
@@ -150,8 +155,8 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
where: {
|
where: {
|
||||||
module: 'device',
|
module: 'device',
|
||||||
operationType: 'update',
|
operationType: 'update',
|
||||||
targetId: device.deviceId
|
targetId: device.deviceId,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(logs.length).toBe(1);
|
expect(logs.length).toBe(1);
|
||||||
@@ -168,7 +173,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
position: 10,
|
position: 10,
|
||||||
height: 2,
|
height: 2,
|
||||||
powerConsumption: 500,
|
powerConsumption: 500,
|
||||||
status: 'running'
|
status: 'running',
|
||||||
});
|
});
|
||||||
|
|
||||||
await request(app)
|
await request(app)
|
||||||
@@ -180,8 +185,8 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
where: {
|
where: {
|
||||||
module: 'device',
|
module: 'device',
|
||||||
operationType: 'delete',
|
operationType: 'delete',
|
||||||
targetId: device.deviceId
|
targetId: device.deviceId,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(logs.length).toBe(1);
|
expect(logs.length).toBe(1);
|
||||||
@@ -197,7 +202,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
position: 15,
|
position: 15,
|
||||||
height: 2,
|
height: 2,
|
||||||
powerConsumption: 500,
|
powerConsumption: 500,
|
||||||
status: 'running'
|
status: 'running',
|
||||||
});
|
});
|
||||||
|
|
||||||
const device2 = await Device.create({
|
const device2 = await Device.create({
|
||||||
@@ -208,7 +213,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
position: 20,
|
position: 20,
|
||||||
height: 2,
|
height: 2,
|
||||||
powerConsumption: 500,
|
powerConsumption: 500,
|
||||||
status: 'running'
|
status: 'running',
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
@@ -218,7 +223,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const logs = await OperationLog.findAll({
|
const logs = await OperationLog.findAll({
|
||||||
where: { operationType: 'batch_delete' }
|
where: { operationType: 'batch_delete' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(logs.length).toBe(1);
|
expect(logs.length).toBe(1);
|
||||||
@@ -234,7 +239,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
position: 25,
|
position: 25,
|
||||||
height: 2,
|
height: 2,
|
||||||
powerConsumption: 500,
|
powerConsumption: 500,
|
||||||
status: 'offline'
|
status: 'offline',
|
||||||
});
|
});
|
||||||
|
|
||||||
const device2 = await Device.create({
|
const device2 = await Device.create({
|
||||||
@@ -245,7 +250,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
position: 30,
|
position: 30,
|
||||||
height: 2,
|
height: 2,
|
||||||
powerConsumption: 500,
|
powerConsumption: 500,
|
||||||
status: 'offline'
|
status: 'offline',
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
@@ -255,7 +260,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const logs = await OperationLog.findAll({
|
const logs = await OperationLog.findAll({
|
||||||
where: { operationType: 'status_change' }
|
where: { operationType: 'status_change' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(logs.length).toBe(1);
|
expect(logs.length).toBe(1);
|
||||||
@@ -273,7 +278,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
roleName: '测试角色',
|
roleName: '测试角色',
|
||||||
roleCode: `test_role_${Date.now()}`,
|
roleCode: `test_role_${Date.now()}`,
|
||||||
permissions: ['read', 'write'],
|
permissions: ['read', 'write'],
|
||||||
status: 'active'
|
status: 'active',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -283,7 +288,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
password: 'Password123!',
|
password: 'Password123!',
|
||||||
realName: '集成测试用户',
|
realName: '集成测试用户',
|
||||||
email: `test_${Date.now()}@example.com`,
|
email: `test_${Date.now()}@example.com`,
|
||||||
roleIds: [testRole.roleId]
|
roleIds: [testRole.roleId],
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
@@ -296,8 +301,8 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
where: {
|
where: {
|
||||||
module: 'user',
|
module: 'user',
|
||||||
operationType: 'create',
|
operationType: 'create',
|
||||||
targetName: userData.username
|
targetName: userData.username,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(logs.length).toBe(1);
|
expect(logs.length).toBe(1);
|
||||||
@@ -314,7 +319,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
password: 'Password123!',
|
password: 'Password123!',
|
||||||
realName: '旧名称用户',
|
realName: '旧名称用户',
|
||||||
email: `old_${Date.now()}@example.com`,
|
email: `old_${Date.now()}@example.com`,
|
||||||
status: 'active'
|
status: 'active',
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
@@ -327,8 +332,8 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
where: {
|
where: {
|
||||||
module: 'user',
|
module: 'user',
|
||||||
operationType: 'update',
|
operationType: 'update',
|
||||||
targetId: user.userId
|
targetId: user.userId,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(logs.length).toBe(1);
|
expect(logs.length).toBe(1);
|
||||||
@@ -345,7 +350,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
password: 'Password123!',
|
password: 'Password123!',
|
||||||
realName: '角色测试用户',
|
realName: '角色测试用户',
|
||||||
email: `role_${Date.now()}@example.com`,
|
email: `role_${Date.now()}@example.com`,
|
||||||
status: 'active'
|
status: 'active',
|
||||||
});
|
});
|
||||||
|
|
||||||
const newRole = await Role.create({
|
const newRole = await Role.create({
|
||||||
@@ -353,12 +358,12 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
roleName: '新测试角色',
|
roleName: '新测试角色',
|
||||||
roleCode: `new_role_${Date.now()}`,
|
roleCode: `new_role_${Date.now()}`,
|
||||||
permissions: ['admin'],
|
permissions: ['admin'],
|
||||||
status: 'active'
|
status: 'active',
|
||||||
});
|
});
|
||||||
|
|
||||||
await UserRole.create({
|
await UserRole.create({
|
||||||
UserId: user.userId,
|
UserId: user.userId,
|
||||||
RoleId: testRole.roleId
|
RoleId: testRole.roleId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
@@ -371,8 +376,8 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
where: {
|
where: {
|
||||||
module: 'user',
|
module: 'user',
|
||||||
operationType: 'permission_change',
|
operationType: 'permission_change',
|
||||||
targetId: user.userId
|
targetId: user.userId,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(logs.length).toBe(1);
|
expect(logs.length).toBe(1);
|
||||||
@@ -391,7 +396,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
password: 'Password123!',
|
password: 'Password123!',
|
||||||
realName: '删除测试用户',
|
realName: '删除测试用户',
|
||||||
email: `del_${Date.now()}@example.com`,
|
email: `del_${Date.now()}@example.com`,
|
||||||
status: 'active'
|
status: 'active',
|
||||||
});
|
});
|
||||||
|
|
||||||
await request(app)
|
await request(app)
|
||||||
@@ -403,8 +408,8 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
where: {
|
where: {
|
||||||
module: 'user',
|
module: 'user',
|
||||||
operationType: 'delete',
|
operationType: 'delete',
|
||||||
targetId: user.userId
|
targetId: user.userId,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(logs.length).toBe(1);
|
expect(logs.length).toBe(1);
|
||||||
@@ -419,7 +424,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
roleCode: `int_test_role_${Date.now()}`,
|
roleCode: `int_test_role_${Date.now()}`,
|
||||||
description: '集成测试用角色',
|
description: '集成测试用角色',
|
||||||
permissions: ['read', 'write'],
|
permissions: ['read', 'write'],
|
||||||
status: 'active'
|
status: 'active',
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
@@ -432,8 +437,8 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
where: {
|
where: {
|
||||||
module: 'role',
|
module: 'role',
|
||||||
operationType: 'create',
|
operationType: 'create',
|
||||||
targetId: response.body.roleId
|
targetId: response.body.roleId,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(logs.length).toBe(1);
|
expect(logs.length).toBe(1);
|
||||||
@@ -450,7 +455,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
roleCode: `old_role_${Date.now()}`,
|
roleCode: `old_role_${Date.now()}`,
|
||||||
description: '旧描述',
|
description: '旧描述',
|
||||||
permissions: ['read'],
|
permissions: ['read'],
|
||||||
status: 'active'
|
status: 'active',
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
@@ -458,7 +463,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({
|
.send({
|
||||||
roleName: `新角色名_${Date.now()}`,
|
roleName: `新角色名_${Date.now()}`,
|
||||||
permissions: ['read', 'write', 'delete']
|
permissions: ['read', 'write', 'delete'],
|
||||||
})
|
})
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
@@ -466,8 +471,8 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
where: {
|
where: {
|
||||||
module: 'role',
|
module: 'role',
|
||||||
operationType: 'update',
|
operationType: 'update',
|
||||||
targetId: role.roleId
|
targetId: role.roleId,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(logs.length).toBe(1);
|
expect(logs.length).toBe(1);
|
||||||
@@ -484,7 +489,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
roleCode: `del_role_${Date.now()}`,
|
roleCode: `del_role_${Date.now()}`,
|
||||||
description: '待删除',
|
description: '待删除',
|
||||||
permissions: ['read'],
|
permissions: ['read'],
|
||||||
status: 'active'
|
status: 'active',
|
||||||
});
|
});
|
||||||
|
|
||||||
await request(app)
|
await request(app)
|
||||||
@@ -496,8 +501,8 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
where: {
|
where: {
|
||||||
module: 'role',
|
module: 'role',
|
||||||
operationType: 'delete',
|
operationType: 'delete',
|
||||||
targetId: role.roleId
|
targetId: role.roleId,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(logs.length).toBe(1);
|
expect(logs.length).toBe(1);
|
||||||
@@ -515,7 +520,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
position: 40,
|
position: 40,
|
||||||
height: 2,
|
height: 2,
|
||||||
powerConsumption: 500,
|
powerConsumption: 500,
|
||||||
status: 'running'
|
status: 'running',
|
||||||
});
|
});
|
||||||
|
|
||||||
await request(app)
|
await request(app)
|
||||||
@@ -533,8 +538,8 @@ describe('设备/用户/角色操作日志集成测试', () => {
|
|||||||
where: {
|
where: {
|
||||||
module: 'device',
|
module: 'device',
|
||||||
operationType: 'update',
|
operationType: 'update',
|
||||||
targetId: device.deviceId
|
targetId: device.deviceId,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(logs.length).toBe(1);
|
expect(logs.length).toBe(1);
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ describe('OperationLog 模型测试', () => {
|
|||||||
operatorId: 'user_001',
|
operatorId: 'user_001',
|
||||||
operatorName: '测试用户',
|
operatorName: '测试用户',
|
||||||
operatorRole: '管理员',
|
operatorRole: '管理员',
|
||||||
result: 'success'
|
result: 'success',
|
||||||
};
|
};
|
||||||
|
|
||||||
const log = await OperationLog.create(logData);
|
const log = await OperationLog.create(logData);
|
||||||
@@ -69,7 +69,7 @@ describe('OperationLog 模型测试', () => {
|
|||||||
operatorName: '测试用户',
|
operatorName: '测试用户',
|
||||||
beforeState,
|
beforeState,
|
||||||
afterState,
|
afterState,
|
||||||
result: 'success'
|
result: 'success',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(log.beforeState).toEqual(beforeState);
|
expect(log.beforeState).toEqual(beforeState);
|
||||||
@@ -80,7 +80,7 @@ describe('OperationLog 模型测试', () => {
|
|||||||
const metadata = {
|
const metadata = {
|
||||||
count: 5,
|
count: 5,
|
||||||
source: 'batch_operation',
|
source: 'batch_operation',
|
||||||
extraInfo: '额外信息'
|
extraInfo: '额外信息',
|
||||||
};
|
};
|
||||||
|
|
||||||
const log = await OperationLog.create({
|
const log = await OperationLog.create({
|
||||||
@@ -93,7 +93,7 @@ describe('OperationLog 模型测试', () => {
|
|||||||
operatorId: 'user_001',
|
operatorId: 'user_001',
|
||||||
operatorName: '测试用户',
|
operatorName: '测试用户',
|
||||||
metadata,
|
metadata,
|
||||||
result: 'success'
|
result: 'success',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(log.metadata).toEqual(metadata);
|
expect(log.metadata).toEqual(metadata);
|
||||||
@@ -111,7 +111,7 @@ describe('OperationLog 模型测试', () => {
|
|||||||
operatorName: '管理员',
|
operatorName: '管理员',
|
||||||
ipAddress: '192.168.1.100',
|
ipAddress: '192.168.1.100',
|
||||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
|
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
|
||||||
result: 'success'
|
result: 'success',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(log.ipAddress).toBe('192.168.1.100');
|
expect(log.ipAddress).toBe('192.168.1.100');
|
||||||
@@ -127,7 +127,7 @@ describe('OperationLog 模型测试', () => {
|
|||||||
targetId: 'DEV005',
|
targetId: 'DEV005',
|
||||||
targetName: 'TEST_DEVICE',
|
targetName: 'TEST_DEVICE',
|
||||||
operatorId: 'user_001',
|
operatorId: 'user_001',
|
||||||
operatorName: '测试用户'
|
operatorName: '测试用户',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(log.result).toBe('success');
|
expect(log.result).toBe('success');
|
||||||
@@ -142,7 +142,7 @@ describe('OperationLog 模型测试', () => {
|
|||||||
targetId: 'role_test',
|
targetId: 'role_test',
|
||||||
targetName: '测试角色',
|
targetName: '测试角色',
|
||||||
operatorId: 'user_001',
|
operatorId: 'user_001',
|
||||||
operatorName: '测试用户'
|
operatorName: '测试用户',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(log.metadata).toEqual({});
|
expect(log.metadata).toEqual({});
|
||||||
@@ -161,7 +161,7 @@ describe('OperationLog 模型测试', () => {
|
|||||||
targetName: '设备A',
|
targetName: '设备A',
|
||||||
operatorId: 'user_001',
|
operatorId: 'user_001',
|
||||||
operatorName: '用户A',
|
operatorName: '用户A',
|
||||||
result: 'success'
|
result: 'success',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
recordId: 'OPLOG_QUERY_002',
|
recordId: 'OPLOG_QUERY_002',
|
||||||
@@ -172,7 +172,7 @@ describe('OperationLog 模型测试', () => {
|
|||||||
targetName: '设备B',
|
targetName: '设备B',
|
||||||
operatorId: 'user_002',
|
operatorId: 'user_002',
|
||||||
operatorName: '用户B',
|
operatorName: '用户B',
|
||||||
result: 'success'
|
result: 'success',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
recordId: 'OPLOG_QUERY_003',
|
recordId: 'OPLOG_QUERY_003',
|
||||||
@@ -183,7 +183,7 @@ describe('OperationLog 模型测试', () => {
|
|||||||
targetName: '用户C',
|
targetName: '用户C',
|
||||||
operatorId: 'user_001',
|
operatorId: 'user_001',
|
||||||
operatorName: '用户A',
|
operatorName: '用户A',
|
||||||
result: 'success'
|
result: 'success',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
recordId: 'OPLOG_QUERY_004',
|
recordId: 'OPLOG_QUERY_004',
|
||||||
@@ -194,35 +194,35 @@ describe('OperationLog 模型测试', () => {
|
|||||||
targetName: '设备D',
|
targetName: '设备D',
|
||||||
operatorId: 'user_001',
|
operatorId: 'user_001',
|
||||||
operatorName: '用户A',
|
operatorName: '用户A',
|
||||||
result: 'failed'
|
result: 'failed',
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('应该能够按 module 查询', async () => {
|
test('应该能够按 module 查询', async () => {
|
||||||
const deviceLogs = await OperationLog.findAll({
|
const deviceLogs = await OperationLog.findAll({
|
||||||
where: { module: 'device' }
|
where: { module: 'device' },
|
||||||
});
|
});
|
||||||
expect(deviceLogs.length).toBe(3);
|
expect(deviceLogs.length).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('应该能够按 operationType 查询', async () => {
|
test('应该能够按 operationType 查询', async () => {
|
||||||
const createLogs = await OperationLog.findAll({
|
const createLogs = await OperationLog.findAll({
|
||||||
where: { operationType: 'create' }
|
where: { operationType: 'create' },
|
||||||
});
|
});
|
||||||
expect(createLogs.length).toBe(2);
|
expect(createLogs.length).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('应该能够按 operatorId 查询', async () => {
|
test('应该能够按 operatorId 查询', async () => {
|
||||||
const user001Logs = await OperationLog.findAll({
|
const user001Logs = await OperationLog.findAll({
|
||||||
where: { operatorId: 'user_001' }
|
where: { operatorId: 'user_001' },
|
||||||
});
|
});
|
||||||
expect(user001Logs.length).toBe(3);
|
expect(user001Logs.length).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('应该能够按 result 查询', async () => {
|
test('应该能够按 result 查询', async () => {
|
||||||
const failedLogs = await OperationLog.findAll({
|
const failedLogs = await OperationLog.findAll({
|
||||||
where: { result: 'failed' }
|
where: { result: 'failed' },
|
||||||
});
|
});
|
||||||
expect(failedLogs.length).toBe(1);
|
expect(failedLogs.length).toBe(1);
|
||||||
});
|
});
|
||||||
@@ -231,8 +231,8 @@ describe('OperationLog 模型测试', () => {
|
|||||||
const { Op } = require('sequelize');
|
const { Op } = require('sequelize');
|
||||||
const logs = await OperationLog.findAll({
|
const logs = await OperationLog.findAll({
|
||||||
where: {
|
where: {
|
||||||
targetId: { [Op.like]: '%DEV%' }
|
targetId: { [Op.like]: '%DEV%' },
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
expect(logs.length).toBe(3);
|
expect(logs.length).toBe(3);
|
||||||
});
|
});
|
||||||
@@ -241,7 +241,7 @@ describe('OperationLog 模型测试', () => {
|
|||||||
const { count, rows } = await OperationLog.findAndCountAll({
|
const { count, rows } = await OperationLog.findAndCountAll({
|
||||||
limit: 2,
|
limit: 2,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
order: [['createdAt', 'DESC']]
|
order: [['createdAt', 'DESC']],
|
||||||
});
|
});
|
||||||
expect(count).toBe(4);
|
expect(count).toBe(4);
|
||||||
expect(rows.length).toBe(2);
|
expect(rows.length).toBe(2);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const {
|
|||||||
logOperation,
|
logOperation,
|
||||||
logDeviceOperation,
|
logDeviceOperation,
|
||||||
logUserOperation,
|
logUserOperation,
|
||||||
logRoleOperation
|
logRoleOperation,
|
||||||
} = require('../utils/operationLogger');
|
} = require('../utils/operationLogger');
|
||||||
|
|
||||||
describe('operationLogger 工具函数测试', () => {
|
describe('operationLogger 工具函数测试', () => {
|
||||||
@@ -22,12 +22,12 @@ describe('operationLogger 工具函数测试', () => {
|
|||||||
user: {
|
user: {
|
||||||
userId: 'user_test_001',
|
userId: 'user_test_001',
|
||||||
realName: '测试用户',
|
realName: '测试用户',
|
||||||
roleName: '管理员'
|
roleName: '管理员',
|
||||||
},
|
},
|
||||||
headers: {
|
headers: {
|
||||||
'x-forwarded-for': '192.168.1.100',
|
'x-forwarded-for': '192.168.1.100',
|
||||||
'user-agent': 'Mozilla/5.0 Test Browser'
|
'user-agent': 'Mozilla/5.0 Test Browser',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
await logOperation({
|
await logOperation({
|
||||||
@@ -39,11 +39,11 @@ describe('operationLogger 工具函数测试', () => {
|
|||||||
beforeState: null,
|
beforeState: null,
|
||||||
afterState: { name: '测试设备', status: 'running' },
|
afterState: { name: '测试设备', status: 'running' },
|
||||||
result: 'success',
|
result: 'success',
|
||||||
req: mockReq
|
req: mockReq,
|
||||||
});
|
});
|
||||||
|
|
||||||
const logs = await OperationLog.findAll({
|
const logs = await OperationLog.findAll({
|
||||||
where: { targetId: 'DEV_TEST_001' }
|
where: { targetId: 'DEV_TEST_001' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(logs.length).toBe(1);
|
expect(logs.length).toBe(1);
|
||||||
@@ -63,7 +63,7 @@ describe('operationLogger 工具函数测试', () => {
|
|||||||
test('应该能够处理没有用户信息的请求', async () => {
|
test('应该能够处理没有用户信息的请求', async () => {
|
||||||
const mockReq = {
|
const mockReq = {
|
||||||
user: null,
|
user: null,
|
||||||
headers: {}
|
headers: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
await logOperation({
|
await logOperation({
|
||||||
@@ -73,11 +73,11 @@ describe('operationLogger 工具函数测试', () => {
|
|||||||
targetId: 'SYSTEM',
|
targetId: 'SYSTEM',
|
||||||
targetName: '系统',
|
targetName: '系统',
|
||||||
result: 'success',
|
result: 'success',
|
||||||
req: mockReq
|
req: mockReq,
|
||||||
});
|
});
|
||||||
|
|
||||||
const logs = await OperationLog.findAll({
|
const logs = await OperationLog.findAll({
|
||||||
where: { targetId: 'SYSTEM' }
|
where: { targetId: 'SYSTEM' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(logs.length).toBe(1);
|
expect(logs.length).toBe(1);
|
||||||
@@ -93,11 +93,11 @@ describe('operationLogger 工具函数测试', () => {
|
|||||||
targetId: 'DEV_NO_REQ',
|
targetId: 'DEV_NO_REQ',
|
||||||
targetName: '无请求设备',
|
targetName: '无请求设备',
|
||||||
result: 'success',
|
result: 'success',
|
||||||
req: null
|
req: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const logs = await OperationLog.findAll({
|
const logs = await OperationLog.findAll({
|
||||||
where: { targetId: 'DEV_NO_REQ' }
|
where: { targetId: 'DEV_NO_REQ' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(logs.length).toBe(1);
|
expect(logs.length).toBe(1);
|
||||||
@@ -108,7 +108,7 @@ describe('operationLogger 工具函数测试', () => {
|
|||||||
test('应该能够记录失败的操作', async () => {
|
test('应该能够记录失败的操作', async () => {
|
||||||
const mockReq = {
|
const mockReq = {
|
||||||
user: { userId: 'user_fail', realName: '失败用户' },
|
user: { userId: 'user_fail', realName: '失败用户' },
|
||||||
headers: {}
|
headers: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
await logOperation({
|
await logOperation({
|
||||||
@@ -119,11 +119,11 @@ describe('operationLogger 工具函数测试', () => {
|
|||||||
targetName: '失败设备',
|
targetName: '失败设备',
|
||||||
result: 'failed',
|
result: 'failed',
|
||||||
req: mockReq,
|
req: mockReq,
|
||||||
metadata: { errorMessage: '设备不存在' }
|
metadata: { errorMessage: '设备不存在' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const logs = await OperationLog.findAll({
|
const logs = await OperationLog.findAll({
|
||||||
where: { targetId: 'DEV_FAIL' }
|
where: { targetId: 'DEV_FAIL' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(logs.length).toBe(1);
|
expect(logs.length).toBe(1);
|
||||||
@@ -133,13 +133,13 @@ describe('operationLogger 工具函数测试', () => {
|
|||||||
test('应该使用提供的 metadata', async () => {
|
test('应该使用提供的 metadata', async () => {
|
||||||
const mockReq = {
|
const mockReq = {
|
||||||
user: { userId: 'user_meta', realName: '元数据用户' },
|
user: { userId: 'user_meta', realName: '元数据用户' },
|
||||||
headers: {}
|
headers: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const customMetadata = {
|
const customMetadata = {
|
||||||
batchCount: 10,
|
batchCount: 10,
|
||||||
source: 'import',
|
source: 'import',
|
||||||
duration: 5000
|
duration: 5000,
|
||||||
};
|
};
|
||||||
|
|
||||||
await logOperation({
|
await logOperation({
|
||||||
@@ -150,11 +150,11 @@ describe('operationLogger 工具函数测试', () => {
|
|||||||
targetName: '批量设备',
|
targetName: '批量设备',
|
||||||
result: 'success',
|
result: 'success',
|
||||||
req: mockReq,
|
req: mockReq,
|
||||||
metadata: customMetadata
|
metadata: customMetadata,
|
||||||
});
|
});
|
||||||
|
|
||||||
const logs = await OperationLog.findAll({
|
const logs = await OperationLog.findAll({
|
||||||
where: { targetId: 'DEV_BATCH' }
|
where: { targetId: 'DEV_BATCH' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(logs[0].metadata).toEqual(customMetadata);
|
expect(logs[0].metadata).toEqual(customMetadata);
|
||||||
@@ -165,22 +165,18 @@ describe('operationLogger 工具函数测试', () => {
|
|||||||
test('应该记录设备创建日志', async () => {
|
test('应该记录设备创建日志', async () => {
|
||||||
const mockReq = {
|
const mockReq = {
|
||||||
user: { userId: 'user_dev', realName: '设备管理员' },
|
user: { userId: 'user_dev', realName: '设备管理员' },
|
||||||
headers: { 'x-forwarded-for': '10.0.0.1' }
|
headers: { 'x-forwarded-for': '10.0.0.1' },
|
||||||
};
|
};
|
||||||
|
|
||||||
await logDeviceOperation(
|
await logDeviceOperation('create', '创建设备 测试服务器 (DEV001)', {
|
||||||
'create',
|
|
||||||
'创建设备 测试服务器 (DEV001)',
|
|
||||||
{
|
|
||||||
targetId: 'DEV001',
|
targetId: 'DEV001',
|
||||||
targetName: '测试服务器',
|
targetName: '测试服务器',
|
||||||
afterState: { deviceId: 'DEV001', name: '测试服务器', status: 'running' },
|
afterState: { deviceId: 'DEV001', name: '测试服务器', status: 'running' },
|
||||||
req: mockReq
|
req: mockReq,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const logs = await OperationLog.findAll({
|
const logs = await OperationLog.findAll({
|
||||||
where: { module: 'device', operationType: 'create' }
|
where: { module: 'device', operationType: 'create' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(logs.length).toBe(1);
|
expect(logs.length).toBe(1);
|
||||||
@@ -193,26 +189,22 @@ describe('operationLogger 工具函数测试', () => {
|
|||||||
test('应该记录设备更新日志并包含状态变更', async () => {
|
test('应该记录设备更新日志并包含状态变更', async () => {
|
||||||
const mockReq = {
|
const mockReq = {
|
||||||
user: { userId: 'user_upd', realName: '更新操作员' },
|
user: { userId: 'user_upd', realName: '更新操作员' },
|
||||||
headers: {}
|
headers: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const beforeState = { name: '旧名称', status: 'offline' };
|
const beforeState = { name: '旧名称', status: 'offline' };
|
||||||
const afterState = { name: '新名称', status: 'running' };
|
const afterState = { name: '新名称', status: 'running' };
|
||||||
|
|
||||||
await logDeviceOperation(
|
await logDeviceOperation('update', '更新设备 DEV002', {
|
||||||
'update',
|
|
||||||
'更新设备 DEV002',
|
|
||||||
{
|
|
||||||
targetId: 'DEV002',
|
targetId: 'DEV002',
|
||||||
targetName: 'DEV002',
|
targetName: 'DEV002',
|
||||||
beforeState,
|
beforeState,
|
||||||
afterState,
|
afterState,
|
||||||
req: mockReq
|
req: mockReq,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const logs = await OperationLog.findAll({
|
const logs = await OperationLog.findAll({
|
||||||
where: { targetId: 'DEV002', operationType: 'update' }
|
where: { targetId: 'DEV002', operationType: 'update' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(logs.length).toBe(1);
|
expect(logs.length).toBe(1);
|
||||||
@@ -223,22 +215,18 @@ describe('operationLogger 工具函数测试', () => {
|
|||||||
test('应该记录设备删除日志', async () => {
|
test('应该记录设备删除日志', async () => {
|
||||||
const mockReq = {
|
const mockReq = {
|
||||||
user: { userId: 'user_del', realName: '删除操作员' },
|
user: { userId: 'user_del', realName: '删除操作员' },
|
||||||
headers: {}
|
headers: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
await logDeviceOperation(
|
await logDeviceOperation('delete', '删除设备 DEV003 (测试服务器)', {
|
||||||
'delete',
|
|
||||||
'删除设备 DEV003 (测试服务器)',
|
|
||||||
{
|
|
||||||
targetId: 'DEV003',
|
targetId: 'DEV003',
|
||||||
targetName: '测试服务器',
|
targetName: '测试服务器',
|
||||||
beforeState: { deviceId: 'DEV003', name: '测试服务器' },
|
beforeState: { deviceId: 'DEV003', name: '测试服务器' },
|
||||||
req: mockReq
|
req: mockReq,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const logs = await OperationLog.findAll({
|
const logs = await OperationLog.findAll({
|
||||||
where: { targetId: 'DEV003', operationType: 'delete' }
|
where: { targetId: 'DEV003', operationType: 'delete' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(logs.length).toBe(1);
|
expect(logs.length).toBe(1);
|
||||||
@@ -247,27 +235,23 @@ describe('operationLogger 工具函数测试', () => {
|
|||||||
test('应该记录批量删除日志', async () => {
|
test('应该记录批量删除日志', async () => {
|
||||||
const mockReq = {
|
const mockReq = {
|
||||||
user: { userId: 'user_batch', realName: '批量操作员' },
|
user: { userId: 'user_batch', realName: '批量操作员' },
|
||||||
headers: {}
|
headers: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
await logDeviceOperation(
|
await logDeviceOperation('batch_delete', '批量删除设备 3台 (DEV_A,DEV_B,DEV_C)', {
|
||||||
'batch_delete',
|
|
||||||
'批量删除设备 3台 (DEV_A,DEV_B,DEV_C)',
|
|
||||||
{
|
|
||||||
targetId: 'DEV_A,DEV_B,DEV_C',
|
targetId: 'DEV_A,DEV_B,DEV_C',
|
||||||
targetName: '3台设备',
|
targetName: '3台设备',
|
||||||
beforeState: [
|
beforeState: [
|
||||||
{ deviceId: 'DEV_A', name: '设备A' },
|
{ deviceId: 'DEV_A', name: '设备A' },
|
||||||
{ deviceId: 'DEV_B', name: '设备B' },
|
{ deviceId: 'DEV_B', name: '设备B' },
|
||||||
{ deviceId: 'DEV_C', name: '设备C' }
|
{ deviceId: 'DEV_C', name: '设备C' },
|
||||||
],
|
],
|
||||||
req: mockReq,
|
req: mockReq,
|
||||||
metadata: { count: 3 }
|
metadata: { count: 3 },
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const logs = await OperationLog.findAll({
|
const logs = await OperationLog.findAll({
|
||||||
where: { operationType: 'batch_delete' }
|
where: { operationType: 'batch_delete' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(logs.length).toBe(1);
|
expect(logs.length).toBe(1);
|
||||||
@@ -277,30 +261,26 @@ describe('operationLogger 工具函数测试', () => {
|
|||||||
test('应该记录状态变更日志', async () => {
|
test('应该记录状态变更日志', async () => {
|
||||||
const mockReq = {
|
const mockReq = {
|
||||||
user: { userId: 'user_status', realName: '状态管理员' },
|
user: { userId: 'user_status', realName: '状态管理员' },
|
||||||
headers: {}
|
headers: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
await logDeviceOperation(
|
await logDeviceOperation('status_change', '批量变更设备状态为"运行中"', {
|
||||||
'status_change',
|
|
||||||
'批量变更设备状态为"运行中"',
|
|
||||||
{
|
|
||||||
targetId: 'DEV_STATUS_1,DEV_STATUS_2',
|
targetId: 'DEV_STATUS_1,DEV_STATUS_2',
|
||||||
targetName: '2台设备',
|
targetName: '2台设备',
|
||||||
beforeState: [
|
beforeState: [
|
||||||
{ deviceId: 'DEV_STATUS_1', status: 'offline' },
|
{ deviceId: 'DEV_STATUS_1', status: 'offline' },
|
||||||
{ deviceId: 'DEV_STATUS_2', status: 'maintenance' }
|
{ deviceId: 'DEV_STATUS_2', status: 'maintenance' },
|
||||||
],
|
],
|
||||||
afterState: [
|
afterState: [
|
||||||
{ deviceId: 'DEV_STATUS_1', status: 'running' },
|
{ deviceId: 'DEV_STATUS_1', status: 'running' },
|
||||||
{ deviceId: 'DEV_STATUS_2', status: 'running' }
|
{ deviceId: 'DEV_STATUS_2', status: 'running' },
|
||||||
],
|
],
|
||||||
req: mockReq,
|
req: mockReq,
|
||||||
metadata: { status: 'running', count: 2 }
|
metadata: { status: 'running', count: 2 },
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const logs = await OperationLog.findAll({
|
const logs = await OperationLog.findAll({
|
||||||
where: { operationType: 'status_change' }
|
where: { operationType: 'status_change' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(logs.length).toBe(1);
|
expect(logs.length).toBe(1);
|
||||||
@@ -312,23 +292,19 @@ describe('operationLogger 工具函数测试', () => {
|
|||||||
test('应该记录用户创建日志', async () => {
|
test('应该记录用户创建日志', async () => {
|
||||||
const mockReq = {
|
const mockReq = {
|
||||||
user: { userId: 'admin', realName: '系统管理员' },
|
user: { userId: 'admin', realName: '系统管理员' },
|
||||||
headers: {}
|
headers: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
await logUserOperation(
|
await logUserOperation('create', '创建用户 new_user', {
|
||||||
'create',
|
|
||||||
'创建用户 new_user',
|
|
||||||
{
|
|
||||||
targetId: 'user_new',
|
targetId: 'user_new',
|
||||||
targetName: 'new_user',
|
targetName: 'new_user',
|
||||||
afterState: { username: 'new_user', email: 'new@example.com' },
|
afterState: { username: 'new_user', email: 'new@example.com' },
|
||||||
req: mockReq,
|
req: mockReq,
|
||||||
metadata: { roleIds: ['role_admin'] }
|
metadata: { roleIds: ['role_admin'] },
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const logs = await OperationLog.findAll({
|
const logs = await OperationLog.findAll({
|
||||||
where: { module: 'user', operationType: 'create' }
|
where: { module: 'user', operationType: 'create' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(logs.length).toBe(1);
|
expect(logs.length).toBe(1);
|
||||||
@@ -339,24 +315,20 @@ describe('operationLogger 工具函数测试', () => {
|
|||||||
test('应该记录权限变更日志', async () => {
|
test('应该记录权限变更日志', async () => {
|
||||||
const mockReq = {
|
const mockReq = {
|
||||||
user: { userId: 'admin', realName: '管理员' },
|
user: { userId: 'admin', realName: '管理员' },
|
||||||
headers: {}
|
headers: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
await logUserOperation(
|
await logUserOperation('permission_change', '变更用户 test_user 的角色', {
|
||||||
'permission_change',
|
|
||||||
'变更用户 test_user 的角色',
|
|
||||||
{
|
|
||||||
targetId: 'user_test',
|
targetId: 'user_test',
|
||||||
targetName: 'test_user',
|
targetName: 'test_user',
|
||||||
beforeState: { roleIds: ['role_viewer'] },
|
beforeState: { roleIds: ['role_viewer'] },
|
||||||
afterState: { roleIds: ['role_admin'] },
|
afterState: { roleIds: ['role_admin'] },
|
||||||
req: mockReq,
|
req: mockReq,
|
||||||
metadata: { oldRoleIds: ['role_viewer'], newRoleIds: ['role_admin'] }
|
metadata: { oldRoleIds: ['role_viewer'], newRoleIds: ['role_admin'] },
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const logs = await OperationLog.findAll({
|
const logs = await OperationLog.findAll({
|
||||||
where: { operationType: 'permission_change' }
|
where: { operationType: 'permission_change' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(logs.length).toBe(1);
|
expect(logs.length).toBe(1);
|
||||||
@@ -367,22 +339,18 @@ describe('operationLogger 工具函数测试', () => {
|
|||||||
test('应该记录用户删除日志', async () => {
|
test('应该记录用户删除日志', async () => {
|
||||||
const mockReq = {
|
const mockReq = {
|
||||||
user: { userId: 'admin', realName: '管理员' },
|
user: { userId: 'admin', realName: '管理员' },
|
||||||
headers: {}
|
headers: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
await logUserOperation(
|
await logUserOperation('delete', '删除用户 deleted_user', {
|
||||||
'delete',
|
|
||||||
'删除用户 deleted_user',
|
|
||||||
{
|
|
||||||
targetId: 'user_deleted',
|
targetId: 'user_deleted',
|
||||||
targetName: 'deleted_user',
|
targetName: 'deleted_user',
|
||||||
beforeState: { username: 'deleted_user', email: 'deleted@example.com' },
|
beforeState: { username: 'deleted_user', email: 'deleted@example.com' },
|
||||||
req: mockReq
|
req: mockReq,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const logs = await OperationLog.findAll({
|
const logs = await OperationLog.findAll({
|
||||||
where: { module: 'user', operationType: 'delete' }
|
where: { module: 'user', operationType: 'delete' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(logs.length).toBe(1);
|
expect(logs.length).toBe(1);
|
||||||
@@ -393,23 +361,19 @@ describe('operationLogger 工具函数测试', () => {
|
|||||||
test('应该记录角色创建日志', async () => {
|
test('应该记录角色创建日志', async () => {
|
||||||
const mockReq = {
|
const mockReq = {
|
||||||
user: { userId: 'admin', realName: '管理员' },
|
user: { userId: 'admin', realName: '管理员' },
|
||||||
headers: {}
|
headers: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
await logRoleOperation(
|
await logRoleOperation('create', '创建角色 测试角色', {
|
||||||
'create',
|
|
||||||
'创建角色 测试角色',
|
|
||||||
{
|
|
||||||
targetId: 'role_test',
|
targetId: 'role_test',
|
||||||
targetName: '测试角色',
|
targetName: '测试角色',
|
||||||
afterState: { roleName: '测试角色', permissions: ['read', 'write'] },
|
afterState: { roleName: '测试角色', permissions: ['read', 'write'] },
|
||||||
req: mockReq,
|
req: mockReq,
|
||||||
metadata: { roleCode: 'test_role', permissions: ['read', 'write'] }
|
metadata: { roleCode: 'test_role', permissions: ['read', 'write'] },
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const logs = await OperationLog.findAll({
|
const logs = await OperationLog.findAll({
|
||||||
where: { module: 'role', operationType: 'create' }
|
where: { module: 'role', operationType: 'create' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(logs.length).toBe(1);
|
expect(logs.length).toBe(1);
|
||||||
@@ -420,27 +384,23 @@ describe('operationLogger 工具函数测试', () => {
|
|||||||
test('应该记录角色更新日志', async () => {
|
test('应该记录角色更新日志', async () => {
|
||||||
const mockReq = {
|
const mockReq = {
|
||||||
user: { userId: 'admin', realName: '管理员' },
|
user: { userId: 'admin', realName: '管理员' },
|
||||||
headers: {}
|
headers: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const beforeState = { roleName: '旧角色', permissions: ['read'] };
|
const beforeState = { roleName: '旧角色', permissions: ['read'] };
|
||||||
const afterState = { roleName: '新角色', permissions: ['read', 'write', 'delete'] };
|
const afterState = { roleName: '新角色', permissions: ['read', 'write', 'delete'] };
|
||||||
|
|
||||||
await logRoleOperation(
|
await logRoleOperation('update', '更新角色 角色A', {
|
||||||
'update',
|
|
||||||
'更新角色 角色A',
|
|
||||||
{
|
|
||||||
targetId: 'role_a',
|
targetId: 'role_a',
|
||||||
targetName: '角色A',
|
targetName: '角色A',
|
||||||
beforeState,
|
beforeState,
|
||||||
afterState,
|
afterState,
|
||||||
req: mockReq,
|
req: mockReq,
|
||||||
metadata: { oldRoleName: '旧角色', oldPermissions: ['read'] }
|
metadata: { oldRoleName: '旧角色', oldPermissions: ['read'] },
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const logs = await OperationLog.findAll({
|
const logs = await OperationLog.findAll({
|
||||||
where: { module: 'role', operationType: 'update' }
|
where: { module: 'role', operationType: 'update' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(logs.length).toBe(1);
|
expect(logs.length).toBe(1);
|
||||||
@@ -451,23 +411,19 @@ describe('operationLogger 工具函数测试', () => {
|
|||||||
test('应该记录角色删除日志', async () => {
|
test('应该记录角色删除日志', async () => {
|
||||||
const mockReq = {
|
const mockReq = {
|
||||||
user: { userId: 'admin', realName: '管理员' },
|
user: { userId: 'admin', realName: '管理员' },
|
||||||
headers: {}
|
headers: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
await logRoleOperation(
|
await logRoleOperation('delete', '删除角色 测试角色B', {
|
||||||
'delete',
|
|
||||||
'删除角色 测试角色B',
|
|
||||||
{
|
|
||||||
targetId: 'role_b',
|
targetId: 'role_b',
|
||||||
targetName: '测试角色B',
|
targetName: '测试角色B',
|
||||||
beforeState: { roleName: '测试角色B', userCount: 0 },
|
beforeState: { roleName: '测试角色B', userCount: 0 },
|
||||||
req: mockReq,
|
req: mockReq,
|
||||||
metadata: { userCount: 0 }
|
metadata: { userCount: 0 },
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const logs = await OperationLog.findAll({
|
const logs = await OperationLog.findAll({
|
||||||
where: { module: 'role', operationType: 'delete' }
|
where: { module: 'role', operationType: 'delete' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(logs.length).toBe(1);
|
expect(logs.length).toBe(1);
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const createTestApp = () => {
|
|||||||
keyword,
|
keyword,
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
result
|
result,
|
||||||
} = req.query;
|
} = req.query;
|
||||||
|
|
||||||
const offset = (parseInt(page) - 1) * parseInt(pageSize);
|
const offset = (parseInt(page) - 1) * parseInt(pageSize);
|
||||||
@@ -72,7 +72,7 @@ const createTestApp = () => {
|
|||||||
where[Op.or] = [
|
where[Op.or] = [
|
||||||
{ operationDescription: { [Op.like]: `%${keyword}%` } },
|
{ operationDescription: { [Op.like]: `%${keyword}%` } },
|
||||||
{ targetName: { [Op.like]: `%${keyword}%` } },
|
{ targetName: { [Op.like]: `%${keyword}%` } },
|
||||||
{ operatorName: { [Op.like]: `%${keyword}%` } }
|
{ operatorName: { [Op.like]: `%${keyword}%` } },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ const createTestApp = () => {
|
|||||||
where,
|
where,
|
||||||
order: [['createdAt', 'DESC']],
|
order: [['createdAt', 'DESC']],
|
||||||
offset,
|
offset,
|
||||||
limit
|
limit,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -105,14 +105,14 @@ const createTestApp = () => {
|
|||||||
total: count,
|
total: count,
|
||||||
page: parseInt(page),
|
page: parseInt(page),
|
||||||
pageSize: parseInt(pageSize),
|
pageSize: parseInt(pageSize),
|
||||||
logs
|
logs,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取操作日志失败:', error);
|
console.error('获取操作日志失败:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '获取操作日志失败'
|
message: '获取操作日志失败',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -124,19 +124,19 @@ const createTestApp = () => {
|
|||||||
if (!log) {
|
if (!log) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '日志记录不存在'
|
message: '日志记录不存在',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: log
|
data: log,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取操作日志详情失败:', error);
|
console.error('获取操作日志详情失败:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '获取操作日志详情失败'
|
message: '获取操作日志详情失败',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -153,7 +153,7 @@ describe('OperationLogs API 路由测试', () => {
|
|||||||
userId: 'test_user_001',
|
userId: 'test_user_001',
|
||||||
username: 'testuser',
|
username: 'testuser',
|
||||||
realName: '测试用户',
|
realName: '测试用户',
|
||||||
roleName: '管理员'
|
roleName: '管理员',
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
@@ -181,16 +181,14 @@ describe('OperationLogs API 路由测试', () => {
|
|||||||
operatorId: testUser.userId,
|
operatorId: testUser.userId,
|
||||||
operatorName: testUser.realName,
|
operatorName: testUser.realName,
|
||||||
operatorRole: testUser.roleName,
|
operatorRole: testUser.roleName,
|
||||||
result: 'success'
|
result: 'success',
|
||||||
};
|
};
|
||||||
return await OperationLog.create({ ...defaultData, ...data });
|
return await OperationLog.create({ ...defaultData, ...data });
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('GET /api/operation-logs', () => {
|
describe('GET /api/operation-logs', () => {
|
||||||
test('未授权访问应该返回 401', async () => {
|
test('未授权访问应该返回 401', async () => {
|
||||||
const response = await request(app)
|
const response = await request(app).get('/api/operation-logs').expect(401);
|
||||||
.get('/api/operation-logs')
|
|
||||||
.expect(401);
|
|
||||||
|
|
||||||
expect(response.body.success).toBe(false);
|
expect(response.body.success).toBe(false);
|
||||||
});
|
});
|
||||||
@@ -258,9 +256,21 @@ describe('OperationLogs API 路由测试', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('应该支持按 keyword 搜索', async () => {
|
test('应该支持按 keyword 搜索', async () => {
|
||||||
await createTestLog({ operationDescription: '创建设备 SERVER_A', recordId: 'OPLOG_KW_1', targetName: '服务器A' });
|
await createTestLog({
|
||||||
await createTestLog({ operationDescription: '更新设备 SERVER_B', recordId: 'OPLOG_KW_2', targetName: '服务器B' });
|
operationDescription: '创建设备 SERVER_A',
|
||||||
await createTestLog({ operationDescription: '删除用户 USER_C', recordId: 'OPLOG_KW_3', targetName: '用户C' });
|
recordId: 'OPLOG_KW_1',
|
||||||
|
targetName: '服务器A',
|
||||||
|
});
|
||||||
|
await createTestLog({
|
||||||
|
operationDescription: '更新设备 SERVER_B',
|
||||||
|
recordId: 'OPLOG_KW_2',
|
||||||
|
targetName: '服务器B',
|
||||||
|
});
|
||||||
|
await createTestLog({
|
||||||
|
operationDescription: '删除用户 USER_C',
|
||||||
|
recordId: 'OPLOG_KW_3',
|
||||||
|
targetName: '用户C',
|
||||||
|
});
|
||||||
|
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
.get('/api/operation-logs')
|
.get('/api/operation-logs')
|
||||||
@@ -269,7 +279,9 @@ describe('OperationLogs API 路由测试', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.body.data.logs).toHaveLength(2);
|
expect(response.body.data.logs).toHaveLength(2);
|
||||||
expect(response.body.data.logs.every(log => log.operationDescription.includes('SERVER'))).toBe(true);
|
expect(
|
||||||
|
response.body.data.logs.every(log => log.operationDescription.includes('SERVER'))
|
||||||
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('应该支持按 result 筛选', async () => {
|
test('应该支持按 result 筛选', async () => {
|
||||||
@@ -291,19 +303,19 @@ describe('OperationLogs API 路由测试', () => {
|
|||||||
module: 'device',
|
module: 'device',
|
||||||
operationType: 'create',
|
operationType: 'create',
|
||||||
result: 'success',
|
result: 'success',
|
||||||
recordId: 'OPLOG_COMB_1'
|
recordId: 'OPLOG_COMB_1',
|
||||||
});
|
});
|
||||||
await createTestLog({
|
await createTestLog({
|
||||||
module: 'device',
|
module: 'device',
|
||||||
operationType: 'update',
|
operationType: 'update',
|
||||||
result: 'success',
|
result: 'success',
|
||||||
recordId: 'OPLOG_COMB_2'
|
recordId: 'OPLOG_COMB_2',
|
||||||
});
|
});
|
||||||
await createTestLog({
|
await createTestLog({
|
||||||
module: 'user',
|
module: 'user',
|
||||||
operationType: 'create',
|
operationType: 'create',
|
||||||
result: 'success',
|
result: 'success',
|
||||||
recordId: 'OPLOG_COMB_3'
|
recordId: 'OPLOG_COMB_3',
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
@@ -324,7 +336,7 @@ describe('OperationLogs API 路由测试', () => {
|
|||||||
recordId: 'OPLOG_DETAIL_001',
|
recordId: 'OPLOG_DETAIL_001',
|
||||||
beforeState: { name: '旧名称' },
|
beforeState: { name: '旧名称' },
|
||||||
afterState: { name: '新名称' },
|
afterState: { name: '新名称' },
|
||||||
metadata: { customField: '自定义值' }
|
metadata: { customField: '自定义值' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ async function unlockUser(username = null) {
|
|||||||
users = await User.findAll({
|
users = await User.findAll({
|
||||||
where: {
|
where: {
|
||||||
username,
|
username,
|
||||||
status: 'locked'
|
status: 'locked',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (users.length === 0) {
|
if (users.length === 0) {
|
||||||
@@ -28,7 +28,7 @@ async function unlockUser(username = null) {
|
|||||||
} else {
|
} else {
|
||||||
// 解锁所有被锁定的用户
|
// 解锁所有被锁定的用户
|
||||||
users = await User.findAll({
|
users = await User.findAll({
|
||||||
where: { status: 'locked' }
|
where: { status: 'locked' },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (users.length === 0) {
|
if (users.length === 0) {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
const cron = require('node-cron');
|
const cron = require('node-cron');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
@@ -89,7 +88,8 @@ function getFileSize(filePath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createAutoBackupTask(settings) {
|
function createAutoBackupTask(settings) {
|
||||||
const { cronExpression, description, backupType, includeFiles, compress, maxCount, maxAgeDays } = settings;
|
const { cronExpression, description, backupType, includeFiles, compress, maxCount, maxAgeDays } =
|
||||||
|
settings;
|
||||||
|
|
||||||
if (!validateCronExpression(cronExpression)) {
|
if (!validateCronExpression(cronExpression)) {
|
||||||
throw new Error('无效的 Cron 表达式');
|
throw new Error('无效的 Cron 表达式');
|
||||||
@@ -106,7 +106,9 @@ function createAutoBackupTask(settings) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('创建新调度器...');
|
console.log('创建新调度器...');
|
||||||
const task = cron.schedule(cronExpression, async function() {
|
const task = cron.schedule(
|
||||||
|
cronExpression,
|
||||||
|
async function () {
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log('============================================');
|
console.log('============================================');
|
||||||
console.log('=== 自动备份任务触发 ===');
|
console.log('=== 自动备份任务触发 ===');
|
||||||
@@ -124,7 +126,7 @@ function createAutoBackupTask(settings) {
|
|||||||
description: `${description} - ${timestamp}`,
|
description: `${description} - ${timestamp}`,
|
||||||
backupType: backupType,
|
backupType: backupType,
|
||||||
includeFiles: includeFiles,
|
includeFiles: includeFiles,
|
||||||
compressed: compress
|
compressed: compress,
|
||||||
});
|
});
|
||||||
logId = log ? log.id : null;
|
logId = log ? log.id : null;
|
||||||
|
|
||||||
@@ -133,7 +135,8 @@ function createAutoBackupTask(settings) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('准备执行备份...');
|
console.log('准备执行备份...');
|
||||||
const backupFunction = backupType === 'incremental' ? createIncrementalBackup : createBackup;
|
const backupFunction =
|
||||||
|
backupType === 'incremental' ? createIncrementalBackup : createBackup;
|
||||||
|
|
||||||
const result = await backupFunction({
|
const result = await backupFunction({
|
||||||
description: `${description} - ${timestamp}`,
|
description: `${description} - ${timestamp}`,
|
||||||
@@ -157,7 +160,7 @@ function createAutoBackupTask(settings) {
|
|||||||
filename: result.filename,
|
filename: result.filename,
|
||||||
filePath: result.path,
|
filePath: result.path,
|
||||||
fileSize: fileSize,
|
fileSize: fileSize,
|
||||||
remoteUploads: uploadResults
|
remoteUploads: uploadResults,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +169,7 @@ function createAutoBackupTask(settings) {
|
|||||||
console.log('无数据变化,跳过备份');
|
console.log('无数据变化,跳过备份');
|
||||||
if (logId) {
|
if (logId) {
|
||||||
await updateLogStatus(logId, 'success', {
|
await updateLogStatus(logId, 'success', {
|
||||||
errorMessage: '无数据变化,跳过备份'
|
errorMessage: '无数据变化,跳过备份',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.log('============================================\n');
|
console.log('============================================\n');
|
||||||
@@ -177,15 +180,17 @@ function createAutoBackupTask(settings) {
|
|||||||
|
|
||||||
if (logId) {
|
if (logId) {
|
||||||
await updateLogStatus(logId, 'failed', {
|
await updateLogStatus(logId, 'failed', {
|
||||||
errorMessage: error.message || '未知错误'
|
errorMessage: error.message || '未知错误',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error('============================================\n');
|
console.error('============================================\n');
|
||||||
}
|
}
|
||||||
}, {
|
},
|
||||||
timezone: 'Asia/Shanghai'
|
{
|
||||||
});
|
timezone: 'Asia/Shanghai',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
schedulers.set('auto-backup', task);
|
schedulers.set('auto-backup', task);
|
||||||
console.log('自动备份任务已成功创建并启动');
|
console.log('自动备份任务已成功创建并启动');
|
||||||
@@ -358,8 +363,9 @@ async function executeBackupNow(options = {}) {
|
|||||||
logType: 'manual',
|
logType: 'manual',
|
||||||
description: options.description || '手动备份',
|
description: options.description || '手动备份',
|
||||||
backupType: backupType,
|
backupType: backupType,
|
||||||
includeFiles: options.includeFiles !== undefined ? options.includeFiles : settings.includeFiles,
|
includeFiles:
|
||||||
compressed: options.compress !== undefined ? options.compress : settings.compress
|
options.includeFiles !== undefined ? options.includeFiles : settings.includeFiles,
|
||||||
|
compressed: options.compress !== undefined ? options.compress : settings.compress,
|
||||||
});
|
});
|
||||||
logId = log ? log.id : null;
|
logId = log ? log.id : null;
|
||||||
|
|
||||||
@@ -370,7 +376,8 @@ async function executeBackupNow(options = {}) {
|
|||||||
const backupFunction = backupType === 'incremental' ? createIncrementalBackup : createBackup;
|
const backupFunction = backupType === 'incremental' ? createIncrementalBackup : createBackup;
|
||||||
const result = await backupFunction({
|
const result = await backupFunction({
|
||||||
description: options.description || '手动备份',
|
description: options.description || '手动备份',
|
||||||
includeFiles: options.includeFiles !== undefined ? options.includeFiles : settings.includeFiles,
|
includeFiles:
|
||||||
|
options.includeFiles !== undefined ? options.includeFiles : settings.includeFiles,
|
||||||
compress: options.compress !== undefined ? options.compress : settings.compress,
|
compress: options.compress !== undefined ? options.compress : settings.compress,
|
||||||
autoClean: true,
|
autoClean: true,
|
||||||
maxCount: settings.maxCount,
|
maxCount: settings.maxCount,
|
||||||
@@ -388,7 +395,7 @@ async function executeBackupNow(options = {}) {
|
|||||||
filename: result.filename,
|
filename: result.filename,
|
||||||
filePath: result.path,
|
filePath: result.path,
|
||||||
fileSize: fileSize,
|
fileSize: fileSize,
|
||||||
remoteUploads: uploadResults
|
remoteUploads: uploadResults,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,7 +411,7 @@ async function executeBackupNow(options = {}) {
|
|||||||
|
|
||||||
if (logId) {
|
if (logId) {
|
||||||
await updateLogStatus(logId, 'failed', {
|
await updateLogStatus(logId, 'failed', {
|
||||||
errorMessage: error.message || '未知错误'
|
errorMessage: error.message || '未知错误',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,4 +454,3 @@ module.exports = {
|
|||||||
executeBackupNow,
|
executeBackupNow,
|
||||||
initAutoBackup,
|
initAutoBackup,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+72
-51
@@ -30,31 +30,31 @@ async function enableForeignKeyChecks() {
|
|||||||
|
|
||||||
// 数据表名称中英文映射
|
// 数据表名称中英文映射
|
||||||
const TABLE_NAME_MAPPING = {
|
const TABLE_NAME_MAPPING = {
|
||||||
'User': '用户',
|
User: '用户',
|
||||||
'Role': '角色',
|
Role: '角色',
|
||||||
'UserRole': '用户角色关联',
|
UserRole: '用户角色关联',
|
||||||
'Permission': '权限',
|
Permission: '权限',
|
||||||
'Room': '机房',
|
Room: '机房',
|
||||||
'Rack': '机柜',
|
Rack: '机柜',
|
||||||
'Device': '设备',
|
Device: '设备',
|
||||||
'DeviceField': '设备自定义字段',
|
DeviceField: '设备自定义字段',
|
||||||
'DevicePort': '设备端口',
|
DevicePort: '设备端口',
|
||||||
'NetworkCard': '网卡',
|
NetworkCard: '网卡',
|
||||||
'Cable': '线缆',
|
Cable: '线缆',
|
||||||
'PendingDevice': '待入库设备',
|
PendingDevice: '待入库设备',
|
||||||
'FaultCategory': '故障分类',
|
FaultCategory: '故障分类',
|
||||||
'Ticket': '工单',
|
Ticket: '工单',
|
||||||
'TicketField': '工单自定义字段',
|
TicketField: '工单自定义字段',
|
||||||
'TicketOperationRecord': '工单操作记录',
|
TicketOperationRecord: '工单操作记录',
|
||||||
'ConsumableCategory': '耗材分类',
|
ConsumableCategory: '耗材分类',
|
||||||
'Consumable': '耗材',
|
Consumable: '耗材',
|
||||||
'ConsumableRecord': '耗材记录',
|
ConsumableRecord: '耗材记录',
|
||||||
'ConsumableLog': '耗材操作日志',
|
ConsumableLog: '耗材操作日志',
|
||||||
'ConsumableLogArchive': '耗材操作日志归档',
|
ConsumableLogArchive: '耗材操作日志归档',
|
||||||
'InventoryPlan': '盘点计划',
|
InventoryPlan: '盘点计划',
|
||||||
'InventoryTask': '盘点任务',
|
InventoryTask: '盘点任务',
|
||||||
'InventoryRecord': '盘点记录',
|
InventoryRecord: '盘点记录',
|
||||||
'SystemSetting': '系统设置',
|
SystemSetting: '系统设置',
|
||||||
};
|
};
|
||||||
|
|
||||||
// 增量备份配置
|
// 增量备份配置
|
||||||
@@ -161,8 +161,13 @@ function getBackupPath() {
|
|||||||
*/
|
*/
|
||||||
function getLastBackupTime() {
|
function getLastBackupTime() {
|
||||||
const backupDir = getBackupPath();
|
const backupDir = getBackupPath();
|
||||||
const files = fs.readdirSync(backupDir)
|
const files = fs
|
||||||
.filter(f => (f.startsWith('backup_') || f.startsWith('incremental_')) && (f.endsWith('.json') || f.endsWith('.json.gz')))
|
.readdirSync(backupDir)
|
||||||
|
.filter(
|
||||||
|
f =>
|
||||||
|
(f.startsWith('backup_') || f.startsWith('incremental_')) &&
|
||||||
|
(f.endsWith('.json') || f.endsWith('.json.gz'))
|
||||||
|
)
|
||||||
.map(f => {
|
.map(f => {
|
||||||
const filePath = path.join(backupDir, f);
|
const filePath = path.join(backupDir, f);
|
||||||
return {
|
return {
|
||||||
@@ -397,7 +402,9 @@ async function createBackup(options = {}) {
|
|||||||
console.log('\n检查旧备份文件...');
|
console.log('\n检查旧备份文件...');
|
||||||
cleanResult = cleanOldBackups({ maxCount, maxAgeDays });
|
cleanResult = cleanOldBackups({ maxCount, maxAgeDays });
|
||||||
if (cleanResult.deletedCount > 0) {
|
if (cleanResult.deletedCount > 0) {
|
||||||
console.log(`已清理 ${cleanResult.deletedCount} 个旧备份,释放 ${cleanResult.freedSizeFormatted}`);
|
console.log(
|
||||||
|
`已清理 ${cleanResult.deletedCount} 个旧备份,释放 ${cleanResult.freedSizeFormatted}`
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log('无需清理旧备份');
|
console.log('无需清理旧备份');
|
||||||
}
|
}
|
||||||
@@ -442,7 +449,8 @@ async function createIncrementalBackup(options = {}) {
|
|||||||
console.log(`开始增量备份(上次备份时间:${lastBackupTime.toISOString()})...`);
|
console.log(`开始增量备份(上次备份时间:${lastBackupTime.toISOString()})...`);
|
||||||
|
|
||||||
// 收集增量数据
|
// 收集增量数据
|
||||||
const { data: incrementalData, totalChangedRecords } = await collectIncrementalData(lastBackupTime);
|
const { data: incrementalData, totalChangedRecords } =
|
||||||
|
await collectIncrementalData(lastBackupTime);
|
||||||
|
|
||||||
if (totalChangedRecords === 0) {
|
if (totalChangedRecords === 0) {
|
||||||
console.log('自上次备份以来没有数据变化,跳过备份');
|
console.log('自上次备份以来没有数据变化,跳过备份');
|
||||||
@@ -520,7 +528,9 @@ async function createIncrementalBackup(options = {}) {
|
|||||||
console.log('\n检查旧备份文件...');
|
console.log('\n检查旧备份文件...');
|
||||||
cleanResult = cleanOldBackups({ maxCount, maxAgeDays });
|
cleanResult = cleanOldBackups({ maxCount, maxAgeDays });
|
||||||
if (cleanResult.deletedCount > 0) {
|
if (cleanResult.deletedCount > 0) {
|
||||||
console.log(`已清理 ${cleanResult.deletedCount} 个旧备份,释放 ${cleanResult.freedSizeFormatted}`);
|
console.log(
|
||||||
|
`已清理 ${cleanResult.deletedCount} 个旧备份,释放 ${cleanResult.freedSizeFormatted}`
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log('无需清理旧备份');
|
console.log('无需清理旧备份');
|
||||||
}
|
}
|
||||||
@@ -638,24 +648,28 @@ async function validateBackupFile(filePath, options = {}) {
|
|||||||
avatars: backupData.files?.avatars?.length || 0,
|
avatars: backupData.files?.avatars?.length || 0,
|
||||||
others: backupData.files?.others?.length || 0,
|
others: backupData.files?.others?.length || 0,
|
||||||
total: (backupData.files?.avatars?.length || 0) + (backupData.files?.others?.length || 0),
|
total: (backupData.files?.avatars?.length || 0) + (backupData.files?.others?.length || 0),
|
||||||
avatarList: backupData.files?.avatars?.map(f => ({
|
avatarList:
|
||||||
|
backupData.files?.avatars?.map(f => ({
|
||||||
filename: f.filename,
|
filename: f.filename,
|
||||||
size: f.size,
|
size: f.size,
|
||||||
})) || [],
|
})) || [],
|
||||||
otherList: backupData.files?.others?.map(f => ({
|
otherList:
|
||||||
|
backupData.files?.others?.map(f => ({
|
||||||
filename: f.filename,
|
filename: f.filename,
|
||||||
size: f.size,
|
size: f.size,
|
||||||
})) || [],
|
})) || [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const metadata = isIncremental ? {
|
const metadata = isIncremental
|
||||||
|
? {
|
||||||
tableCount: Object.keys(backupData.fullData || {}).length,
|
tableCount: Object.keys(backupData.fullData || {}).length,
|
||||||
incrementalTableCount: Object.keys(backupData.incrementalData || {}).length,
|
incrementalTableCount: Object.keys(backupData.incrementalData || {}).length,
|
||||||
totalRecords,
|
totalRecords,
|
||||||
totalChangedRecords: backupData.metadata?.totalChangedRecords || totalRecords,
|
totalChangedRecords: backupData.metadata?.totalChangedRecords || totalRecords,
|
||||||
fileCount: fileDetails.total,
|
fileCount: fileDetails.total,
|
||||||
lastBackupTime: backupData.lastBackupTime,
|
lastBackupTime: backupData.lastBackupTime,
|
||||||
} : {
|
}
|
||||||
|
: {
|
||||||
tableCount: Object.keys(backupData.data).length,
|
tableCount: Object.keys(backupData.data).length,
|
||||||
totalRecords,
|
totalRecords,
|
||||||
fileCount: fileDetails.total,
|
fileCount: fileDetails.total,
|
||||||
@@ -679,11 +693,7 @@ async function validateBackupFile(filePath, options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function restoreData(backupData, options = {}) {
|
async function restoreData(backupData, options = {}) {
|
||||||
const {
|
const { overwriteExisting = true, skipTables = [], onProgress = () => {} } = options;
|
||||||
overwriteExisting = true,
|
|
||||||
skipTables = [],
|
|
||||||
onProgress = () => {},
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
const results = {
|
const results = {
|
||||||
tablesRestored: 0,
|
tablesRestored: 0,
|
||||||
@@ -733,12 +743,18 @@ async function restoreData(backupData, options = {}) {
|
|||||||
const processedRecords = tableData.map(record => {
|
const processedRecords = tableData.map(record => {
|
||||||
const processed = { ...record };
|
const processed = { ...record };
|
||||||
|
|
||||||
if (tableName === 'Device' && processed.customFields !== undefined && processed.customFields !== null) {
|
if (
|
||||||
|
tableName === 'Device' &&
|
||||||
|
processed.customFields !== undefined &&
|
||||||
|
processed.customFields !== null
|
||||||
|
) {
|
||||||
if (typeof processed.customFields === 'string') {
|
if (typeof processed.customFields === 'string') {
|
||||||
try {
|
try {
|
||||||
processed.customFields = JSON.parse(processed.customFields);
|
processed.customFields = JSON.parse(processed.customFields);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`解析 Device.customFields 失败:${processed.deviceId}, 错误:${e.message}`);
|
console.warn(
|
||||||
|
`解析 Device.customFields 失败:${processed.deviceId}, 错误:${e.message}`
|
||||||
|
);
|
||||||
processed.customFields = {};
|
processed.customFields = {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -992,19 +1008,17 @@ async function restoreBackup(filePath, options = {}) {
|
|||||||
filesRestored: fileResults.filesRestored,
|
filesRestored: fileResults.filesRestored,
|
||||||
errors: dataResults.errors,
|
errors: dataResults.errors,
|
||||||
tableDetails: dataResults.tableDetails, // 每个表的详细恢复信息
|
tableDetails: dataResults.tableDetails, // 每个表的详细恢复信息
|
||||||
fileDetails: backupData.files ? {
|
fileDetails: backupData.files
|
||||||
|
? {
|
||||||
avatars: backupData.files.avatars?.length || 0,
|
avatars: backupData.files.avatars?.length || 0,
|
||||||
others: backupData.files.others?.length || 0,
|
others: backupData.files.others?.length || 0,
|
||||||
} : null,
|
}
|
||||||
|
: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanOldBackups(options = {}) {
|
function cleanOldBackups(options = {}) {
|
||||||
const {
|
const { maxCount = 30, maxAgeDays = 90, dryRun = false } = options;
|
||||||
maxCount = 30,
|
|
||||||
maxAgeDays = 90,
|
|
||||||
dryRun = false,
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
const backupPath = getBackupPath();
|
const backupPath = getBackupPath();
|
||||||
|
|
||||||
@@ -1015,8 +1029,13 @@ function cleanOldBackups(options = {}) {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
|
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
const files = fs.readdirSync(backupPath)
|
const files = fs
|
||||||
.filter(f => (f.startsWith('backup_') || f.startsWith('uploaded_') || f.startsWith('incremental_')) && (f.endsWith('.json') || f.endsWith('.json.gz')))
|
.readdirSync(backupPath)
|
||||||
|
.filter(
|
||||||
|
f =>
|
||||||
|
(f.startsWith('backup_') || f.startsWith('uploaded_') || f.startsWith('incremental_')) &&
|
||||||
|
(f.endsWith('.json') || f.endsWith('.json.gz'))
|
||||||
|
)
|
||||||
.map(f => {
|
.map(f => {
|
||||||
const filePath = path.join(backupPath, f);
|
const filePath = path.join(backupPath, f);
|
||||||
const stats = fs.statSync(filePath);
|
const stats = fs.statSync(filePath);
|
||||||
@@ -1070,7 +1089,9 @@ function cleanOldBackups(options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatBytes(bytes) {
|
function formatBytes(bytes) {
|
||||||
if (bytes === 0) return '0 B';
|
if (bytes === 0) {
|
||||||
|
return '0 B';
|
||||||
|
}
|
||||||
const k = 1024;
|
const k = 1024;
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
|||||||
+29
-16
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
const BackupLog = require('../models/BackupLog');
|
const BackupLog = require('../models/BackupLog');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
@@ -13,7 +12,7 @@ async function createLogEntry(options) {
|
|||||||
backupType: backupType || 'full',
|
backupType: backupType || 'full',
|
||||||
includeFiles: includeFiles || false,
|
includeFiles: includeFiles || false,
|
||||||
compressed: compressed || false,
|
compressed: compressed || false,
|
||||||
startTime: new Date()
|
startTime: new Date(),
|
||||||
});
|
});
|
||||||
return log;
|
return log;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -39,14 +38,24 @@ async function updateLogStatus(logId, status, options = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.filename) updateData.filename = options.filename;
|
if (options.filename) {
|
||||||
if (options.filePath) updateData.filePath = options.filePath;
|
updateData.filename = options.filename;
|
||||||
if (options.fileSize) updateData.fileSize = options.fileSize;
|
}
|
||||||
if (options.errorMessage) updateData.errorMessage = options.errorMessage;
|
if (options.filePath) {
|
||||||
if (options.remoteUploads) updateData.remoteUploads = options.remoteUploads;
|
updateData.filePath = options.filePath;
|
||||||
|
}
|
||||||
|
if (options.fileSize) {
|
||||||
|
updateData.fileSize = options.fileSize;
|
||||||
|
}
|
||||||
|
if (options.errorMessage) {
|
||||||
|
updateData.errorMessage = options.errorMessage;
|
||||||
|
}
|
||||||
|
if (options.remoteUploads) {
|
||||||
|
updateData.remoteUploads = options.remoteUploads;
|
||||||
|
}
|
||||||
|
|
||||||
await BackupLog.update(updateData, {
|
await BackupLog.update(updateData, {
|
||||||
where: { id: logId }
|
where: { id: logId },
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -61,8 +70,12 @@ async function getBackupLogs(options = {}) {
|
|||||||
const { page = 1, pageSize = 20, logType, status } = options;
|
const { page = 1, pageSize = 20, logType, status } = options;
|
||||||
const where = {};
|
const where = {};
|
||||||
|
|
||||||
if (logType) where.logType = logType;
|
if (logType) {
|
||||||
if (status) where.status = status;
|
where.logType = logType;
|
||||||
|
}
|
||||||
|
if (status) {
|
||||||
|
where.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
const offset = (page - 1) * pageSize;
|
const offset = (page - 1) * pageSize;
|
||||||
|
|
||||||
@@ -70,7 +83,7 @@ async function getBackupLogs(options = {}) {
|
|||||||
where,
|
where,
|
||||||
order: [['createdAt', 'DESC']],
|
order: [['createdAt', 'DESC']],
|
||||||
limit: pageSize,
|
limit: pageSize,
|
||||||
offset
|
offset,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -78,7 +91,7 @@ async function getBackupLogs(options = {}) {
|
|||||||
total: count,
|
total: count,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
totalPages: Math.ceil(count / pageSize)
|
totalPages: Math.ceil(count / pageSize),
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取备份日志失败:', error);
|
console.error('获取备份日志失败:', error);
|
||||||
@@ -103,9 +116,9 @@ async function deleteOldLogs(days = 30) {
|
|||||||
const deletedCount = await BackupLog.destroy({
|
const deletedCount = await BackupLog.destroy({
|
||||||
where: {
|
where: {
|
||||||
createdAt: {
|
createdAt: {
|
||||||
[require('sequelize').Op.lt]: cutoffDate
|
[require('sequelize').Op.lt]: cutoffDate,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`删除了 ${deletedCount} 条旧备份日志`);
|
console.log(`删除了 ${deletedCount} 条旧备份日志`);
|
||||||
@@ -121,5 +134,5 @@ module.exports = {
|
|||||||
updateLogStatus,
|
updateLogStatus,
|
||||||
getBackupLogs,
|
getBackupLogs,
|
||||||
getBackupLogById,
|
getBackupLogById,
|
||||||
deleteOldLogs
|
deleteOldLogs,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,15 +10,19 @@ const ensureLogDir = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatLogEntry = (entry) => {
|
const formatLogEntry = entry => {
|
||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
return JSON.stringify({
|
return (
|
||||||
|
JSON.stringify({
|
||||||
timestamp,
|
timestamp,
|
||||||
...entry,
|
...entry,
|
||||||
}) + '\n';
|
}) + '\n'
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const logDangerousOperation = async (req, {
|
const logDangerousOperation = async (
|
||||||
|
req,
|
||||||
|
{
|
||||||
operationType,
|
operationType,
|
||||||
operationName,
|
operationName,
|
||||||
targetType,
|
targetType,
|
||||||
@@ -28,7 +32,8 @@ const logDangerousOperation = async (req, {
|
|||||||
metadata = {},
|
metadata = {},
|
||||||
success = true,
|
success = true,
|
||||||
errorMessage = null,
|
errorMessage = null,
|
||||||
}) => {
|
}
|
||||||
|
) => {
|
||||||
ensureLogDir();
|
ensureLogDir();
|
||||||
|
|
||||||
const clientIp = req?.ip || req?.connection?.remoteAddress || 'unknown';
|
const clientIp = req?.ip || req?.connection?.remoteAddress || 'unknown';
|
||||||
@@ -57,7 +62,9 @@ const logDangerousOperation = async (req, {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
fs.appendFileSync(DANGEROUS_OPERATIONS_LOG, formatLogEntry(logEntry));
|
fs.appendFileSync(DANGEROUS_OPERATIONS_LOG, formatLogEntry(logEntry));
|
||||||
console.log(`[DANGEROUS-OP] ${logEntry.operationName} by ${logEntry.username} - ${success ? 'SUCCESS' : 'FAILED'}`);
|
console.log(
|
||||||
|
`[DANGEROUS-OP] ${logEntry.operationName} by ${logEntry.username} - ${success ? 'SUCCESS' : 'FAILED'}`
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to write dangerous operation log:', error);
|
console.error('Failed to write dangerous operation log:', error);
|
||||||
}
|
}
|
||||||
@@ -74,13 +81,15 @@ const getDangerousOperationsLogs = (filters = {}) => {
|
|||||||
const content = fs.readFileSync(DANGEROUS_OPERATIONS_LOG, 'utf-8');
|
const content = fs.readFileSync(DANGEROUS_OPERATIONS_LOG, 'utf-8');
|
||||||
const lines = content.split('\n').filter(line => line.trim());
|
const lines = content.split('\n').filter(line => line.trim());
|
||||||
|
|
||||||
let logs = lines.map(line => {
|
let logs = lines
|
||||||
|
.map(line => {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(line);
|
return JSON.parse(line);
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}).filter(log => log !== null);
|
})
|
||||||
|
.filter(log => log !== null);
|
||||||
|
|
||||||
if (filters.operationType) {
|
if (filters.operationType) {
|
||||||
logs = logs.filter(log => log.operationType === filters.operationType);
|
logs = logs.filter(log => log.operationType === filters.operationType);
|
||||||
@@ -103,7 +112,9 @@ const getDangerousOperationsLogs = (filters = {}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (filters.username) {
|
if (filters.username) {
|
||||||
logs = logs.filter(log => log.username?.toLowerCase().includes(filters.username.toLowerCase()));
|
logs = logs.filter(log =>
|
||||||
|
log.username?.toLowerCase().includes(filters.username.toLowerCase())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filters.riskLevel) {
|
if (filters.riskLevel) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const checkDatabase = async () => {
|
|||||||
const result = {
|
const result = {
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
type: dbDialect,
|
type: dbDialect,
|
||||||
message: '数据库连接正常'
|
message: '数据库连接正常',
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -35,19 +35,19 @@ const checkCriticalConfig = () => {
|
|||||||
checks.push({
|
checks.push({
|
||||||
key: 'JWT_SECRET',
|
key: 'JWT_SECRET',
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: 'JWT_SECRET 未配置'
|
message: 'JWT_SECRET 未配置',
|
||||||
});
|
});
|
||||||
} else if (jwtSecret.length < 32) {
|
} else if (jwtSecret.length < 32) {
|
||||||
checks.push({
|
checks.push({
|
||||||
key: 'JWT_SECRET',
|
key: 'JWT_SECRET',
|
||||||
status: 'warning',
|
status: 'warning',
|
||||||
message: 'JWT_SECRET 长度不足,建议至少 32 字符'
|
message: 'JWT_SECRET 长度不足,建议至少 32 字符',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
checks.push({
|
checks.push({
|
||||||
key: 'JWT_SECRET',
|
key: 'JWT_SECRET',
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
message: 'JWT_SECRET 已配置'
|
message: 'JWT_SECRET 已配置',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,14 +55,14 @@ const checkCriticalConfig = () => {
|
|||||||
checks.push({
|
checks.push({
|
||||||
key: 'PORT',
|
key: 'PORT',
|
||||||
status: port ? 'ok' : 'warning',
|
status: port ? 'ok' : 'warning',
|
||||||
message: port ? `服务端口: ${port}` : '使用默认端口 8000'
|
message: port ? `服务端口: ${port}` : '使用默认端口 8000',
|
||||||
});
|
});
|
||||||
|
|
||||||
const dbType = process.env.DB_TYPE || 'sqlite';
|
const dbType = process.env.DB_TYPE || 'sqlite';
|
||||||
checks.push({
|
checks.push({
|
||||||
key: 'DB_TYPE',
|
key: 'DB_TYPE',
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
message: `数据库类型: ${dbType}`
|
message: `数据库类型: ${dbType}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (dbType === 'mysql') {
|
if (dbType === 'mysql') {
|
||||||
@@ -72,7 +72,7 @@ const checkCriticalConfig = () => {
|
|||||||
checks.push({
|
checks.push({
|
||||||
key: 'MYSQL_CONFIG',
|
key: 'MYSQL_CONFIG',
|
||||||
status: 'warning',
|
status: 'warning',
|
||||||
message: 'MySQL 配置不完整'
|
message: 'MySQL 配置不完整',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,7 @@ const checkCriticalConfig = () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
status: overallStatus,
|
status: overallStatus,
|
||||||
checks
|
checks,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -93,17 +93,25 @@ const getSystemInfo = () => {
|
|||||||
const memUsage = process.memoryUsage();
|
const memUsage = process.memoryUsage();
|
||||||
const uptime = process.uptime();
|
const uptime = process.uptime();
|
||||||
|
|
||||||
const formatUptime = (seconds) => {
|
const formatUptime = seconds => {
|
||||||
const days = Math.floor(seconds / 86400);
|
const days = Math.floor(seconds / 86400);
|
||||||
const hours = Math.floor((seconds % 86400) / 3600);
|
const hours = Math.floor((seconds % 86400) / 3600);
|
||||||
const minutes = Math.floor((seconds % 3600) / 60);
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
const secs = Math.floor(seconds % 60);
|
const secs = Math.floor(seconds % 60);
|
||||||
|
|
||||||
const parts = [];
|
const parts = [];
|
||||||
if (days > 0) parts.push(`${days}天`);
|
if (days > 0) {
|
||||||
if (hours > 0) parts.push(`${hours}小时`);
|
parts.push(`${days}天`);
|
||||||
if (minutes > 0) parts.push(`${minutes}分钟`);
|
}
|
||||||
if (secs > 0 || parts.length === 0) parts.push(`${secs}秒`);
|
if (hours > 0) {
|
||||||
|
parts.push(`${hours}小时`);
|
||||||
|
}
|
||||||
|
if (minutes > 0) {
|
||||||
|
parts.push(`${minutes}分钟`);
|
||||||
|
}
|
||||||
|
if (secs > 0 || parts.length === 0) {
|
||||||
|
parts.push(`${secs}秒`);
|
||||||
|
}
|
||||||
|
|
||||||
return parts.join(' ');
|
return parts.join(' ');
|
||||||
};
|
};
|
||||||
@@ -112,13 +120,13 @@ const getSystemInfo = () => {
|
|||||||
nodeVersion: process.version,
|
nodeVersion: process.version,
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
memory: {
|
memory: {
|
||||||
heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024 * 100) / 100,
|
heapUsed: Math.round((memUsage.heapUsed / 1024 / 1024) * 100) / 100,
|
||||||
heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024 * 100) / 100,
|
heapTotal: Math.round((memUsage.heapTotal / 1024 / 1024) * 100) / 100,
|
||||||
rss: Math.round(memUsage.rss / 1024 / 1024 * 100) / 100,
|
rss: Math.round((memUsage.rss / 1024 / 1024) * 100) / 100,
|
||||||
unit: 'MB'
|
unit: 'MB',
|
||||||
},
|
},
|
||||||
uptime: formatUptime(uptime),
|
uptime: formatUptime(uptime),
|
||||||
uptimeSeconds: Math.round(uptime)
|
uptimeSeconds: Math.round(uptime),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -129,7 +137,7 @@ const performHealthCheck = async () => {
|
|||||||
|
|
||||||
const allChecks = [
|
const allChecks = [
|
||||||
{ name: 'database', ...dbCheck },
|
{ name: 'database', ...dbCheck },
|
||||||
{ name: 'config', ...configCheck }
|
{ name: 'config', ...configCheck },
|
||||||
];
|
];
|
||||||
|
|
||||||
const overallStatus = allChecks.every(c => c.status === 'ok')
|
const overallStatus = allChecks.every(c => c.status === 'ok')
|
||||||
@@ -143,10 +151,10 @@ const performHealthCheck = async () => {
|
|||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
service: {
|
service: {
|
||||||
name: 'IDC设备管理系统',
|
name: 'IDC设备管理系统',
|
||||||
version: '1.0.0'
|
version: '1.0.0',
|
||||||
},
|
},
|
||||||
checks: allChecks,
|
checks: allChecks,
|
||||||
system: systemInfo
|
system: systemInfo,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -154,5 +162,5 @@ module.exports = {
|
|||||||
performHealthCheck,
|
performHealthCheck,
|
||||||
checkDatabase,
|
checkDatabase,
|
||||||
checkCriticalConfig,
|
checkCriticalConfig,
|
||||||
getSystemInfo
|
getSystemInfo,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,26 +4,27 @@ const generateRecordId = () => {
|
|||||||
return `OPLOG_${Date.now().toString(36)}_${Math.random().toString(36).substr(2, 9)}`;
|
return `OPLOG_${Date.now().toString(36)}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getOperatorInfo = (req) => {
|
const getOperatorInfo = req => {
|
||||||
if (!req || !req.user) {
|
if (!req || !req.user) {
|
||||||
return {
|
return {
|
||||||
operatorId: 'system',
|
operatorId: 'system',
|
||||||
operatorName: '系统',
|
operatorName: '系统',
|
||||||
operatorRole: null
|
operatorRole: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
operatorId: req.user.userId || req.user.id || 'unknown',
|
operatorId: req.user.userId || req.user.id || 'unknown',
|
||||||
operatorName: req.user.realName || req.user.username || '未知用户',
|
operatorName: req.user.realName || req.user.username || '未知用户',
|
||||||
operatorRole: req.user.roleName || req.user.role || null
|
operatorRole: req.user.roleName || req.user.role || null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getClientInfo = (req) => {
|
const getClientInfo = req => {
|
||||||
if (!req) {
|
if (!req) {
|
||||||
return { ipAddress: null, userAgent: null };
|
return { ipAddress: null, userAgent: null };
|
||||||
}
|
}
|
||||||
const ipAddress = req.headers['x-forwarded-for'] ||
|
const ipAddress =
|
||||||
|
req.headers['x-forwarded-for'] ||
|
||||||
req.headers['x-real-ip'] ||
|
req.headers['x-real-ip'] ||
|
||||||
req.connection?.remoteAddress ||
|
req.connection?.remoteAddress ||
|
||||||
req.ip ||
|
req.ip ||
|
||||||
@@ -39,7 +40,7 @@ const DEVICE_TYPE_MAP = {
|
|||||||
storage: '存储设备',
|
storage: '存储设备',
|
||||||
firewall: '防火墙',
|
firewall: '防火墙',
|
||||||
loadbalancer: '负载均衡器',
|
loadbalancer: '负载均衡器',
|
||||||
other: '其他设备'
|
other: '其他设备',
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateDeviceDescription = (operation, device, options = {}) => {
|
const generateDeviceDescription = (operation, device, options = {}) => {
|
||||||
@@ -48,7 +49,7 @@ const generateDeviceDescription = (operation, device, options = {}) => {
|
|||||||
includePosition = true,
|
includePosition = true,
|
||||||
includeSerial = true,
|
includeSerial = true,
|
||||||
includeIp = true,
|
includeIp = true,
|
||||||
includeModel = true
|
includeModel = true,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
const deviceType = DEVICE_TYPE_MAP[device.type] || device.type || '设备';
|
const deviceType = DEVICE_TYPE_MAP[device.type] || device.type || '设备';
|
||||||
@@ -94,7 +95,7 @@ const buildDeviceMetadata = (device, extra = {}) => {
|
|||||||
position: device.position !== undefined ? device.position : null,
|
position: device.position !== undefined ? device.position : null,
|
||||||
roomId: device.roomId || null,
|
roomId: device.roomId || null,
|
||||||
roomName: device.roomName || null,
|
roomName: device.roomName || null,
|
||||||
...extra
|
...extra,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -108,7 +109,7 @@ async function logOperation({
|
|||||||
afterState,
|
afterState,
|
||||||
result = 'success',
|
result = 'success',
|
||||||
req,
|
req,
|
||||||
metadata = {}
|
metadata = {},
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
const operatorInfo = getOperatorInfo(req);
|
const operatorInfo = getOperatorInfo(req);
|
||||||
@@ -129,14 +130,18 @@ async function logOperation({
|
|||||||
result,
|
result,
|
||||||
ipAddress: clientInfo.ipAddress,
|
ipAddress: clientInfo.ipAddress,
|
||||||
userAgent: clientInfo.userAgent,
|
userAgent: clientInfo.userAgent,
|
||||||
metadata
|
metadata,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('记录操作日志失败:', error);
|
console.error('记录操作日志失败:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logDeviceOperation(operationType, operationDescription, { targetId, targetName, beforeState, afterState, result, req, metadata = {} }) {
|
async function logDeviceOperation(
|
||||||
|
operationType,
|
||||||
|
operationDescription,
|
||||||
|
{ targetId, targetName, beforeState, afterState, result, req, metadata = {} }
|
||||||
|
) {
|
||||||
return logOperation({
|
return logOperation({
|
||||||
module: 'device',
|
module: 'device',
|
||||||
operationType,
|
operationType,
|
||||||
@@ -147,11 +152,15 @@ async function logDeviceOperation(operationType, operationDescription, { targetI
|
|||||||
afterState,
|
afterState,
|
||||||
result,
|
result,
|
||||||
req,
|
req,
|
||||||
metadata
|
metadata,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logUserOperation(operationType, operationDescription, { targetId, targetName, beforeState, afterState, result, req, metadata = {} }) {
|
async function logUserOperation(
|
||||||
|
operationType,
|
||||||
|
operationDescription,
|
||||||
|
{ targetId, targetName, beforeState, afterState, result, req, metadata = {} }
|
||||||
|
) {
|
||||||
return logOperation({
|
return logOperation({
|
||||||
module: 'user',
|
module: 'user',
|
||||||
operationType,
|
operationType,
|
||||||
@@ -162,11 +171,15 @@ async function logUserOperation(operationType, operationDescription, { targetId,
|
|||||||
afterState,
|
afterState,
|
||||||
result,
|
result,
|
||||||
req,
|
req,
|
||||||
metadata
|
metadata,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logRoleOperation(operationType, operationDescription, { targetId, targetName, beforeState, afterState, result, req, metadata = {} }) {
|
async function logRoleOperation(
|
||||||
|
operationType,
|
||||||
|
operationDescription,
|
||||||
|
{ targetId, targetName, beforeState, afterState, result, req, metadata = {} }
|
||||||
|
) {
|
||||||
return logOperation({
|
return logOperation({
|
||||||
module: 'role',
|
module: 'role',
|
||||||
operationType,
|
operationType,
|
||||||
@@ -177,7 +190,7 @@ async function logRoleOperation(operationType, operationDescription, { targetId,
|
|||||||
afterState,
|
afterState,
|
||||||
result,
|
result,
|
||||||
req,
|
req,
|
||||||
metadata
|
metadata,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,5 +200,5 @@ module.exports = {
|
|||||||
logUserOperation,
|
logUserOperation,
|
||||||
logRoleOperation,
|
logRoleOperation,
|
||||||
generateDeviceDescription,
|
generateDeviceDescription,
|
||||||
buildDeviceMetadata
|
buildDeviceMetadata,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -241,7 +241,9 @@ async function uploadToRemote(config, localFilePath, remotePath) {
|
|||||||
const duration = Date.now() - startTime;
|
const duration = Date.now() - startTime;
|
||||||
const fileSize = fs.statSync(localFilePath).size;
|
const fileSize = fs.statSync(localFilePath).size;
|
||||||
|
|
||||||
console.log(`远端上传完成 [${config.protocol}] - 耗时:${duration}ms, 文件大小:${(fileSize / 1024).toFixed(2)}KB`);
|
console.log(
|
||||||
|
`远端上传完成 [${config.protocol}] - 耗时:${duration}ms, 文件大小:${(fileSize / 1024).toFixed(2)}KB`
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ const DEFAULT_CONFIG = {
|
|||||||
* 加密敏感信息
|
* 加密敏感信息
|
||||||
*/
|
*/
|
||||||
function encrypt(text) {
|
function encrypt(text) {
|
||||||
if (!text) return '';
|
if (!text) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
const algorithm = 'aes-256-cbc';
|
const algorithm = 'aes-256-cbc';
|
||||||
const key = crypto.scryptSync(process.env.JWT_SECRET || 'default-secret', 'salt', 32);
|
const key = crypto.scryptSync(process.env.JWT_SECRET || 'default-secret', 'salt', 32);
|
||||||
const iv = crypto.randomBytes(16);
|
const iv = crypto.randomBytes(16);
|
||||||
@@ -42,7 +44,9 @@ function encrypt(text) {
|
|||||||
* 解密敏感信息
|
* 解密敏感信息
|
||||||
*/
|
*/
|
||||||
function decrypt(text) {
|
function decrypt(text) {
|
||||||
if (!text) return '';
|
if (!text) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const algorithm = 'aes-256-cbc';
|
const algorithm = 'aes-256-cbc';
|
||||||
const key = crypto.scryptSync(process.env.JWT_SECRET || 'default-secret', 'salt', 32);
|
const key = crypto.scryptSync(process.env.JWT_SECRET || 'default-secret', 'salt', 32);
|
||||||
|
|||||||
@@ -7,41 +7,48 @@ const createDeviceSchema = Joi.object({
|
|||||||
name: Joi.string().required().max(100).messages({
|
name: Joi.string().required().max(100).messages({
|
||||||
'string.empty': '设备名称不能为空',
|
'string.empty': '设备名称不能为空',
|
||||||
'string.max': '设备名称不能超过100个字符',
|
'string.max': '设备名称不能超过100个字符',
|
||||||
'any.required': '设备名称是必填字段'
|
'any.required': '设备名称是必填字段',
|
||||||
}),
|
}),
|
||||||
type: Joi.string().required().valid(...DEVICE_TYPES).messages({
|
type: Joi.string()
|
||||||
|
.required()
|
||||||
|
.valid(...DEVICE_TYPES)
|
||||||
|
.messages({
|
||||||
'any.only': `设备类型必须是以下之一: ${DEVICE_TYPES.join(', ')}`,
|
'any.only': `设备类型必须是以下之一: ${DEVICE_TYPES.join(', ')}`,
|
||||||
'any.required': '设备类型是必填字段'
|
'any.required': '设备类型是必填字段',
|
||||||
}),
|
}),
|
||||||
model: Joi.string().allow('', null).max(100),
|
model: Joi.string().allow('', null).max(100),
|
||||||
serialNumber: Joi.string().required().max(100).messages({
|
serialNumber: Joi.string().required().max(100).messages({
|
||||||
'string.empty': '序列号不能为空',
|
'string.empty': '序列号不能为空',
|
||||||
'string.max': '序列号不能超过100个字符',
|
'string.max': '序列号不能超过100个字符',
|
||||||
'any.required': '序列号是必填字段'
|
'any.required': '序列号是必填字段',
|
||||||
}),
|
}),
|
||||||
rackId: Joi.string().allow('', null).max(50),
|
rackId: Joi.string().allow('', null).max(50),
|
||||||
position: Joi.number().integer().min(1).max(100).allow(null),
|
position: Joi.number().integer().min(1).max(100).allow(null),
|
||||||
height: Joi.number().integer().min(1).max(50).allow(null),
|
height: Joi.number().integer().min(1).max(50).allow(null),
|
||||||
powerConsumption: Joi.number().min(0).max(100000).allow(null),
|
powerConsumption: Joi.number().min(0).max(100000).allow(null),
|
||||||
ipAddress: Joi.string().allow('', null).max(50),
|
ipAddress: Joi.string().allow('', null).max(50),
|
||||||
status: Joi.string().valid(...DEVICE_STATUS).default('offline'),
|
status: Joi.string()
|
||||||
|
.valid(...DEVICE_STATUS)
|
||||||
|
.default('offline'),
|
||||||
purchaseDate: Joi.date().allow(null),
|
purchaseDate: Joi.date().allow(null),
|
||||||
warrantyExpiry: Joi.date().allow(null),
|
warrantyExpiry: Joi.date().allow(null),
|
||||||
description: Joi.string().allow('', null).max(500),
|
description: Joi.string().allow('', null).max(500),
|
||||||
customFields: Joi.object().allow(null)
|
customFields: Joi.object().allow(null),
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateDeviceSchema = Joi.object({
|
const updateDeviceSchema = Joi.object({
|
||||||
name: Joi.string().max(100).messages({
|
name: Joi.string().max(100).messages({
|
||||||
'string.empty': '设备名称不能为空',
|
'string.empty': '设备名称不能为空',
|
||||||
'string.max': '设备名称不能超过100个字符'
|
'string.max': '设备名称不能超过100个字符',
|
||||||
}),
|
}),
|
||||||
type: Joi.string().valid(...DEVICE_TYPES).messages({
|
type: Joi.string()
|
||||||
'any.only': `设备类型必须是以下之一: ${DEVICE_TYPES.join(', ')}`
|
.valid(...DEVICE_TYPES)
|
||||||
|
.messages({
|
||||||
|
'any.only': `设备类型必须是以下之一: ${DEVICE_TYPES.join(', ')}`,
|
||||||
}),
|
}),
|
||||||
model: Joi.string().allow('', null).max(100),
|
model: Joi.string().allow('', null).max(100),
|
||||||
serialNumber: Joi.string().max(100).messages({
|
serialNumber: Joi.string().max(100).messages({
|
||||||
'string.max': '序列号不能超过100个字符'
|
'string.max': '序列号不能超过100个字符',
|
||||||
}),
|
}),
|
||||||
rackId: Joi.string().allow('', null).max(50),
|
rackId: Joi.string().allow('', null).max(50),
|
||||||
position: Joi.number().integer().min(1).max(100).allow(null),
|
position: Joi.number().integer().min(1).max(100).allow(null),
|
||||||
@@ -52,82 +59,58 @@ const updateDeviceSchema = Joi.object({
|
|||||||
purchaseDate: Joi.date().allow(null),
|
purchaseDate: Joi.date().allow(null),
|
||||||
warrantyExpiry: Joi.date().allow(null),
|
warrantyExpiry: Joi.date().allow(null),
|
||||||
description: Joi.string().allow('', null).max(500),
|
description: Joi.string().allow('', null).max(500),
|
||||||
customFields: Joi.object().allow(null)
|
customFields: Joi.object().allow(null),
|
||||||
}).min(1).messages({
|
})
|
||||||
'object.min': '至少需要提供一个字段进行更新'
|
.min(1)
|
||||||
|
.messages({
|
||||||
|
'object.min': '至少需要提供一个字段进行更新',
|
||||||
});
|
});
|
||||||
|
|
||||||
const batchDeviceIdsSchema = Joi.object({
|
const batchDeviceIdsSchema = Joi.object({
|
||||||
deviceIds: Joi.array()
|
deviceIds: Joi.array().items(Joi.string().required()).min(1).required().messages({
|
||||||
.items(Joi.string().required())
|
|
||||||
.min(1)
|
|
||||||
.required()
|
|
||||||
.messages({
|
|
||||||
'array.base': '设备ID列表必须是数组',
|
'array.base': '设备ID列表必须是数组',
|
||||||
'array.min': '至少需要提供一个设备ID',
|
'array.min': '至少需要提供一个设备ID',
|
||||||
'any.required': '设备ID列表是必填字段'
|
'any.required': '设备ID列表是必填字段',
|
||||||
})
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const batchStatusSchema = Joi.object({
|
const batchStatusSchema = Joi.object({
|
||||||
deviceIds: Joi.array()
|
deviceIds: Joi.array().items(Joi.string().required()).min(1).required().messages({
|
||||||
.items(Joi.string().required())
|
|
||||||
.min(1)
|
|
||||||
.required()
|
|
||||||
.messages({
|
|
||||||
'array.base': '设备ID列表必须是数组',
|
'array.base': '设备ID列表必须是数组',
|
||||||
'array.min': '至少需要提供一个设备ID',
|
'array.min': '至少需要提供一个设备ID',
|
||||||
'any.required': '设备ID列表是必填字段'
|
'any.required': '设备ID列表是必填字段',
|
||||||
}),
|
}),
|
||||||
status: Joi.string()
|
status: Joi.string()
|
||||||
.valid(...DEVICE_STATUS)
|
.valid(...DEVICE_STATUS)
|
||||||
.required()
|
.required()
|
||||||
.messages({
|
.messages({
|
||||||
'any.only': `状态必须是以下之一: ${DEVICE_STATUS.join(', ')}`,
|
'any.only': `状态必须是以下之一: ${DEVICE_STATUS.join(', ')}`,
|
||||||
'any.required': '状态是必填字段'
|
'any.required': '状态是必填字段',
|
||||||
})
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const batchMoveSchema = Joi.object({
|
const batchMoveSchema = Joi.object({
|
||||||
deviceIds: Joi.array()
|
deviceIds: Joi.array().items(Joi.string().required()).min(1).required(),
|
||||||
.items(Joi.string().required())
|
targetRackId: Joi.string().required().max(50).messages({
|
||||||
.min(1)
|
|
||||||
.required(),
|
|
||||||
targetRackId: Joi.string()
|
|
||||||
.required()
|
|
||||||
.max(50)
|
|
||||||
.messages({
|
|
||||||
'string.empty': '目标机柜ID不能为空',
|
'string.empty': '目标机柜ID不能为空',
|
||||||
'any.required': '目标机柜ID是必填字段'
|
'any.required': '目标机柜ID是必填字段',
|
||||||
}),
|
}),
|
||||||
startPosition: Joi.number()
|
startPosition: Joi.number().integer().min(1).allow(null),
|
||||||
.integer()
|
|
||||||
.min(1)
|
|
||||||
.allow(null)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const queryDeviceSchema = Joi.object({
|
const queryDeviceSchema = Joi.object({
|
||||||
keyword: Joi.string()
|
keyword: Joi.string().max(100).allow(''),
|
||||||
.max(100)
|
|
||||||
.allow(''),
|
|
||||||
status: Joi.string()
|
status: Joi.string()
|
||||||
.valid(...DEVICE_STATUS, 'all')
|
.valid(...DEVICE_STATUS, 'all')
|
||||||
.allow(''),
|
.allow(''),
|
||||||
type: Joi.string()
|
type: Joi.string()
|
||||||
.valid(...DEVICE_TYPES, 'all')
|
.valid(...DEVICE_TYPES, 'all')
|
||||||
.allow(''),
|
.allow(''),
|
||||||
rackId: Joi.string()
|
rackId: Joi.string().max(50).allow(''),
|
||||||
.max(50)
|
roomId: Joi.string().max(50).allow(''),
|
||||||
.allow(''),
|
isIdle: Joi.boolean().allow('').optional(),
|
||||||
page: Joi.number()
|
page: Joi.number().integer().min(1).default(1),
|
||||||
.integer()
|
pageSize: Joi.number().integer().min(1).max(10000).default(10),
|
||||||
.min(1)
|
|
||||||
.default(1),
|
|
||||||
pageSize: Joi.number()
|
|
||||||
.integer()
|
|
||||||
.min(1)
|
|
||||||
.max(10000)
|
|
||||||
.default(10)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -138,5 +121,5 @@ module.exports = {
|
|||||||
batchMoveSchema,
|
batchMoveSchema,
|
||||||
queryDeviceSchema,
|
queryDeviceSchema,
|
||||||
DEVICE_TYPES,
|
DEVICE_TYPES,
|
||||||
DEVICE_STATUS
|
DEVICE_STATUS,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,70 +11,49 @@ const createRackSchema = Joi.object({
|
|||||||
.allow('', null)
|
.allow('', null)
|
||||||
.messages({
|
.messages({
|
||||||
'string.max': '机柜ID不能超过50个字符',
|
'string.max': '机柜ID不能超过50个字符',
|
||||||
'string.pattern.base': '机柜ID只能包含字母、数字、下划线和横线'
|
'string.pattern.base': '机柜ID只能包含字母、数字、下划线和横线',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
name: Joi.string()
|
name: Joi.string().required().max(100).messages({
|
||||||
.required()
|
|
||||||
.max(100)
|
|
||||||
.messages({
|
|
||||||
'string.empty': '机柜名称不能为空',
|
'string.empty': '机柜名称不能为空',
|
||||||
'string.max': '机柜名称不能超过100个字符',
|
'string.max': '机柜名称不能超过100个字符',
|
||||||
'any.required': '机柜名称是必填字段'
|
'any.required': '机柜名称是必填字段',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
height: Joi.number()
|
height: Joi.number().integer().min(1).max(100).default(42).messages({
|
||||||
.integer()
|
|
||||||
.min(1)
|
|
||||||
.max(100)
|
|
||||||
.default(42)
|
|
||||||
.messages({
|
|
||||||
'number.base': '高度必须是数字',
|
'number.base': '高度必须是数字',
|
||||||
'number.integer': '高度必须是整数',
|
'number.integer': '高度必须是整数',
|
||||||
'number.min': '高度不能小于1',
|
'number.min': '高度不能小于1',
|
||||||
'number.max': '高度不能大于100'
|
'number.max': '高度不能大于100',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
maxPower: Joi.number()
|
maxPower: Joi.number().min(0).max(1000000).default(10000).messages({
|
||||||
.min(0)
|
|
||||||
.max(1000000)
|
|
||||||
.default(10000)
|
|
||||||
.messages({
|
|
||||||
'number.base': '最大功率必须是数字',
|
'number.base': '最大功率必须是数字',
|
||||||
'number.min': '最大功率不能小于0',
|
'number.min': '最大功率不能小于0',
|
||||||
'number.max': '最大功率不能超过1000000'
|
'number.max': '最大功率不能超过1000000',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
currentPower: Joi.number()
|
currentPower: Joi.number().min(0).default(0).messages({
|
||||||
.min(0)
|
|
||||||
.default(0)
|
|
||||||
.messages({
|
|
||||||
'number.base': '当前功率必须是数字',
|
'number.base': '当前功率必须是数字',
|
||||||
'number.min': '当前功率不能小于0'
|
'number.min': '当前功率不能小于0',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
status: Joi.string()
|
status: Joi.string()
|
||||||
.valid(...RACK_STATUS)
|
.valid(...RACK_STATUS)
|
||||||
.default('active')
|
.default('active')
|
||||||
.messages({
|
.messages({
|
||||||
'any.only': `状态必须是以下之一: ${RACK_STATUS.join(', ')}`
|
'any.only': `状态必须是以下之一: ${RACK_STATUS.join(', ')}`,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
roomId: Joi.string()
|
roomId: Joi.string().required().max(50).messages({
|
||||||
.required()
|
|
||||||
.max(50)
|
|
||||||
.messages({
|
|
||||||
'string.empty': '机房ID不能为空',
|
'string.empty': '机房ID不能为空',
|
||||||
'string.max': '机房ID不能超过50个字符',
|
'string.max': '机房ID不能超过50个字符',
|
||||||
'any.required': '机房ID是必填字段'
|
'any.required': '机房ID是必填字段',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
description: Joi.string()
|
description: Joi.string().max(500).allow('', null).messages({
|
||||||
.max(500)
|
'string.max': '描述不能超过500个字符',
|
||||||
.allow('', null)
|
}),
|
||||||
.messages({
|
|
||||||
'string.max': '描述不能超过500个字符'
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 更新机柜验证Schema
|
// 更新机柜验证Schema
|
||||||
@@ -84,86 +63,62 @@ const updateRackSchema = Joi.object({
|
|||||||
.pattern(/^[a-zA-Z0-9_-]+$/)
|
.pattern(/^[a-zA-Z0-9_-]+$/)
|
||||||
.messages({
|
.messages({
|
||||||
'string.max': '机柜ID不能超过50个字符',
|
'string.max': '机柜ID不能超过50个字符',
|
||||||
'string.pattern.base': '机柜ID只能包含字母、数字、下划线和横线'
|
'string.pattern.base': '机柜ID只能包含字母、数字、下划线和横线',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
name: Joi.string()
|
name: Joi.string().max(100).messages({
|
||||||
.max(100)
|
'string.max': '机柜名称不能超过100个字符',
|
||||||
.messages({
|
|
||||||
'string.max': '机柜名称不能超过100个字符'
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
height: Joi.number()
|
height: Joi.number().integer().min(1).max(100).messages({
|
||||||
.integer()
|
|
||||||
.min(1)
|
|
||||||
.max(100)
|
|
||||||
.messages({
|
|
||||||
'number.base': '高度必须是数字',
|
'number.base': '高度必须是数字',
|
||||||
'number.integer': '高度必须是整数',
|
'number.integer': '高度必须是整数',
|
||||||
'number.min': '高度不能小于1',
|
'number.min': '高度不能小于1',
|
||||||
'number.max': '高度不能大于100'
|
'number.max': '高度不能大于100',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
maxPower: Joi.number()
|
maxPower: Joi.number().min(0).max(1000000).messages({
|
||||||
.min(0)
|
|
||||||
.max(1000000)
|
|
||||||
.messages({
|
|
||||||
'number.base': '最大功率必须是数字',
|
'number.base': '最大功率必须是数字',
|
||||||
'number.min': '最大功率不能小于0',
|
'number.min': '最大功率不能小于0',
|
||||||
'number.max': '最大功率不能超过1000000'
|
'number.max': '最大功率不能超过1000000',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
currentPower: Joi.number()
|
currentPower: Joi.number().min(0).messages({
|
||||||
.min(0)
|
|
||||||
.messages({
|
|
||||||
'number.base': '当前功率必须是数字',
|
'number.base': '当前功率必须是数字',
|
||||||
'number.min': '当前功率不能小于0'
|
'number.min': '当前功率不能小于0',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
status: Joi.string()
|
status: Joi.string()
|
||||||
.valid(...RACK_STATUS)
|
.valid(...RACK_STATUS)
|
||||||
.messages({
|
.messages({
|
||||||
'any.only': `状态必须是以下之一: ${RACK_STATUS.join(', ')}`
|
'any.only': `状态必须是以下之一: ${RACK_STATUS.join(', ')}`,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
roomId: Joi.string()
|
roomId: Joi.string().max(50),
|
||||||
.max(50),
|
|
||||||
|
|
||||||
description: Joi.string()
|
description: Joi.string().max(500).allow('', null).messages({
|
||||||
.max(500)
|
'string.max': '描述不能超过500个字符',
|
||||||
.allow('', null)
|
}),
|
||||||
.messages({
|
|
||||||
'string.max': '描述不能超过500个字符'
|
|
||||||
})
|
})
|
||||||
}).min(1).messages({
|
.min(1)
|
||||||
'object.min': '至少需要提供一个字段进行更新'
|
.messages({
|
||||||
|
'object.min': '至少需要提供一个字段进行更新',
|
||||||
});
|
});
|
||||||
|
|
||||||
// 查询机柜验证Schema
|
// 查询机柜验证Schema
|
||||||
const queryRackSchema = Joi.object({
|
const queryRackSchema = Joi.object({
|
||||||
roomId: Joi.string()
|
roomId: Joi.string().max(50).allow(''),
|
||||||
.max(50)
|
|
||||||
.allow(''),
|
|
||||||
status: Joi.string()
|
status: Joi.string()
|
||||||
.valid(...RACK_STATUS, 'all')
|
.valid(...RACK_STATUS, 'all')
|
||||||
.allow(''),
|
.allow(''),
|
||||||
keyword: Joi.string()
|
keyword: Joi.string().max(100).allow(''),
|
||||||
.max(100)
|
page: Joi.number().integer().min(1).default(1),
|
||||||
.allow(''),
|
pageSize: Joi.number().integer().min(1).max(100).default(10),
|
||||||
page: Joi.number()
|
|
||||||
.integer()
|
|
||||||
.min(1)
|
|
||||||
.default(1),
|
|
||||||
pageSize: Joi.number()
|
|
||||||
.integer()
|
|
||||||
.min(1)
|
|
||||||
.max(100)
|
|
||||||
.default(10)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createRackSchema,
|
createRackSchema,
|
||||||
updateRackSchema,
|
updateRackSchema,
|
||||||
queryRackSchema,
|
queryRackSchema,
|
||||||
RACK_STATUS
|
RACK_STATUS,
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user