V0.21.15.250612_alpha:数据监控页面添加CSV文件上传

This commit is contained in:
jinchao 2025-06-12 14:50:27 +08:00
parent 9fd0d530c4
commit fc46cee50f
15 changed files with 793 additions and 187 deletions

3
Release/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
log/*
testData/*
Packages/*

Binary file not shown.

View File

@ -46,6 +46,8 @@ add_library(XNMonitorServer SHARED
DataMonitorFactory.cpp
DataInjectThread.h
DataInjectThread.cpp
CSVDataInjectThread.h
CSVDataInjectThread.cpp
)
#

View File

@ -0,0 +1,148 @@
#include "CSVDataInjectThread.h"
#include "DataMonitorFactory.h"
CSVDataInjectThread::CSVDataInjectThread(std::string csvFilePath)
: m_csvFilePath(csvFilePath), m_running(false), m_nextExecuteTime(0)
{
}
CSVDataInjectThread::~CSVDataInjectThread()
{
stop();
}
bool CSVDataInjectThread::Initialize(std::vector<std::string> structNames)
{
m_csvFile.open(m_csvFilePath);
if (!m_csvFile.is_open()) {
return false;
}
std::string headerLine;
if (!std::getline(m_csvFile, headerLine)) {
return false;
}
std::vector<std::string> interfaceNames;
std::stringstream ss(headerLine);
std::string field;
std::getline(ss, field, ',');
while (std::getline(ss, field, ',')) {
interfaceNames.push_back(field);
}
// 将结构体和接口名称一一对应
if (structNames.size() != interfaceNames.size()) {
return false;
}
for (int i = 0; i < structNames.size(); i++) {
m_structInterfaceMap[structNames[i]].push_back(interfaceNames[i]);
}
for (const auto &[structName, interfaceNames] : m_structInterfaceMap) {
auto dataMonitor = DataMonitorFactory::GetInstance(structName);
if (dataMonitor == nullptr) {
return false;
}
if (dataMonitor->isInitialized()) {
m_alreadyStartedMonitors[structName] = dataMonitor;
} else {
m_notStartedMonitors[structName] = dataMonitor;
}
}
return true;
}
void CSVDataInjectThread::start()
{
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_running) {
m_running = true;
m_thread = std::thread(&CSVDataInjectThread::threadFunc, this);
}
}
void CSVDataInjectThread::stop()
{
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_running) {
m_running = false;
m_cv.notify_all();
}
}
if (m_thread.joinable()) {
m_thread.join();
}
// 关闭文件
if (m_csvFile.is_open()) {
m_csvFile.close();
}
// 释放未启动的监控器
for (const auto &[structName, dataMonitor] : m_notStartedMonitors) {
DataMonitorFactory::ReleaseInstance(structName);
}
m_notStartedMonitors.clear();
m_alreadyStartedMonitors.clear();
}
void CSVDataInjectThread::updateData()
{
// 读取下一行数据
std::string line;
if (!std::getline(m_csvFile, line)) {
// 文件读取完毕,停止线程
m_running = false;
return;
}
// 解析数据
std::stringstream ss(line);
std::string field;
std::getline(ss, field, ',');
double timeStamp = std::stod(field);
m_nextExecuteTime = static_cast<int64_t>(timeStamp * 1000); // 转换为毫秒
// 解析每个结构体的数据
for (const auto &[structName, interfaceNames] : m_structInterfaceMap) {
std::unordered_map<std::string, std::string> dataMap;
for (const auto &interfaceName : interfaceNames) {
std::getline(ss, field, ',');
dataMap[interfaceName] = field;
}
m_data[structName] = dataMap;
}
}
void CSVDataInjectThread::threadFunc()
{
// 读取第一行数据
updateData();
while (m_running) {
int64_t nextTime = m_nextExecuteTime;
// 等待直到到达执行时间
auto now = std::chrono::system_clock::now();
auto targetTime = std::chrono::system_clock::from_time_t(nextTime / 1000)
+ std::chrono::milliseconds(nextTime % 1000);
if (now < targetTime) {
std::this_thread::sleep_until(targetTime);
}
// 执行数据注入
for (const auto &[structName, dataMonitor] : m_alreadyStartedMonitors) {
if (dataMonitor && m_data.find(structName) != m_data.end()) {
dataMonitor->setDataByString(m_data[structName]);
}
}
// 读取下一行数据
updateData();
}
}

View File

@ -0,0 +1,68 @@
#pragma once
#include "DataMonitor.h"
#include <thread>
#include <atomic>
#include <mutex>
#include <condition_variable>
#include <fstream>
/**
* @brief 线
*/
class CSVDataInjectThread
{
public:
/**
* @brief
* @param csvFilePath CSV文件路径
*/
CSVDataInjectThread(std::string csvFilePath);
/**
* @brief
*/
~CSVDataInjectThread();
bool Initialize(std::vector<std::string> structNames);
/**
* @brief 线
*/
void start();
/**
* @brief 线
*/
void stop();
/**
* @brief CSV文件读取下一行数据并更新执行时间
* 线
*/
void updateData();
private:
/**
* @brief 线
*/
void threadFunc();
private:
std::string m_csvFilePath;
std::ifstream m_csvFile;
std::unordered_map<std::string, std::vector<std::string>> m_structInterfaceMap;
std::thread m_thread; ///< 数据注入线程
std::atomic<bool> m_running; ///< 线程运行标志
std::mutex m_mutex; ///< 互斥锁
std::condition_variable m_cv; ///< 条件变量
std::unordered_map<std::string, DataMonitorBasePtr>
m_alreadyStartedMonitors; ///< 已经启动的数据监控器
std::unordered_map<std::string, DataMonitorBasePtr>
m_notStartedMonitors; ///< 未启动的数据监控器
std::unordered_map<std::string, std::unordered_map<std::string, std::string>>
m_data; ///< 要注入的数据
std::atomic<int64_t> m_nextExecuteTime; ///< 下一次执行的时间点
};
using CSVDataInjectThreadPtr = std::shared_ptr<CSVDataInjectThread>;

