@ -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 - 15 min . spec . ts -- reporter = list -- timeout = 0
* 运 行 : npx playwright test collector - 15 min . spec . ts -- reporter = list -- timeout = 0
*
* 架 构 说 明 :
* 网 关 ( 9000 ) : / a d m i n / a p i / s t a r t - a d d r e s s , / a d m i n / a p i / s t o p - a d d r e s s , / a d m i n / a p i / s t a t u s ( 汇 总 )
* 模 拟 端 口 ( N ) : / a d m i n / a p i / n e t w o r k , / a d m i n / a p i / s t a t s , / a d m i n / a p i / f u l l - s u m m a r y 等
* /
* /
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:900 1 ';
const SIM_GATEWAY = 'http://localhost:900 0 ';
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 */
/** 辅助:调用模拟器 **单地址** 管理API( stats/network/full-summary等) */
async function simApi ( p ort: number , p ath: string , body? : any ) {
async function simApi ( p ath: string , body? : any ) {
const ctx = await request . newContext ( );
const ctx = await request . newContext ( { timeout : 10000 } );
if ( body ) {
if ( body ) {
return ctx . post ( ` http://localhost: ${ p ort} ${ path } ` , { data : body , headers : { 'Content-Type' : 'application/json' } } ) ;
return ctx . post ( ` http://localhost: ${ simP ort} ${ path } ` , { data : body , headers : { 'Content-Type' : 'application/json' } } ) ;
}
}
return ctx . get ( ` http://localhost: ${ p ort} ${ path } ` ) ;
return ctx . get ( ` http://localhost: ${ simP ort} ${ path } ` ) ;
}
}
/** 辅助:调用模拟器 网关API */
/** 辅助:调用模拟器 ** 网关** API( start-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: ${ sim ulation Port} / ` ]
[ ` http://localhost: ${ sim Port} / ` ]
) ;
) ;
log ( ` ✓ 更新采集地址URL → http://localhost: ${ sim ulation Port} / ` ) ;
log ( ` ✓ 更新采集地址URL → http://localhost: ${ sim Port} / ` ) ;
// 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 + 6 0000) ;
test . setTimeout ( PHASE_DURATIONS . normal2 + 12 0000) ;
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 ) ;
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 ` ) ;
try {
if ( dataCheck . ok ( ) ) {
const dataCheck = await simApi ( '/admin/api/status' ) ;
const status = await dataCheck . json ( ) as any ;
if ( dataCheck . ok ( ) ) {
log ( ` 当前状态: 在线 ${ status . onlineDevices } 台, 运行 ${ status . isRunning } ` ) ;
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 } 秒... ` ) ;
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 {
if ( snapshot . ok ( ) ) phaseSnapshots [ 'normal2' ] = await snapshot . json ( ) ;
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' ) ;
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 ( 6 0000) ;
test . setTimeout ( 12 0000) ;
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 ) ; // 允许1 0%或5个的误差
const tolerance = Math . max ( simTotal * 0.1 5 , 5 ) ; // 允许1 5%或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 , 2 0) ) {
for ( const err of errorLogData . slice ( 0 , 3 0) ) {
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:${sim ulation Port}/ (id=1) |
| 采 集 地 址 | http : //localhost:${sim Port}/ (id=1) |
# # 二 、 测 试 阶 段 时 间 表
# # 二 、 测 试 阶 段 时 间 表
@ -796,7 +848,7 @@ ${errorSummary || '| (无异常记录) | - | - | - |'}
# # 八 、 结 论
# # 八 、 结 论
$ { m ismatchCount === 0 ? '✅ 所有设备零件数量完全匹配,数据采集准确性验证通过。' : ` ⚠️ 有 ${ m ismatchCount} 台设备零件数存在差异,需进一步排查。 ` }
$ { globalM ismatchCount === 0 ? '✅ 所有设备零件数量完全匹配,数据采集准确性验证通过。' : ` ⚠️ 有 ${ globalM ismatchCount} 台设备零件数存在差异,需进一步排查。 ` }
- 采 集 记 录 总 数 : $ { dbRecords [ 0 ] . cnt }
- 采 集 记 录 总 数 : $ { dbRecords [ 0 ] . cnt }
- 产 量 分 段 总 数 : $ { dbSegments [ 0 ] . cnt } ( 已 结 账 $ { dbSettled [ 0 ] . cnt } )
- 产 量 分 段 总 数 : $ { dbSegments [ 0 ] . cnt } ( 已 结 账 $ { dbSettled [ 0 ] . cnt } )