Compare commits
25 Commits
9e3a759646
...
b9555b807c
| Author | SHA1 | Date |
|---|---|---|
|
|
b9555b807c | 3 days ago |
|
|
1600570b60 | 3 days ago |
|
|
06d04c244e | 3 days ago |
|
|
72cb43c493 | 3 days ago |
|
|
4b70b8eacf | 3 days ago |
|
|
b74c3db6af | 3 days ago |
|
|
add981876b | 3 days ago |
|
|
ccdfec31bb | 3 days ago |
|
|
0563da73e8 | 3 days ago |
|
|
089f3e502a | 3 days ago |
|
|
78b7dfea19 | 4 days ago |
|
|
2d698b277d | 4 days ago |
|
|
e09fdc1329 | 4 days ago |
|
|
c9cca32757 | 4 days ago |
|
|
6e468089ea | 4 days ago |
|
|
7d9634af48 | 4 days ago |
|
|
e3f37d5433 | 4 days ago |
|
|
23eda3751f | 4 days ago |
|
|
5a7c1b3436 | 5 days ago |
|
|
eedf5fa8be | 5 days ago |
|
|
acdc502be2 | 5 days ago |
|
|
0212ed6afc | 6 days ago |
|
|
d69817bf45 | 6 days ago |
|
|
d8f59250d7 | 6 days ago |
|
|
e9802a195d | 6 days ago |
@ -0,0 +1,37 @@
|
|||||||
|
name: CI-Windows-WindowsServiceStatus
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, feat/windows-service-status-auto ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main, feat/windows-service-status-auto ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-test:
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Restore NuGet packages
|
||||||
|
run: dotnet restore
|
||||||
|
|
||||||
|
- name: Build backend
|
||||||
|
run: dotnet build -c Release --no-restore
|
||||||
|
|
||||||
|
- name: Run Windows service related tests
|
||||||
|
run: dotnet test tests/CncService.Tests/CncService.Tests.csproj -c Release --no-build -v minimal --filter "FullyQualifiedName~WindowsServiceCheckerTests|FullyQualifiedName~DashboardServiceTests"
|
||||||
|
|
||||||
|
- name: Build frontend
|
||||||
|
run: |
|
||||||
|
cd frontend
|
||||||
|
npm ci
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
- name: Test summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo "=== CI Summary ==="
|
||||||
|
echo "Backend: Build + WindowsService/Dashboard tests"
|
||||||
|
echo "Frontend: Build (vue-tsc + vite)"
|
||||||
|
echo "=================="
|
||||||
@ -0,0 +1,280 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"device": "fanake_1.8",
|
||||||
|
"desc": "西-1.8",
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"id": "_io_status",
|
||||||
|
"desc": "设备状态",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "1.00000",
|
||||||
|
"time": "2026-04-10 17:36:38"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag2",
|
||||||
|
"desc": "当前轴数",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "4.00000",
|
||||||
|
"time": "2026-04-10 17:36:34"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag5",
|
||||||
|
"desc": "执行的NC主程序名",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "1566.NC",
|
||||||
|
"time": "2026-04-10 17:36:35"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag6",
|
||||||
|
"desc": "执行的NC主程序号",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "N0",
|
||||||
|
"time": "2026-04-10 17:36:35"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag7",
|
||||||
|
"desc": "当前加工程序内容",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "<1566.NC>\nG40G49G80\n( NAME: Administrator )\n( M",
|
||||||
|
"time": "2026-04-10 17:36:35"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag8",
|
||||||
|
"desc": "当前加工零件数",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "1219.00000",
|
||||||
|
"time": "2026-04-10 17:36:35"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag9",
|
||||||
|
"desc": "运行状态",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "0.00000",
|
||||||
|
"time": "2026-04-10 17:36:36"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag11",
|
||||||
|
"desc": "操作模式",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "1.00000",
|
||||||
|
"time": "2026-04-10 17:36:36"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag14",
|
||||||
|
"desc": "当前主轴倍率",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "100.00000",
|
||||||
|
"time": "2026-04-10 17:36:36"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag17",
|
||||||
|
"desc": "主轴设定速度",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "300.00000",
|
||||||
|
"time": "2026-04-10 17:36:36"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag18",
|
||||||
|
"desc": "进给设定速度",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "0.00000",
|
||||||
|
"time": "2026-04-10 17:36:36"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag19",
|
||||||
|
"desc": "主轴实际速度",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "0.00000",
|
||||||
|
"time": "2026-04-10 17:36:36"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag20",
|
||||||
|
"desc": "进给实际转速",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "0.00000",
|
||||||
|
"time": "2026-04-10 17:36:37"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag21",
|
||||||
|
"desc": "主轴负载",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "0.00000",
|
||||||
|
"time": "2026-04-10 17:36:37"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag22",
|
||||||
|
"desc": "开机时间",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "23558160.00000",
|
||||||
|
"time": "2026-04-10 17:36:37"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag23",
|
||||||
|
"desc": "运行时间",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "18224.00000",
|
||||||
|
"time": "2026-04-10 17:36:37"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag24",
|
||||||
|
"desc": "切削时间",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "6848959.00000",
|
||||||
|
"time": "2026-04-10 17:36:37"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag25",
|
||||||
|
"desc": "循环时间",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "699.00000",
|
||||||
|
"time": "2026-04-10 17:36:38"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag26",
|
||||||
|
"desc": "加工状态",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "G01",
|
||||||
|
"time": "2026-04-10 17:36:38"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"device": "fanake_1.9",
|
||||||
|
"desc": "西-1.9",
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"id": "_io_status",
|
||||||
|
"desc": "设备状态",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "1.00000",
|
||||||
|
"time": "2026-04-10 17:36:38"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag2",
|
||||||
|
"desc": "当前轴数",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "4.00000",
|
||||||
|
"time": "2026-04-10 17:36:34"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag5",
|
||||||
|
"desc": "执行的NC主程序名",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "O1",
|
||||||
|
"time": "2026-04-10 17:36:35"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag6",
|
||||||
|
"desc": "执行的NC主程序号",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "N20",
|
||||||
|
"time": "2026-04-10 17:36:35"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag7",
|
||||||
|
"desc": "当前加工程序内容",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "G99 G83 Z-43.000 Q3.000 R3.000 F60. \nG80 \nG00 Z",
|
||||||
|
"time": "2026-04-10 17:36:35"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag8",
|
||||||
|
"desc": "当前加工零件数",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "62.00000",
|
||||||
|
"time": "2026-04-10 17:36:35"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag9",
|
||||||
|
"desc": "运行状态",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "3.00000",
|
||||||
|
"time": "2026-04-10 17:36:36"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag11",
|
||||||
|
"desc": "操作模式",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "10.00000",
|
||||||
|
"time": "2026-04-10 17:36:36"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag14",
|
||||||
|
"desc": "当前主轴倍率",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "100.00000",
|
||||||
|
"time": "2026-04-10 17:36:36"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag17",
|
||||||
|
"desc": "主轴设定速度",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "450.00000",
|
||||||
|
"time": "2026-04-10 17:36:36"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag18",
|
||||||
|
"desc": "进给设定速度",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "60.00000",
|
||||||
|
"time": "2026-04-10 17:36:36"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag19",
|
||||||
|
"desc": "主轴实际速度",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "450.00000",
|
||||||
|
"time": "2026-04-10 17:36:36"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag20",
|
||||||
|
"desc": "进给实际转速",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "60.00000",
|
||||||
|
"time": "2026-04-10 17:36:37"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag21",
|
||||||
|
"desc": "主轴负载",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "25.00000",
|
||||||
|
"time": "2026-04-10 17:36:37"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag22",
|
||||||
|
"desc": "开机时间",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "23784960.00000",
|
||||||
|
"time": "2026-04-10 17:36:37"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag23",
|
||||||
|
"desc": "运行时间",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "24253.00000",
|
||||||
|
"time": "2026-04-10 17:36:37"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag24",
|
||||||
|
"desc": "切削时间",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "8009398.00000",
|
||||||
|
"time": "2026-04-10 17:36:38"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag25",
|
||||||
|
"desc": "循环时间",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "82.00000",
|
||||||
|
"time": "2026-04-10 17:36:38"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Tag26",
|
||||||
|
"desc": "加工状态",
|
||||||
|
"quality": "0",
|
||||||
|
"value": "G01",
|
||||||
|
"time": "2026-04-10 17:36:38"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<configSections>
|
||||||
|
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" processFilters="true" />
|
||||||
|
</configSections>
|
||||||
|
|
||||||
|
<log4net>
|
||||||
|
<root>
|
||||||
|
<level value="INFO" />
|
||||||
|
<appender-ref value="RollingFileAppender" />
|
||||||
|
</root>
|
||||||
|
<appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender">
|
||||||
|
<!-- 将日志分等级滚动,按时间与大小混合滚动 -->
|
||||||
|
<file value="logs/simulator-" />
|
||||||
|
<datePattern value="yyyy-MM-dd'.log'" />
|
||||||
|
<rollingStyle value="Composite" />
|
||||||
|
<MaxSizeRollBackups value="10" />
|
||||||
|
<MaximumFileSize value="50MB" />
|
||||||
|
<layout type="log4net.Layout.PatternLayout">
|
||||||
|
<conversionPattern value="[%date %-5level] %message%newline" />
|
||||||
|
</layout>
|
||||||
|
</appender>
|
||||||
|
</log4net>
|
||||||
|
|
||||||
|
</configuration>
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using CncService.LogAnalyzer;
|
||||||
|
using CncService.Models;
|
||||||
|
|
||||||
|
namespace CncService
|
||||||
|
{
|
||||||
|
// 扩展日志写入与分析结果传回接口,供分区日志写入及分析摘要能力使用
|
||||||
|
public interface ILogIngestionService
|
||||||
|
{
|
||||||
|
// 写入采集日志及其分析摘要,返回写入是否成功
|
||||||
|
Task<bool> WriteLogAsync(LogRecord record, LogAnalysisResult analysis);
|
||||||
|
|
||||||
|
// 读取最新一条日志及其分析摘要(用于后台看板等场景的快速查询示例)
|
||||||
|
Task<LogIngestionResult> GetLatestLogAsync(string machineId, string programName);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
namespace CncService.Models
|
||||||
|
{
|
||||||
|
// Minimal result wrapper for latest log fetch
|
||||||
|
public class LogIngestionResult
|
||||||
|
{
|
||||||
|
public long LogId { get; set; }
|
||||||
|
public string Message { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CncService.Models
|
||||||
|
{
|
||||||
|
// Represents a raw log entry captured by the ingestion service
|
||||||
|
public class LogRecord
|
||||||
|
{
|
||||||
|
public long LogId { get; set; }
|
||||||
|
public string MachineId { get; set; }
|
||||||
|
public string ProgramName { get; set; }
|
||||||
|
public DateTime LogTime { get; set; }
|
||||||
|
public string Action { get; set; }
|
||||||
|
public string Result { get; set; }
|
||||||
|
public string RawData { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using CncService;
|
||||||
|
using CncService.Models;
|
||||||
|
using CncService.LogAnalyzer;
|
||||||
|
|
||||||
|
namespace CncWebApi.Controllers
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class LogIngestionController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly ILogIngestionService _logIngestionService;
|
||||||
|
|
||||||
|
public LogIngestionController(ILogIngestionService logIngestionService)
|
||||||
|
{
|
||||||
|
_logIngestionService = logIngestionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("ingest")]
|
||||||
|
public async Task<IActionResult> Ingest([FromBody] LogIngestionRequest request)
|
||||||
|
{
|
||||||
|
if (request == null)
|
||||||
|
return BadRequest("请求为空");
|
||||||
|
|
||||||
|
var record = new LogRecord
|
||||||
|
{
|
||||||
|
LogId = request.LogId,
|
||||||
|
MachineId = request.MachineId,
|
||||||
|
ProgramName = request.ProgramName,
|
||||||
|
LogTime = request.LogTime ?? DateTime.UtcNow,
|
||||||
|
Action = request.Action,
|
||||||
|
Result = request.Result,
|
||||||
|
RawData = request.RawData
|
||||||
|
};
|
||||||
|
|
||||||
|
var analysis = new LogAnalysisResult
|
||||||
|
{
|
||||||
|
Summary = request.AnalysisSummary,
|
||||||
|
DetailsJson = request.DetailsJson,
|
||||||
|
Confidence = request.Confidence
|
||||||
|
};
|
||||||
|
|
||||||
|
var ok = await _logIngestionService.WriteLogAsync(record, analysis);
|
||||||
|
if (ok)
|
||||||
|
{
|
||||||
|
return Ok(new { success = true, logId = record.LogId, analysisSummary = analysis.Summary });
|
||||||
|
}
|
||||||
|
return StatusCode(500, new { success = false, message = "写入失败" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LogIngestionRequest
|
||||||
|
{
|
||||||
|
public long LogId { get; set; }
|
||||||
|
public string MachineId { get; set; }
|
||||||
|
public string ProgramName { get; set; }
|
||||||
|
public DateTime? LogTime { get; set; }
|
||||||
|
public string Action { get; set; }
|
||||||
|
public string Result { get; set; }
|
||||||
|
public string RawData { get; set; }
|
||||||
|
public string AnalysisSummary { get; set; }
|
||||||
|
public string DetailsJson { get; set; }
|
||||||
|
public double? Confidence { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
CncSimulator 项目根 README
|
||||||
|
This repository contains a self-contained CNC data sampling simulator for .NET Framework 4.7.2.
|
||||||
|
具体实现参考 simulator.json、Config/SimulatorConfig.cs、Device/DeviceState.cs、Device/DeviceSimulator.cs、Generator/FanucDataGenerator.cs、Core/SimulatorServer.cs、Core/SimulatorEngine.cs、Program.cs。
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
-- Partitioned logs table draft
|
||||||
|
-- 目标:按月分区日志表,提升写入吞吐和查询历史的性能
|
||||||
|
-- 说明:本草案为初步设计,待评审后落地实现
|
||||||
|
-- Assumptions:
|
||||||
|
-- - MariaDB 10.x 版本,支持分区按 RANGE (TO_DAYS(log_time))
|
||||||
|
-- - 日志字段与现有采集日志表接近
|
||||||
|
-- - 每月一个分区,覆盖历史数据的归档策略待定
|
||||||
|
DROP TABLE IF EXISTS logs_partitioned;
|
||||||
|
CREATE TABLE logs_partitioned (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
machine_id INT NOT NULL,
|
||||||
|
program_name VARCHAR(128) NOT NULL,
|
||||||
|
log_time DATETIME NOT NULL,
|
||||||
|
log_level VARCHAR(16) DEFAULT 'INFO',
|
||||||
|
raw_payload JSON,
|
||||||
|
analysis_summary TEXT,
|
||||||
|
analysis_version VARCHAR(64) DEFAULT 'v1',
|
||||||
|
-- 便于按机床与时间筛选的组合索引
|
||||||
|
KEY idx_machine_time (machine_id, log_time),
|
||||||
|
KEY idx_program_time (program_name, log_time)
|
||||||
|
)
|
||||||
|
PARTITION BY RANGE (TO_DAYS(log_time)) (
|
||||||
|
PARTITION p202401 VALUES LESS THAN (TO_DAYS('2024-02-01')),
|
||||||
|
PARTITION p202402 VALUES LESS THAN (TO_DAYS('2024-03-01')),
|
||||||
|
PARTITION p202403 VALUES LESS THAN (TO_DAYS('2024-04-01')),
|
||||||
|
PARTITION p202404 VALUES LESS THAN (TO_DAYS('2024-05-01')),
|
||||||
|
PARTITION p202405 VALUES LESS THAN (TO_DAYS('2024-06-01')),
|
||||||
|
PARTITION p202406 VALUES LESS THAN (TO_DAYS('2024-07-01')),
|
||||||
|
PARTITION p202407 VALUES LESS THAN (TO_DAYS('2024-08-01')),
|
||||||
|
PARTITION p202408 VALUES LESS THAN (TO_DAYS('2024-09-01')),
|
||||||
|
PARTITION p202409 VALUES LESS THAN (TO_DAYS('2024-10-01')),
|
||||||
|
PARTITION p202410 VALUES LESS THAN (TO_DAYS('2024-11-01')),
|
||||||
|
PARTITION p202411 VALUES LESS THAN (TO_DAYS('2024-12-01')),
|
||||||
|
PARTITION p202412 VALUES LESS THAN (TO_DAYS('2025-01-01')),
|
||||||
|
PARTITION p202501 VALUES LESS THAN (TO_DAYS('2025-02-01'))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 备注:
|
||||||
|
- 未来月份的分区建议通过定期执行脚本自动追加分区
|
||||||
|
- 可以通过 ALTER TABLE logs_partitioned REORGANIZE PARTITION ...? 进行滚动归档
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
import type { ApiResponse, PaginatedResponse } from '@/types'
|
||||||
|
|
||||||
|
// --- 采集日志数据模型 ---
|
||||||
|
export interface CollectAnalysis {
|
||||||
|
id: number
|
||||||
|
analysisTime: string
|
||||||
|
collectAddressId: number
|
||||||
|
addressName?: string
|
||||||
|
machineId: number
|
||||||
|
machineName?: string
|
||||||
|
analysisType: string
|
||||||
|
previousProgram?: string
|
||||||
|
currentProgram?: string
|
||||||
|
partCountDelta?: number
|
||||||
|
analysisSummary?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CollectCycle {
|
||||||
|
id: number
|
||||||
|
cycleTime: string
|
||||||
|
collectAddressId: number
|
||||||
|
addressName?: string
|
||||||
|
totalMachines: number
|
||||||
|
successCount: number
|
||||||
|
failCount: number
|
||||||
|
hasAnomaly: number
|
||||||
|
changeDistribution?: string
|
||||||
|
cycleSummary?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CollectRaw {
|
||||||
|
id: number
|
||||||
|
logTime: string
|
||||||
|
sourceAddress?: string
|
||||||
|
contentPreview?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 公开的 API 封装 ---
|
||||||
|
// 获取分析记录列表
|
||||||
|
export function fetchAnalysisList(params?: {
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
dateRange?: string[] | null
|
||||||
|
addressId?: number
|
||||||
|
machineId?: number
|
||||||
|
analysisType?: string
|
||||||
|
programName?: string
|
||||||
|
keyword?: string
|
||||||
|
}) {
|
||||||
|
return request.get<{ items: CollectAnalysis[]; total: number }>(
|
||||||
|
'/admin/collect-log/analysis',
|
||||||
|
{ params }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取分析详情
|
||||||
|
export function fetchAnalysisDetail(id: number) {
|
||||||
|
return request.get<CollectAnalysis>(`/admin/collect-log/analysis/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据原始日志检索分析记录
|
||||||
|
export function fetchAnalysisByRaw(rawLogId: number | string) {
|
||||||
|
return request.get<{ items: CollectAnalysis[] }>(`/admin/collect-log/analysis/by-raw/${rawLogId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取采集周期列表
|
||||||
|
export function fetchCycleList(params?: {
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
dateRange?: string[] | null
|
||||||
|
addressId?: number
|
||||||
|
hasAnomaly?: string
|
||||||
|
}) {
|
||||||
|
return request.get<{ items: CollectCycle[]; total: number }>(
|
||||||
|
'/admin/collect-log/cycle',
|
||||||
|
{ params }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取原始日志列表
|
||||||
|
export function fetchRawList(params?: {
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
dateRange?: string[] | null
|
||||||
|
addressId?: number
|
||||||
|
}) {
|
||||||
|
return request.get<{ items: CollectRaw[]; total: number }>(
|
||||||
|
'/admin/collect-log/raw',
|
||||||
|
{ params }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {}
|
||||||
@ -0,0 +1,223 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
import type { ApiResponse } from '@/types'
|
||||||
|
|
||||||
|
// --- 模拟器数据模型 ---
|
||||||
|
|
||||||
|
/** 模拟器连接状态 */
|
||||||
|
export interface SimulatorPing {
|
||||||
|
running: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 数据库采集地址(模拟器返回) */
|
||||||
|
export interface SimulatorAddress {
|
||||||
|
dbId: number
|
||||||
|
name: string
|
||||||
|
url: string
|
||||||
|
machineCount: number
|
||||||
|
machines: { id: number; deviceCode: string; name: string }[]
|
||||||
|
isRunning: boolean
|
||||||
|
runningPort: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 模拟状态汇总 */
|
||||||
|
export interface SimulatorStatus {
|
||||||
|
dbAddressId: number
|
||||||
|
name: string
|
||||||
|
port: number
|
||||||
|
isRunning: boolean
|
||||||
|
totalDevices: number
|
||||||
|
onlineDevices: number
|
||||||
|
requestCount: number
|
||||||
|
dataChangeInterval: number
|
||||||
|
totalParts: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 设备状态 */
|
||||||
|
export interface DeviceStatus {
|
||||||
|
deviceCode: string
|
||||||
|
desc: string
|
||||||
|
scenario: string
|
||||||
|
isOnline: boolean
|
||||||
|
programName: string
|
||||||
|
partCount: number
|
||||||
|
runStatus: number
|
||||||
|
operateMode: number
|
||||||
|
spindleSpeedSet: number
|
||||||
|
spindleSpeedActual: number
|
||||||
|
feedSpeedSet: number
|
||||||
|
feedSpeedActual: number
|
||||||
|
spindleLoad: number
|
||||||
|
machiningStatus: string
|
||||||
|
scenarioTick: number
|
||||||
|
scenarioDuration: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单地址详情状态 */
|
||||||
|
export interface AddressStatus {
|
||||||
|
name: string
|
||||||
|
port: number
|
||||||
|
isRunning: boolean
|
||||||
|
requestCount: number
|
||||||
|
successCount: number
|
||||||
|
failCount: number
|
||||||
|
totalDevices: number
|
||||||
|
onlineDevices: number
|
||||||
|
dataChangeInterval: number
|
||||||
|
scenarioMode: string
|
||||||
|
networkError: string
|
||||||
|
startTime: string
|
||||||
|
uptime: string
|
||||||
|
devices: DeviceStatus[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 零件统计 */
|
||||||
|
export interface AddressStats {
|
||||||
|
totalDevices: number
|
||||||
|
onlineDevices: number
|
||||||
|
totalParts: number
|
||||||
|
partsByDevice: Record<string, {
|
||||||
|
desc: string
|
||||||
|
totalParts: number
|
||||||
|
currentProgram: string
|
||||||
|
currentPartCount: number
|
||||||
|
programs: Record<string, number>
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 请求日志 */
|
||||||
|
export interface SimulatorLog {
|
||||||
|
index: number
|
||||||
|
timestamp: string
|
||||||
|
deviceCount: number
|
||||||
|
keyData: string
|
||||||
|
duration: number
|
||||||
|
fullJson: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 事件历史 */
|
||||||
|
export interface EventHistory {
|
||||||
|
timestamp: string
|
||||||
|
deviceCode: string
|
||||||
|
eventType: string
|
||||||
|
oldProgram: string
|
||||||
|
newProgram: string
|
||||||
|
partCountBefore: number
|
||||||
|
partCountAfter: number
|
||||||
|
detail: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 网关API ---
|
||||||
|
|
||||||
|
/** 探测模拟器是否运行 */
|
||||||
|
export function pingSimulator() {
|
||||||
|
return request.get<SimulatorPing>('/admin/simulator/ping')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取数据库采集地址列表 */
|
||||||
|
export function fetchSimulatorAddresses() {
|
||||||
|
return request.get<SimulatorAddress[]>('/admin/simulator/addresses')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取所有模拟状态汇总 */
|
||||||
|
export function fetchSimulatorStatus() {
|
||||||
|
return request.get<SimulatorStatus[]>('/admin/simulator/status')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 启动指定地址的模拟 */
|
||||||
|
export function startSimulator(data: { dbAddressId: number; deviceCodes?: string[] }) {
|
||||||
|
return request.post('/admin/simulator/start', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 停止指定地址的模拟 */
|
||||||
|
export function stopSimulator(data: { dbAddressId: number }) {
|
||||||
|
return request.post('/admin/simulator/stop', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 启动所有地址的模拟 */
|
||||||
|
export function startAllSimulators() {
|
||||||
|
return request.post('/admin/simulator/start-all')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 停止所有地址的模拟 */
|
||||||
|
export function stopAllSimulators() {
|
||||||
|
return request.post('/admin/simulator/stop-all')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重新加载数据库配置 */
|
||||||
|
export function reloadSimulator() {
|
||||||
|
return request.post('/admin/simulator/reload')
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 单地址API ---
|
||||||
|
|
||||||
|
/** 获取单地址状态 */
|
||||||
|
export function fetchAddressStatus(port: number) {
|
||||||
|
return request.get<AddressStatus>(`/admin/simulator/address/${port}/status`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 启动单地址数据模拟 */
|
||||||
|
export function startAddressSimulation(port: number) {
|
||||||
|
return request.post(`/admin/simulator/address/${port}/start`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 停止单地址数据模拟 */
|
||||||
|
export function stopAddressSimulation(port: number) {
|
||||||
|
return request.post(`/admin/simulator/address/${port}/stop`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 触发设备事件 */
|
||||||
|
export function triggerDeviceEvent(port: number, data: { deviceId: string; eventType: string }) {
|
||||||
|
return request.post(`/admin/simulator/address/${port}/event`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 修改数据变化频率 */
|
||||||
|
export function setAddressInterval(port: number, data: { value: number }) {
|
||||||
|
return request.post(`/admin/simulator/address/${port}/interval`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 设置网络异常类型 */
|
||||||
|
export function setNetworkError(port: number, data: { type: string }) {
|
||||||
|
return request.post(`/admin/simulator/address/${port}/network`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 切换剧本模式 */
|
||||||
|
export function setScenarioMode(port: number, data: { mode: string }) {
|
||||||
|
return request.post(`/admin/simulator/address/${port}/mode`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取请求日志 */
|
||||||
|
export function fetchAddressLogs(port: number) {
|
||||||
|
return request.get<SimulatorLog[]>(`/admin/simulator/address/${port}/logs`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取零件统计 */
|
||||||
|
export function fetchAddressStats(port: number) {
|
||||||
|
return request.get<AddressStats>(`/admin/simulator/address/${port}/stats`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 添加设备 */
|
||||||
|
export function addDevice(port: number, data: { deviceCode: string; desc: string }) {
|
||||||
|
return request.post(`/admin/simulator/address/${port}/add-device`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 移除设备 */
|
||||||
|
export function removeDevice(port: number, data: { deviceCode: string }) {
|
||||||
|
return request.post(`/admin/simulator/address/${port}/remove-device`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取事件历史 */
|
||||||
|
export function fetchEventHistory(port: number) {
|
||||||
|
return request.get<EventHistory[]>(`/admin/simulator/address/${port}/event-history`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取完整汇总 */
|
||||||
|
export function fetchFullSummary(port: number) {
|
||||||
|
return request.get(`/admin/simulator/address/${port}/full-summary`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取异常日志 */
|
||||||
|
export function fetchErrorLog(port: number) {
|
||||||
|
return request.get(`/admin/simulator/address/${port}/error-log`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"gatewayPort": 9000,
|
||||||
|
"addresses": [
|
||||||
|
{
|
||||||
|
"name": "FANUC-1号模拟",
|
||||||
|
"port": 9001,
|
||||||
|
"brand": "fanuc",
|
||||||
|
"dataChangeInterval": 10,
|
||||||
|
"scenarioMode": "auto",
|
||||||
|
"devices": [
|
||||||
|
{ "deviceCode": "CNC-A001", "desc": "西栋1号", "initialProgram": "O0001", "initialPartCount": 50 },
|
||||||
|
{ "deviceCode": "CNC-006", "desc": "6号机床", "initialProgram": "O0002", "initialPartCount": 120 },
|
||||||
|
{ "deviceCode": "CNC-008", "desc": "8号机床", "initialProgram": "O0003", "initialPartCount": 0 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "FANUC-2号模拟",
|
||||||
|
"port": 9002,
|
||||||
|
"brand": "fanuc",
|
||||||
|
"dataChangeInterval": 15,
|
||||||
|
"scenarioMode": "auto",
|
||||||
|
"devices": [
|
||||||
|
{ "deviceCode": "CNC-B002", "desc": "B栋2号", "initialProgram": "1566.NC", "initialPartCount": 80 },
|
||||||
|
{ "deviceCode": "CNC-005", "desc": "验证机床", "initialProgram": "TEST001", "initialPartCount": 10 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
namespace CncModels.Dto.CollectLog
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 采集分析详情(基于 CollectAnalysisListItem 的扩展字段)
|
||||||
|
/// </summary>
|
||||||
|
public class CollectAnalysisDetail : CollectAnalysisListItem
|
||||||
|
{
|
||||||
|
public decimal? PreviousPartCount { get; set; }
|
||||||
|
public decimal? CurrentPartCount { get; set; }
|
||||||
|
public string PreviousStatus { get; set; }
|
||||||
|
public string CurrentStatus { get; set; }
|
||||||
|
public string AnalysisDetail { get; set; }
|
||||||
|
public long RawLogId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
namespace CncModels.Dto.CollectLog
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 采集分析列表项
|
||||||
|
/// </summary>
|
||||||
|
public class CollectAnalysisListItem
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string AnalysisTime { get; set; }
|
||||||
|
public int CollectAddressId { get; set; }
|
||||||
|
public int MachineId { get; set; }
|
||||||
|
public string MachineName { get; set; }
|
||||||
|
public string AnalysisType { get; set; }
|
||||||
|
public string PreviousProgram { get; set; }
|
||||||
|
public string CurrentProgram { get; set; }
|
||||||
|
public decimal? PartCountDelta { get; set; }
|
||||||
|
public string AnalysisSummary { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CncModels.Dto.CollectLog
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 采集分析列表查询条件(分页)
|
||||||
|
/// </summary>
|
||||||
|
public class CollectAnalysisQuery : PagedQuery
|
||||||
|
{
|
||||||
|
public DateTime? StartDate { get; set; }
|
||||||
|
public DateTime? EndDate { get; set; }
|
||||||
|
public int? CollectAddressId { get; set; }
|
||||||
|
public int? MachineId { get; set; }
|
||||||
|
public string AnalysisType { get; set; }
|
||||||
|
public string ProgramName { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
namespace CncModels.Dto.CollectLog
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 采集周期列表项
|
||||||
|
/// </summary>
|
||||||
|
public class CollectCycleListItem
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string CycleTime { get; set; }
|
||||||
|
public int CollectAddressId { get; set; }
|
||||||
|
public string AddressName { get; set; }
|
||||||
|
public int TotalMachines { get; set; }
|
||||||
|
public int SuccessCount { get; set; }
|
||||||
|
public int FailCount { get; set; }
|
||||||
|
public int HasAnomaly { get; set; }
|
||||||
|
public string ChangeDistribution { get; set; }
|
||||||
|
public string CycleSummary { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CncModels.Dto.CollectLog
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 采集周期查询条件(分页)
|
||||||
|
/// </summary>
|
||||||
|
public class CollectCycleQuery : PagedQuery
|
||||||
|
{
|
||||||
|
public DateTime? StartDate { get; set; }
|
||||||
|
public DateTime? EndDate { get; set; }
|
||||||
|
public int? CollectAddressId { get; set; }
|
||||||
|
public int? HasAnomaly { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CncModels.Dto.CollectLog
|
||||||
|
{
|
||||||
|
/// <summary>回放请求参数</summary>
|
||||||
|
public class ReplayRequest { public DateTime Date { get; set; } }
|
||||||
|
|
||||||
|
/// <summary>回放预览结果</summary>
|
||||||
|
public class ReplayPreview {
|
||||||
|
public DateTime Date { get; set; }
|
||||||
|
public int RawLogCount { get; set; }
|
||||||
|
public int AffectedMachineCount { get; set; }
|
||||||
|
public int AffectedRecordCount { get; set; }
|
||||||
|
public int AffectedSegmentCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>回放执行结果</summary>
|
||||||
|
public class ReplayResult {
|
||||||
|
public DateTime Date { get; set; }
|
||||||
|
public int ClearedRecordCount { get; set; }
|
||||||
|
public int ClearedSegmentCount { get; set; }
|
||||||
|
public int RebuiltRecordCount { get; set; }
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string ErrorMessage { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
namespace CncModels.Enum
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 采集分析的分析类型枚举(以字符串常量形式提供)
|
||||||
|
/// </summary>
|
||||||
|
public static class AnalysisType
|
||||||
|
{
|
||||||
|
public const string NORMAL_UNCHANGED = "NORMAL_UNCHANGED";
|
||||||
|
public const string PART_COUNT_INCREASE = "PART_COUNT_INCREASE";
|
||||||
|
public const string PROGRAM_SWITCH = "PROGRAM_SWITCH";
|
||||||
|
public const string MANUAL_RESET = "MANUAL_RESET";
|
||||||
|
public const string DEVICE_ONLINE = "DEVICE_ONLINE";
|
||||||
|
public const string DEVICE_OFFLINE = "DEVICE_OFFLINE";
|
||||||
|
public const string NEW_DEVICE_FOUND = "NEW_DEVICE_FOUND";
|
||||||
|
public const string DATA_ANOMALY = "DATA_ANOMALY";
|
||||||
|
public const string COLLECTION_FAILED = "COLLECTION_FAILED";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using CncModels.Dto;
|
||||||
|
using CncModels.Dto.CollectLog;
|
||||||
|
using CncModels.Entity;
|
||||||
|
|
||||||
|
namespace CncRepository.Interface
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 采集分析仓储接口
|
||||||
|
/// </summary>
|
||||||
|
public interface ICollectAnalysisRepository
|
||||||
|
{
|
||||||
|
PagedResult<CollectAnalysisListItem> GetAnalysisList(CollectAnalysisQuery query);
|
||||||
|
CollectAnalysisDetail GetAnalysisDetail(long id);
|
||||||
|
List<CollectAnalysisListItem> GetAnalysisByRawLogId(long rawLogId);
|
||||||
|
long Create(CollectAnalysis entity);
|
||||||
|
int DeleteBeforeDate(DateTime date);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using CncModels.Dto;
|
||||||
|
using CncModels.Dto.CollectLog;
|
||||||
|
using CncModels.Entity;
|
||||||
|
|
||||||
|
namespace CncRepository.Interface
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 采集周期仓储接口
|
||||||
|
/// </summary>
|
||||||
|
public interface ICollectCycleRepository
|
||||||
|
{
|
||||||
|
PagedResult<CollectCycleListItem> GetCycleList(CollectCycleQuery query);
|
||||||
|
long Create(CollectCycle entity);
|
||||||
|
int DeleteBeforeDate(DateTime date);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using CncModels.Dto;
|
||||||
|
using CncModels.Dto.CollectLog;
|
||||||
|
using CncModels.Constants;
|
||||||
|
using CncService.Interface;
|
||||||
|
using CncRepository.Interface;
|
||||||
|
|
||||||
|
namespace CncService.Impl
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 采集日志相关的业务实现
|
||||||
|
/// </summary>
|
||||||
|
public class CollectLogService : ICollectLogService
|
||||||
|
{
|
||||||
|
private readonly ICollectAnalysisRepository _analysisRepository;
|
||||||
|
private readonly ICollectCycleRepository _cycleRepository;
|
||||||
|
|
||||||
|
public CollectLogService(ICollectAnalysisRepository analysisRepository, ICollectCycleRepository cycleRepository)
|
||||||
|
{
|
||||||
|
_analysisRepository = analysisRepository ?? throw new ArgumentNullException(nameof(analysisRepository));
|
||||||
|
_cycleRepository = cycleRepository ?? throw new ArgumentNullException(nameof(cycleRepository));
|
||||||
|
}
|
||||||
|
|
||||||
|
public PagedResult<CollectAnalysisListItem> GetAnalysisList(CollectAnalysisQuery query)
|
||||||
|
{
|
||||||
|
if (query == null) throw new BusinessException(ErrorCode.BadRequest, "查询参数不能为空");
|
||||||
|
return _analysisRepository.GetAnalysisList(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CollectAnalysisDetail GetAnalysisDetail(long id)
|
||||||
|
{
|
||||||
|
var detail = _analysisRepository.GetAnalysisDetail(id);
|
||||||
|
if (detail == null) throw new BusinessException(ErrorCode.NotFound, "采集分析记录不存在");
|
||||||
|
return detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CollectAnalysisListItem> GetAnalysisByRawLogId(long rawLogId)
|
||||||
|
{
|
||||||
|
return _analysisRepository.GetAnalysisByRawLogId(rawLogId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PagedResult<CollectCycleListItem> GetCycleList(CollectCycleQuery query)
|
||||||
|
{
|
||||||
|
if (query == null) throw new BusinessException(ErrorCode.BadRequest, "查询参数不能为空");
|
||||||
|
return _cycleRepository.GetCycleList(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using CncModels.Dto;
|
||||||
|
using CncModels.Dto.CollectLog;
|
||||||
|
|
||||||
|
namespace CncService.Interface
|
||||||
|
{
|
||||||
|
public interface ICollectLogService
|
||||||
|
{
|
||||||
|
/// <summary>分页查询采集分析日志</summary>
|
||||||
|
PagedResult<CollectAnalysisListItem> GetAnalysisList(CollectAnalysisQuery query);
|
||||||
|
|
||||||
|
/// <summary>获取单条采集分析日志的详情</summary>
|
||||||
|
CollectAnalysisDetail GetAnalysisDetail(long id);
|
||||||
|
|
||||||
|
/// <summary>根据原始日志ID查找相关联的分析记录</summary>
|
||||||
|
List<CollectAnalysisListItem> GetAnalysisByRawLogId(long rawLogId);
|
||||||
|
|
||||||
|
/// <summary>分页查询采集周期信息</summary>
|
||||||
|
PagedResult<CollectCycleListItem> GetCycleList(CollectCycleQuery query);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CncService.Interface
|
||||||
|
{
|
||||||
|
// Windows 服务状态枚举,用于和心跳状态区分不同场景
|
||||||
|
public enum ServiceStatusEnum
|
||||||
|
{
|
||||||
|
NotInstalled,
|
||||||
|
Stopped,
|
||||||
|
Running,
|
||||||
|
Starting,
|
||||||
|
StartFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Windows 服务检测接口(用于管理后台对采集服务的状态检测与控制)
|
||||||
|
/// </summary>
|
||||||
|
public interface IWindowsServiceChecker
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取指定服务的当前状态
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serviceName">服务名</param>
|
||||||
|
/// <returns>服务状态枚举</returns>
|
||||||
|
ServiceStatusEnum GetServiceStatus(string serviceName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 尝试启动指定服务,并在给定超时内等待就绪
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serviceName">服务名</param>
|
||||||
|
/// <param name="timeoutSeconds">超时(秒)</param>
|
||||||
|
/// <returns>(是否成功, 详细信息)</returns>
|
||||||
|
(bool Success, string Message) TryStartService(string serviceName, int timeoutSeconds);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 尝试停止指定服务,并在给定超时内等待停止
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serviceName">服务名</param>
|
||||||
|
/// <param name="timeoutSeconds">超时(秒)</param>
|
||||||
|
/// <returns>(是否成功, 详细信息)</returns>
|
||||||
|
(bool Success, string Message) TryStopService(string serviceName, int timeoutSeconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,82 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Web.Http;
|
||||||
|
using CncModels.Dto;
|
||||||
|
using CncModels.Dto.CollectLog;
|
||||||
|
using CncModels.Entity;
|
||||||
|
using CncService.Interface;
|
||||||
|
using CncRepository.Interface;
|
||||||
|
using CncWebApi.Infrastructure;
|
||||||
|
using System.Web.Http.Description;
|
||||||
|
|
||||||
|
namespace CncWebApi.Controllers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 采集日志管理控制器
|
||||||
|
/// </summary>
|
||||||
|
[RoutePrefix("api/admin/collect-log")]
|
||||||
|
[JwtAuthFilter]
|
||||||
|
public class CollectLogController : ApiController
|
||||||
|
{
|
||||||
|
private readonly ICollectLogService _collectLogService;
|
||||||
|
private readonly ICollectRawRepository _rawRepository;
|
||||||
|
|
||||||
|
public CollectLogController(ICollectLogService collectLogService, ICollectRawRepository rawRepository)
|
||||||
|
{
|
||||||
|
_collectLogService = collectLogService ?? throw new ArgumentNullException(nameof(collectLogService));
|
||||||
|
_rawRepository = rawRepository ?? throw new ArgumentNullException(nameof(rawRepository));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>分页查询采集分析记录</summary>
|
||||||
|
[HttpGet]
|
||||||
|
[Route("analysis")]
|
||||||
|
[ResponseType(typeof(ApiResponse<PagedResult<CollectAnalysisListItem>>))]
|
||||||
|
public IHttpActionResult GetAnalysisList([FromUri] CollectAnalysisQuery query)
|
||||||
|
{
|
||||||
|
if (query == null) query = new CollectAnalysisQuery();
|
||||||
|
var result = _collectLogService.GetAnalysisList(query);
|
||||||
|
return Ok(ApiResponse<PagedResult<CollectAnalysisListItem>>.Success(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>获取采集分析详情</summary>
|
||||||
|
[HttpGet]
|
||||||
|
[Route("analysis/{id:long}")]
|
||||||
|
[ResponseType(typeof(ApiResponse<CollectAnalysisDetail>))]
|
||||||
|
public IHttpActionResult GetAnalysisDetail(long id)
|
||||||
|
{
|
||||||
|
var detail = _collectLogService.GetAnalysisDetail(id);
|
||||||
|
return Ok(ApiResponse<CollectAnalysisDetail>.Success(detail));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>根据原始日志ID查询关联的分析记录</summary>
|
||||||
|
[HttpGet]
|
||||||
|
[Route("analysis/by-raw/{rawLogId:long}")]
|
||||||
|
[ResponseType(typeof(ApiResponse<List<CollectAnalysisListItem>>))]
|
||||||
|
public IHttpActionResult GetAnalysisByRawLogId(long rawLogId)
|
||||||
|
{
|
||||||
|
var list = _collectLogService.GetAnalysisByRawLogId(rawLogId);
|
||||||
|
return Ok(ApiResponse<List<CollectAnalysisListItem>>.Success(list));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>分页查询采集周期</summary>
|
||||||
|
[HttpGet]
|
||||||
|
[Route("cycle")]
|
||||||
|
[ResponseType(typeof(ApiResponse<PagedResult<CollectCycleListItem>>))]
|
||||||
|
public IHttpActionResult GetCycleList([FromUri] CollectCycleQuery query)
|
||||||
|
{
|
||||||
|
if (query == null) query = new CollectCycleQuery();
|
||||||
|
var result = _collectLogService.GetCycleList(query);
|
||||||
|
return Ok(ApiResponse<PagedResult<CollectCycleListItem>>.Success(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>查询原始采集日志</summary>
|
||||||
|
[HttpGet]
|
||||||
|
[Route("raw")]
|
||||||
|
[ResponseType(typeof(ApiResponse<PagedResult<CollectRaw>>))]
|
||||||
|
public IHttpActionResult GetRawList([FromUri] int? collectAddressId = null, [FromUri] int page = 1, [FromUri] int pageSize = 20)
|
||||||
|
{
|
||||||
|
var result = _rawRepository.GetByAddressId(collectAddressId ?? 0, page, pageSize);
|
||||||
|
return Ok(ApiResponse<PagedResult<CollectRaw>>.Success(result));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Web.Http;
|
||||||
|
using System.Web.Http.Description;
|
||||||
|
using CncModels.Dto;
|
||||||
|
using CncModels.Dto.CollectLog;
|
||||||
|
using CncService.Interface;
|
||||||
|
using CncWebApi.Infrastructure;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace CncWebApi.Controllers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 数据回放控制器
|
||||||
|
/// </summary>
|
||||||
|
[RoutePrefix("api/admin/replay")]
|
||||||
|
[JwtAuthFilter]
|
||||||
|
public class ReplayController : ApiController
|
||||||
|
{
|
||||||
|
private readonly IReplayService _replayService;
|
||||||
|
|
||||||
|
public ReplayController(IReplayService replayService)
|
||||||
|
{
|
||||||
|
_replayService = replayService ?? throw new ArgumentNullException(nameof(replayService));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>预览回放影响范围</summary>
|
||||||
|
[HttpPost]
|
||||||
|
[Route("preview")]
|
||||||
|
[ResponseType(typeof(ApiResponse<ReplayPreview>))]
|
||||||
|
public IHttpActionResult Preview([FromBody] ReplayRequest request)
|
||||||
|
{
|
||||||
|
if (request == null) return BadRequest("请求参数错误");
|
||||||
|
var result = _replayService.PreviewReplay(request.Date);
|
||||||
|
return Ok(ApiResponse<ReplayPreview>.Success(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>执行回放</summary>
|
||||||
|
[HttpPost]
|
||||||
|
[Route("execute")]
|
||||||
|
[ResponseType(typeof(ApiResponse<ReplayResult>))]
|
||||||
|
public IHttpActionResult Execute([FromBody] ReplayRequest request)
|
||||||
|
{
|
||||||
|
if (request == null) return BadRequest("请求参数错误");
|
||||||
|
var result = _replayService.ExecuteReplay(request.Date);
|
||||||
|
return Ok(ApiResponse<ReplayResult>.Success(result));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,125 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using CncModels.Entity;
|
||||||
|
using CncRepository.Impl;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace CncRepository.Tests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 品牌字段映射仓储测试
|
||||||
|
/// </summary>
|
||||||
|
[Collection("Database")]
|
||||||
|
public class BrandFieldMappingRepositoryTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly BrandFieldMappingRepository _repo;
|
||||||
|
|
||||||
|
public BrandFieldMappingRepositoryTests()
|
||||||
|
{
|
||||||
|
_repo = new BrandFieldMappingRepository(TestDb.ConnectionString);
|
||||||
|
TestDb.TruncateAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
TestDb.TruncateAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetByBrandId_返回所有映射含禁用()
|
||||||
|
{
|
||||||
|
// 插入2条启用 + 1条禁用
|
||||||
|
TestDb.Execute(@"INSERT INTO cnc_brand_field_mapping (brand_id, standard_field, field_name, match_by, data_type, is_required, is_enabled, created_at)
|
||||||
|
VALUES (1, 'program_name', 'Tag5', 'id', 'string', 1, 1, NOW()),
|
||||||
|
(1, 'part_count', 'Tag8', 'id', 'number', 1, 1, NOW()),
|
||||||
|
(1, 'spindle_load', 'Tag21', 'id', 'number', 0, 0, NOW())");
|
||||||
|
var result = _repo.GetByBrandId(1);
|
||||||
|
Assert.Equal(3, result.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetEnabledByBrandId_只返回启用的映射()
|
||||||
|
{
|
||||||
|
TestDb.Execute(@"INSERT IGNORE INTO cnc_brand (id, brand_name, device_field, tags_path, is_enabled, created_at, updated_at)
|
||||||
|
VALUES (1, 'FANUC', 'device', 'tags', 1, NOW(), NOW())");
|
||||||
|
TestDb.Execute(@"INSERT INTO cnc_brand_field_mapping (brand_id, standard_field, field_name, match_by, data_type, is_required, is_enabled, created_at)
|
||||||
|
VALUES (1, 'program_name', 'Tag5', 'id', 'string', 1, 1, NOW()),
|
||||||
|
(1, 'part_count', 'Tag8', 'id', 'number', 1, 1, NOW()),
|
||||||
|
(1, 'spindle_load', 'Tag21', 'id', 'number', 0, 0, NOW())");
|
||||||
|
var result = _repo.GetEnabledByBrandId(1);
|
||||||
|
Assert.Equal(2, result.Count);
|
||||||
|
Assert.All(result, m => Assert.Equal(1, m.IsEnabled));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Create_默认启用()
|
||||||
|
{
|
||||||
|
// 确保 brand_id=1 存在
|
||||||
|
TestDb.Execute(@"INSERT IGNORE INTO cnc_brand (id, brand_name, device_field, tags_path, is_enabled, created_at, updated_at)
|
||||||
|
VALUES (1, 'FANUC', 'device', 'tags', 1, NOW(), NOW())");
|
||||||
|
var entity = new BrandFieldMapping
|
||||||
|
{
|
||||||
|
BrandId = 1,
|
||||||
|
StandardField = "program_name",
|
||||||
|
FieldName = "Tag5",
|
||||||
|
MatchBy = "id",
|
||||||
|
DataType = "string",
|
||||||
|
IsRequired = 1,
|
||||||
|
IsEnabled = 1,
|
||||||
|
CreatedAt = DateTime.Now
|
||||||
|
};
|
||||||
|
var id = _repo.Create(entity);
|
||||||
|
Assert.True(id > 0);
|
||||||
|
var loaded = _repo.GetById(id);
|
||||||
|
Assert.Equal(1, loaded.IsEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Update_修改启用状态()
|
||||||
|
{
|
||||||
|
TestDb.Execute(@"INSERT INTO cnc_brand_field_mapping (brand_id, standard_field, field_name, match_by, data_type, is_required, is_enabled, created_at)
|
||||||
|
VALUES (1, 'program_name', 'Tag5', 'id', 'string', 1, 1, NOW())");
|
||||||
|
var id = TestDb.QuerySingle<int>("SELECT MAX(id) FROM cnc_brand_field_mapping");
|
||||||
|
var entity = _repo.GetById(id);
|
||||||
|
entity.IsEnabled = 0;
|
||||||
|
var result = _repo.Update(entity);
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.Equal(0, _repo.GetById(id).IsEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BatchCreate_批量插入保留启用状态()
|
||||||
|
{
|
||||||
|
// 确保 brand_id=1 存在
|
||||||
|
TestDb.Execute(@"INSERT IGNORE INTO cnc_brand (id, brand_name, device_field, tags_path, is_enabled, created_at, updated_at)
|
||||||
|
VALUES (1, 'FANUC', 'device', 'tags', 1, NOW(), NOW())");
|
||||||
|
var mappings = new[]
|
||||||
|
{
|
||||||
|
new BrandFieldMapping { StandardField = "f1", FieldName = "Tag1", MatchBy = "id", DataType = "string", IsRequired = 0, IsEnabled = 1, CreatedAt = DateTime.Now },
|
||||||
|
new BrandFieldMapping { StandardField = "f2", FieldName = "Tag2", MatchBy = "id", DataType = "number", IsRequired = 0, IsEnabled = 1, CreatedAt = DateTime.Now },
|
||||||
|
new BrandFieldMapping { StandardField = "f3", FieldName = "Tag3", MatchBy = "id", DataType = "string", IsRequired = 0, IsEnabled = 0, CreatedAt = DateTime.Now },
|
||||||
|
}.ToList();
|
||||||
|
var count = _repo.BatchCreate(1, mappings);
|
||||||
|
Assert.Equal(3, count);
|
||||||
|
var all = _repo.GetByBrandId(1);
|
||||||
|
Assert.Equal(3, all.Count);
|
||||||
|
var enabled = _repo.GetEnabledByBrandId(1);
|
||||||
|
Assert.Equal(2, enabled.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Update_修改字段名和启用状态同时生效()
|
||||||
|
{
|
||||||
|
TestDb.Execute(@"INSERT INTO cnc_brand_field_mapping (brand_id, standard_field, field_name, match_by, data_type, is_required, is_enabled, created_at)
|
||||||
|
VALUES (1, 'program_name', 'Tag5', 'id', 'string', 1, 1, NOW())");
|
||||||
|
var id = TestDb.QuerySingle<int>("SELECT MAX(id) FROM cnc_brand_field_mapping");
|
||||||
|
var entity = _repo.GetById(id);
|
||||||
|
entity.FieldName = "Tag5_New";
|
||||||
|
entity.IsEnabled = 0;
|
||||||
|
_repo.Update(entity);
|
||||||
|
var loaded = _repo.GetById(id);
|
||||||
|
Assert.Equal("Tag5_New", loaded.FieldName);
|
||||||
|
Assert.Equal(0, loaded.IsEnabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,26 +1,18 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net472</TargetFramework>
|
<TargetFramework>net472</TargetFramework>
|
||||||
<PlatformTarget>x64</PlatformTarget>
|
<IsPackable>false</IsPackable>
|
||||||
<RootNamespace>CncService.Tests</RootNamespace>
|
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||||
<AssemblyName>CncService.Tests</AssemblyName>
|
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
|
||||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies.net472" Version="1.0.3" PrivateAssets="all" />
|
<PackageReference Include="xunit" Version="2.4.2" />
|
||||||
<PackageReference Include="xunit" Version="2.8.1" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1" />
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PackageReference Include="Moq" Version="4.20.72" />
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\src\CncModels\CncModels.csproj" />
|
<ProjectReference Include="../../src/CncService/CncService.csproj" />
|
||||||
<ProjectReference Include="..\..\src\CncRepository\CncRepository.csproj" />
|
|
||||||
<ProjectReference Include="..\..\src\CncService\CncService.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -1,149 +1,118 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Xunit;
|
||||||
using CncModels.Dto.Dashboard;
|
using CncModels.Dto.Dashboard;
|
||||||
using CncService;
|
using CncModels.Entity;
|
||||||
|
using CncRepository.Interface;
|
||||||
|
using CncService.Interface;
|
||||||
using CncService.Impl;
|
using CncService.Impl;
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace CncService.Tests
|
namespace CncService.Tests
|
||||||
{
|
{
|
||||||
/// <summary>
|
// Fake repositories to isolate DashboardService.GetCollectorStatus tests
|
||||||
/// DashboardService 仪表盘测试
|
public class FakeDashboardRepository : IDashboardRepository
|
||||||
/// 测试场景:汇总查询、车间产量、机床排名、工人排名、趋势、状态分布、采集器状态
|
|
||||||
/// </summary>
|
|
||||||
[Collection("Database")]
|
|
||||||
public class DashboardServiceTests : IDisposable
|
|
||||||
{
|
{
|
||||||
private readonly DashboardService _service;
|
public DashboardSummaryResponse GetSummary(int something) => new DashboardSummaryResponse();
|
||||||
|
public List<WorkshopProductionResponse> GetWorkshopProduction(DateTime startDate, DateTime endDate) => new List<WorkshopProductionResponse>();
|
||||||
public DashboardServiceTests()
|
public List<MachineRankResponse> GetMachineRank(DateTime startDate, DateTime endDate, int top, int something, string sortOrder = "desc") => new List<MachineRankResponse>();
|
||||||
{
|
public List<WorkerRankResponse> GetWorkerRank(DateTime startDate, DateTime endDate, int top, string sortOrder = "desc") => new List<WorkerRankResponse>();
|
||||||
TestDb.TruncateAll();
|
public List<dynamic> GetProductionTrend(int days) => new List<dynamic>();
|
||||||
_service = ServiceFactory.CreateDashboardService();
|
public object GetMachineStatusDistribution(int something) => new object();
|
||||||
}
|
public List<AlertListItem> GetRecentAlerts(int count) => new List<AlertListItem>();
|
||||||
|
}
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
TestDb.TruncateAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======== GetSummary ========
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetSummary_无数据_返回默认汇总()
|
|
||||||
{
|
|
||||||
var summary = _service.GetSummary();
|
|
||||||
Assert.NotNull(summary);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======== GetWorkshopProduction ========
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetWorkshopProduction_无数据_返回空列表()
|
|
||||||
{
|
|
||||||
var result = _service.GetWorkshopProduction(null, null);
|
|
||||||
Assert.NotNull(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetWorkshopProduction_指定日期范围()
|
|
||||||
{
|
|
||||||
var start = new DateTime(2026, 1, 1);
|
|
||||||
var end = new DateTime(2026, 12, 31);
|
|
||||||
var result = _service.GetWorkshopProduction(start, end);
|
|
||||||
Assert.NotNull(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======== GetMachineRank ========
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetMachineRank_无数据_返回空列表()
|
|
||||||
{
|
|
||||||
var result = _service.GetMachineRank(null, null, 10);
|
|
||||||
Assert.NotNull(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetMachineRank_指定Top数量()
|
|
||||||
{
|
|
||||||
var result = _service.GetMachineRank(null, null, 5);
|
|
||||||
Assert.NotNull(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======== GetWorkerRank ========
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetWorkerRank_无数据_返回空列表()
|
|
||||||
{
|
|
||||||
var result = _service.GetWorkerRank(null, null, 10);
|
|
||||||
Assert.NotNull(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======== GetProductionTrend ========
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetProductionTrend_默认7天()
|
|
||||||
{
|
|
||||||
var result = _service.GetProductionTrend();
|
|
||||||
Assert.NotNull(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetProductionTrend_指定天数()
|
|
||||||
{
|
|
||||||
var result = _service.GetProductionTrend(30);
|
|
||||||
Assert.NotNull(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======== GetMachineStatusDistribution ========
|
|
||||||
|
|
||||||
[Fact]
|
public class FakeCollectorHeartbeatRepository : ICollectorHeartbeatRepository
|
||||||
public void GetMachineStatusDistribution_无数据_返回结果()
|
{
|
||||||
{
|
private readonly CollectorHeartbeat _latest;
|
||||||
var result = _service.GetMachineStatusDistribution();
|
public FakeCollectorHeartbeatRepository(CollectorHeartbeat latest) { _latest = latest; }
|
||||||
Assert.NotNull(result);
|
public long Create(CollectorHeartbeat entity) => 1;
|
||||||
}
|
public CollectorHeartbeat GetLatest(string serviceId) => _latest;
|
||||||
|
public int DeleteBeforeDate(DateTime date) => 0;
|
||||||
|
}
|
||||||
|
|
||||||
// ======== GetRecentAlerts ========
|
public class FakeWindowsServiceChecker : IWindowsServiceChecker
|
||||||
|
{
|
||||||
|
private readonly ServiceStatusEnum _status;
|
||||||
|
public FakeWindowsServiceChecker(ServiceStatusEnum status) { _status = status; }
|
||||||
|
public ServiceStatusEnum GetServiceStatus(string serviceName) => _status;
|
||||||
|
public (bool, string) TryStartService(string serviceName, int timeoutSeconds) => (
|
||||||
|
_status == ServiceStatusEnum.NotInstalled ? false : true,
|
||||||
|
_status == ServiceStatusEnum.NotInstalled ? "NotInstalled" : "Started");
|
||||||
|
public (bool, string) TryStopService(string serviceName, int timeoutSeconds) => (
|
||||||
|
true, "Stopped");
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
public class FakeSysConfigRepository : ISysConfigRepository
|
||||||
public void GetRecentAlerts_无数据_返回空列表()
|
{
|
||||||
{
|
public SysConfig GetByKey(string configKey) => new SysConfig { ConfigKey = configKey, ConfigValue = "300" };
|
||||||
var result = _service.GetRecentAlerts(5);
|
public List<SysConfig> GetAll() => new List<SysConfig>();
|
||||||
Assert.NotNull(result);
|
public bool UpdateValue(int id, string value) => true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class DashboardServiceTests
|
||||||
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetRecentAlerts_有告警数据()
|
public void GetCollectorStatus_With_NotInstalled_Service_Returns_NotInstalled_State()
|
||||||
{
|
{
|
||||||
TestDb.Execute(@"INSERT INTO cnc_collect_address (name, url, brand_id, collect_interval, is_enabled, created_at, updated_at)
|
// Arrange
|
||||||
VALUES ('测试地址', 'http://test', 1, 30, 1, NOW(), NOW())");
|
var latest = new CollectorHeartbeat { Id = 1, ServiceId = "collector-service", Status = "running", UptimeSeconds = 120, LastCollectTime = DateTime.Now, CreatedAt = DateTime.Now };
|
||||||
TestDb.Execute(@"INSERT INTO cnc_machine (device_code, name, workshop_id, collect_address_id, ip_address, brand_id, is_enabled, is_online, created_at, updated_at)
|
var dashboardRepo = new FakeDashboardRepository();
|
||||||
VALUES ('M001', '机床1', 1, 1, '0.0.0.0', 1, 1, 0, NOW(), NOW())");
|
var heartbeatRepo = new FakeCollectorHeartbeatRepository(latest);
|
||||||
TestDb.Execute(@"INSERT INTO cnc_alert (alert_type, machine_id, title, is_resolved, created_at)
|
var checker = new FakeWindowsServiceChecker(CncService.Interface.ServiceStatusEnum.NotInstalled);
|
||||||
VALUES ('offline', 1, '告警1', 0, NOW()),
|
|
||||||
('offline', 1, '告警2', 0, NOW())");
|
var svc = new DashboardService(dashboardRepo, heartbeatRepo, new FakeSysConfigRepository(), checker);
|
||||||
|
|
||||||
var result = _service.GetRecentAlerts(5);
|
// Act
|
||||||
Assert.True(result.Count >= 2);
|
var resultObj = svc.GetCollectorStatus();
|
||||||
|
var t = resultObj.GetType();
|
||||||
|
var statusProp = t.GetProperty("status");
|
||||||
|
var serviceStatusProp = t.GetProperty("serviceStatus");
|
||||||
|
var uptimeProp = t.GetProperty("uptimeSeconds");
|
||||||
|
var lastCollectTimeProp = t.GetProperty("lastCollectTime");
|
||||||
|
Assert.NotNull(statusProp);
|
||||||
|
Assert.NotNull(serviceStatusProp);
|
||||||
|
var statusVal = statusProp.GetValue(resultObj) as string;
|
||||||
|
var serviceStatusVal = serviceStatusProp.GetValue(resultObj) as string;
|
||||||
|
Assert.Equal("stopped", statusVal);
|
||||||
|
Assert.Equal("NotInstalled", serviceStatusVal);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======== GetCollectorStatus ========
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetCollectorStatus_无心跳_返回未运行()
|
public void GetCollectorStatus_With_Running_Heartbeats_Returns_Running_State()
|
||||||
{
|
{
|
||||||
var result = _service.GetCollectorStatus();
|
var latest = new CollectorHeartbeat { Id = 1, ServiceId = "collector-service", Status = "running", UptimeSeconds = 60, LastCollectTime = DateTime.Now, CreatedAt = DateTime.Now };
|
||||||
Assert.NotNull(result);
|
var dashboardRepo = new FakeDashboardRepository();
|
||||||
|
var heartbeatRepo = new FakeCollectorHeartbeatRepository(latest);
|
||||||
|
var checker = new FakeWindowsServiceChecker(CncService.Interface.ServiceStatusEnum.Running);
|
||||||
|
var svc = new DashboardService(dashboardRepo, heartbeatRepo, new FakeSysConfigRepository(), checker);
|
||||||
|
|
||||||
|
var resultObj = svc.GetCollectorStatus();
|
||||||
|
var t = resultObj.GetType();
|
||||||
|
var serviceStatusProp = t.GetProperty("serviceStatus");
|
||||||
|
var statusProp = t.GetProperty("status");
|
||||||
|
var serviceStatusVal = serviceStatusProp.GetValue(resultObj) as string;
|
||||||
|
var statusVal = statusProp.GetValue(resultObj) as string;
|
||||||
|
Assert.Equal("Running", serviceStatusVal);
|
||||||
|
Assert.Equal("running", statusVal);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetCollectorStatus_有最近心跳_返回运行中()
|
public void GetCollectorStatus_With_Starting_ServiceStatus_Returns_Starting_State()
|
||||||
{
|
{
|
||||||
TestDb.Execute(@"INSERT INTO log_collector_heartbeat (service_id, status, last_collect_time, success_count, fail_count, created_at)
|
var latest = new CollectorHeartbeat { Id = 2, ServiceId = "collector-service", Status = "running", UptimeSeconds = 120, LastCollectTime = DateTime.Now, CreatedAt = DateTime.Now };
|
||||||
VALUES ('collector-service', 'running', NOW(), 1, 0, NOW())");
|
var dashboardRepo = new FakeDashboardRepository();
|
||||||
|
var heartbeatRepo = new FakeCollectorHeartbeatRepository(latest);
|
||||||
var result = _service.GetCollectorStatus();
|
var checker = new FakeWindowsServiceChecker(CncService.Interface.ServiceStatusEnum.Starting);
|
||||||
Assert.NotNull(result);
|
var svc = new DashboardService(dashboardRepo, heartbeatRepo, new FakeSysConfigRepository(), checker);
|
||||||
|
|
||||||
|
var resultObj = svc.GetCollectorStatus();
|
||||||
|
var t = resultObj.GetType();
|
||||||
|
var serviceStatusProp = t.GetProperty("serviceStatus");
|
||||||
|
var statusProp = t.GetProperty("status");
|
||||||
|
var serviceStatusVal = serviceStatusProp.GetValue(resultObj) as string;
|
||||||
|
var statusVal = statusProp.GetValue(resultObj) as string;
|
||||||
|
Assert.Equal("Starting", serviceStatusVal);
|
||||||
|
Assert.Equal("running", statusVal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue