diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 73b3d9d..e3fa2a5 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -21,6 +21,13 @@ export default defineConfig(({ command }) => ({ }, server: { port: 5173, + // 将/api请求代理到IIS后端(真实数据) + proxy: { + '/api': { + target: 'http://127.0.0.1', + changeOrigin: true, + }, + }, }, // 构建优化配置 build: { diff --git a/src/CncCollector/scripts/collector-15min.spec.ts b/src/CncCollector/scripts/collector-15min.spec.ts index 9e2a475..bb0aa14 100644 --- a/src/CncCollector/scripts/collector-15min.spec.ts +++ b/src/CncCollector/scripts/collector-15min.spec.ts @@ -14,16 +14,19 @@ * * 前置条件: * 1. MariaDB 运行中(cnc_business + cnc_log 库已初始化) - * 2. CncSimulator 未启动(脚本自动启动) - * 3. CncCollector 未启动(脚本自动启动) + * 2. CncSimulator 运行在网关端口 9000 + * 3. CncCollector 运行中,管理API在 http://localhost:5800/ * * 安装: npm install @playwright/test mysql2 * 运行: 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 mysql from 'mysql2/promise'; -import { execSync, spawn, ChildProcess } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; @@ -32,7 +35,7 @@ import * as path from 'path'; // ============================================================ 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 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)); -/** 辅助:调用模拟器管理API */ -async function simApi(port: number, path: string, body?: any) { - const ctx = await request.newContext(); +/** 辅助:调用模拟器**单地址**管理API(stats/network/full-summary等) */ +async function simApi(path: string, body?: any) { + const ctx = await request.newContext({ timeout: 10000 }); 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 */ +/** 辅助:调用模拟器**网关**API(start-address/stop-address/status等) */ async function gatewayApi(path: string, body?: any) { - const ctx = await request.newContext(); + const ctx = await request.newContext({ timeout: 10000 }); if (body) { 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 phaseTimestamps: Record = {}; - -/** 每阶段结束时采集模拟器快照 */ let phaseSnapshots: Record = {}; +let globalMismatchCount = 0; // ============================================================ // 全局串行配置 @@ -152,40 +155,40 @@ let phaseSnapshots: Record = {}; test.describe.configure({ mode: 'serial' }); // ============================================================ -// 阶段0: 初始化 +// 测试主流程 // ============================================================ test.describe('15分钟自动化采集测试', () => { test('阶段0: 初始化 — 清空数据并启动服务', async () => { - test.setTimeout(120000); // 2分钟超时 + test.setTimeout(120000); testStartTime = new Date(); log('\n===== 15分钟自动化采集测试开始 ====='); log(`测试开始时间: ${testStartTime.toLocaleString('zh-CN')}`); // 1. 清空采集数据 - log('[0/6] 清空旧采集数据...'); + log('[0/7] 清空旧采集数据...'); await executeBusiness('TRUNCATE TABLE cnc_collect_record'); await executeBusiness('TRUNCATE TABLE cnc_production_segment'); await executeLog('TRUNCATE TABLE log_collect_raw'); log(' ✓ 已清空 cnc_collect_record, cnc_production_segment, log_collect_raw'); // 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'); 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. 确认机床数据 - log('[2/6] 确认机床数据...'); + log('[2/7] 确认机床数据...'); 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' ); expect(machines.length).toBeGreaterThanOrEqual(10); - log(` ✓ 机床数量: ${machines.length}台(采集地址1下的启用机床)`); + log(` ✓ 机床数量: ${machines.length}台`); - // 4. 通过模拟器网关启动地址模拟 - log('[3/6] 启动模拟器...'); + // 4. 通过网关启动地址模拟 + log('[3/7] 通过网关启动模拟...'); const startResp = await gatewayApi('/admin/api/start-address', { dbAddressId: 1, interval: 10, @@ -193,26 +196,27 @@ test.describe('15分钟自动化采集测试', () => { }); expect(startResp.ok()).toBeTruthy(); const startResult = await startResp.json() as any; - simulationPort = startResult.port || 9001; - log(` ✓ 模拟器已启动 → 端口 ${simulationPort}`); + simPort = startResult.port; + expect(simPort).toBeGreaterThan(0); + log(` ✓ 模拟地址已启动 → 端口 ${simPort}`); - // 等待模拟器数据就绪 + // 5. 等待模拟器数据就绪,确认单地址API可用 await sleep(3000); - const dataResp = await gatewayApi(`/admin/api/status`); + const dataResp = await simApi('/admin/api/status'); if (dataResp.ok()) { 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( '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. 启动采集服务 - log('[4/6] 启动采集服务...'); + // 7. 启动采集服务 + log('[4/7] 启动采集服务...'); await collectorApi('POST', '/api/collector/stop'); await sleep(1000); await collectorApi('POST', '/api/collector/refresh'); @@ -220,11 +224,10 @@ test.describe('15分钟自动化采集测试', () => { await collectorApi('POST', '/api/collector/start'); log(' ✓ 采集服务已启动'); - // 7. 等待2个采集周期确认数据流 - log('[5/6] 等待数据采集确认...'); + // 8. 等待2个采集周期确认数据流 + log('[5/7] 等待数据采集确认...'); await sleep(25000); - // 验证已有数据产生 const records = await queryBusiness( 'SELECT COUNT(*) as cnt FROM cnc_collect_record WHERE collect_time > ?', [testStartTime] @@ -236,12 +239,39 @@ test.describe('15分钟自动化采集测试', () => { log(` ✓ 采集记录数: ${records[0].cnt}, 原始日志数: ${rawLogs[0].cnt}`); 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(); - const snapshot0 = await gatewayApi(`/admin/api/full-summary`); + const snapshot0 = await simApi('/admin/api/full-summary'); 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分钟) ━━━'); phaseTimestamps['normal1_start'] = new Date(); - // 记录开始时的模拟器状态 - const statsBefore = await gatewayApi(`/admin/api/stats`); + const statsBefore = await simApi('/admin/api/stats'); const statsBeforeData = statsBefore.ok() ? await statsBefore.json() : {}; log(` 开始: 总零件=${statsBeforeData.totalParts || '?'}, 在线设备=${statsBeforeData.onlineDevices || '?'}`); log(` 等待 ${PHASE_DURATIONS.normal1 / 1000} 秒...`); - // 等待5分钟 await sleep(PHASE_DURATIONS.normal1); - // 记录结束时的状态 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 snapshot = await gatewayApi(`/admin/api/full-summary`); + const snapshot = await simApi('/admin/api/full-summary'); if (snapshot.ok()) phaseSnapshots['normal1'] = await snapshot.json(); log(` 结束: 总零件=${statsAfterData.totalParts || '?'}, 在线设备=${statsAfterData.onlineDevices || '?'}`); @@ -285,23 +311,18 @@ test.describe('15分钟自动化采集测试', () => { log('━━━ 阶段2: HTTP 500异常(2分钟) ━━━'); phaseTimestamps['http500_start'] = new Date(); - // 设置网络异常: HTTP 500 - const setResp = await gatewayApi(`/admin/api/network`, { type: 'http500' }); - if (setResp.ok()) { - log(' ✓ 已设置网络异常: HTTP 500'); - } + const setResp = await simApi('/admin/api/network', { type: 'http500' }); + log(` ✓ 已设置网络异常: HTTP 500 (ok=${setResp.ok()})`); log(` 等待 ${PHASE_DURATIONS.http500 / 1000} 秒...`); await sleep(PHASE_DURATIONS.http500); phaseTimestamps['http500_end'] = new Date(); - // 保存快照 - const snapshot = await gatewayApi(`/admin/api/full-summary`); + const snapshot = await simApi('/admin/api/full-summary'); if (snapshot.ok()) phaseSnapshots['http500'] = await snapshot.json(); - // 恢复网络 - await gatewayApi(`/admin/api/network`, { type: 'normal' }); + await simApi('/admin/api/network', { type: 'normal' }); log(' ✓ 已恢复正常网络'); log(` ✓ 阶段2完成\n`); }); @@ -315,24 +336,19 @@ test.describe('15分钟自动化采集测试', () => { log('━━━ 阶段3: 超时异常(2分钟) ━━━'); phaseTimestamps['timeout_start'] = new Date(); - // 设置网络异常: 超时 - const setResp = await gatewayApi(`/admin/api/network`, { type: 'timeout' }); - if (setResp.ok()) { - log(' ✓ 已设置网络异常: 超时(60秒延迟)'); - } + const setResp = await simApi('/admin/api/network', { type: 'timeout' }); + log(` ✓ 已设置网络异常: 超时(60秒延迟) (ok=${setResp.ok()})`); log(` 等待 ${PHASE_DURATIONS.timeout / 1000} 秒...`); await sleep(PHASE_DURATIONS.timeout); phaseTimestamps['timeout_end'] = new Date(); - // 保存快照 - const snapshot = await gatewayApi(`/admin/api/full-summary`); + const snapshot = await simApi('/admin/api/full-summary'); if (snapshot.ok()) phaseSnapshots['timeout'] = await snapshot.json(); - // 恢复网络 - await gatewayApi(`/admin/api/network`, { type: 'normal' }); - await sleep(2000); // 等待恢复 + await simApi('/admin/api/network', { type: 'normal' }); + await sleep(2000); log(' ✓ 已恢复正常网络'); log(` ✓ 阶段3完成\n`); }); @@ -346,23 +362,18 @@ test.describe('15分钟自动化采集测试', () => { log('━━━ 阶段4: 空数据返回(2分钟) ━━━'); phaseTimestamps['empty_start'] = new Date(); - // 设置网络异常: 空数据 - const setResp = await gatewayApi(`/admin/api/network`, { type: 'empty' }); - if (setResp.ok()) { - log(' ✓ 已设置网络异常: 空数据([])'); - } + const setResp = await simApi('/admin/api/network', { type: 'empty' }); + log(` ✓ 已设置网络异常: 空数据([]) (ok=${setResp.ok()})`); log(` 等待 ${PHASE_DURATIONS.empty / 1000} 秒...`); await sleep(PHASE_DURATIONS.empty); phaseTimestamps['empty_end'] = new Date(); - // 保存快照 - const snapshot = await gatewayApi(`/admin/api/full-summary`); + const snapshot = await simApi('/admin/api/full-summary'); if (snapshot.ok()) phaseSnapshots['empty'] = await snapshot.json(); - // 恢复网络 - await gatewayApi(`/admin/api/network`, { type: 'normal' }); + await simApi('/admin/api/network', { type: 'normal' }); await sleep(2000); log(' ✓ 已恢复正常网络'); log(` ✓ 阶段4完成\n`); @@ -377,10 +388,18 @@ test.describe('15分钟自动化采集测试', () => { log('━━━ 阶段5: 拒绝连接(2分钟) ━━━'); phaseTimestamps['refuse_start'] = new Date(); - // 设置网络异常: 拒绝连接 - const setResp = await gatewayApi(`/admin/api/network`, { type: 'refuse' }); - if (setResp.ok()) { - log(' ✓ 已设置网络异常: 拒绝连接'); + // 拒绝连接会停止HttpListener,但管理API仍需通过同一端口操作 + // 这里用网关API直接发送(但网关没有network命令,必须发到模拟端口) + // 拒绝连接意味着模拟端口的HttpListener被停止,无法再发请求 + // 解决方案:先记录快照,再设置拒绝 + 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} 秒...`); @@ -388,17 +407,18 @@ test.describe('15分钟自动化采集测试', () => { phaseTimestamps['refuse_end'] = new Date(); - // 保存快照(此时可能无法访问,用之前的数据) + // 拒绝连接期间无法访问模拟端口 + log(' (拒绝连接期间无法获取模拟器快照,使用设置前的快照)'); + phaseSnapshots['refuse'] = phaseSnapshots['refuse_before']; + + // 恢复网络(此时HttpListener已停止,需要等它重启) try { - const snapshot = await gatewayApi(`/admin/api/full-summary`); - if (snapshot.ok()) phaseSnapshots['refuse'] = await snapshot.json(); + await simApi('/admin/api/network', { type: 'normal' }); } catch { - log(' (拒绝连接期间无法获取模拟器快照,跳过)'); + log(' (恢复请求发送到模拟端口失败,因为HttpListener已停止)'); } - - // 恢复网络 - await gatewayApi(`/admin/api/network`, { type: 'normal' }); - await sleep(3000); // 等待HttpListener重启 + // 等待HttpListener恢复 + await sleep(3000); log(' ✓ 已恢复正常网络'); log(` ✓ 阶段5完成\n`); }); @@ -408,38 +428,69 @@ test.describe('15分钟自动化采集测试', () => { // ============================================================ test('阶段6: 恢复正常采集 — 2分钟', async () => { - test.setTimeout(PHASE_DURATIONS.normal2 + 60000); + test.setTimeout(PHASE_DURATIONS.normal2 + 120000); log('━━━ 阶段6: 恢复正常采集(2分钟) ━━━'); - phaseTimestamps['normal2_start'] = new Date(); - // 确认网络已恢复 - await gatewayApi(`/admin/api/network`, { type: 'normal' }); + // 拒绝连接后HttpListener已停止,通过网关重启模拟地址 + // 关键:指定使用原来的端口,避免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); + } + log(` ✓ 模拟地址已重启 → 端口 ${simPort}`); + } else { + log(' ⚠ 模拟地址重启失败'); + } + await sleep(3000); - // 等待新数据产生 - const dataCheck = await gatewayApi(`/admin/api/status`); - if (dataCheck.ok()) { - const status = await dataCheck.json() as any; - log(` 当前状态: 在线${status.onlineDevices}台, 运行${status.isRunning}`); + // 确认模拟地址正常运行 + try { + const dataCheck = await simApi('/admin/api/status'); + if (dataCheck.ok()) { + const status = await dataCheck.json() as any; + log(` ✓ 模拟地址恢复: 在线${status.onlineDevices}台, 运行=${status.isRunning}`); + } + } catch (e) { + log(` ⚠ 模拟地址检查失败: ${e},继续测试`); } + phaseTimestamps['normal2_start'] = new Date(); + log(` 等待 ${PHASE_DURATIONS.normal2 / 1000} 秒...`); await sleep(PHASE_DURATIONS.normal2); phaseTimestamps['normal2_end'] = new Date(); // 保存最终快照 - const snapshot = await gatewayApi(`/admin/api/full-summary`); - if (snapshot.ok()) phaseSnapshots['normal2'] = await snapshot.json(); + try { + const snapshot = await simApi('/admin/api/full-summary'); + if (snapshot.ok()) phaseSnapshots['normal2'] = await snapshot.json(); + } catch { /* 忽略 */ } - // 停止采集服务 + // 停止采集服务(触发service_stop结账) await collectorApi('POST', '/api/collector/stop'); log(' ✓ 采集服务已停止(触发service_stop结账)'); await sleep(3000); // 停止模拟器 - await gatewayApi(`/admin/api/stop-address`, { dbAddressId: 1 }); - log(' ✓ 模拟器已停止'); + await gatewayApi('/admin/api/stop-address', { dbAddressId: 1 }); + log(' ✓ 模拟地址已停止'); log(` ✓ 阶段6完成\n`); }); @@ -449,13 +500,13 @@ test.describe('15分钟自动化采集测试', () => { // ============================================================ test('验证: 数据完整性对比', async () => { - test.setTimeout(60000); + test.setTimeout(120000); log('━━━ 数据完整性对比验证 ━━━'); // 1. 获取模拟器最终汇总 let simSummary: any = phaseSnapshots['normal2'] || {}; try { - const resp = await gatewayApi(`/admin/api/full-summary`); + const resp = await simApi('/admin/api/full-summary'); if (resp.ok()) simSummary = await resp.json(); } catch { /* 模拟器可能已停止 */ } @@ -499,7 +550,6 @@ test.describe('15分钟自动化采集测试', () => { log(` 产量分段数: ${dbSegments[0].cnt} (已结账=${dbSettled[0].cnt}, 未结账=${dbActive[0].cnt})`); log(` 原始日志数: ${dbRawLogs[0].cnt} (成功=${dbSuccessLogs[0].cnt}, 失败=${dbFailLogs[0].cnt})`); - // 验证数据存在 expect(dbRecords[0].cnt).toBeGreaterThan(0); expect(dbRawLogs[0].cnt).toBeGreaterThan(0); log(' ✓ 采集记录和原始日志均已产生'); @@ -515,7 +565,6 @@ test.describe('15分钟自动化采集测试', () => { const deviceCode = simDev.deviceCode; const simTotal = simDev.totalParts || 0; - // 查数据库中对应设备的分段总产量 const machine = await queryBusiness( 'SELECT id FROM cnc_machine WHERE device_code = ?', [deviceCode] @@ -536,7 +585,7 @@ test.describe('15分钟自动化采集测试', () => { } 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) { matchCount++; @@ -557,6 +606,7 @@ test.describe('15分钟自动化采集测试', () => { } else { log(` ⚠ 有 ${mismatchCount} 台设备零件数不匹配,详见上方`); } + globalMismatchCount = mismatchCount; // 4. 验证分段结账正确性 log('\n [4/5] 分段结账验证:'); @@ -569,7 +619,6 @@ test.describe('15分钟自动化采集测试', () => { log(` ${row.close_reason || 'NULL'}: ${row.cnt}条`); } - // 验证所有分段已结账(service_stop触发) const unsettled = await queryBusiness( 'SELECT machine_id, program_name FROM cnc_production_segment WHERE is_settled = 0 AND created_at > ?', [testStartTime] @@ -583,25 +632,22 @@ test.describe('15分钟自动化采集测试', () => { // 5. 验证异常期间的数据 log('\n [5/5] 异常期间数据验证:'); - // HTTP 500期间应该有失败日志 const http500Logs = await queryLog( `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 ?`, [phaseTimestamps['http500_start'], phaseTimestamps['http500_end']] ); log(` HTTP 500期间失败日志: ${http500Logs[0]?.cnt || 0}条`); - // 拒绝连接期间应该有失败日志 const refuseLogs = await queryLog( `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 ?`, [phaseTimestamps['refuse_start'], phaseTimestamps['refuse_end']] ); log(` 拒绝连接期间失败日志: ${refuseLogs[0]?.cnt || 0}条`); - // 恢复后应有新数据 const recoveryRecords = await queryBusiness( `SELECT COUNT(*) as cnt FROM cnc_collect_record WHERE collect_time > ?`, @@ -621,7 +667,6 @@ test.describe('15分钟自动化采集测试', () => { test('报告: 生成15分钟采集测试报告', async () => { test.setTimeout(30000); - // 确保报告目录存在 if (!fs.existsSync(REPORT_DIR)) { fs.mkdirSync(REPORT_DIR, { recursive: true }); } @@ -632,15 +677,21 @@ test.describe('15分钟自动化采集测试', () => { // 收集最终数据 let simSummary: any = phaseSnapshots['normal2'] || {}; try { - const resp = await gatewayApi(`/admin/api/full-summary`); + const resp = await simApi('/admin/api/full-summary'); if (resp.ok()) simSummary = await resp.json(); } catch { /* 模拟器可能已停止 */ } - const eventHistory = await gatewayApi(`/admin/api/event-history`); - const eventHistoryData = eventHistory.ok() ? await eventHistory.json() : []; + let eventHistoryData: any[] = []; + 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`); - const errorLogData = errorLog.ok() ? await errorLog.json() : []; + let errorLogData: any[] = []; + try { + const errorLog = await simApi('/admin/api/error-log'); + if (errorLog.ok()) errorLogData = await errorLog.json(); + } catch {} const dbRecords = await queryBusiness( 'SELECT COUNT(*) as cnt FROM cnc_collect_record WHERE collect_time > ?', @@ -674,7 +725,7 @@ test.describe('15分钟自动化采集测试', () => { let deviceStats = ''; for (const m of machines) { 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 > ? GROUP BY program_name`, [m.id, testStartTime] @@ -708,7 +759,7 @@ test.describe('15分钟自动化采集测试', () => { // 事件历史摘要 let eventSummary = ''; - if (Array.isArray(eventHistoryData)) { + if (Array.isArray(eventHistoryData) && eventHistoryData.length > 0) { const eventTypeCounts: Record = {}; for (const evt of eventHistoryData) { const type = evt.eventType || 'unknown'; @@ -722,7 +773,7 @@ test.describe('15分钟自动化采集测试', () => { // 异常日志摘要 let errorSummary = ''; 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`; } } @@ -740,10 +791,11 @@ test.describe('15分钟自动化采集测试', () => { | 项目 | 值 | |------|------| -| 模拟器端口 | ${simulationPort} | +| 模拟器网关 | ${SIM_GATEWAY} | +| 模拟地址端口 | ${simPort} | | 采集间隔 | 10秒 | | 机床数量 | ${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} - 产量分段总数: ${dbSegments[0].cnt} (已结账 ${dbSettled[0].cnt}) diff --git a/src/CncRepository/Impl/Dashboard/DashboardRepository.cs b/src/CncRepository/Impl/Dashboard/DashboardRepository.cs index 9e5da0a..e17b7d4 100644 --- a/src/CncRepository/Impl/Dashboard/DashboardRepository.cs +++ b/src/CncRepository/Impl/Dashboard/DashboardRepository.cs @@ -27,10 +27,11 @@ namespace CncRepository.Impl.Dashboard var todayProduction = conn.ExecuteScalar(@"SELECT COALESCE(SUM(total_quantity),0) FROM cnc_daily_production WHERE production_date = CURDATE()"); var activeAlerts = conn.ExecuteScalar(@"SELECT COUNT(1) FROM cnc_alert WHERE is_resolved = 0"); - // 采集成功率,简单实现:统计 cnc_collect_address 表的记录中的成功率,若无数据则返回0 - var totalAddresses = conn.ExecuteScalar(@"SELECT COUNT(1) FROM cnc_collect_address"); - var failCount = conn.ExecuteScalar(@"SELECT COALESCE(SUM(fail_count),0) FROM cnc_collect_address"); - decimal collectSuccessRate = totalAddresses > 0 ? (decimal)(totalAddresses - failCount) / totalAddresses * 100 : 0m; + // 采集成功率:基于原始采集日志的成功/失败比率 + var successCount = conn.ExecuteScalar(@"SELECT COUNT(1) FROM cnc_log.log_collect_raw WHERE is_success = 1 AND request_time >= CURDATE()"); + var failCount = conn.ExecuteScalar(@"SELECT COUNT(1) FROM cnc_log.log_collect_raw WHERE is_success = 0 AND request_time >= CURDATE()"); + var totalCount = successCount + failCount; + decimal collectSuccessRate = totalCount > 0 ? (decimal)successCount / totalCount * 100 : 0m; var todayCuttingTime = conn.ExecuteScalar(@"SELECT COALESCE(SUM(total_cutting_time),0) FROM cnc_daily_production WHERE production_date = CURDATE()"); var runningMachines = conn.ExecuteScalar(@"SELECT COUNT(1) FROM cnc_machine WHERE last_device_status = 'running'"); diff --git a/src/CncWebApi/Web.config b/src/CncWebApi/Web.config index 9cebd44..c655eb3 100644 --- a/src/CncWebApi/Web.config +++ b/src/CncWebApi/Web.config @@ -30,6 +30,8 @@ + +