feat(server): 添加公共路径认证中间件和批量操作功能
refactor(routes): 重新组织路由顺序并添加批量删除功能 - 在server.js中添加公共路径认证中间件 - 为cables、devicePorts等路由添加批量删除功能 - 重新组织路由顺序,将相关操作分组 - 添加工单导出功能
This commit is contained in:
+32
-30
@@ -500,36 +500,7 @@ router.put('/:cableId', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/:cableId', async (req, res) => {
|
||||
try {
|
||||
// 先获取接线信息,用于后续恢复端口状态
|
||||
const cable = await Cable.findByPk(req.params.cableId);
|
||||
|
||||
if (!cable) {
|
||||
return res.status(404).json({ error: '接线不存在' });
|
||||
}
|
||||
|
||||
const { sourceDeviceId, sourcePort, targetDeviceId, targetPort } = cable;
|
||||
|
||||
const deleted = await Cable.destroy({
|
||||
where: { cableId: req.params.cableId },
|
||||
});
|
||||
|
||||
if (deleted) {
|
||||
// 自动将源端口和目标端口状态恢复为free
|
||||
await freePort(sourceDeviceId, sourcePort);
|
||||
await freePort(targetDeviceId, targetPort);
|
||||
|
||||
res.status(204).json();
|
||||
} else {
|
||||
res.status(404).json({ error: '接线不存在' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除接线失败:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 批量删除接线
|
||||
router.delete('/batch', async (req, res) => {
|
||||
try {
|
||||
const { cableIds } = req.body;
|
||||
@@ -563,6 +534,37 @@ router.delete('/batch', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 删除单个接线
|
||||
router.delete('/:cableId', async (req, res) => {
|
||||
try {
|
||||
// 先获取接线信息,用于后续恢复端口状态
|
||||
const cable = await Cable.findByPk(req.params.cableId);
|
||||
|
||||
if (!cable) {
|
||||
return res.status(404).json({ error: '接线不存在' });
|
||||
}
|
||||
|
||||
const { sourceDeviceId, sourcePort, targetDeviceId, targetPort } = cable;
|
||||
|
||||
const deleted = await Cable.destroy({
|
||||
where: { cableId: req.params.cableId },
|
||||
});
|
||||
|
||||
if (deleted) {
|
||||
// 自动将源端口和目标端口状态恢复为free
|
||||
await freePort(sourceDeviceId, sourcePort);
|
||||
await freePort(targetDeviceId, targetPort);
|
||||
|
||||
res.status(204).json();
|
||||
} else {
|
||||
res.status(404).json({ error: '接线不存在' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除接线失败:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:cableId', async (req, res) => {
|
||||
try {
|
||||
const cable = await Cable.findByPk(req.params.cableId, {
|
||||
|
||||
@@ -1116,6 +1116,59 @@ router.post('/logs/import', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 查询归档记录列表
|
||||
router.get('/archives', async (req, res) => {
|
||||
try {
|
||||
const { keyword, page = 1, pageSize = 10 } = req.query;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const where = {};
|
||||
|
||||
if (keyword) {
|
||||
where[Op.or] = [
|
||||
{ consumableId: { [Op.like]: `%${keyword}%` } },
|
||||
{ consumableName: { [Op.like]: `%${keyword}%` } },
|
||||
{ archiveId: { [Op.like]: `%${keyword}%` } },
|
||||
];
|
||||
}
|
||||
|
||||
const { count, rows } = await ConsumableLogArchive.findAndCountAll({
|
||||
where,
|
||||
offset,
|
||||
limit: parseInt(pageSize),
|
||||
order: [['deletedAt', 'DESC']],
|
||||
});
|
||||
|
||||
res.json({
|
||||
total: count,
|
||||
archives: rows,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 查询单个归档记录详情
|
||||
router.get('/archives/:archiveId', async (req, res) => {
|
||||
try {
|
||||
const { archiveId } = req.params;
|
||||
|
||||
const archive = await ConsumableLogArchive.findOne({
|
||||
where: { archiveId },
|
||||
});
|
||||
|
||||
if (!archive) {
|
||||
return res.status(404).json({ error: '归档记录不存在' });
|
||||
}
|
||||
|
||||
res.json(archive);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const consumable = await Consumable.findByPk(req.params.id);
|
||||
@@ -1286,59 +1339,6 @@ router.delete('/:id', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 查询归档记录列表
|
||||
router.get('/archives', async (req, res) => {
|
||||
try {
|
||||
const { keyword, page = 1, pageSize = 10 } = req.query;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const where = {};
|
||||
|
||||
if (keyword) {
|
||||
where[Op.or] = [
|
||||
{ consumableId: { [Op.like]: `%${keyword}%` } },
|
||||
{ consumableName: { [Op.like]: `%${keyword}%` } },
|
||||
{ archiveId: { [Op.like]: `%${keyword}%` } },
|
||||
];
|
||||
}
|
||||
|
||||
const { count, rows } = await ConsumableLogArchive.findAndCountAll({
|
||||
where,
|
||||
offset,
|
||||
limit: parseInt(pageSize),
|
||||
order: [['deletedAt', 'DESC']],
|
||||
});
|
||||
|
||||
res.json({
|
||||
total: count,
|
||||
archives: rows,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 查询单个归档记录详情
|
||||
router.get('/archives/:archiveId', async (req, res) => {
|
||||
try {
|
||||
const { archiveId } = req.params;
|
||||
|
||||
const archive = await ConsumableLogArchive.findOne({
|
||||
where: { archiveId },
|
||||
});
|
||||
|
||||
if (!archive) {
|
||||
return res.status(404).json({ error: '归档记录不存在' });
|
||||
}
|
||||
|
||||
res.json(archive);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 修改日志记录
|
||||
router.put('/logs/:id', async (req, res) => {
|
||||
const transaction = await sequelize.transaction();
|
||||
|
||||
+85
-139
@@ -325,6 +325,30 @@ router.put('/:portId', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 批量删除端口
|
||||
router.delete('/batch', async (req, res) => {
|
||||
try {
|
||||
const { portIds } = req.body;
|
||||
|
||||
if (!portIds || !Array.isArray(portIds) || portIds.length === 0) {
|
||||
return res.status(400).json({ error: '请提供有效的端口ID列表' });
|
||||
}
|
||||
|
||||
const deletedCount = await DevicePort.destroy({
|
||||
where: { portId: { [Op.in]: portIds } },
|
||||
});
|
||||
|
||||
res.json({
|
||||
message: `批量删除成功,已删除 ${deletedCount} 个端口`,
|
||||
deletedCount,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('批量删除端口失败:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 删除单个端口
|
||||
router.delete('/:portId', async (req, res) => {
|
||||
try {
|
||||
const port = await DevicePort.findByPk(req.params.portId);
|
||||
@@ -365,28 +389,6 @@ router.delete('/:portId', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/batch', async (req, res) => {
|
||||
try {
|
||||
const { portIds } = req.body;
|
||||
|
||||
if (!portIds || !Array.isArray(portIds) || portIds.length === 0) {
|
||||
return res.status(400).json({ error: '请提供有效的端口ID列表' });
|
||||
}
|
||||
|
||||
const deletedCount = await DevicePort.destroy({
|
||||
where: { portId: { [Op.in]: portIds } },
|
||||
});
|
||||
|
||||
res.json({
|
||||
message: `批量删除成功,已删除 ${deletedCount} 个端口`,
|
||||
deletedCount,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('批量删除端口失败:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/batch-delete', async (req, res) => {
|
||||
try {
|
||||
const { portIds } = req.body;
|
||||
@@ -409,6 +411,67 @@ router.post('/batch-delete', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 导出所有端口
|
||||
router.get('/export/all', async (req, res) => {
|
||||
try {
|
||||
const { keyword, status, portType, portSpeed, deviceId, page = 1, pageSize = 5000 } = req.query;
|
||||
|
||||
const parsedPage = Math.max(1, parseInt(page) || 1);
|
||||
const parsedPageSize = Math.min(10000, Math.max(1, parseInt(pageSize) || 5000));
|
||||
const offset = (parsedPage - 1) * parsedPageSize;
|
||||
|
||||
const where = {};
|
||||
|
||||
if (keyword) {
|
||||
where[Op.or] = [
|
||||
{ portName: { [Op.like]: `%${keyword}%` } },
|
||||
{ deviceId: { [Op.like]: `%${keyword}%` } },
|
||||
];
|
||||
}
|
||||
|
||||
if (status && status !== 'all') {
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
if (portType && portType !== 'all') {
|
||||
where.portType = portType;
|
||||
}
|
||||
|
||||
if (portSpeed && portSpeed !== 'all') {
|
||||
where.portSpeed = portSpeed;
|
||||
}
|
||||
|
||||
if (deviceId && deviceId !== 'all') {
|
||||
where.deviceId = deviceId;
|
||||
}
|
||||
|
||||
const { count, rows } = await DevicePort.findAndCountAll({
|
||||
where,
|
||||
include: [
|
||||
{
|
||||
model: Device,
|
||||
as: 'device',
|
||||
attributes: ['deviceId', 'name', 'type'],
|
||||
},
|
||||
],
|
||||
order: [['createdAt', 'DESC']],
|
||||
offset,
|
||||
limit: parsedPageSize,
|
||||
});
|
||||
|
||||
res.json({
|
||||
total: count,
|
||||
ports: rows,
|
||||
page: parsedPage,
|
||||
pageSize: parsedPageSize,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取端口列表失败:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取单个端口
|
||||
router.get('/:portId', async (req, res) => {
|
||||
try {
|
||||
const port = await DevicePort.findByPk(req.params.portId, {
|
||||
@@ -432,121 +495,4 @@ router.get('/:portId', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/export/all', async (req, res) => {
|
||||
try {
|
||||
const { keyword, status, portType, portSpeed, deviceId, page = 1, pageSize = 5000 } = req.query;
|
||||
|
||||
const parsedPage = Math.max(1, parseInt(page) || 1);
|
||||
const parsedPageSize = Math.min(10000, Math.max(1, parseInt(pageSize) || 5000));
|
||||
const offset = (parsedPage - 1) * parsedPageSize;
|
||||
|
||||
const where = {};
|
||||
|
||||
if (deviceId) {
|
||||
where.deviceId = deviceId;
|
||||
}
|
||||
|
||||
if (status && status !== 'all') {
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
if (portType && portType !== 'all') {
|
||||
where.portType = portType;
|
||||
}
|
||||
|
||||
if (portSpeed && portSpeed !== 'all') {
|
||||
where.portSpeed = portSpeed;
|
||||
}
|
||||
|
||||
const timeoutMs = 30000;
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error('查询超时,请尝试缩小查询范围或减少pageSize')), timeoutMs);
|
||||
});
|
||||
|
||||
const countResult = await Promise.race([
|
||||
DevicePort.findAll({
|
||||
where,
|
||||
include: [
|
||||
{
|
||||
model: Device,
|
||||
as: 'device',
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||
include: [
|
||||
{
|
||||
model: require('../models/Rack'),
|
||||
as: 'rack',
|
||||
attributes: ['rackId', 'name'],
|
||||
include: [
|
||||
{
|
||||
model: require('../models/Room'),
|
||||
as: 'room',
|
||||
attributes: ['roomId', 'name'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
model: NetworkCard,
|
||||
as: 'networkCard',
|
||||
attributes: ['nicId', 'name'],
|
||||
},
|
||||
],
|
||||
order: [['createdAt', 'DESC']],
|
||||
limit: parsedPageSize,
|
||||
offset: offset,
|
||||
subQuery: false,
|
||||
}),
|
||||
timeoutPromise,
|
||||
]);
|
||||
|
||||
const ports = countResult;
|
||||
|
||||
const statusMap = {
|
||||
free: '空闲',
|
||||
occupied: '占用',
|
||||
fault: '故障',
|
||||
};
|
||||
|
||||
const exportData = ports.map(port => ({
|
||||
端口ID: port.portId,
|
||||
设备ID: port.deviceId,
|
||||
设备名称: port.device?.name || '-',
|
||||
设备类型: port.device?.type || '-',
|
||||
机房: port.device?.rack?.room?.name || '-',
|
||||
机架: port.device?.rack?.name || '-',
|
||||
网卡名称: port.networkCard?.name || '-',
|
||||
端口名称: port.portName,
|
||||
端口类型: port.portType,
|
||||
端口速率: port.portSpeed,
|
||||
状态: statusMap[port.status] || port.status,
|
||||
VLAN_ID: port.vlanId || '-',
|
||||
描述: port.description || '-',
|
||||
创建时间: port.createdAt ? new Date(port.createdAt).toLocaleString('zh-CN') : '-',
|
||||
}));
|
||||
|
||||
let filteredExportData = exportData;
|
||||
if (keyword) {
|
||||
const searchLower = keyword.toLowerCase();
|
||||
filteredExportData = exportData.filter(
|
||||
item =>
|
||||
item.端口名称?.toLowerCase().includes(searchLower) ||
|
||||
item.端口类型?.toLowerCase().includes(searchLower) ||
|
||||
item.设备名称?.toLowerCase().includes(searchLower) ||
|
||||
item.描述?.toLowerCase().includes(searchLower)
|
||||
);
|
||||
}
|
||||
|
||||
res.json({
|
||||
page: parsedPage,
|
||||
pageSize: parsedPageSize,
|
||||
total: filteredExportData.length,
|
||||
ports: filteredExportData,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('导出端口失败:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -131,6 +131,39 @@ router.get('/device/:deviceId/with-ports', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 查询网卡
|
||||
router.get('/find', async (req, res) => {
|
||||
try {
|
||||
const { deviceId, deviceSn, name } = req.query;
|
||||
|
||||
if ((!deviceId && !deviceSn) || !name) {
|
||||
return res.status(400).json({ error: '缺少设备ID/SN或网卡名称' });
|
||||
}
|
||||
|
||||
let device;
|
||||
if (deviceSn) {
|
||||
device = await Device.findOne({ where: { serialNumber: deviceSn } });
|
||||
if (!device) {
|
||||
return res.json({ nicId: null });
|
||||
}
|
||||
}
|
||||
|
||||
const networkCard = await NetworkCard.findOne({
|
||||
where: { deviceId: device ? device.deviceId : deviceId, name },
|
||||
});
|
||||
|
||||
if (!networkCard) {
|
||||
return res.json({ nicId: null });
|
||||
}
|
||||
|
||||
res.json(networkCard);
|
||||
} catch (error) {
|
||||
console.error('查找网卡失败:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取单个网卡
|
||||
router.get('/:nicId', async (req, res) => {
|
||||
try {
|
||||
const networkCard = await NetworkCard.findByPk(req.params.nicId, {
|
||||
@@ -170,37 +203,6 @@ router.get('/:nicId/ports', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/find', async (req, res) => {
|
||||
try {
|
||||
const { deviceId, deviceSn, name } = req.query;
|
||||
|
||||
if ((!deviceId && !deviceSn) || !name) {
|
||||
return res.status(400).json({ error: '缺少设备ID/SN或网卡名称' });
|
||||
}
|
||||
|
||||
let device;
|
||||
if (deviceSn) {
|
||||
device = await Device.findOne({ where: { serialNumber: deviceSn } });
|
||||
if (!device) {
|
||||
return res.json({ nicId: null });
|
||||
}
|
||||
}
|
||||
|
||||
const networkCard = await NetworkCard.findOne({
|
||||
where: { deviceId: device ? device.deviceId : deviceId, name },
|
||||
});
|
||||
|
||||
if (!networkCard) {
|
||||
return res.json({ nicId: null });
|
||||
}
|
||||
|
||||
res.json(networkCard);
|
||||
} catch (error) {
|
||||
console.error('查找网卡失败:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const { nicId, deviceId, name, description, slotNumber, model, manufacturer, status } =
|
||||
|
||||
+156
-155
@@ -310,6 +310,162 @@ router.get('/', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 导出工单
|
||||
const TICKET_EXPORT_FIELDS = [
|
||||
{ fieldName: 'ticketId', displayName: '工单编号' },
|
||||
{ fieldName: 'title', displayName: '标题' },
|
||||
{ fieldName: 'deviceName', displayName: '设备名称' },
|
||||
{ fieldName: 'deviceModel', displayName: '设备型号' },
|
||||
{ fieldName: 'serialNumber', displayName: '设备序列号' },
|
||||
{ fieldName: 'faultCategory', displayName: '故障分类' },
|
||||
{ fieldName: 'faultSubCategory', displayName: '故障子分类' },
|
||||
{ fieldName: 'priority', displayName: '优先级' },
|
||||
{ fieldName: 'status', displayName: '状态' },
|
||||
{ fieldName: 'description', displayName: '故障描述' },
|
||||
{ fieldName: 'expectedCompletionDate', displayName: '期望完成时间' },
|
||||
{ fieldName: 'reporterId', displayName: '报告人ID' },
|
||||
{ fieldName: 'reporterName', displayName: '报告人' },
|
||||
{ fieldName: 'assigneeId', displayName: '处理人ID' },
|
||||
{ fieldName: 'assigneeName', displayName: '处理人' },
|
||||
{ fieldName: 'location', displayName: '设备位置' },
|
||||
{ fieldName: 'resolution', displayName: '解决方案' },
|
||||
{ fieldName: 'completionDate', displayName: '完成时间' },
|
||||
{ fieldName: 'evaluation', displayName: '评价' },
|
||||
{ fieldName: 'evaluationRating', displayName: '评价星级' },
|
||||
{ fieldName: 'createdAt', displayName: '创建时间' },
|
||||
{ fieldName: 'updatedAt', displayName: '更新时间' },
|
||||
];
|
||||
|
||||
router.get('/export', async (req, res) => {
|
||||
try {
|
||||
const { keyword, status, priority, faultCategory, deviceId, format = 'csv', ticketIds } = req.query;
|
||||
|
||||
const where = {};
|
||||
|
||||
if (ticketIds) {
|
||||
const ids = Array.isArray(ticketIds) ? ticketIds : [ticketIds];
|
||||
where.ticketId = { [Op.in]: ids };
|
||||
} else {
|
||||
if (keyword) {
|
||||
where[Op.or] = [
|
||||
{ ticketId: { [Op.like]: `%${keyword}%` } },
|
||||
{ title: { [Op.like]: `%${keyword}%` } },
|
||||
{ deviceName: { [Op.like]: `%${keyword}%` } },
|
||||
{ serialNumber: { [Op.like]: `%${keyword}%` } },
|
||||
{ description: { [Op.like]: `%${keyword}%` } },
|
||||
];
|
||||
}
|
||||
|
||||
if (status && status !== 'all') {
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
if (priority && priority !== 'all') {
|
||||
where.priority = priority;
|
||||
}
|
||||
|
||||
if (faultCategory && faultCategory !== 'all') {
|
||||
where.faultCategory = faultCategory;
|
||||
}
|
||||
|
||||
if (deviceId && deviceId !== 'all') {
|
||||
where.deviceId = deviceId;
|
||||
}
|
||||
}
|
||||
|
||||
const tickets = await Ticket.findAll({
|
||||
where,
|
||||
include: [
|
||||
{ model: User, as: 'reporter', attributes: ['userId', 'username'] },
|
||||
{ model: Device, attributes: ['deviceId', 'name', 'type', 'model', 'serialNumber'] },
|
||||
],
|
||||
order: [['createdAt', 'DESC']],
|
||||
});
|
||||
|
||||
const exportData = tickets.map(ticket => {
|
||||
const item = {};
|
||||
TICKET_EXPORT_FIELDS.forEach(({ fieldName, displayName }) => {
|
||||
let value = ticket[fieldName];
|
||||
|
||||
if (fieldName === 'priority') {
|
||||
const priorityMap = { low: '低', medium: '中', high: '高', urgent: '紧急' };
|
||||
value = priorityMap[value] || value;
|
||||
} else if (fieldName === 'status') {
|
||||
const statusMap = { pending: '待处理', in_progress: '处理中', completed: '已完成', closed: '已关闭' };
|
||||
value = statusMap[value] || value;
|
||||
} else if (fieldName === 'expectedCompletionDate' || fieldName === 'completionDate' || fieldName === 'createdAt' || fieldName === 'updatedAt') {
|
||||
value = value ? new Date(value).toLocaleString('zh-CN') : '';
|
||||
}
|
||||
|
||||
item[displayName] = value !== null && value !== undefined ? String(value) : '';
|
||||
});
|
||||
|
||||
if (ticket.metadata && typeof ticket.metadata === 'object') {
|
||||
Object.entries(ticket.metadata).forEach(([key, val]) => {
|
||||
const customDisplayName = key;
|
||||
item[customDisplayName] = val !== null && val !== undefined ? String(val) : '';
|
||||
});
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
|
||||
if (format === 'json') {
|
||||
return res.setHeader('Content-Type', 'application/json; charset=utf-8')
|
||||
.setHeader('Content-Disposition', `attachment; filename=tickets_${Date.now()}.json`)
|
||||
.json({ success: true, data: exportData, total: exportData.length });
|
||||
}
|
||||
|
||||
if (format === 'xlsx') {
|
||||
const worksheet = XLSX.utils.json_to_sheet(exportData);
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, '工单数据');
|
||||
const xlsxBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'buffer' });
|
||||
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
res.setHeader('Content-Disposition', `attachment; filename=tickets_${Date.now()}.xlsx`);
|
||||
return res.send(xlsxBuffer);
|
||||
}
|
||||
|
||||
const headers = [
|
||||
...TICKET_EXPORT_FIELDS.map(f => ({ id: f.displayName, title: f.displayName })),
|
||||
];
|
||||
|
||||
if (tickets.length > 0 && tickets[0].metadata && typeof tickets[0].metadata === 'object') {
|
||||
Object.keys(tickets[0].metadata).forEach(key => {
|
||||
headers.push({ id: key, title: key });
|
||||
});
|
||||
}
|
||||
|
||||
if (!fs.existsSync(path.join(__dirname, '../temp'))) {
|
||||
fs.mkdirSync(path.join(__dirname, '../temp'));
|
||||
}
|
||||
|
||||
const tempFilePath = path.join(__dirname, `../temp/tickets_export_${Date.now()}.csv`);
|
||||
|
||||
const csvWriter = createObjectCsvWriter({
|
||||
path: tempFilePath,
|
||||
header: headers,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
|
||||
await csvWriter.writeRecords(exportData);
|
||||
|
||||
const csvContent = fs.readFileSync(tempFilePath, 'utf8');
|
||||
const bom = '\uFEFF';
|
||||
const csvWithBom = bom + csvContent;
|
||||
|
||||
fs.unlinkSync(tempFilePath);
|
||||
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', `attachment; filename=tickets_${Date.now()}.csv`);
|
||||
return res.send(csvWithBom);
|
||||
} catch (error) {
|
||||
console.error('导出工单失败:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取单个工单详情
|
||||
router.get('/:ticketId', async (req, res) => {
|
||||
try {
|
||||
@@ -654,159 +810,4 @@ router.post('/:ticketId/evaluate', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
const TICKET_EXPORT_FIELDS = [
|
||||
{ fieldName: 'ticketId', displayName: '工单编号' },
|
||||
{ fieldName: 'title', displayName: '标题' },
|
||||
{ fieldName: 'deviceName', displayName: '设备名称' },
|
||||
{ fieldName: 'deviceModel', displayName: '设备型号' },
|
||||
{ fieldName: 'serialNumber', displayName: '设备序列号' },
|
||||
{ fieldName: 'faultCategory', displayName: '故障分类' },
|
||||
{ fieldName: 'faultSubCategory', displayName: '故障子分类' },
|
||||
{ fieldName: 'priority', displayName: '优先级' },
|
||||
{ fieldName: 'status', displayName: '状态' },
|
||||
{ fieldName: 'description', displayName: '故障描述' },
|
||||
{ fieldName: 'expectedCompletionDate', displayName: '期望完成时间' },
|
||||
{ fieldName: 'reporterId', displayName: '报告人ID' },
|
||||
{ fieldName: 'reporterName', displayName: '报告人' },
|
||||
{ fieldName: 'assigneeId', displayName: '处理人ID' },
|
||||
{ fieldName: 'assigneeName', displayName: '处理人' },
|
||||
{ fieldName: 'location', displayName: '设备位置' },
|
||||
{ fieldName: 'resolution', displayName: '解决方案' },
|
||||
{ fieldName: 'completionDate', displayName: '完成时间' },
|
||||
{ fieldName: 'evaluation', displayName: '评价' },
|
||||
{ fieldName: 'evaluationRating', displayName: '评价星级' },
|
||||
{ fieldName: 'createdAt', displayName: '创建时间' },
|
||||
{ fieldName: 'updatedAt', displayName: '更新时间' },
|
||||
];
|
||||
|
||||
router.get('/export', async (req, res) => {
|
||||
try {
|
||||
const { keyword, status, priority, faultCategory, deviceId, format = 'csv', ticketIds } = req.query;
|
||||
|
||||
const where = {};
|
||||
|
||||
if (ticketIds) {
|
||||
const ids = Array.isArray(ticketIds) ? ticketIds : [ticketIds];
|
||||
where.ticketId = { [Op.in]: ids };
|
||||
} else {
|
||||
if (keyword) {
|
||||
where[Op.or] = [
|
||||
{ ticketId: { [Op.like]: `%${keyword}%` } },
|
||||
{ title: { [Op.like]: `%${keyword}%` } },
|
||||
{ deviceName: { [Op.like]: `%${keyword}%` } },
|
||||
{ serialNumber: { [Op.like]: `%${keyword}%` } },
|
||||
{ description: { [Op.like]: `%${keyword}%` } },
|
||||
];
|
||||
}
|
||||
|
||||
if (status && status !== 'all') {
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
if (priority && priority !== 'all') {
|
||||
where.priority = priority;
|
||||
}
|
||||
|
||||
if (faultCategory && faultCategory !== 'all') {
|
||||
where.faultCategory = faultCategory;
|
||||
}
|
||||
|
||||
if (deviceId && deviceId !== 'all') {
|
||||
where.deviceId = deviceId;
|
||||
}
|
||||
}
|
||||
|
||||
const tickets = await Ticket.findAll({
|
||||
where,
|
||||
include: [
|
||||
{ model: User, as: 'reporter', attributes: ['userId', 'username'] },
|
||||
{ model: Device, attributes: ['deviceId', 'name', 'type', 'model', 'serialNumber'] },
|
||||
],
|
||||
order: [['createdAt', 'DESC']],
|
||||
});
|
||||
|
||||
const exportData = tickets.map(ticket => {
|
||||
const item = {};
|
||||
TICKET_EXPORT_FIELDS.forEach(({ fieldName, displayName }) => {
|
||||
let value = ticket[fieldName];
|
||||
|
||||
if (fieldName === 'priority') {
|
||||
const priorityMap = { low: '低', medium: '中', high: '高', urgent: '紧急' };
|
||||
value = priorityMap[value] || value;
|
||||
} else if (fieldName === 'status') {
|
||||
const statusMap = { pending: '待处理', in_progress: '处理中', completed: '已完成', closed: '已关闭' };
|
||||
value = statusMap[value] || value;
|
||||
} else if (fieldName === 'expectedCompletionDate' || fieldName === 'completionDate' || fieldName === 'createdAt' || fieldName === 'updatedAt') {
|
||||
value = value ? new Date(value).toLocaleString('zh-CN') : '';
|
||||
}
|
||||
|
||||
item[displayName] = value !== null && value !== undefined ? String(value) : '';
|
||||
});
|
||||
|
||||
if (ticket.metadata && typeof ticket.metadata === 'object') {
|
||||
Object.entries(ticket.metadata).forEach(([key, val]) => {
|
||||
const customDisplayName = key;
|
||||
item[customDisplayName] = val !== null && val !== undefined ? String(val) : '';
|
||||
});
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
|
||||
if (format === 'json') {
|
||||
return res.setHeader('Content-Type', 'application/json; charset=utf-8')
|
||||
.setHeader('Content-Disposition', `attachment; filename=tickets_${Date.now()}.json`)
|
||||
.json({ success: true, data: exportData, total: exportData.length });
|
||||
}
|
||||
|
||||
if (format === 'xlsx') {
|
||||
const worksheet = XLSX.utils.json_to_sheet(exportData);
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, '工单数据');
|
||||
const xlsxBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'buffer' });
|
||||
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
res.setHeader('Content-Disposition', `attachment; filename=tickets_${Date.now()}.xlsx`);
|
||||
return res.send(xlsxBuffer);
|
||||
}
|
||||
|
||||
const headers = [
|
||||
...TICKET_EXPORT_FIELDS.map(f => ({ id: f.displayName, title: f.displayName })),
|
||||
];
|
||||
|
||||
if (tickets.length > 0 && tickets[0].metadata && typeof tickets[0].metadata === 'object') {
|
||||
Object.keys(tickets[0].metadata).forEach(key => {
|
||||
headers.push({ id: key, title: key });
|
||||
});
|
||||
}
|
||||
|
||||
if (!fs.existsSync(path.join(__dirname, '../temp'))) {
|
||||
fs.mkdirSync(path.join(__dirname, '../temp'));
|
||||
}
|
||||
|
||||
const tempFilePath = path.join(__dirname, `../temp/tickets_export_${Date.now()}.csv`);
|
||||
|
||||
const csvWriter = createObjectCsvWriter({
|
||||
path: tempFilePath,
|
||||
header: headers,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
|
||||
await csvWriter.writeRecords(exportData);
|
||||
|
||||
const csvContent = fs.readFileSync(tempFilePath, 'utf8');
|
||||
const bom = '\uFEFF';
|
||||
const csvWithBom = bom + csvContent;
|
||||
|
||||
fs.unlinkSync(tempFilePath);
|
||||
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', `attachment; filename=tickets_${Date.now()}.csv`);
|
||||
return res.send(csvWithBom);
|
||||
} catch (error) {
|
||||
console.error('导出工单失败:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -223,9 +223,32 @@ async function initializeApp() {
|
||||
|
||||
const swaggerUi = require('swagger-ui-express');
|
||||
const { specs, customCSS } = require('./swagger');
|
||||
const { authMiddleware } = require('./middleware/auth');
|
||||
|
||||
initializeApp();
|
||||
|
||||
const PUBLIC_PATHS = [
|
||||
'/auth',
|
||||
'/health',
|
||||
'/docs',
|
||||
'/api-docs',
|
||||
'/api-docs.json',
|
||||
];
|
||||
|
||||
const isPublicPath = (path) => {
|
||||
if (path === '' || path === '/') {
|
||||
return true;
|
||||
}
|
||||
return PUBLIC_PATHS.some((publicPath) => path === publicPath || path.startsWith(publicPath + '/'));
|
||||
};
|
||||
|
||||
app.use('/api', (req, res, next) => {
|
||||
if (isPublicPath(req.path)) {
|
||||
return next();
|
||||
}
|
||||
return authMiddleware(req, res, next);
|
||||
});
|
||||
|
||||
const deviceRoutes = require('./routes/devices');
|
||||
const rackRoutes = require('./routes/racks');
|
||||
const roomRoutes = require('./routes/rooms');
|
||||
|
||||
Reference in New Issue
Block a user