View File

@ -8,6 +8,7 @@
#include "SystemControl.h"
#include "DataMonitorFactory.h"
#include "DataInjectThread.h"
#include "CSVDataInjectThread.h"
// 全局变量
static bool g_initialized = false;
@ -19,6 +20,8 @@ bool g_modelInfoMonitorStarted = false;
SystemControl *systemControl = nullptr;
bool g_systemControlStarted = false;
CSVDataInjectThreadPtr g_csvDataInjectThread;
// 初始化函数实现
int XN_Initialize(const char *domainId, int domainIdLen, char *errorMsg, int errorMsgSize)
{
@ -587,8 +590,79 @@ int XNMONITORSERVER_EXPORT XN_InjectDataInterfaceFromCsv(const char *structName,
const int csvFilePathLen, char *infoMsg,
int infoMsgSize)
{
// TODO: 从csv文件中注入数据接口
return -1;
std::vector<std::string> structNames;
std::string structNameStr(structName, structNameLen);
try {
nlohmann::json structNamesJson = nlohmann::json::parse(structNameStr);
if (!structNamesJson.is_array()) {
if (infoMsg && infoMsgSize > 0) {
strncpy(infoMsg, "Invalid struct name format - expected JSON array",
infoMsgSize - 1);
infoMsg[infoMsgSize - 1] = '\0';
}
return -1;
}
for (const auto &structNameJson : structNamesJson) {
structNames.push_back(structNameJson.get<std::string>());
}
} catch (const nlohmann::json::parse_error &e) {
if (infoMsg && infoMsgSize > 0) {
strncpy(infoMsg, "Invalid JSON format", infoMsgSize - 1);
infoMsg[infoMsgSize - 1] = '\0';
}
return -1;
}
std::string csvFilePathStr(csvFilePath, csvFilePathLen);
if (csvFilePathStr.empty()) {
if (infoMsg && infoMsgSize > 0) {
strncpy(infoMsg, "CSV 文件路径为空", infoMsgSize - 1);
infoMsg[infoMsgSize - 1] = '\0';
}
return -1;
}
if (!std::filesystem::exists(csvFilePathStr)) {
if (infoMsg && infoMsgSize > 0) {
strncpy(infoMsg, "CSV 文件不存在", infoMsgSize - 1);
infoMsg[infoMsgSize - 1] = '\0';
}
return -1;
}
if (g_csvDataInjectThread == nullptr) {
g_csvDataInjectThread = std::make_shared<CSVDataInjectThread>(csvFilePathStr);
bool ret = g_csvDataInjectThread->Initialize(structNames);
if (ret) {
g_csvDataInjectThread->start();
} else {
if (infoMsg && infoMsgSize > 0) {
strncpy(infoMsg, "CSV 注入线程初始化失败", infoMsgSize - 1);
infoMsg[infoMsgSize - 1] = '\0';
}
return -1;
}
} else {
if (infoMsg && infoMsgSize > 0) {
strncpy(infoMsg, "CSV 注入线程已在运行", infoMsgSize - 1);
infoMsg[infoMsgSize - 1] = '\0';
}
return -1;
}
return 0;
}
int XN_StopCsvDataInject(char *infoMsg, int infoMsgSize)
{
if (g_csvDataInjectThread == nullptr) {
if (infoMsg && infoMsgSize > 0) {
strncpy(infoMsg, "CSV 注入线程已不存在", infoMsgSize - 1);
infoMsg[infoMsgSize - 1] = '\0';
}
return -1;
}
g_csvDataInjectThread->stop();
g_csvDataInjectThread.reset(); // 释放智能指针
return 0;
}
int XNMONITORSERVER_EXPORT XN_StartCollectData(const char *structName, const int structNameLen,

View File

@ -210,14 +210,23 @@ extern "C"
* @param structNameLen
* @param csvFilePath csv文件路径
* @param csvFilePathLen csv文件路径长度
* @param injectTimes
* @param infoMsg
* @param infoMsgSize
* @return 0: , -1:
*/
int XNMONITORSERVER_EXPORT XN_InjectDataInterfaceFromCsv(
const char *structName, const int structNameLen, const char *csvFilePath,
const int csvFilePathLen, int injectTimes, char *infoMsg, int infoMsgSize);
int XNMONITORSERVER_EXPORT XN_InjectDataInterfaceFromCsv(const char *structName,
const int structNameLen,
const char *csvFilePath,
const int csvFilePathLen,
char *infoMsg, int infoMsgSize);
/**
* @brief csv数据注入
* @param infoMsg
* @param infoMsgSize
* @return 0: , -1:
*/
int XNMONITORSERVER_EXPORT XN_StopCsvDataInject(char *infoMsg, int infoMsgSize);
//******************** csv数据采集 *********************

5
XNSimHtml/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
log/*
logs/*
upload/*
uploads/*
node_modules/*

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -23,11 +23,7 @@ class DataMonitor extends HTMLElement {
this.handleTreeItemDblClick = this.handleTreeItemDblClick.bind(this);
this.handlePlot = this.handlePlot.bind(this);
this.handleDelete = this.handleDelete.bind(this);
this.handleCsvInject = this.handleCsvInject.bind(this);
this.handleModalClose = this.handleModalClose.bind(this);
this.handleModalCancel = this.handleModalCancel.bind(this);
this.handleModalConfirm = this.handleModalConfirm.bind(this);
this.handleFileInput = this.handleFileInput.bind(this);
this.handleCsvFileSelect = this.handleCsvFileSelect.bind(this);
this.validateCsvFile = this.validateCsvFile.bind(this);
// 确保 FloatingChartWindow 组件已注册
@ -1145,6 +1141,15 @@ class DataMonitor extends HTMLElement {
cursor: not-allowed;
}
.csv-file-name {
font-size: 14px;
color: #666;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.modal-overlay {
position: fixed;
top: 0;
@ -1313,42 +1318,16 @@ class DataMonitor extends HTMLElement {
</div>
</div>
<div class="toolbar-right">
<button class="csv-inject-button" id="csvInjectButton">
<img src="assets/icons/png/file.png" alt="CSV" style="width: 16px; height: 16px;">
CSV文件注入
<input type="file" id="csvFileInput" accept=".csv" style="display: none;" />
<span id="csvFileName" class="csv-file-name"></span>
<button class="csv-inject-button" id="csvUploadButton">
<img src="assets/icons/png/upload.png" alt="上传" style="width: 16px; height: 16px;">
上传CSV文件
</button>
<button class="csv-inject-button" id="csvInjectButton" style="display: none;">
<img src="assets/icons/png/inject.png" alt="注入" style="width: 16px; height: 16px;">
CSV数据注入
</button>
</div>
</div>
<div class="modal-overlay" id="csvModal">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">CSV文件注入</h3>
<button class="modal-close" id="modalClose">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>选择CSV文件</label>
<div class="file-input-wrapper">
<div class="file-input-trigger" id="fileInputTrigger">
点击或拖拽文件到此处
</div>
<input type="file" id="csvFileInput" accept=".csv" />
</div>
<div class="file-name" id="fileName"></div>
</div>
<div class="form-group">
<label>注入频率 (Hz)</label>
<input type="number" id="injectFrequency" min="10" max="10000" step="10" value="100" />
</div>
<div class="form-group">
<label>注入时长 ()</label>
<input type="number" id="injectDuration" min="1" max="3600" step="1" value="60" />
</div>
</div>
<div class="modal-footer">
<button class="modal-button secondary" id="modalCancel">取消</button>
<button class="modal-button primary" id="modalConfirm">确认</button>
</div>
</div>
</div>
<div class="monitor-container">
@ -1791,58 +1770,17 @@ class DataMonitor extends HTMLElement {
});
// 添加CSV文件注入相关的事件监听
const csvInjectButton = this.shadowRoot.getElementById('csvInjectButton');
const modal = this.shadowRoot.getElementById('csvModal');
const modalClose = this.shadowRoot.getElementById('modalClose');
const modalCancel = this.shadowRoot.getElementById('modalCancel');
const modalConfirm = this.shadowRoot.getElementById('modalConfirm');
const csvUploadButton = this.shadowRoot.getElementById('csvUploadButton');
const fileInput = this.shadowRoot.getElementById('csvFileInput');
const fileInputTrigger = this.shadowRoot.getElementById('fileInputTrigger');
if (csvInjectButton) {
csvInjectButton.addEventListener('click', this.handleCsvInject);
}
if (modalClose) {
modalClose.addEventListener('click', this.handleModalClose);
}
if (modalCancel) {
modalCancel.addEventListener('click', this.handleModalCancel);
}
if (modalConfirm) {
modalConfirm.addEventListener('click', this.handleModalConfirm);
if (csvUploadButton && fileInput) {
csvUploadButton.addEventListener('click', () => {
fileInput.click();
});
}
if (fileInput) {
fileInput.addEventListener('change', this.handleFileInput);
}
if (fileInputTrigger) {
fileInputTrigger.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
fileInputTrigger.style.borderColor = '#1890ff';
});
fileInputTrigger.addEventListener('dragleave', (e) => {
e.preventDefault();
e.stopPropagation();
fileInputTrigger.style.borderColor = '#d9d9d9';
});
fileInputTrigger.addEventListener('drop', (e) => {
e.preventDefault();
e.stopPropagation();
fileInputTrigger.style.borderColor = '#d9d9d9';
const files = e.dataTransfer.files;
if (files.length > 0) {
fileInput.files = files;
this.handleFileInput({ target: fileInput });
}
});
fileInput.addEventListener('change', this.handleCsvFileSelect);
}
}
@ -1952,80 +1890,115 @@ class DataMonitor extends HTMLElement {
}
/**
* @description 处理CSV文件注入按钮点击事件
* @description 处理CSV文件选择事件
* @param {Event} event - 文件选择事件
*/
handleCsvInject() {
const modal = this.shadowRoot.getElementById('csvModal');
if (modal) {
modal.classList.add('active');
}
}
/**
* @description 处理模态框关闭事件
*/
handleModalClose() {
const modal = this.shadowRoot.getElementById('csvModal');
if (modal) {
modal.classList.remove('active');
}
}
/**
* @description 处理模态框取消按钮点击事件
*/
handleModalCancel() {
this.handleModalClose();
}
/**
* @description 处理模态框确认按钮点击事件
*/
async handleModalConfirm() {
const fileInput = this.shadowRoot.getElementById('csvFileInput');
const frequencyInput = this.shadowRoot.getElementById('injectFrequency');
const durationInput = this.shadowRoot.getElementById('injectDuration');
if (!fileInput.files.length) {
alert('请选择CSV文件');
return;
}
const file = fileInput.files[0];
const frequency = parseInt(frequencyInput.value);
const duration = parseInt(durationInput.value);
async handleCsvFileSelect(event) {
const file = event.target.files[0];
if (!file) return;
if (!this.validateCsvFile(file)) {
return;
}
try {
// TODO: 实现CSV文件注入逻辑
console.log('CSV文件注入:', {
file: file.name,
frequency,
duration
// 创建 FormData 对象
const formData = new FormData();
formData.append('file', file);
// 上传文件
const response = await fetch('/api/filesystem/upload', {
method: 'POST',
body: formData
});
this.handleModalClose();
} catch (error) {
console.error('CSV文件注入失败:', error);
alert(`CSV文件注入失败: ${error.message}`);
}
}
const result = await response.json();
if (!response.ok) {
throw new Error(result.message || '文件上传失败');
}
/**
* @description 处理文件输入事件
* @param {Event} event - 文件输入事件
*/
handleFileInput(event) {
const file = event.target.files[0];
const fileName = this.shadowRoot.getElementById('fileName');
if (file) {
fileName.textContent = file.name;
} else {
fileName.textContent = '';
if (!result.success) {
throw new Error(result.message || '文件上传失败');
}
console.log('CSV文件上传成功:', result.file.name);
// 验证CSV文件头部
const validateResponse = await fetch(`/api/filesystem/validate-csv-headers?filename=${encodeURIComponent(result.file.name)}`);
if (!validateResponse.ok) {
throw new Error('验证CSV文件头部失败');
}
const validateResult = await validateResponse.json();
if (!validateResult.success) {
throw new Error(validateResult.message || '验证CSV文件头部失败');
}
// 检查每个头部是否在接口表中
const missingInterfaces = [];
const invalidInterfaces = [];
// 检查第一个接口(时间)是否在接口表中
const firstHeader = validateResult.headers[0];
const firstHeaderExists = this.interfaces.some(interfaceItem =>
interfaceItem.InterfaceName === firstHeader
);
if (firstHeaderExists) {
invalidInterfaces.push(`第一个接口 "${firstHeader}" 不应该在接口表中`);
}
// 检查其他接口是否在接口表中
for (let i = 1; i < validateResult.headers.length; i++) {
const header = validateResult.headers[i];
const exists = this.interfaces.some(interfaceItem =>
interfaceItem.InterfaceName === header
);
if (!exists) {
missingInterfaces.push(header);
}
}
// 合并错误信息
const errorMessages = [];
if (invalidInterfaces.length > 0) {
errorMessages.push(invalidInterfaces.join('\n'));
}
if (missingInterfaces.length > 0) {
errorMessages.push(`以下接口在系统中不存在:\n${missingInterfaces.join('\n')}`);
}
if (errorMessages.length > 0) {
// 删除已上传的文件
try {
const deleteResponse = await fetch(`/api/filesystem/upload/${encodeURIComponent(result.file.name)}`, {
method: 'DELETE'
});
if (!deleteResponse.ok) {
console.error('删除验证失败的文件时出错');
}
} catch (deleteError) {
console.error('删除验证失败的文件时出错:', deleteError);
}
throw new Error(errorMessages.join('\n\n'));
}
console.log('CSV文件头部验证通过');
// 更新文件名显示
const csvFileName = this.shadowRoot.getElementById('csvFileName');
if (csvFileName) {
csvFileName.textContent = result.file.name;
}
// 显示数据注入按钮
const csvInjectButton = this.shadowRoot.getElementById('csvInjectButton');
if (csvInjectButton) {
csvInjectButton.style.display = 'flex';
}
} catch (error) {
console.error('CSV文件处理失败:', error);
alert(`CSV文件处理失败: ${error.message}`);
}
}
@ -2035,7 +2008,19 @@ class DataMonitor extends HTMLElement {
* @returns {boolean} 是否验证通过
*/
validateCsvFile(file) {
// TODO: 实现CSV文件验证逻辑
// 检查文件类型
if (!file.name.toLowerCase().endsWith('.csv')) {
alert('请选择CSV文件');
return false;
}
// 检查文件大小限制为10MB
const maxSize = 10 * 1024 * 1024; // 10MB
if (file.size > maxSize) {
alert('文件大小不能超过10MB');
return false;
}
return true;
}
@ -2052,6 +2037,6 @@ class DataMonitor extends HTMLElement {
this.stopDataUpdateTimer();
}
}
customElements.define('data-monitor', DataMonitor);

View File

@ -281,4 +281,90 @@ router.post('/stop-continuous', async (req, res) => {
}
});
/**
* @brief 停止所有持续注入数据
* @route POST /api/data-monitor/stop-all-continuous
* @returns {Object} 返回停止结果
*/
router.post('/stop-all-continuous', async (req, res) => {
try {
// 获取所有正在注入的结构体
const injectingStructs = Array.from(continuousInjectStatus.keys());
if (injectingStructs.length === 0) {
return res.json({
success: true,
message: '当前没有正在进行的注入'
});
}
// 停止所有结构体的注入
const results = [];
for (const structName of injectingStructs) {
const result = systemMonitor.stopInjectContinuous(structName);
if (result.includes('失败')) {
results.push({ structName, success: false, message: result });
} else {
results.push({ structName, success: true, message: '停止成功' });
}
continuousInjectStatus.delete(structName);
}
// 检查是否所有结构体都成功停止
const allSuccess = results.every(r => r.success);
res.json({
success: allSuccess,
message: allSuccess ? '所有持续注入已停止' : '部分持续注入停止失败',
details: results
});
} catch (error) {
res.status(500).json({ success: false, message: `停止所有持续注入数据失败: ${error.message}` });
}
});
/**
* @brief 从CSV文件注入数据
* @route POST /api/data-monitor/inject-csv
* @param {string} structName - 结构体名称
* @param {string} csvFilePath - CSV文件路径
* @returns {Object} 返回注入结果
*/
router.post('/inject-csv', async (req, res) => {
try {
const { structName, csvFilePath} = req.body;
if (!structName || !csvFilePath) {
return res.status(400).json({
success: false,
message: '结构体名称、CSV文件路径和注入次数不能为空'
});
}
const result = systemMonitor.injectDataInterfaceFromCsv(structName, csvFilePath);
if (result.includes('失败')) {
return res.status(500).json({ success: false, message: result });
}
res.json({ success: true, message: result });
} catch (error) {
res.status(500).json({ success: false, message: `从CSV文件注入数据失败: ${error.message}` });
}
});
/**
* @brief 停止CSV数据注入
* @route POST /api/data-monitor/stop-csv-inject
* @returns {Object} 返回停止结果
*/
router.post('/stop-csv-inject', async (req, res) => {
try {
const result = systemMonitor.stopCsvDataInject();
if (result.includes('失败')) {
return res.status(500).json({ success: false, message: result });
}
res.json({ success: true, message: result });
} catch (error) {
res.status(500).json({ success: false, message: `停止CSV数据注入失败: ${error.message}` });
}
});
module.exports = router;

View File

@ -1,8 +1,47 @@
const express = require('express');
const router = express.Router();
const fs = require('fs').promises;
const fsPromises = require('fs').promises;
const fs = require('fs');
const path = require('path');
const { getActualLogPath } = require('../utils/file-utils');
const multer = require('multer');
const { getActualLogPath, getUploadPath, saveUploadedFile, isAllowedFileType } = require('../utils/file-utils');
// 获取上传目录路径
const uploadPath = getUploadPath();
// 配置 multer 存储
const storage = multer.diskStorage({
destination: async function (req, file, cb) {
try {
const uploadPath = await getUploadPath();
cb(null, uploadPath);
} catch (error) {
cb(error);
}
},
filename: function (req, file, cb) {
cb(null, file.originalname);
}
});
// 文件过滤器
const fileFilter = (req, file, cb) => {
// 允许的文件类型
const allowedTypes = ['.csv'];
if (isAllowedFileType(file.originalname, allowedTypes)) {
cb(null, true);
} else {
cb(new Error('不支持的文件类型'));
}
};
const upload = multer({
storage: storage,
fileFilter: fileFilter,
limits: {
fileSize: 10 * 1024 * 1024 // 限制文件大小为10MB
}
});
// 读取目录内容
router.get('/readdir', async (req, res) => {
@ -17,7 +56,7 @@ router.get('/readdir', async (req, res) => {
// 检查目录是否存在
try {
const stats = await fs.stat(logDirPath);
const stats = await fsPromises.stat(logDirPath);
if (!stats.isDirectory()) {
return res.status(400).json({ error: '指定的路径不是目录' });
}
@ -25,7 +64,7 @@ router.get('/readdir', async (req, res) => {
// 如果目录不存在,尝试创建它
if (statError.code === 'ENOENT') {
try {
await fs.mkdir(logDirPath, { recursive: true });
await fsPromises.mkdir(logDirPath, { recursive: true });
} catch (mkdirError) {
console.error('创建日志目录失败:', mkdirError);
return res.status(500).json({ error: '创建日志目录失败' });
@ -36,7 +75,7 @@ router.get('/readdir', async (req, res) => {
}
// 读取目录内容
const files = await fs.readdir(logDirPath);
const files = await fsPromises.readdir(logDirPath);
// 返回文件列表
res.json({ files });
@ -78,7 +117,7 @@ router.get('/stat', async (req, res) => {
}
// 获取文件状态
const stats = await fs.stat(filePath);
const stats = await fsPromises.stat(filePath);
// 返回文件信息
res.json({
@ -133,7 +172,7 @@ router.get('/readFile', async (req, res) => {
}
// 获取文件状态
const stats = await fs.stat(filePath);
const stats = await fsPromises.stat(filePath);
// 检查文件大小,限制读取过大的文件
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
@ -142,7 +181,7 @@ router.get('/readFile', async (req, res) => {
}
// 读取文件内容
const content = await fs.readFile(filePath, 'utf-8');
const content = await fsPromises.readFile(filePath, 'utf-8');
// 设置响应头
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
@ -164,4 +203,159 @@ router.get('/readFile', async (req, res) => {
}
});
// 获取上传文件列表
router.get('/upload-files', async (req, res) => {
try {
const uploadPath = await getUploadPath();
// 读取目录内容
const files = await fsPromises.readdir(uploadPath);
// 获取每个文件的详细信息
const fileDetails = await Promise.all(files.map(async (fileName) => {
const filePath = path.join(uploadPath, fileName);
const stats = await fsPromises.stat(filePath);
return {
name: fileName,
size: stats.size,
created: stats.birthtime,
modified: stats.mtime,
path: filePath
};
}));
res.json({ files: fileDetails });
} catch (error) {
console.error('获取上传文件列表失败:', error);
res.status(500).json({ error: '获取上传文件列表失败', message: error.message });
}
});
// 上传文件
router.post('/upload', upload.single('file'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: '未提供文件' });
}
// 获取上传目录路径
const uploadPath = await getUploadPath();
// 确保上传目录存在
try {
await fsPromises.access(uploadPath);
} catch (error) {
if (error.code === 'ENOENT') {
// 如果目录不存在,创建它
await fsPromises.mkdir(uploadPath, { recursive: true });
} else {
throw error;
}
}
// 保存文件并获取最终路径
const finalPath = await saveUploadedFile(req.file);
// 获取文件状态
const stats = await fsPromises.stat(finalPath);
res.json({
success: true,
file: {
name: path.basename(finalPath),
size: stats.size,
path: finalPath,
created: stats.birthtime,
modified: stats.mtime
}
});
} catch (error) {
console.error('文件上传失败:', error);
res.status(500).json({ error: '文件上传失败', message: error.message });
}
});
// 删除上传的文件
router.delete('/upload/:filename', async (req, res) => {
try {
const { filename } = req.params;
const uploadPath = await getUploadPath();
const filePath = path.join(uploadPath, filename);
// 安全检查:确保文件在上传目录内
if (!filePath.startsWith(uploadPath)) {
return res.status(403).json({ error: '无权删除该文件' });
}
// 删除文件
await fsPromises.unlink(filePath);
res.json({ success: true, message: '文件删除成功' });
} catch (error) {
console.error('删除文件失败:', error);
if (error.code === 'ENOENT') {
return res.status(404).json({ error: '文件不存在' });
}
res.status(500).json({ error: '删除文件失败', message: error.message });
}
});
// 验证CSV文件头部
router.get('/validate-csv-headers', async (req, res) => {
try {
const { filename } = req.query;
if (!filename) {
return res.status(400).json({
success: false,
message: '未提供文件名'
});
}
// 获取上传目录路径
const uploadPath = await getUploadPath();
const filePath = path.join(uploadPath, filename);
// 检查文件是否存在
try {
await fsPromises.access(filePath);
} catch (error) {
return res.status(404).json({
success: false,
message: '文件不存在'
});
}
// 只读取文件的第一行
const fileStream = fs.createReadStream(filePath, { encoding: 'utf8' });
let firstLine = '';
try {
for await (const chunk of fileStream) {
const lines = chunk.split('\n');
firstLine = lines[0];
break;
}
} finally {
// 确保关闭文件流
fileStream.destroy();
}
// 解析CSV头部
const headers = firstLine.split(',').map(header => header.trim());
res.json({
success: true,
headers: headers
});
} catch (error) {
console.error('验证CSV文件头部失败:', error);
res.status(500).json({
success: false,
message: '验证CSV文件头部失败: ' + error.message
});
}
});
module.exports = router;

View File

@ -135,24 +135,9 @@ async function getUploadPath() {
// 保存上传的文件
async function saveUploadedFile(file) {
try {
const uploadPath = await getUploadPath();
const filePath = path.join(uploadPath, file.originalname);
// 如果文件已存在,添加时间戳
if (fs.existsSync(filePath)) {
const timestamp = new Date().getTime();
const ext = path.extname(file.originalname);
const name = path.basename(file.originalname, ext);
const newFileName = `${name}_${timestamp}${ext}`;
return path.join(uploadPath, newFileName);
}
return filePath;
} catch (error) {
console.error('处理上传文件路径失败:', error);
throw error;
}
const uploadPath = await getUploadPath();
const filePath = path.join(uploadPath, file.originalname);
return filePath;
}
// 检查文件类型是否允许

View File

@ -59,7 +59,9 @@ try {
'XN_GetDataMonitorInfo': ['int', [StringType, 'int', StringType, 'int', StringType, 'int', StringType, 'int']],
'XN_InjectDataInterface': ['int', [StringType, 'int', StringType, 'int', StringType, 'int']],
'XN_StartInjectContinuous': ['int', [StringType, 'int', StringType, 'int', 'double', StringType, 'int']],
'XN_StopInjectContinuous': ['int', [StringType, 'int', StringType, 'int']]
'XN_StopInjectContinuous': ['int', [StringType, 'int', StringType, 'int']],
'XN_InjectDataInterfaceFromCsv': ['int', [StringType, 'int', StringType, 'int', StringType, 'int']],
'XN_StopCsvDataInject': ['int', [StringType, 'int']]
});
} catch (error) {
console.error(`加载 ${monitorLibName} 失败:`, error);
@ -437,6 +439,49 @@ function stopInjectContinuous(structName) {
}
}
// 从CSV文件注入数据
function injectDataInterfaceFromCsv(structName, csvFilePath) {
if (!monitorLib) {
return '监控服务器库未加载';
}
try {
const errorMsg = Buffer.alloc(1024);
const result = monitorLib.XN_InjectDataInterfaceFromCsv(
structName,
structName.length,
csvFilePath,
csvFilePath.length,
errorMsg,
errorMsg.length
);
if (result !== 0) {
return `注入失败: ${errorMsg.toString('utf8').replace(/\0/g, '')}`;
}
return '注入成功';
} catch (error) {
return `注入失败: ${error.message}`;
}
}
// 停止CSV数据注入
function stopCsvDataInject() {
if (!monitorLib) {
return '监控服务器库未加载';
}
try {
const errorMsg = Buffer.alloc(1024);
const result = monitorLib.XN_StopCsvDataInject(errorMsg, errorMsg.length);
if (result !== 0) {
return `停止失败: ${errorMsg.toString('utf8').replace(/\0/g, '')}`;
}
return '停止成功';
} catch (error) {
return `停止失败: ${error.message}`;
}
}
module.exports = {
loginLib,
monitorLib,
@ -460,5 +505,7 @@ module.exports = {
getDataMonitorInfo,
injectDataInterface,
startInjectContinuous,
stopInjectContinuous
stopInjectContinuous,
injectDataInterfaceFromCsv,
stopCsvDataInject
};