feat: 实现ID生成器并重构模型ID生成逻辑
refactor: 统一ID生成方式,使用新的idGenerator模块 refactor: 重构模型ID生成逻辑,允许空ID并在创建前自动生成 fix(roomSchema): 使机房ID字段变为可选并更新前端表单验证 style: 格式化代码并优化导入语句 test: 添加操作日志集成测试文件 docs: 添加错误处理模块文档
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require('sequelize');
|
||||||
const { sequelize } = require('../db');
|
const { sequelize } = require('../db');
|
||||||
const Device = require('./Device');
|
const Device = require('./Device');
|
||||||
|
const { generateId } = require('../utils/idGenerator');
|
||||||
|
|
||||||
const Cable = sequelize.define(
|
const Cable = sequelize.define(
|
||||||
'Cable',
|
'Cable',
|
||||||
@@ -8,7 +9,7 @@ const Cable = sequelize.define(
|
|||||||
cableId: {
|
cableId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
allowNull: false,
|
allowNull: true,
|
||||||
unique: true,
|
unique: true,
|
||||||
},
|
},
|
||||||
sourceDeviceId: {
|
sourceDeviceId: {
|
||||||
@@ -90,6 +91,13 @@ const Cable = sequelize.define(
|
|||||||
{ fields: ['sourceDeviceId', 'targetDeviceId'] },
|
{ fields: ['sourceDeviceId', 'targetDeviceId'] },
|
||||||
{ fields: ['cableLabel'] },
|
{ fields: ['cableLabel'] },
|
||||||
],
|
],
|
||||||
|
hooks: {
|
||||||
|
beforeCreate: (cable) => {
|
||||||
|
if (!cable.cableId) {
|
||||||
|
cable.cableId = generateId({ prefix: 'CBL' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require('sequelize');
|
||||||
const { sequelize } = require('../db');
|
const { sequelize } = require('../db');
|
||||||
|
const { generateId } = require('../utils/idGenerator');
|
||||||
|
|
||||||
const Consumable = sequelize.define(
|
const Consumable = sequelize.define(
|
||||||
'Consumable',
|
'Consumable',
|
||||||
@@ -7,7 +8,7 @@ const Consumable = sequelize.define(
|
|||||||
consumableId: {
|
consumableId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
allowNull: false,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
@@ -79,6 +80,13 @@ const Consumable = sequelize.define(
|
|||||||
{ fields: ['category', 'status'] },
|
{ fields: ['category', 'status'] },
|
||||||
{ fields: ['updatedAt'] },
|
{ fields: ['updatedAt'] },
|
||||||
],
|
],
|
||||||
|
hooks: {
|
||||||
|
beforeCreate: (consumable) => {
|
||||||
|
if (!consumable.consumableId) {
|
||||||
|
consumable.consumableId = generateId({ prefix: 'CON' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require('sequelize');
|
||||||
const { sequelize } = require('../db');
|
const { sequelize } = require('../db');
|
||||||
|
const { generateId } = require('../utils/idGenerator');
|
||||||
const Rack = require('./Rack');
|
const Rack = require('./Rack');
|
||||||
const Warehouse = require('./Warehouse');
|
const Warehouse = require('./Warehouse');
|
||||||
|
|
||||||
@@ -9,7 +10,7 @@ const Device = sequelize.define(
|
|||||||
deviceId: {
|
deviceId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
allowNull: false,
|
allowNull: true,
|
||||||
unique: true,
|
unique: true,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
@@ -107,6 +108,13 @@ const Device = sequelize.define(
|
|||||||
{ fields: ['rackId', 'position'] },
|
{ fields: ['rackId', 'position'] },
|
||||||
{ fields: ['rackId', 'position', 'isIdle'] },
|
{ fields: ['rackId', 'position', 'isIdle'] },
|
||||||
],
|
],
|
||||||
|
hooks: {
|
||||||
|
beforeCreate: (device) => {
|
||||||
|
if (!device.deviceId) {
|
||||||
|
device.deviceId = generateId({ prefix: 'DEV' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require('sequelize');
|
||||||
const { sequelize } = require('../db');
|
const { sequelize } = require('../db');
|
||||||
|
const { generateId } = require('../utils/idGenerator');
|
||||||
const Room = require('./Room');
|
const Room = require('./Room');
|
||||||
|
|
||||||
const Rack = sequelize.define(
|
const Rack = sequelize.define(
|
||||||
@@ -8,7 +9,7 @@ const Rack = sequelize.define(
|
|||||||
rackId: {
|
rackId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
allowNull: false,
|
allowNull: true,
|
||||||
unique: true,
|
unique: true,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
@@ -45,6 +46,13 @@ const Rack = sequelize.define(
|
|||||||
tableName: 'racks',
|
tableName: 'racks',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
indexes: [{ fields: ['roomId'] }, { fields: ['status'] }, { fields: ['roomId', 'status'] }],
|
indexes: [{ fields: ['roomId'] }, { fields: ['status'] }, { fields: ['roomId', 'status'] }],
|
||||||
|
hooks: {
|
||||||
|
beforeCreate: (rack) => {
|
||||||
|
if (!rack.rackId) {
|
||||||
|
rack.rackId = generateId({ prefix: 'RCK' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require('sequelize');
|
||||||
const { sequelize } = require('../db');
|
const { sequelize } = require('../db');
|
||||||
|
const { generateId } = require('../utils/idGenerator');
|
||||||
|
|
||||||
const Role = sequelize.define(
|
const Role = sequelize.define(
|
||||||
'Role',
|
'Role',
|
||||||
@@ -7,7 +8,7 @@ const Role = sequelize.define(
|
|||||||
roleId: {
|
roleId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
allowNull: false,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
roleName: {
|
roleName: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
@@ -38,6 +39,13 @@ const Role = sequelize.define(
|
|||||||
{
|
{
|
||||||
tableName: 'roles',
|
tableName: 'roles',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
|
hooks: {
|
||||||
|
beforeCreate: (role) => {
|
||||||
|
if (!role.roleId) {
|
||||||
|
role.roleId = generateId({ prefix: 'ROLE' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+40
-1
@@ -1,13 +1,45 @@
|
|||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require('sequelize');
|
||||||
const { sequelize } = require('../db');
|
const { sequelize } = require('../db');
|
||||||
|
|
||||||
|
async function generateRoomSequenceId() {
|
||||||
|
const RoomModel = require('./Room');
|
||||||
|
const prefix = 'ROOM';
|
||||||
|
const paddingLength = 6;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const latestRoom = await RoomModel.findOne({
|
||||||
|
attributes: ['roomId'],
|
||||||
|
where: {
|
||||||
|
roomId: {
|
||||||
|
[require('sequelize').Op.like]: `${prefix}%`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
order: [['roomId', 'DESC']],
|
||||||
|
});
|
||||||
|
|
||||||
|
let nextNumber = 1;
|
||||||
|
if (latestRoom && latestRoom.roomId) {
|
||||||
|
const numStr = latestRoom.roomId.replace(prefix, '');
|
||||||
|
const currentNum = parseInt(numStr, 10);
|
||||||
|
if (!isNaN(currentNum)) {
|
||||||
|
nextNumber = currentNum + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${prefix}${nextNumber.toString().padStart(paddingLength, '0')}`;
|
||||||
|
} catch (error) {
|
||||||
|
const fallbackId = `${prefix}${Date.now().toString().slice(-6)}`;
|
||||||
|
return fallbackId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const Room = sequelize.define(
|
const Room = sequelize.define(
|
||||||
'Room',
|
'Room',
|
||||||
{
|
{
|
||||||
roomId: {
|
roomId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
allowNull: false,
|
allowNull: true,
|
||||||
unique: true,
|
unique: true,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
@@ -38,6 +70,13 @@ const Room = sequelize.define(
|
|||||||
tableName: 'rooms',
|
tableName: 'rooms',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
indexes: [{ fields: ['status'] }, { fields: ['name'] }],
|
indexes: [{ fields: ['status'] }, { fields: ['name'] }],
|
||||||
|
hooks: {
|
||||||
|
beforeCreate: async (room) => {
|
||||||
|
if (!room.roomId) {
|
||||||
|
room.roomId = await generateRoomSequenceId();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const { DataTypes } = require('sequelize');
|
|||||||
const { sequelize } = require('../db');
|
const { sequelize } = require('../db');
|
||||||
const User = require('./User');
|
const User = require('./User');
|
||||||
const Device = require('./Device');
|
const Device = require('./Device');
|
||||||
|
const { generateId } = require('../utils/idGenerator');
|
||||||
|
|
||||||
const Ticket = sequelize.define(
|
const Ticket = sequelize.define(
|
||||||
'Ticket',
|
'Ticket',
|
||||||
@@ -9,7 +10,7 @@ const Ticket = sequelize.define(
|
|||||||
ticketId: {
|
ticketId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
allowNull: false,
|
allowNull: true,
|
||||||
unique: true,
|
unique: true,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
@@ -128,6 +129,13 @@ const Ticket = sequelize.define(
|
|||||||
{ fields: ['assigneeId'] },
|
{ fields: ['assigneeId'] },
|
||||||
{ fields: ['createdAt'] },
|
{ fields: ['createdAt'] },
|
||||||
],
|
],
|
||||||
|
hooks: {
|
||||||
|
beforeCreate: (ticket) => {
|
||||||
|
if (!ticket.ticketId) {
|
||||||
|
ticket.ticketId = generateId({ prefix: 'TKT' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require('sequelize');
|
||||||
const { sequelize } = require('../db');
|
const { sequelize } = require('../db');
|
||||||
|
const { generateId } = require('../utils/idGenerator');
|
||||||
|
|
||||||
const User = sequelize.define(
|
const User = sequelize.define(
|
||||||
'User',
|
'User',
|
||||||
@@ -7,7 +8,7 @@ const User = sequelize.define(
|
|||||||
userId: {
|
userId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
allowNull: false,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
username: {
|
username: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
@@ -62,6 +63,13 @@ const User = sequelize.define(
|
|||||||
tableName: 'users',
|
tableName: 'users',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
indexes: [{ fields: ['status'] }, { fields: ['username'] }, { fields: ['email'] }],
|
indexes: [{ fields: ['status'] }, { fields: ['username'] }, { fields: ['email'] }],
|
||||||
|
hooks: {
|
||||||
|
beforeCreate: (user) => {
|
||||||
|
if (!user.userId) {
|
||||||
|
user.userId = generateId({ prefix: 'USR' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require('sequelize');
|
||||||
const { sequelize } = require('../db');
|
const { sequelize } = require('../db');
|
||||||
|
const { generateId } = require('../utils/idGenerator');
|
||||||
|
|
||||||
const Warehouse = sequelize.define(
|
const Warehouse = sequelize.define(
|
||||||
'Warehouse',
|
'Warehouse',
|
||||||
@@ -7,7 +8,7 @@ const Warehouse = sequelize.define(
|
|||||||
warehouseId: {
|
warehouseId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
allowNull: false,
|
allowNull: true,
|
||||||
unique: true,
|
unique: true,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
@@ -36,6 +37,13 @@ const Warehouse = sequelize.define(
|
|||||||
tableName: 'warehouses',
|
tableName: 'warehouses',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
indexes: [{ fields: ['status'] }, { fields: ['name'] }],
|
indexes: [{ fields: ['status'] }, { fields: ['name'] }],
|
||||||
|
hooks: {
|
||||||
|
beforeCreate: (warehouse) => {
|
||||||
|
if (!warehouse.warehouseId) {
|
||||||
|
warehouse.warehouseId = generateId({ prefix: 'WH' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -11,13 +11,10 @@ const {
|
|||||||
USERNAME_MIN_LENGTH,
|
USERNAME_MIN_LENGTH,
|
||||||
USERNAME_MAX_LENGTH,
|
USERNAME_MAX_LENGTH,
|
||||||
} = require('../config');
|
} = require('../config');
|
||||||
|
const { generateId } = require('../utils/idGenerator');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const generateId = () => {
|
|
||||||
return 'user_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 9);
|
|
||||||
};
|
|
||||||
|
|
||||||
router.post('/register', async (req, res) => {
|
router.post('/register', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { username, password, email, phone, realName } = req.body;
|
const { username, password, email, phone, realName } = req.body;
|
||||||
@@ -57,7 +54,7 @@ router.post('/register', async (req, res) => {
|
|||||||
const isFirstUser = userCount === 0;
|
const isFirstUser = userCount === 0;
|
||||||
|
|
||||||
const user = await User.create({
|
const user = await User.create({
|
||||||
userId: generateId(),
|
userId: generateId({ prefix: 'USR' }),
|
||||||
username,
|
username,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
email,
|
email,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const { sequelize } = require('../db');
|
|||||||
const Consumable = require('../models/Consumable');
|
const Consumable = require('../models/Consumable');
|
||||||
const ConsumableLog = require('../models/ConsumableLog');
|
const ConsumableLog = require('../models/ConsumableLog');
|
||||||
const { importJobManager } = require('../utils/importJobManager');
|
const { importJobManager } = require('../utils/importJobManager');
|
||||||
|
const { generateId } = require('../utils/idGenerator');
|
||||||
|
|
||||||
const SUPPORTED_FIELDS = [
|
const SUPPORTED_FIELDS = [
|
||||||
'consumableId',
|
'consumableId',
|
||||||
@@ -54,7 +55,10 @@ const parseSnList = snStr => {
|
|||||||
if (!snStr) return [];
|
if (!snStr) return [];
|
||||||
if (Array.isArray(snStr)) return snStr;
|
if (Array.isArray(snStr)) return snStr;
|
||||||
if (typeof snStr === 'string') {
|
if (typeof snStr === 'string') {
|
||||||
return snStr.split(/[,,;;\n]/).map(s => s.trim()).filter(Boolean);
|
return snStr
|
||||||
|
.split(/[,,;;\n]/)
|
||||||
|
.map(s => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
@@ -108,8 +112,7 @@ router.post('/consumables/background', async (req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const consumableId =
|
const consumableId = mappedItem.consumableId || generateId({ prefix: 'CON' });
|
||||||
mappedItem.consumableId || mappedItem.name + `_${Date.now()}`;
|
|
||||||
const name = mappedItem.name;
|
const name = mappedItem.name;
|
||||||
const category = mappedItem.category;
|
const category = mappedItem.category;
|
||||||
|
|
||||||
@@ -128,9 +131,7 @@ router.post('/consumables/background', async (req, res) => {
|
|||||||
category,
|
category,
|
||||||
unit: mappedItem.unit || '个',
|
unit: mappedItem.unit || '个',
|
||||||
currentStock:
|
currentStock:
|
||||||
snList.length > 0
|
snList.length > 0 ? snList.length : parseInt(mappedItem.currentStock) || 0,
|
||||||
? snList.length
|
|
||||||
: parseInt(mappedItem.currentStock) || 0,
|
|
||||||
minStock: parseInt(mappedItem.minStock) || 10,
|
minStock: parseInt(mappedItem.minStock) || 10,
|
||||||
maxStock: parseInt(mappedItem.maxStock) || 0,
|
maxStock: parseInt(mappedItem.maxStock) || 0,
|
||||||
unitPrice: parseFloat(mappedItem.unitPrice) || 0,
|
unitPrice: parseFloat(mappedItem.unitPrice) || 0,
|
||||||
@@ -210,7 +211,13 @@ router.post('/consumables/background', async (req, res) => {
|
|||||||
{ transaction }
|
{ transaction }
|
||||||
);
|
);
|
||||||
|
|
||||||
importJobManager.incrementProgress(jobId, existingConsumable && mode === 'update' ? 0 : 1, 0, 0, existingConsumable ? 1 : 0);
|
importJobManager.incrementProgress(
|
||||||
|
jobId,
|
||||||
|
existingConsumable && mode === 'update' ? 0 : 1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
existingConsumable ? 1 : 0
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
results.failed++;
|
results.failed++;
|
||||||
results.errors.push(`第 ${rowNumber} 行: ${error.message}`);
|
results.errors.push(`第 ${rowNumber} 行: ${error.message}`);
|
||||||
@@ -294,13 +301,28 @@ router.get('/consumables/field-mappings', async (req, res) => {
|
|||||||
{ source: '单位', target: 'unit', required: false, description: '计量单位,默认"个"' },
|
{ source: '单位', target: 'unit', required: false, description: '计量单位,默认"个"' },
|
||||||
{ source: '当前库存', target: 'currentStock', required: false, description: '当前库存数量' },
|
{ source: '当前库存', target: 'currentStock', required: false, description: '当前库存数量' },
|
||||||
{ source: '最小库存', target: 'minStock', required: false, description: '安全库存预警值' },
|
{ source: '最小库存', target: 'minStock', required: false, description: '安全库存预警值' },
|
||||||
{ source: '最大库存', target: 'maxStock', required: false, description: '最大库存限制,0表示无限制' },
|
{
|
||||||
|
source: '最大库存',
|
||||||
|
target: 'maxStock',
|
||||||
|
required: false,
|
||||||
|
description: '最大库存限制,0表示无限制',
|
||||||
|
},
|
||||||
{ source: '单价', target: 'unitPrice', required: false, description: '耗材单价' },
|
{ source: '单价', target: 'unitPrice', required: false, description: '耗材单价' },
|
||||||
{ source: '供应商', target: 'supplier', required: false, description: '供应商名称' },
|
{ source: '供应商', target: 'supplier', required: false, description: '供应商名称' },
|
||||||
{ source: '存放位置', target: 'location', required: false, description: '仓库内存放位置' },
|
{ source: '存放位置', target: 'location', required: false, description: '仓库内存放位置' },
|
||||||
{ source: '描述', target: 'description', required: false, description: '耗材详细描述' },
|
{ source: '描述', target: 'description', required: false, description: '耗材详细描述' },
|
||||||
{ source: 'SN序列号', target: 'snList', required: false, description: '序列号列表,用逗号分隔' },
|
{
|
||||||
{ source: '状态', target: 'status', required: false, description: '状态:active启用,inactive停用' },
|
source: 'SN序列号',
|
||||||
|
target: 'snList',
|
||||||
|
required: false,
|
||||||
|
description: '序列号列表,用逗号分隔',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: '状态',
|
||||||
|
target: 'status',
|
||||||
|
required: false,
|
||||||
|
description: '状态:active启用,inactive停用',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const systemFields = [
|
const systemFields = [
|
||||||
|
|||||||
@@ -471,16 +471,7 @@ router.post('/quick-inout', async (req, res) => {
|
|||||||
while (attempt < RETRY.MAX_RETRIES) {
|
while (attempt < RETRY.MAX_RETRIES) {
|
||||||
const transaction = await sequelize.transaction();
|
const transaction = await sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
const {
|
const { consumableId, type, quantity, operator, reason, notes, snList, deviceId } = req.body;
|
||||||
consumableId,
|
|
||||||
type,
|
|
||||||
quantity,
|
|
||||||
operator,
|
|
||||||
reason,
|
|
||||||
notes,
|
|
||||||
snList,
|
|
||||||
deviceId,
|
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
const consumable = await Consumable.findByPk(consumableId, { transaction });
|
const consumable = await Consumable.findByPk(consumableId, { transaction });
|
||||||
if (!consumable) {
|
if (!consumable) {
|
||||||
|
|||||||
@@ -171,7 +171,10 @@ router.post('/batch', async (req, res) => {
|
|||||||
|
|
||||||
let device;
|
let device;
|
||||||
if (portData.deviceSn) {
|
if (portData.deviceSn) {
|
||||||
device = await Device.findOne({ where: { serialNumber: portData.deviceSn }, transaction });
|
device = await Device.findOne({
|
||||||
|
where: { serialNumber: portData.deviceSn },
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
if (!device) {
|
if (!device) {
|
||||||
throw new Error(`设备SN ${portData.deviceSn} 不存在`);
|
throw new Error(`设备SN ${portData.deviceSn} 不存在`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -728,12 +728,12 @@ async function generateDeviceId() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
attributes: [
|
attributes: [
|
||||||
[sequelize.fn('MAX', sequelize.literal("CAST(SUBSTR(deviceId, 4) AS INTEGER)")), 'maxNum'],
|
[sequelize.fn('MAX', sequelize.literal('CAST(SUBSTR(deviceId, 4) AS INTEGER)')), 'maxNum'],
|
||||||
],
|
],
|
||||||
raw: true,
|
raw: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const maxNumber = (result && result.maxNum) ? parseInt(result.maxNum, 10) : 0;
|
const maxNumber = result && result.maxNum ? parseInt(result.maxNum, 10) : 0;
|
||||||
const newNumber = maxNumber + 1;
|
const newNumber = maxNumber + 1;
|
||||||
return `DEV${String(newNumber).padStart(3, '0')}`;
|
return `DEV${String(newNumber).padStart(3, '0')}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const User = require('../models/User');
|
|||||||
const PendingDevice = require('../models/PendingDevice');
|
const PendingDevice = require('../models/PendingDevice');
|
||||||
const { authMiddleware, authorize } = require('../middleware/auth');
|
const { authMiddleware, authorize } = require('../middleware/auth');
|
||||||
const { PAGINATION } = require('../config');
|
const { PAGINATION } = require('../config');
|
||||||
|
const { generateId } = require('../utils/idGenerator');
|
||||||
|
|
||||||
InventoryTask.belongsTo(InventoryPlan, { foreignKey: 'planId', as: 'Plan' });
|
InventoryTask.belongsTo(InventoryPlan, { foreignKey: 'planId', as: 'Plan' });
|
||||||
InventoryPlan.hasMany(InventoryTask, { foreignKey: 'planId', as: 'Tasks' });
|
InventoryPlan.hasMany(InventoryTask, { foreignKey: 'planId', as: 'Tasks' });
|
||||||
@@ -23,20 +24,16 @@ InventoryPlan.hasMany(InventoryRecord, { foreignKey: 'planId', as: 'Records' });
|
|||||||
InventoryRecord.belongsTo(Device, { foreignKey: 'deviceId', as: 'Device' });
|
InventoryRecord.belongsTo(Device, { foreignKey: 'deviceId', as: 'Device' });
|
||||||
InventoryRecord.belongsTo(User, { foreignKey: 'checkedBy', as: 'Checker' });
|
InventoryRecord.belongsTo(User, { foreignKey: 'checkedBy', as: 'Checker' });
|
||||||
|
|
||||||
function generateId(prefix) {
|
|
||||||
return `${prefix}${Date.now().toString(36).toUpperCase()}${Math.random().toString(36).substr(2, 6).toUpperCase()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function generatePlanId() {
|
function generatePlanId() {
|
||||||
return `PLAN${Date.now().toString(36).toUpperCase()}${Math.random().toString(36).substr(2, 4).toUpperCase()}`;
|
return generateId({ prefix: 'PLAN' });
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateTaskId() {
|
function generateTaskId() {
|
||||||
return `TASK${Date.now().toString(36).toUpperCase()}${Math.random().toString(36).substr(2, 4).toUpperCase()}`;
|
return generateId({ prefix: 'TASK' });
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateRecordId() {
|
function generateRecordId() {
|
||||||
return `REC${Date.now().toString(36).toUpperCase()}${Math.random().toString(36).substr(2, 6).toUpperCase()}`;
|
return generateId({ prefix: 'REC' });
|
||||||
}
|
}
|
||||||
|
|
||||||
router.use(authMiddleware);
|
router.use(authMiddleware);
|
||||||
|
|||||||
@@ -281,7 +281,10 @@ router.post('/batch', async (req, res) => {
|
|||||||
|
|
||||||
let device;
|
let device;
|
||||||
if (cardData.deviceSn) {
|
if (cardData.deviceSn) {
|
||||||
device = await Device.findOne({ where: { serialNumber: cardData.deviceSn }, transaction });
|
device = await Device.findOne({
|
||||||
|
where: { serialNumber: cardData.deviceSn },
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
if (!device) {
|
if (!device) {
|
||||||
throw new Error(`设备SN ${cardData.deviceSn} 不存在`);
|
throw new Error(`设备SN ${cardData.deviceSn} 不存在`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,10 @@ const UserRole = require('../models/UserRole');
|
|||||||
const User = require('../models/User');
|
const User = require('../models/User');
|
||||||
const { authMiddleware } = require('../middleware/auth');
|
const { authMiddleware } = require('../middleware/auth');
|
||||||
const { logRoleOperation } = require('../utils/operationLogger');
|
const { logRoleOperation } = require('../utils/operationLogger');
|
||||||
|
const { generateId } = require('../utils/idGenerator');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const generateId = () => {
|
|
||||||
return 'role_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 9);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { Op } = require('sequelize');
|
const { Op } = require('sequelize');
|
||||||
|
|
||||||
router.get('/', authMiddleware, async (req, res) => {
|
router.get('/', authMiddleware, async (req, res) => {
|
||||||
@@ -129,7 +126,7 @@ router.post('/', authMiddleware, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const role = await Role.create({
|
const role = await Role.create({
|
||||||
roleId: generateId(),
|
roleId: generateId({ prefix: 'ROLE' }),
|
||||||
roleName,
|
roleName,
|
||||||
roleCode,
|
roleCode,
|
||||||
description,
|
description,
|
||||||
|
|||||||
@@ -338,7 +338,15 @@ const TICKET_EXPORT_FIELDS = [
|
|||||||
|
|
||||||
router.get('/export', async (req, res) => {
|
router.get('/export', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { keyword, status, priority, faultCategory, deviceId, format = 'csv', ticketIds } = req.query;
|
const {
|
||||||
|
keyword,
|
||||||
|
status,
|
||||||
|
priority,
|
||||||
|
faultCategory,
|
||||||
|
deviceId,
|
||||||
|
format = 'csv',
|
||||||
|
ticketIds,
|
||||||
|
} = req.query;
|
||||||
|
|
||||||
const where = {};
|
const where = {};
|
||||||
|
|
||||||
@@ -391,9 +399,19 @@ router.get('/export', async (req, res) => {
|
|||||||
const priorityMap = { low: '低', medium: '中', high: '高', urgent: '紧急' };
|
const priorityMap = { low: '低', medium: '中', high: '高', urgent: '紧急' };
|
||||||
value = priorityMap[value] || value;
|
value = priorityMap[value] || value;
|
||||||
} else if (fieldName === 'status') {
|
} else if (fieldName === 'status') {
|
||||||
const statusMap = { pending: '待处理', in_progress: '处理中', completed: '已完成', closed: '已关闭' };
|
const statusMap = {
|
||||||
|
pending: '待处理',
|
||||||
|
in_progress: '处理中',
|
||||||
|
completed: '已完成',
|
||||||
|
closed: '已关闭',
|
||||||
|
};
|
||||||
value = statusMap[value] || value;
|
value = statusMap[value] || value;
|
||||||
} else if (fieldName === 'expectedCompletionDate' || fieldName === 'completionDate' || fieldName === 'createdAt' || fieldName === 'updatedAt') {
|
} else if (
|
||||||
|
fieldName === 'expectedCompletionDate' ||
|
||||||
|
fieldName === 'completionDate' ||
|
||||||
|
fieldName === 'createdAt' ||
|
||||||
|
fieldName === 'updatedAt'
|
||||||
|
) {
|
||||||
value = value ? new Date(value).toLocaleString('zh-CN') : '';
|
value = value ? new Date(value).toLocaleString('zh-CN') : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,7 +429,8 @@ router.get('/export', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (format === 'json') {
|
if (format === 'json') {
|
||||||
return res.setHeader('Content-Type', 'application/json; charset=utf-8')
|
return res
|
||||||
|
.setHeader('Content-Type', 'application/json; charset=utf-8')
|
||||||
.setHeader('Content-Disposition', `attachment; filename=tickets_${Date.now()}.json`)
|
.setHeader('Content-Disposition', `attachment; filename=tickets_${Date.now()}.json`)
|
||||||
.json({ success: true, data: exportData, total: exportData.length });
|
.json({ success: true, data: exportData, total: exportData.length });
|
||||||
}
|
}
|
||||||
@@ -422,7 +441,10 @@ router.get('/export', async (req, res) => {
|
|||||||
XLSX.utils.book_append_sheet(workbook, worksheet, '工单数据');
|
XLSX.utils.book_append_sheet(workbook, worksheet, '工单数据');
|
||||||
const xlsxBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'buffer' });
|
const xlsxBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'buffer' });
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
res.setHeader(
|
||||||
|
'Content-Type',
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||||
|
);
|
||||||
res.setHeader('Content-Disposition', `attachment; filename=tickets_${Date.now()}.xlsx`);
|
res.setHeader('Content-Disposition', `attachment; filename=tickets_${Date.now()}.xlsx`);
|
||||||
return res.send(xlsxBuffer);
|
return res.send(xlsxBuffer);
|
||||||
}
|
}
|
||||||
@@ -554,7 +576,7 @@ router.post('/', async (req, res) => {
|
|||||||
return res.status(400).json({ error: '请选择设备或手动输入设备信息' });
|
return res.status(400).json({ error: '请选择设备或手动输入设备信息' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const ticketId = `TKT${Date.now().toString(36).toUpperCase()}${Math.random().toString(36).substr(2, 4).toUpperCase()}`;
|
const ticketId = generateId({ prefix: 'TKT' });
|
||||||
|
|
||||||
const ticket = await Ticket.create({
|
const ticket = await Ticket.create({
|
||||||
ticketId,
|
ticketId,
|
||||||
@@ -607,9 +629,17 @@ router.put('/:ticketId', async (req, res) => {
|
|||||||
|
|
||||||
// 白名单过滤:只允许更新安全字段,防止覆盖 ticketId/createdAt 等关键字段
|
// 白名单过滤:只允许更新安全字段,防止覆盖 ticketId/createdAt 等关键字段
|
||||||
const ALLOWED_UPDATE_FIELDS = [
|
const ALLOWED_UPDATE_FIELDS = [
|
||||||
'title', 'description', 'category', 'priority', 'location',
|
'title',
|
||||||
'contactPerson', 'contactPhone', 'contactEmail',
|
'description',
|
||||||
'expectedDate', 'attachments', 'customFields',
|
'category',
|
||||||
|
'priority',
|
||||||
|
'location',
|
||||||
|
'contactPerson',
|
||||||
|
'contactPhone',
|
||||||
|
'contactEmail',
|
||||||
|
'expectedDate',
|
||||||
|
'attachments',
|
||||||
|
'customFields',
|
||||||
];
|
];
|
||||||
const updateData = {};
|
const updateData = {};
|
||||||
ALLOWED_UPDATE_FIELDS.forEach(field => {
|
ALLOWED_UPDATE_FIELDS.forEach(field => {
|
||||||
|
|||||||
@@ -109,7 +109,8 @@ const migrations = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '耗材日志设备关联',
|
name: '耗材日志设备关联',
|
||||||
description: '为 consumable_logs 表添加 deviceId、deviceName、rackId、rackName、roomId、roomName 字段',
|
description:
|
||||||
|
'为 consumable_logs 表添加 deviceId、deviceName、rackId、rackName、roomId、roomName 字段',
|
||||||
migrate: migrateConsumableLogDeviceAssociation,
|
migrate: migrateConsumableLogDeviceAssociation,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -761,9 +762,7 @@ async function migrateDevicePositionIndexes() {
|
|||||||
const existingIndexes = await sequelize.query(`SHOW INDEX FROM ${tableName}`, {
|
const existingIndexes = await sequelize.query(`SHOW INDEX FROM ${tableName}`, {
|
||||||
type: sequelize.QueryTypes.SELECT,
|
type: sequelize.QueryTypes.SELECT,
|
||||||
});
|
});
|
||||||
const indexExists = existingIndexes.some(
|
const indexExists = existingIndexes.some(existing => existing.Key_name === idx.name);
|
||||||
existing => existing.Key_name === idx.name
|
|
||||||
);
|
|
||||||
|
|
||||||
if (indexExists) {
|
if (indexExists) {
|
||||||
console.log(' → 索引已存在,跳过');
|
console.log(' → 索引已存在,跳过');
|
||||||
@@ -780,7 +779,10 @@ async function migrateDevicePositionIndexes() {
|
|||||||
console.log(' ✓ 索引创建成功');
|
console.log(' ✓ 索引创建成功');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.message.includes('already exists') || error.message.includes('Duplicate key name')) {
|
if (
|
||||||
|
error.message.includes('already exists') ||
|
||||||
|
error.message.includes('Duplicate key name')
|
||||||
|
) {
|
||||||
console.log(' → 索引已存在,跳过');
|
console.log(' → 索引已存在,跳过');
|
||||||
} else {
|
} else {
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -33,9 +33,13 @@ async function migrateCableFields() {
|
|||||||
for (const column of newColumns) {
|
for (const column of newColumns) {
|
||||||
if (!existingColumns.includes(column.name)) {
|
if (!existingColumns.includes(column.name)) {
|
||||||
if (dbDialect === 'sqlite') {
|
if (dbDialect === 'sqlite') {
|
||||||
await sequelize.query(`ALTER TABLE ${tableName} ADD COLUMN ${column.name} ${column.type}`);
|
await sequelize.query(
|
||||||
|
`ALTER TABLE ${tableName} ADD COLUMN ${column.name} ${column.type}`
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
await sequelize.query(`ALTER TABLE ${tableName} ADD COLUMN ${column.name} ${column.type}`);
|
await sequelize.query(
|
||||||
|
`ALTER TABLE ${tableName} ADD COLUMN ${column.name} ${column.type}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
console.log(` ✓ 添加字段: ${column.name}`);
|
console.log(` ✓ 添加字段: ${column.name}`);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+13
-10
@@ -8,6 +8,7 @@ const cors = require('cors');
|
|||||||
const fileUpload = require('express-fileupload');
|
const fileUpload = require('express-fileupload');
|
||||||
const { sequelize } = require('./db');
|
const { sequelize } = require('./db');
|
||||||
const { FILE_UPLOAD } = require('./config');
|
const { FILE_UPLOAD } = require('./config');
|
||||||
|
const { generateId } = require('./utils/idGenerator');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 8000;
|
const PORT = process.env.PORT || 8000;
|
||||||
@@ -175,7 +176,7 @@ async function initFaultCategories() {
|
|||||||
for (const cat of defaultCategories) {
|
for (const cat of defaultCategories) {
|
||||||
const existing = await FaultCategory.findOne({ where: { name: cat.name } });
|
const existing = await FaultCategory.findOne({ where: { name: cat.name } });
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
const categoryId = `CAT${Date.now().toString(36).toUpperCase()}${Math.random().toString(36).substr(2, 4).toUpperCase()}`;
|
const categoryId = generateId({ prefix: 'CAT' });
|
||||||
await FaultCategory.create({
|
await FaultCategory.create({
|
||||||
categoryId,
|
categoryId,
|
||||||
...cat,
|
...cat,
|
||||||
@@ -226,14 +227,16 @@ const { specs, customCSS } = require('./swagger');
|
|||||||
const { authMiddleware } = require('./middleware/auth');
|
const { authMiddleware } = require('./middleware/auth');
|
||||||
const loadRoutes = require('./utils/routeLoader');
|
const loadRoutes = require('./utils/routeLoader');
|
||||||
|
|
||||||
initializeApp().then(() => {
|
initializeApp()
|
||||||
app.listen(PORT, () => {
|
.then(() => {
|
||||||
console.log(`服务器运行在 http://localhost:${PORT}`);
|
app.listen(PORT, () => {
|
||||||
|
console.log(`服务器运行在 http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('应用初始化失败:', err);
|
||||||
|
process.exit(1);
|
||||||
});
|
});
|
||||||
}).catch(err => {
|
|
||||||
console.error('应用初始化失败:', err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
const PUBLIC_PATHS = [
|
const PUBLIC_PATHS = [
|
||||||
'/auth',
|
'/auth',
|
||||||
@@ -244,11 +247,11 @@ const PUBLIC_PATHS = [
|
|||||||
'/system-settings/system/info',
|
'/system-settings/system/info',
|
||||||
];
|
];
|
||||||
|
|
||||||
const isPublicPath = (path) => {
|
const isPublicPath = path => {
|
||||||
if (path === '' || path === '/') {
|
if (path === '' || path === '/') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return PUBLIC_PATHS.some((publicPath) => path === publicPath || path.startsWith(publicPath + '/'));
|
return PUBLIC_PATHS.some(publicPath => path === publicPath || path.startsWith(publicPath + '/'));
|
||||||
};
|
};
|
||||||
|
|
||||||
app.use('/api', (req, res, next) => {
|
app.use('/api', (req, res, next) => {
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
class AppError extends Error {
|
||||||
|
constructor(code, message, statusCode = 500, details = null) {
|
||||||
|
super(message);
|
||||||
|
this.code = code;
|
||||||
|
this.statusCode = statusCode;
|
||||||
|
this.details = details;
|
||||||
|
this.isOperational = true;
|
||||||
|
Error.captureStackTrace(this, this.constructor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ERROR_CODES = {
|
||||||
|
BAD_REQUEST: 'BAD_REQUEST',
|
||||||
|
UNAUTHORIZED: 'UNAUTHORIZED',
|
||||||
|
FORBIDDEN: 'FORBIDDEN',
|
||||||
|
NOT_FOUND: 'NOT_FOUND',
|
||||||
|
CONFLICT: 'CONFLICT',
|
||||||
|
VALIDATION_ERROR: 'VALIDATION_ERROR',
|
||||||
|
INTERNAL_ERROR: 'INTERNAL_ERROR',
|
||||||
|
DATABASE_ERROR: 'DATABASE_ERROR',
|
||||||
|
NETWORK_ERROR: 'NETWORK_ERROR',
|
||||||
|
};
|
||||||
|
|
||||||
|
const STATUS_CODE_MAP = {
|
||||||
|
400: ERROR_CODES.BAD_REQUEST,
|
||||||
|
401: ERROR_CODES.UNAUTHORIZED,
|
||||||
|
403: ERROR_CODES.FORBIDDEN,
|
||||||
|
404: ERROR_CODES.NOT_FOUND,
|
||||||
|
409: ERROR_CODES.CONFLICT,
|
||||||
|
500: ERROR_CODES.INTERNAL_ERROR,
|
||||||
|
};
|
||||||
|
|
||||||
|
const FRIENDLY_MESSAGES = {
|
||||||
|
[ERROR_CODES.BAD_REQUEST]: '请求参数错误,请检查输入',
|
||||||
|
[ERROR_CODES.UNAUTHORIZED]: '未授权访问,请先登录',
|
||||||
|
[ERROR_CODES.FORBIDDEN]: '无权执行此操作',
|
||||||
|
[ERROR_CODES.NOT_FOUND]: '请求的资源不存在',
|
||||||
|
[ERROR_CODES.CONFLICT]: '数据冲突,请刷新后重试',
|
||||||
|
[ERROR_CODES.VALIDATION_ERROR]: '数据验证失败',
|
||||||
|
[ERROR_CODES.INTERNAL_ERROR]: '服务器内部错误,请稍后重试',
|
||||||
|
[ERROR_CODES.DATABASE_ERROR]: '数据库操作失败',
|
||||||
|
[ERROR_CODES.NETWORK_ERROR]: '网络连接错误',
|
||||||
|
};
|
||||||
|
|
||||||
|
function logError(error, req = null) {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const logData = {
|
||||||
|
timestamp,
|
||||||
|
message: error.message,
|
||||||
|
code: error.code,
|
||||||
|
statusCode: error.statusCode,
|
||||||
|
stack: error.stack,
|
||||||
|
details: error.details,
|
||||||
|
userAgent: req?.headers?.['user-agent'],
|
||||||
|
ip: req?.ip,
|
||||||
|
path: req?.path,
|
||||||
|
method: req?.method,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.error('=== ERROR LOG ===');
|
||||||
|
console.error(JSON.stringify(logData, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildErrorResponse(error) {
|
||||||
|
const code = error.code || STATUS_CODE_MAP[error.statusCode] || ERROR_CODES.INTERNAL_ERROR;
|
||||||
|
const message =
|
||||||
|
error.message || FRIENDLY_MESSAGES[code] || FRIENDLY_MESSAGES[ERROR_CODES.INTERNAL_ERROR];
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
code,
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error.details) {
|
||||||
|
response.error.details = error.details;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createError(code, message, statusCode = 500, details = null) {
|
||||||
|
return new AppError(code, message, statusCode, details);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendErrorResponse(res, error, req = null) {
|
||||||
|
logError(error, req);
|
||||||
|
|
||||||
|
let statusCode = 500;
|
||||||
|
let appError = error;
|
||||||
|
|
||||||
|
if (!(error instanceof AppError)) {
|
||||||
|
if (error.name === 'SequelizeValidationError') {
|
||||||
|
statusCode = 400;
|
||||||
|
const details = error.errors.map(err => ({
|
||||||
|
field: err.path,
|
||||||
|
message: err.message,
|
||||||
|
}));
|
||||||
|
appError = createError(
|
||||||
|
ERROR_CODES.VALIDATION_ERROR,
|
||||||
|
FRIENDLY_MESSAGES[ERROR_CODES.VALIDATION_ERROR],
|
||||||
|
statusCode,
|
||||||
|
details
|
||||||
|
);
|
||||||
|
} else if (error.name === 'SequelizeUniqueConstraintError') {
|
||||||
|
statusCode = 409;
|
||||||
|
appError = createError(ERROR_CODES.CONFLICT, '数据已存在', statusCode);
|
||||||
|
} else {
|
||||||
|
statusCode = error.statusCode || 500;
|
||||||
|
appError = createError(
|
||||||
|
STATUS_CODE_MAP[statusCode] || ERROR_CODES.INTERNAL_ERROR,
|
||||||
|
FRIENDLY_MESSAGES[STATUS_CODE_MAP[statusCode] || ERROR_CODES.INTERNAL_ERROR],
|
||||||
|
statusCode,
|
||||||
|
process.env.NODE_ENV === 'development' ? error.stack : null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
statusCode = appError.statusCode || statusCode;
|
||||||
|
res.status(statusCode).json(buildErrorResponse(appError));
|
||||||
|
}
|
||||||
|
|
||||||
|
function errorHandlerMiddleware(err, req, res, next) {
|
||||||
|
sendErrorResponse(res, err, req);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
AppError,
|
||||||
|
ERROR_CODES,
|
||||||
|
createError,
|
||||||
|
logError,
|
||||||
|
buildErrorResponse,
|
||||||
|
sendErrorResponse,
|
||||||
|
errorHandlerMiddleware,
|
||||||
|
};
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
|
||||||
|
const DEFAULT_CONFIG = {
|
||||||
|
prefix: '',
|
||||||
|
timestampBase: 36,
|
||||||
|
timestampUpperCase: true,
|
||||||
|
randomLength: 4,
|
||||||
|
randomBase: 36,
|
||||||
|
randomUpperCase: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
function generateId(config = {}) {
|
||||||
|
const {
|
||||||
|
prefix,
|
||||||
|
timestampBase,
|
||||||
|
timestampUpperCase,
|
||||||
|
randomLength,
|
||||||
|
randomBase,
|
||||||
|
randomUpperCase,
|
||||||
|
} = { ...DEFAULT_CONFIG, ...config };
|
||||||
|
|
||||||
|
const timestamp = Date.now().toString(timestampBase);
|
||||||
|
const formattedTimestamp = timestampUpperCase ? timestamp.toUpperCase() : timestamp;
|
||||||
|
|
||||||
|
let randomStr = '';
|
||||||
|
for (let i = 0; i < randomLength; i++) {
|
||||||
|
randomStr += Math.floor(Math.random() * randomBase).toString(randomBase);
|
||||||
|
}
|
||||||
|
const formattedRandom = randomUpperCase ? randomStr.toUpperCase() : randomStr;
|
||||||
|
|
||||||
|
return `${prefix}${formattedTimestamp}${formattedRandom}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateTicketId() {
|
||||||
|
return generateId({ prefix: 'TKT', randomLength: 4 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateRoleId() {
|
||||||
|
return generateId({
|
||||||
|
prefix: 'role_',
|
||||||
|
timestampUpperCase: false,
|
||||||
|
randomLength: 9,
|
||||||
|
randomUpperCase: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function generatePlanId() {
|
||||||
|
return generateId({ prefix: 'PLAN', randomLength: 4 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateTaskId() {
|
||||||
|
return generateId({ prefix: 'TASK', randomLength: 4 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateRecordId() {
|
||||||
|
return generateId({ prefix: 'REC', randomLength: 6 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function generatePendingId() {
|
||||||
|
return generateId({ prefix: 'PEND', randomLength: 4 });
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
generateId,
|
||||||
|
generateTicketId,
|
||||||
|
generateRoleId,
|
||||||
|
generatePlanId,
|
||||||
|
generateTaskId,
|
||||||
|
generateRecordId,
|
||||||
|
generatePendingId,
|
||||||
|
DEFAULT_CONFIG,
|
||||||
|
};
|
||||||
@@ -127,9 +127,7 @@ class ImportJobManager extends EventEmitter {
|
|||||||
error: job.error,
|
error: job.error,
|
||||||
startTime: job.startTime,
|
startTime: job.startTime,
|
||||||
endTime: job.endTime,
|
endTime: job.endTime,
|
||||||
elapsedTime: job.startTime
|
elapsedTime: job.startTime ? Date.now() - new Date(job.startTime).getTime() : null,
|
||||||
? Date.now() - new Date(job.startTime).getTime()
|
|
||||||
: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return progress;
|
return progress;
|
||||||
@@ -164,9 +162,12 @@ class ImportJobManager extends EventEmitter {
|
|||||||
|
|
||||||
const importJobManager = new ImportJobManager();
|
const importJobManager = new ImportJobManager();
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(
|
||||||
importJobManager.cleanupOldJobs();
|
() => {
|
||||||
}, 60 * 60 * 1000);
|
importJobManager.cleanupOldJobs();
|
||||||
|
},
|
||||||
|
60 * 60 * 1000
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
importJobManager,
|
importJobManager,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const routesConfig = require('../config/routes');
|
|||||||
function loadRoutes(app) {
|
function loadRoutes(app) {
|
||||||
const routesDir = path.join(__dirname, '../routes');
|
const routesDir = path.join(__dirname, '../routes');
|
||||||
|
|
||||||
routesConfig.forEach((routeConfig) => {
|
routesConfig.forEach(routeConfig => {
|
||||||
try {
|
try {
|
||||||
const routerPath = path.join(routesDir, routeConfig.file);
|
const routerPath = path.join(routesDir, routeConfig.file);
|
||||||
const router = require(routerPath);
|
const router = require(routerPath);
|
||||||
|
|||||||
@@ -3,14 +3,13 @@ const Joi = require('joi');
|
|||||||
// 创建机房验证Schema
|
// 创建机房验证Schema
|
||||||
const createRoomSchema = Joi.object({
|
const createRoomSchema = Joi.object({
|
||||||
roomId: Joi.string()
|
roomId: Joi.string()
|
||||||
.required()
|
.optional()
|
||||||
.max(50)
|
.max(50)
|
||||||
.pattern(/^[a-zA-Z0-9_-]+$/)
|
.pattern(/^[a-zA-Z0-9_-]+$/)
|
||||||
.messages({
|
.messages({
|
||||||
'string.empty': '机房ID不能为空',
|
'string.empty': '机房ID不能为空',
|
||||||
'string.max': '机房ID不能超过50个字符',
|
'string.max': '机房ID不能超过50个字符',
|
||||||
'string.pattern.base': '机房ID只能包含字母、数字、下划线和横线',
|
'string.pattern.base': '机房ID只能包含字母、数字、下划线和横线',
|
||||||
'any.required': '机房ID是必填字段',
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
name: Joi.string().required().max(100).messages({
|
name: Joi.string().required().max(100).messages({
|
||||||
|
|||||||
@@ -778,9 +778,9 @@ function RoomManagement() {
|
|||||||
<Form.Item
|
<Form.Item
|
||||||
name="roomId"
|
name="roomId"
|
||||||
label="机房ID"
|
label="机房ID"
|
||||||
rules={[{ required: true, message: '请输入机房ID' }]}
|
rules={[]}
|
||||||
>
|
>
|
||||||
<Input placeholder="请输入机房ID" style={{ borderRadius: '8px' }} />
|
<Input placeholder="不填写则自动生成" style={{ borderRadius: '8px' }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
|
|||||||
Reference in New Issue
Block a user