修复采集成功率计算bug、添加IIS localhost绑定和Vite proxy配置

- DashboardRepository: 采集成功率改用 log_collect_raw 的成功/失败比率计算(修复-800%的bug)
- Web.config: 添加 runAllManagedModulesForAllRequests=true 使API路由生效
- Vite: 添加/api代理到IIS后端(127.0.0.1),前端显示真实数据
- 测试脚本: 阶段6指定原端口重启避免URL不同步,添加仪表盘API验证
main
haoliang 4 days ago
parent 36cb666af3
commit 45cb84c08d

@ -21,6 +21,13 @@ export default defineConfig(({ command }) => ({
}, },
server: { server: {
port: 5173, port: 5173,
// 将/api请求代理到IIS后端真实数据
proxy: {
'/api': {
target: 'http://127.0.0.1',
changeOrigin: true,
},
},
}, },
// 构建优化配置 // 构建优化配置
build: { build: {

@ -14,16 +14,19 @@
* *
* : * :
* 1. MariaDB cnc_business + cnc_log * 1. MariaDB cnc_business + cnc_log
* 2. CncSimulator * 2. CncSimulator 9000
* 3. CncCollector * 3. CncCollector API http://localhost:5800/
* *
* : npm install @playwright/test mysql2 * : npm install @playwright/test mysql2
* : npx playwright test collector-15min.spec.ts --reporter=list --timeout=0 * : npx playwright test collector-15min.spec.ts --reporter=list --timeout=0
*
* :
* (9000): /admin/api/start-address, /admin/api/stop-address, /admin/api/status()
* (N): /admin/api/network, /admin/api/stats, /admin/api/full-summary
*/ */
import { test, expect, request } from '@playwright/test'; import { test, expect, request } from '@playwright/test';
import mysql from 'mysql2/promise'; import mysql from 'mysql2/promise';
import { execSync, spawn, ChildProcess } from 'child_process';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
@ -32,7 +35,7 @@ import * as path from 'path';
// ============================================================ // ============================================================
const API_BASE = 'http://localhost:5800'; const API_BASE = 'http://localhost:5800';
const SIM_GATEWAY = 'http://localhost:9001'; const SIM_GATEWAY = 'http://localhost:9000';
const API_KEY = 'collector_api_key_2026'; const API_KEY = 'collector_api_key_2026';
const HEADERS = { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' }; const HEADERS = { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' };
@ -111,18 +114,18 @@ async function collectorApi(method: string, path: string) {
/** 辅助:等待指定毫秒 */ /** 辅助:等待指定毫秒 */
const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)); const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
/** 辅助:调用模拟器管理API */ /** 辅助:调用模拟器**单地址**管理APIstats/network/full-summary等 */
async function simApi(port: number, path: string, body?: any) { async function simApi(path: string, body?: any) {
const ctx = await request.newContext(); const ctx = await request.newContext({ timeout: 10000 });
if (body) { if (body) {
return ctx.post(`http://localhost:${port}${path}`, { data: body, headers: { 'Content-Type': 'application/json' } }); return ctx.post(`http://localhost:${simPort}${path}`, { data: body, headers: { 'Content-Type': 'application/json' } });
} }
return ctx.get(`http://localhost:${port}${path}`); return ctx.get(`http://localhost:${simPort}${path}`);
} }
/** 辅助:调用模拟器网关API */ /** 辅助:调用模拟器**网关**APIstart-address/stop-address/status等 */
async function gatewayApi(path: string, body?: any) { async function gatewayApi(path: string, body?: any) {
const ctx = await request.newContext(); const ctx = await request.newContext({ timeout: 10000 });
if (body) { if (body) {
return ctx.post(`${SIM_GATEWAY}${path}`, { data: body, headers: { 'Content-Type': 'application/json' } }); return ctx.post(`${SIM_GATEWAY}${path}`, { data: body, headers: { 'Content-Type': 'application/json' } });
} }
@ -138,12 +141,12 @@ function log(msg: string) {
// 全局变量 // 全局变量
// ============================================================ // ============================================================
let simulationPort = 0; /** 模拟地址端口由start-address动态分配 */
let simPort = 0;
let testStartTime: Date; let testStartTime: Date;
let phaseTimestamps: Record<string, Date> = {}; let phaseTimestamps: Record<string, Date> = {};
/** 每阶段结束时采集模拟器快照 */
let phaseSnapshots: Record<string, any> = {}; let phaseSnapshots: Record<string, any> = {};
let globalMismatchCount = 0;
// ============================================================ // ============================================================
// 全局串行配置 // 全局串行配置
@ -152,40 +155,40 @@ let phaseSnapshots: Record<string, any> = {};
test.describe.configure({ mode: 'serial' }); test.describe.configure({ mode: 'serial' });
// ============================================================ // ============================================================
// 阶段0: 初始化 // 测试主流程
// ============================================================ // ============================================================
test.describe('15分钟自动化采集测试', () => { test.describe('15分钟自动化采集测试', () => {
test('阶段0: 初始化 — 清空数据并启动服务', async () => { test('阶段0: 初始化 — 清空数据并启动服务', async () => {
test.setTimeout(120000); // 2分钟超时 test.setTimeout(120000);
testStartTime = new Date(); testStartTime = new Date();
log('\n===== 15分钟自动化采集测试开始 ====='); log('\n===== 15分钟自动化采集测试开始 =====');
log(`测试开始时间: ${testStartTime.toLocaleString('zh-CN')}`); log(`测试开始时间: ${testStartTime.toLocaleString('zh-CN')}`);
// 1. 清空采集数据 // 1. 清空采集数据
log('[0/6] 清空旧采集数据...'); log('[0/7] 清空旧采集数据...');
await executeBusiness('TRUNCATE TABLE cnc_collect_record'); await executeBusiness('TRUNCATE TABLE cnc_collect_record');
await executeBusiness('TRUNCATE TABLE cnc_production_segment'); await executeBusiness('TRUNCATE TABLE cnc_production_segment');
await executeLog('TRUNCATE TABLE log_collect_raw'); await executeLog('TRUNCATE TABLE log_collect_raw');
log(' ✓ 已清空 cnc_collect_record, cnc_production_segment, log_collect_raw'); log(' ✓ 已清空 cnc_collect_record, cnc_production_segment, log_collect_raw');
// 2. 确认采集地址配置 // 2. 确认采集地址配置
log('[1/6] 确认采集地址配置...'); log('[1/7] 确认采集地址配置...');
const addrs = await queryBusiness('SELECT id, url, is_enabled, collect_interval FROM cnc_collect_address WHERE id = 1'); const addrs = await queryBusiness('SELECT id, url, is_enabled, collect_interval FROM cnc_collect_address WHERE id = 1');
expect(addrs.length).toBe(1); expect(addrs.length).toBe(1);
log(` ✓ 采集地址: id=${addrs[0].id}, url=${addrs[0].url}, interval=${addrs[0].collect_interval}s`); log(` ✓ 采集地址: id=${addrs[0].id}, interval=${addrs[0].collect_interval}s`);
// 3. 确认机床数据 // 3. 确认机床数据
log('[2/6] 确认机床数据...'); log('[2/7] 确认机床数据...');
const machines = await queryBusiness( const machines = await queryBusiness(
'SELECT id, device_code, name, collect_address_id, is_enabled FROM cnc_machine WHERE collect_address_id = 1 AND is_enabled = 1' 'SELECT id, device_code, name, collect_address_id, is_enabled FROM cnc_machine WHERE collect_address_id = 1 AND is_enabled = 1'
); );
expect(machines.length).toBeGreaterThanOrEqual(10); expect(machines.length).toBeGreaterThanOrEqual(10);
log(` ✓ 机床数量: ${machines.length}采集地址1下的启用机床`); log(` ✓ 机床数量: ${machines.length}`);
// 4. 通过模拟器网关启动地址模拟 // 4. 通过网关启动地址模拟
log('[3/6] 启动模拟器...'); log('[3/7] 通过网关启动模拟...');
const startResp = await gatewayApi('/admin/api/start-address', { const startResp = await gatewayApi('/admin/api/start-address', {
dbAddressId: 1, dbAddressId: 1,
interval: 10, interval: 10,
@ -193,26 +196,27 @@ test.describe('15分钟自动化采集测试', () => {
}); });
expect(startResp.ok()).toBeTruthy(); expect(startResp.ok()).toBeTruthy();
const startResult = await startResp.json() as any; const startResult = await startResp.json() as any;
simulationPort = startResult.port || 9001; simPort = startResult.port;
log(` ✓ 模拟器已启动 → 端口 ${simulationPort}`); expect(simPort).toBeGreaterThan(0);
log(` ✓ 模拟地址已启动 → 端口 ${simPort}`);
// 等待模拟器数据就绪 // 5. 等待模拟器数据就绪确认单地址API可用
await sleep(3000); await sleep(3000);
const dataResp = await gatewayApi(`/admin/api/status`); const dataResp = await simApi('/admin/api/status');
if (dataResp.ok()) { if (dataResp.ok()) {
const status = await dataResp.json() as any; const status = await dataResp.json() as any;
log(` ✓ 模拟状态: ${status.totalDevices}台设备, 在线${status.onlineDevices}`); log(` ✓ 模拟地址状态: ${status.totalDevices}台设备, 在线${status.onlineDevices}, 运行=${status.isRunning}`);
} }
// 5. 更新采集地址URL指向模拟端口 // 6. 更新采集地址URL指向模拟端口
await executeBusiness( await executeBusiness(
'UPDATE cnc_collect_address SET url = ?, is_enabled = 1 WHERE id = 1', 'UPDATE cnc_collect_address SET url = ?, is_enabled = 1 WHERE id = 1',
[`http://localhost:${simulationPort}/`] [`http://localhost:${simPort}/`]
); );
log(` ✓ 更新采集地址URL → http://localhost:${simulationPort}/`); log(` ✓ 更新采集地址URL → http://localhost:${simPort}/`);
// 6. 启动采集服务 // 7. 启动采集服务
log('[4/6] 启动采集服务...'); log('[4/7] 启动采集服务...');
await collectorApi('POST', '/api/collector/stop'); await collectorApi('POST', '/api/collector/stop');
await sleep(1000); await sleep(1000);
await collectorApi('POST', '/api/collector/refresh'); await collectorApi('POST', '/api/collector/refresh');
@ -220,11 +224,10 @@ test.describe('15分钟自动化采集测试', () => {
await collectorApi('POST', '/api/collector/start'); await collectorApi('POST', '/api/collector/start');
log(' ✓ 采集服务已启动'); log(' ✓ 采集服务已启动');
// 7. 等待2个采集周期确认数据流 // 8. 等待2个采集周期确认数据流
log('[5/6] 等待数据采集确认...'); log('[5/7] 等待数据采集确认...');
await sleep(25000); await sleep(25000);
// 验证已有数据产生
const records = await queryBusiness( const records = await queryBusiness(
'SELECT COUNT(*) as cnt FROM cnc_collect_record WHERE collect_time > ?', 'SELECT COUNT(*) as cnt FROM cnc_collect_record WHERE collect_time > ?',
[testStartTime] [testStartTime]
@ -236,12 +239,39 @@ test.describe('15分钟自动化采集测试', () => {
log(` ✓ 采集记录数: ${records[0].cnt}, 原始日志数: ${rawLogs[0].cnt}`); log(` ✓ 采集记录数: ${records[0].cnt}, 原始日志数: ${rawLogs[0].cnt}`);
expect(records[0].cnt).toBeGreaterThan(0); expect(records[0].cnt).toBeGreaterThan(0);
// 8. 保存阶段0快照 // 8.5 验证后台仪表盘API有数据确认IIS+前端能拿到采集数据)
log('[5.5/7] 验证后台仪表盘API...');
try {
const loginResp = await fetch('http://127.0.0.1/api/admin/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'admin', password: 'admin123' })
});
if (loginResp.ok) {
const loginData = await loginResp.json() as any;
const token = loginData.data?.token;
if (token) {
const dashResp = await fetch('http://127.0.0.1/api/admin/dashboard/summary', {
headers: { Authorization: `Bearer ${token}` }
});
if (dashResp.ok) {
const dashData = await dashResp.json() as any;
const d = dashData.data;
log(` ✓ 后台仪表盘: 在线机床=${d.onlineCount}/${d.totalMachines}, 成功率=${d.collectSuccessRate?.toFixed(1)}%, 告警=${d.activeAlerts}`);
}
}
}
} catch {
log(' ⚠ 后台API不可达IIS可能未配置跳过验证');
}
// 9. 保存初始快照
phaseTimestamps['init'] = new Date(); phaseTimestamps['init'] = new Date();
const snapshot0 = await gatewayApi(`/admin/api/full-summary`); const snapshot0 = await simApi('/admin/api/full-summary');
if (snapshot0.ok()) phaseSnapshots['init'] = await snapshot0.json(); if (snapshot0.ok()) phaseSnapshots['init'] = await snapshot0.json();
log('[6/6] 初始化完成开始15分钟采集测试\n'); log('[6/7] 初始化完成');
log(`[7/7] 开始15分钟采集测试网关=${SIM_GATEWAY}, 模拟端口=${simPort}\n`);
}); });
// ============================================================ // ============================================================
@ -253,23 +283,19 @@ test.describe('15分钟自动化采集测试', () => {
log('━━━ 阶段1: 正常采集5分钟 ━━━'); log('━━━ 阶段1: 正常采集5分钟 ━━━');
phaseTimestamps['normal1_start'] = new Date(); phaseTimestamps['normal1_start'] = new Date();
// 记录开始时的模拟器状态 const statsBefore = await simApi('/admin/api/stats');
const statsBefore = await gatewayApi(`/admin/api/stats`);
const statsBeforeData = statsBefore.ok() ? await statsBefore.json() : {}; const statsBeforeData = statsBefore.ok() ? await statsBefore.json() : {};
log(` 开始: 总零件=${statsBeforeData.totalParts || '?'}, 在线设备=${statsBeforeData.onlineDevices || '?'}`); log(` 开始: 总零件=${statsBeforeData.totalParts || '?'}, 在线设备=${statsBeforeData.onlineDevices || '?'}`);
log(` 等待 ${PHASE_DURATIONS.normal1 / 1000} 秒...`); log(` 等待 ${PHASE_DURATIONS.normal1 / 1000} 秒...`);
// 等待5分钟
await sleep(PHASE_DURATIONS.normal1); await sleep(PHASE_DURATIONS.normal1);
// 记录结束时的状态
phaseTimestamps['normal1_end'] = new Date(); phaseTimestamps['normal1_end'] = new Date();
const statsAfter = await gatewayApi(`/admin/api/stats`); const statsAfter = await simApi('/admin/api/stats');
const statsAfterData = statsAfter.ok() ? await statsAfter.json() : {}; const statsAfterData = statsAfter.ok() ? await statsAfter.json() : {};
// 保存快照 const snapshot = await simApi('/admin/api/full-summary');
const snapshot = await gatewayApi(`/admin/api/full-summary`);
if (snapshot.ok()) phaseSnapshots['normal1'] = await snapshot.json(); if (snapshot.ok()) phaseSnapshots['normal1'] = await snapshot.json();
log(` 结束: 总零件=${statsAfterData.totalParts || '?'}, 在线设备=${statsAfterData.onlineDevices || '?'}`); log(` 结束: 总零件=${statsAfterData.totalParts || '?'}, 在线设备=${statsAfterData.onlineDevices || '?'}`);
@ -285,23 +311,18 @@ test.describe('15分钟自动化采集测试', () => {
log('━━━ 阶段2: HTTP 500异常2分钟 ━━━'); log('━━━ 阶段2: HTTP 500异常2分钟 ━━━');
phaseTimestamps['http500_start'] = new Date(); phaseTimestamps['http500_start'] = new Date();
// 设置网络异常: HTTP 500 const setResp = await simApi('/admin/api/network', { type: 'http500' });
const setResp = await gatewayApi(`/admin/api/network`, { type: 'http500' }); log(` ✓ 已设置网络异常: HTTP 500 (ok=${setResp.ok()})`);
if (setResp.ok()) {
log(' ✓ 已设置网络异常: HTTP 500');
}
log(` 等待 ${PHASE_DURATIONS.http500 / 1000} 秒...`); log(` 等待 ${PHASE_DURATIONS.http500 / 1000} 秒...`);
await sleep(PHASE_DURATIONS.http500); await sleep(PHASE_DURATIONS.http500);
phaseTimestamps['http500_end'] = new Date(); phaseTimestamps['http500_end'] = new Date();
// 保存快照 const snapshot = await simApi('/admin/api/full-summary');
const snapshot = await gatewayApi(`/admin/api/full-summary`);
if (snapshot.ok()) phaseSnapshots['http500'] = await snapshot.json(); if (snapshot.ok()) phaseSnapshots['http500'] = await snapshot.json();
// 恢复网络 await simApi('/admin/api/network', { type: 'normal' });
await gatewayApi(`/admin/api/network`, { type: 'normal' });
log(' ✓ 已恢复正常网络'); log(' ✓ 已恢复正常网络');
log(` ✓ 阶段2完成\n`); log(` ✓ 阶段2完成\n`);
}); });
@ -315,24 +336,19 @@ test.describe('15分钟自动化采集测试', () => {
log('━━━ 阶段3: 超时异常2分钟 ━━━'); log('━━━ 阶段3: 超时异常2分钟 ━━━');
phaseTimestamps['timeout_start'] = new Date(); phaseTimestamps['timeout_start'] = new Date();
// 设置网络异常: 超时 const setResp = await simApi('/admin/api/network', { type: 'timeout' });
const setResp = await gatewayApi(`/admin/api/network`, { type: 'timeout' }); log(` ✓ 已设置网络异常: 超时(60秒延迟) (ok=${setResp.ok()})`);
if (setResp.ok()) {
log(' ✓ 已设置网络异常: 超时(60秒延迟)');
}
log(` 等待 ${PHASE_DURATIONS.timeout / 1000} 秒...`); log(` 等待 ${PHASE_DURATIONS.timeout / 1000} 秒...`);
await sleep(PHASE_DURATIONS.timeout); await sleep(PHASE_DURATIONS.timeout);
phaseTimestamps['timeout_end'] = new Date(); phaseTimestamps['timeout_end'] = new Date();
// 保存快照 const snapshot = await simApi('/admin/api/full-summary');
const snapshot = await gatewayApi(`/admin/api/full-summary`);
if (snapshot.ok()) phaseSnapshots['timeout'] = await snapshot.json(); if (snapshot.ok()) phaseSnapshots['timeout'] = await snapshot.json();
// 恢复网络 await simApi('/admin/api/network', { type: 'normal' });
await gatewayApi(`/admin/api/network`, { type: 'normal' }); await sleep(2000);
await sleep(2000); // 等待恢复
log(' ✓ 已恢复正常网络'); log(' ✓ 已恢复正常网络');
log(` ✓ 阶段3完成\n`); log(` ✓ 阶段3完成\n`);
}); });
@ -346,23 +362,18 @@ test.describe('15分钟自动化采集测试', () => {
log('━━━ 阶段4: 空数据返回2分钟 ━━━'); log('━━━ 阶段4: 空数据返回2分钟 ━━━');
phaseTimestamps['empty_start'] = new Date(); phaseTimestamps['empty_start'] = new Date();
// 设置网络异常: 空数据 const setResp = await simApi('/admin/api/network', { type: 'empty' });
const setResp = await gatewayApi(`/admin/api/network`, { type: 'empty' }); log(` ✓ 已设置网络异常: 空数据([]) (ok=${setResp.ok()})`);
if (setResp.ok()) {
log(' ✓ 已设置网络异常: 空数据([])');
}
log(` 等待 ${PHASE_DURATIONS.empty / 1000} 秒...`); log(` 等待 ${PHASE_DURATIONS.empty / 1000} 秒...`);
await sleep(PHASE_DURATIONS.empty); await sleep(PHASE_DURATIONS.empty);
phaseTimestamps['empty_end'] = new Date(); phaseTimestamps['empty_end'] = new Date();
// 保存快照 const snapshot = await simApi('/admin/api/full-summary');
const snapshot = await gatewayApi(`/admin/api/full-summary`);
if (snapshot.ok()) phaseSnapshots['empty'] = await snapshot.json(); if (snapshot.ok()) phaseSnapshots['empty'] = await snapshot.json();
// 恢复网络 await simApi('/admin/api/network', { type: 'normal' });
await gatewayApi(`/admin/api/network`, { type: 'normal' });
await sleep(2000); await sleep(2000);
log(' ✓ 已恢复正常网络'); log(' ✓ 已恢复正常网络');
log(` ✓ 阶段4完成\n`); log(` ✓ 阶段4完成\n`);
@ -377,10 +388,18 @@ test.describe('15分钟自动化采集测试', () => {
log('━━━ 阶段5: 拒绝连接2分钟 ━━━'); log('━━━ 阶段5: 拒绝连接2分钟 ━━━');
phaseTimestamps['refuse_start'] = new Date(); phaseTimestamps['refuse_start'] = new Date();
// 设置网络异常: 拒绝连接 // 拒绝连接会停止HttpListener但管理API仍需通过同一端口操作
const setResp = await gatewayApi(`/admin/api/network`, { type: 'refuse' }); // 这里用网关API直接发送但网关没有network命令必须发到模拟端口
if (setResp.ok()) { // 拒绝连接意味着模拟端口的HttpListener被停止无法再发请求
log(' ✓ 已设置网络异常: 拒绝连接'); // 解决方案:先记录快照,再设置拒绝
const snapshotBefore = await simApi('/admin/api/full-summary');
if (snapshotBefore.ok()) phaseSnapshots['refuse_before'] = await snapshotBefore.json();
try {
const setResp = await simApi('/admin/api/network', { type: 'refuse' });
log(` ✓ 已设置网络异常: 拒绝连接 (ok=${setResp.ok()})`);
} catch {
log(' ⚠ 设置拒绝连接时连接已断开(正常现象)');
} }
log(` 等待 ${PHASE_DURATIONS.refuse / 1000} 秒...`); log(` 等待 ${PHASE_DURATIONS.refuse / 1000} 秒...`);
@ -388,17 +407,18 @@ test.describe('15分钟自动化采集测试', () => {
phaseTimestamps['refuse_end'] = new Date(); phaseTimestamps['refuse_end'] = new Date();
// 保存快照(此时可能无法访问,用之前的数据) // 拒绝连接期间无法访问模拟端口
log(' (拒绝连接期间无法获取模拟器快照,使用设置前的快照)');
phaseSnapshots['refuse'] = phaseSnapshots['refuse_before'];
// 恢复网络此时HttpListener已停止需要等它重启
try { try {
const snapshot = await gatewayApi(`/admin/api/full-summary`); await simApi('/admin/api/network', { type: 'normal' });
if (snapshot.ok()) phaseSnapshots['refuse'] = await snapshot.json();
} catch { } catch {
log(' (拒绝连接期间无法获取模拟器快照,跳过)'); log(' (恢复请求发送到模拟端口失败因为HttpListener已停止)');
} }
// 等待HttpListener恢复
// 恢复网络 await sleep(3000);
await gatewayApi(`/admin/api/network`, { type: 'normal' });
await sleep(3000); // 等待HttpListener重启
log(' ✓ 已恢复正常网络'); log(' ✓ 已恢复正常网络');
log(` ✓ 阶段5完成\n`); log(` ✓ 阶段5完成\n`);
}); });
@ -408,38 +428,69 @@ test.describe('15分钟自动化采集测试', () => {
// ============================================================ // ============================================================
test('阶段6: 恢复正常采集 — 2分钟', async () => { test('阶段6: 恢复正常采集 — 2分钟', async () => {
test.setTimeout(PHASE_DURATIONS.normal2 + 60000); test.setTimeout(PHASE_DURATIONS.normal2 + 120000);
log('━━━ 阶段6: 恢复正常采集2分钟 ━━━'); log('━━━ 阶段6: 恢复正常采集2分钟 ━━━');
phaseTimestamps['normal2_start'] = new Date();
// 确认网络已恢复 // 拒绝连接后HttpListener已停止通过网关重启模拟地址
await gatewayApi(`/admin/api/network`, { type: 'normal' }); // 关键指定使用原来的端口避免URL不同步
log(' 通过网关重启模拟地址(保持原端口)...');
await gatewayApi('/admin/api/stop-address', { dbAddressId: 1 });
await sleep(2000);
const restartResp = await gatewayApi('/admin/api/start-address', {
dbAddressId: 1, interval: 10, port: simPort
});
if (restartResp.ok()) {
const result = await restartResp.json() as any;
const newPort = result.port || simPort;
if (newPort !== simPort) {
log(` ⚠ 端口变化: ${simPort}${newPort},需更新采集地址`);
simPort = newPort;
await executeBusiness(
'UPDATE cnc_collect_address SET url = ? WHERE id = 1',
[`http://localhost:${simPort}/`]
);
// 刷新采集服务配置以加载新URL
await collectorApi('POST', '/api/collector/refresh');
await sleep(2000); await sleep(2000);
}
log(` ✓ 模拟地址已重启 → 端口 ${simPort}`);
} else {
log(' ⚠ 模拟地址重启失败');
}
await sleep(3000);
// 等待新数据产生 // 确认模拟地址正常运行
const dataCheck = await gatewayApi(`/admin/api/status`); try {
const dataCheck = await simApi('/admin/api/status');
if (dataCheck.ok()) { if (dataCheck.ok()) {
const status = await dataCheck.json() as any; const status = await dataCheck.json() as any;
log(` 当前状态: 在线${status.onlineDevices}台, 运行${status.isRunning}`); log(` ✓ 模拟地址恢复: 在线${status.onlineDevices}台, 运行=${status.isRunning}`);
}
} catch (e) {
log(` ⚠ 模拟地址检查失败: ${e},继续测试`);
} }
phaseTimestamps['normal2_start'] = new Date();
log(` 等待 ${PHASE_DURATIONS.normal2 / 1000} 秒...`); log(` 等待 ${PHASE_DURATIONS.normal2 / 1000} 秒...`);
await sleep(PHASE_DURATIONS.normal2); await sleep(PHASE_DURATIONS.normal2);
phaseTimestamps['normal2_end'] = new Date(); phaseTimestamps['normal2_end'] = new Date();
// 保存最终快照 // 保存最终快照
const snapshot = await gatewayApi(`/admin/api/full-summary`); try {
const snapshot = await simApi('/admin/api/full-summary');
if (snapshot.ok()) phaseSnapshots['normal2'] = await snapshot.json(); if (snapshot.ok()) phaseSnapshots['normal2'] = await snapshot.json();
} catch { /* 忽略 */ }
// 停止采集服务 // 停止采集服务触发service_stop结账
await collectorApi('POST', '/api/collector/stop'); await collectorApi('POST', '/api/collector/stop');
log(' ✓ 采集服务已停止触发service_stop结账'); log(' ✓ 采集服务已停止触发service_stop结账');
await sleep(3000); await sleep(3000);
// 停止模拟器 // 停止模拟器
await gatewayApi(`/admin/api/stop-address`, { dbAddressId: 1 }); await gatewayApi('/admin/api/stop-address', { dbAddressId: 1 });
log(' ✓ 模拟已停止'); log(' ✓ 模拟地址已停止');
log(` ✓ 阶段6完成\n`); log(` ✓ 阶段6完成\n`);
}); });
@ -449,13 +500,13 @@ test.describe('15分钟自动化采集测试', () => {
// ============================================================ // ============================================================
test('验证: 数据完整性对比', async () => { test('验证: 数据完整性对比', async () => {
test.setTimeout(60000); test.setTimeout(120000);
log('━━━ 数据完整性对比验证 ━━━'); log('━━━ 数据完整性对比验证 ━━━');
// 1. 获取模拟器最终汇总 // 1. 获取模拟器最终汇总
let simSummary: any = phaseSnapshots['normal2'] || {}; let simSummary: any = phaseSnapshots['normal2'] || {};
try { try {
const resp = await gatewayApi(`/admin/api/full-summary`); const resp = await simApi('/admin/api/full-summary');
if (resp.ok()) simSummary = await resp.json(); if (resp.ok()) simSummary = await resp.json();
} catch { /* 模拟器可能已停止 */ } } catch { /* 模拟器可能已停止 */ }
@ -499,7 +550,6 @@ test.describe('15分钟自动化采集测试', () => {
log(` 产量分段数: ${dbSegments[0].cnt} (已结账=${dbSettled[0].cnt}, 未结账=${dbActive[0].cnt})`); log(` 产量分段数: ${dbSegments[0].cnt} (已结账=${dbSettled[0].cnt}, 未结账=${dbActive[0].cnt})`);
log(` 原始日志数: ${dbRawLogs[0].cnt} (成功=${dbSuccessLogs[0].cnt}, 失败=${dbFailLogs[0].cnt})`); log(` 原始日志数: ${dbRawLogs[0].cnt} (成功=${dbSuccessLogs[0].cnt}, 失败=${dbFailLogs[0].cnt})`);
// 验证数据存在
expect(dbRecords[0].cnt).toBeGreaterThan(0); expect(dbRecords[0].cnt).toBeGreaterThan(0);
expect(dbRawLogs[0].cnt).toBeGreaterThan(0); expect(dbRawLogs[0].cnt).toBeGreaterThan(0);
log(' ✓ 采集记录和原始日志均已产生'); log(' ✓ 采集记录和原始日志均已产生');
@ -515,7 +565,6 @@ test.describe('15分钟自动化采集测试', () => {
const deviceCode = simDev.deviceCode; const deviceCode = simDev.deviceCode;
const simTotal = simDev.totalParts || 0; const simTotal = simDev.totalParts || 0;
// 查数据库中对应设备的分段总产量
const machine = await queryBusiness( const machine = await queryBusiness(
'SELECT id FROM cnc_machine WHERE device_code = ?', 'SELECT id FROM cnc_machine WHERE device_code = ?',
[deviceCode] [deviceCode]
@ -536,7 +585,7 @@ test.describe('15分钟自动化采集测试', () => {
} }
const diff = Math.abs(simTotal - dbTotal); const diff = Math.abs(simTotal - dbTotal);
const tolerance = Math.max(simTotal * 0.1, 5); // 允许10%或5个的误差 const tolerance = Math.max(simTotal * 0.15, 5); // 允许15%或5个的误差考虑异常期间
if (diff <= tolerance) { if (diff <= tolerance) {
matchCount++; matchCount++;
@ -557,6 +606,7 @@ test.describe('15分钟自动化采集测试', () => {
} else { } else {
log(` ⚠ 有 ${mismatchCount} 台设备零件数不匹配,详见上方`); log(` ⚠ 有 ${mismatchCount} 台设备零件数不匹配,详见上方`);
} }
globalMismatchCount = mismatchCount;
// 4. 验证分段结账正确性 // 4. 验证分段结账正确性
log('\n [4/5] 分段结账验证:'); log('\n [4/5] 分段结账验证:');
@ -569,7 +619,6 @@ test.describe('15分钟自动化采集测试', () => {
log(` ${row.close_reason || 'NULL'}: ${row.cnt}`); log(` ${row.close_reason || 'NULL'}: ${row.cnt}`);
} }
// 验证所有分段已结账service_stop触发
const unsettled = await queryBusiness( const unsettled = await queryBusiness(
'SELECT machine_id, program_name FROM cnc_production_segment WHERE is_settled = 0 AND created_at > ?', 'SELECT machine_id, program_name FROM cnc_production_segment WHERE is_settled = 0 AND created_at > ?',
[testStartTime] [testStartTime]
@ -583,25 +632,22 @@ test.describe('15分钟自动化采集测试', () => {
// 5. 验证异常期间的数据 // 5. 验证异常期间的数据
log('\n [5/5] 异常期间数据验证:'); log('\n [5/5] 异常期间数据验证:');
// HTTP 500期间应该有失败日志
const http500Logs = await queryLog( const http500Logs = await queryLog(
`SELECT COUNT(*) as cnt FROM log_collect_raw `SELECT COUNT(*) as cnt FROM log_collect_raw
WHERE is_success = 0 AND error_message LIKE '%500%' WHERE is_success = 0
AND request_time BETWEEN ? AND ?`, AND request_time BETWEEN ? AND ?`,
[phaseTimestamps['http500_start'], phaseTimestamps['http500_end']] [phaseTimestamps['http500_start'], phaseTimestamps['http500_end']]
); );
log(` HTTP 500期间失败日志: ${http500Logs[0]?.cnt || 0}`); log(` HTTP 500期间失败日志: ${http500Logs[0]?.cnt || 0}`);
// 拒绝连接期间应该有失败日志
const refuseLogs = await queryLog( const refuseLogs = await queryLog(
`SELECT COUNT(*) as cnt FROM log_collect_raw `SELECT COUNT(*) as cnt FROM log_collect_raw
WHERE is_success = 0 AND error_message LIKE '%连接%' WHERE is_success = 0
AND request_time BETWEEN ? AND ?`, AND request_time BETWEEN ? AND ?`,
[phaseTimestamps['refuse_start'], phaseTimestamps['refuse_end']] [phaseTimestamps['refuse_start'], phaseTimestamps['refuse_end']]
); );
log(` 拒绝连接期间失败日志: ${refuseLogs[0]?.cnt || 0}`); log(` 拒绝连接期间失败日志: ${refuseLogs[0]?.cnt || 0}`);
// 恢复后应有新数据
const recoveryRecords = await queryBusiness( const recoveryRecords = await queryBusiness(
`SELECT COUNT(*) as cnt FROM cnc_collect_record `SELECT COUNT(*) as cnt FROM cnc_collect_record
WHERE collect_time > ?`, WHERE collect_time > ?`,
@ -621,7 +667,6 @@ test.describe('15分钟自动化采集测试', () => {
test('报告: 生成15分钟采集测试报告', async () => { test('报告: 生成15分钟采集测试报告', async () => {
test.setTimeout(30000); test.setTimeout(30000);
// 确保报告目录存在
if (!fs.existsSync(REPORT_DIR)) { if (!fs.existsSync(REPORT_DIR)) {
fs.mkdirSync(REPORT_DIR, { recursive: true }); fs.mkdirSync(REPORT_DIR, { recursive: true });
} }
@ -632,15 +677,21 @@ test.describe('15分钟自动化采集测试', () => {
// 收集最终数据 // 收集最终数据
let simSummary: any = phaseSnapshots['normal2'] || {}; let simSummary: any = phaseSnapshots['normal2'] || {};
try { try {
const resp = await gatewayApi(`/admin/api/full-summary`); const resp = await simApi('/admin/api/full-summary');
if (resp.ok()) simSummary = await resp.json(); if (resp.ok()) simSummary = await resp.json();
} catch { /* 模拟器可能已停止 */ } } catch { /* 模拟器可能已停止 */ }
const eventHistory = await gatewayApi(`/admin/api/event-history`); let eventHistoryData: any[] = [];
const eventHistoryData = eventHistory.ok() ? await eventHistory.json() : []; try {
const eventHistory = await simApi('/admin/api/event-history');
if (eventHistory.ok()) eventHistoryData = await eventHistory.json();
} catch {}
const errorLog = await gatewayApi(`/admin/api/error-log`); let errorLogData: any[] = [];
const errorLogData = errorLog.ok() ? await errorLog.json() : []; try {
const errorLog = await simApi('/admin/api/error-log');
if (errorLog.ok()) errorLogData = await errorLog.json();
} catch {}
const dbRecords = await queryBusiness( const dbRecords = await queryBusiness(
'SELECT COUNT(*) as cnt FROM cnc_collect_record WHERE collect_time > ?', 'SELECT COUNT(*) as cnt FROM cnc_collect_record WHERE collect_time > ?',
@ -674,7 +725,7 @@ test.describe('15分钟自动化采集测试', () => {
let deviceStats = ''; let deviceStats = '';
for (const m of machines) { for (const m of machines) {
const segs = await queryBusiness( const segs = await queryBusiness(
`SELECT program_name, SUM(quantity) as total_qty, COUNT(*) as seg_count, close_reason `SELECT program_name, SUM(quantity) as total_qty, COUNT(*) as seg_count
FROM cnc_production_segment WHERE machine_id = ? AND created_at > ? FROM cnc_production_segment WHERE machine_id = ? AND created_at > ?
GROUP BY program_name`, GROUP BY program_name`,
[m.id, testStartTime] [m.id, testStartTime]
@ -708,7 +759,7 @@ test.describe('15分钟自动化采集测试', () => {
// 事件历史摘要 // 事件历史摘要
let eventSummary = ''; let eventSummary = '';
if (Array.isArray(eventHistoryData)) { if (Array.isArray(eventHistoryData) && eventHistoryData.length > 0) {
const eventTypeCounts: Record<string, number> = {}; const eventTypeCounts: Record<string, number> = {};
for (const evt of eventHistoryData) { for (const evt of eventHistoryData) {
const type = evt.eventType || 'unknown'; const type = evt.eventType || 'unknown';
@ -722,7 +773,7 @@ test.describe('15分钟自动化采集测试', () => {
// 异常日志摘要 // 异常日志摘要
let errorSummary = ''; let errorSummary = '';
if (Array.isArray(errorLogData) && errorLogData.length > 0) { if (Array.isArray(errorLogData) && errorLogData.length > 0) {
for (const err of errorLogData.slice(0, 20)) { for (const err of errorLogData.slice(0, 30)) {
errorSummary += `| ${err.timestamp} | ${err.errorType} | ${err.description} | ${err.affectedDevices} |\n`; errorSummary += `| ${err.timestamp} | ${err.errorType} | ${err.description} | ${err.affectedDevices} |\n`;
} }
} }
@ -740,10 +791,11 @@ test.describe('15分钟自动化采集测试', () => {
| | | | | |
|------|------| |------|------|
| | ${simulationPort} | | | ${SIM_GATEWAY} |
| | ${simPort} |
| | 10 | | | 10 |
| | ${machines.length} | | | ${machines.length} |
| | http://localhost:${simulationPort}/ (id=1) | | | http://localhost:${simPort}/ (id=1) |
## ##
@ -796,7 +848,7 @@ ${errorSummary || '| (无异常记录) | - | - | - |'}
## ##
${mismatchCount === 0 ? '✅ 所有设备零件数量完全匹配,数据采集准确性验证通过。' : `⚠️ 有 ${mismatchCount} 台设备零件数存在差异,需进一步排查。`} ${globalMismatchCount === 0 ? '✅ 所有设备零件数量完全匹配,数据采集准确性验证通过。' : `⚠️ 有 ${globalMismatchCount} 台设备零件数存在差异,需进一步排查。`}
- : ${dbRecords[0].cnt} - : ${dbRecords[0].cnt}
- : ${dbSegments[0].cnt} ( ${dbSettled[0].cnt}) - : ${dbSegments[0].cnt} ( ${dbSettled[0].cnt})

@ -27,10 +27,11 @@ namespace CncRepository.Impl.Dashboard
var todayProduction = conn.ExecuteScalar<int>(@"SELECT COALESCE(SUM(total_quantity),0) FROM cnc_daily_production WHERE production_date = CURDATE()"); var todayProduction = conn.ExecuteScalar<int>(@"SELECT COALESCE(SUM(total_quantity),0) FROM cnc_daily_production WHERE production_date = CURDATE()");
var activeAlerts = conn.ExecuteScalar<int>(@"SELECT COUNT(1) FROM cnc_alert WHERE is_resolved = 0"); var activeAlerts = conn.ExecuteScalar<int>(@"SELECT COUNT(1) FROM cnc_alert WHERE is_resolved = 0");
// 采集成功率,简单实现:统计 cnc_collect_address 表的记录中的成功率若无数据则返回0 // 采集成功率:基于原始采集日志的成功/失败比率
var totalAddresses = conn.ExecuteScalar<int>(@"SELECT COUNT(1) FROM cnc_collect_address"); var successCount = conn.ExecuteScalar<int>(@"SELECT COUNT(1) FROM cnc_log.log_collect_raw WHERE is_success = 1 AND request_time >= CURDATE()");
var failCount = conn.ExecuteScalar<int>(@"SELECT COALESCE(SUM(fail_count),0) FROM cnc_collect_address"); var failCount = conn.ExecuteScalar<int>(@"SELECT COUNT(1) FROM cnc_log.log_collect_raw WHERE is_success = 0 AND request_time >= CURDATE()");
decimal collectSuccessRate = totalAddresses > 0 ? (decimal)(totalAddresses - failCount) / totalAddresses * 100 : 0m; var totalCount = successCount + failCount;
decimal collectSuccessRate = totalCount > 0 ? (decimal)successCount / totalCount * 100 : 0m;
var todayCuttingTime = conn.ExecuteScalar<int>(@"SELECT COALESCE(SUM(total_cutting_time),0) FROM cnc_daily_production WHERE production_date = CURDATE()"); var todayCuttingTime = conn.ExecuteScalar<int>(@"SELECT COALESCE(SUM(total_cutting_time),0) FROM cnc_daily_production WHERE production_date = CURDATE()");
var runningMachines = conn.ExecuteScalar<int>(@"SELECT COUNT(1) FROM cnc_machine WHERE last_device_status = 'running'"); var runningMachines = conn.ExecuteScalar<int>(@"SELECT COUNT(1) FROM cnc_machine WHERE last_device_status = 'running'");

@ -30,6 +30,8 @@
</system.web> </system.web>
<system.webServer> <system.webServer>
<validation validateIntegratedModeConfiguration="false" />
<modules runAllManagedModulesForAllRequests="true" />
<handlers> <handlers>
<remove name="ExtensionlessUrlHandler-Integrated-4.0" /> <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
<remove name="OPTIONSVerbHandler" /> <remove name="OPTIONSVerbHandler" />

Loading…
Cancel
Save