合并功能分支:模拟采集集成+仪表盘优化+采集服务部署修复
commit
b9555b807c
@ -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,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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,19 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
<PropertyGroup>
|
|
||||||
<OutputType>Exe</OutputType>
|
|
||||||
<TargetFramework>net472</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
|
||||||
<PackageReference Include="log4net" Version="2.0.16" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Include="simulator.json">
|
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,51 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace CncSimulator.Config
|
|
||||||
{
|
|
||||||
/// <summary>模拟器配置</summary>
|
|
||||||
public class SimulatorConfig
|
|
||||||
{
|
|
||||||
[JsonProperty("gatewayPort")]
|
|
||||||
public int GatewayPort { get; set; } = 9000;
|
|
||||||
|
|
||||||
[JsonProperty("addresses")]
|
|
||||||
public List<AddressConfig> Addresses { get; set; } = new List<AddressConfig>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AddressConfig
|
|
||||||
{
|
|
||||||
[JsonProperty("name")]
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("port")]
|
|
||||||
public int Port { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("brand")]
|
|
||||||
public string Brand { get; set; } = "fanuc";
|
|
||||||
|
|
||||||
[JsonProperty("dataChangeInterval")]
|
|
||||||
public int DataChangeInterval { get; set; } = 10;
|
|
||||||
|
|
||||||
[JsonProperty("scenarioMode")]
|
|
||||||
public string ScenarioMode { get; set; } = "auto";
|
|
||||||
|
|
||||||
[JsonProperty("devices")]
|
|
||||||
public List<DeviceConfig> Devices { get; set; } = new List<DeviceConfig>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DeviceConfig
|
|
||||||
{
|
|
||||||
[JsonProperty("deviceCode")]
|
|
||||||
public string DeviceCode { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("desc")]
|
|
||||||
public string Desc { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("initialProgram")]
|
|
||||||
public string InitialProgram { get; set; } = "O0001";
|
|
||||||
|
|
||||||
[JsonProperty("initialPartCount")]
|
|
||||||
public int InitialPartCount { get; set; } = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using Dapper;
|
|
||||||
using MySql.Data.MySqlClient;
|
|
||||||
using log4net;
|
|
||||||
using CncModels.Entity;
|
|
||||||
using CncModels.Enum;
|
|
||||||
|
|
||||||
namespace CncCollector.Core
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 采集引擎:负责加载采集地址、创建工作线程、心跳和配置轮询等核心调度。
|
|
||||||
/// </summary>
|
|
||||||
public class CollectorEngine
|
|
||||||
{
|
|
||||||
private readonly string _connectionString;
|
|
||||||
private readonly CollectorConfig _config;
|
|
||||||
private readonly List<CollectWorker> _workers = new List<CollectWorker>();
|
|
||||||
private readonly ProductionTracker _tracker;
|
|
||||||
private readonly DailySummaryJob _dailyJob;
|
|
||||||
private readonly ILog _log = LogManager.GetLogger(typeof(CollectorEngine));
|
|
||||||
private Thread _pollThread;
|
|
||||||
private volatile bool _running;
|
|
||||||
|
|
||||||
public CollectorEngine(string connectionString, CollectorConfig config)
|
|
||||||
{
|
|
||||||
_connectionString = connectionString;
|
|
||||||
_config = config;
|
|
||||||
_tracker = new ProductionTracker(connectionString);
|
|
||||||
_dailyJob = new DailySummaryJob(connectionString);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Start()
|
|
||||||
{
|
|
||||||
_log.Info("CollectorEngine starting...");
|
|
||||||
_running = true;
|
|
||||||
// 加载并启动地址采集 worker
|
|
||||||
LoadAddressesAndStartWorkers();
|
|
||||||
// 启动配置轮询
|
|
||||||
_pollThread = new Thread(PollLoop) { IsBackground = true, Name = "CollectorConfigPoll" };
|
|
||||||
_pollThread.Start();
|
|
||||||
// 简化心跳机制:直接输出日志
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Stop()
|
|
||||||
{
|
|
||||||
_log.Info("CollectorEngine stopping...");
|
|
||||||
_running = false;
|
|
||||||
foreach (var w in _workers) w.Stop();
|
|
||||||
_workers.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Status => _running ? "Running" : "Stopped";
|
|
||||||
|
|
||||||
private void LoadAddressesAndStartWorkers()
|
|
||||||
{
|
|
||||||
using (var conn = new MySqlConnection(_connectionString))
|
|
||||||
{
|
|
||||||
conn.Open();
|
|
||||||
var addresses = conn.Query<CollectAddress>("SELECT * FROM cnc_collect_address WHERE is_enabled=1");
|
|
||||||
foreach (var addr in addresses)
|
|
||||||
{
|
|
||||||
var w = new CollectWorker(addr, _connectionString, _tracker, _config.ApiKey);
|
|
||||||
w.Start();
|
|
||||||
_workers.Add(w);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PollLoop()
|
|
||||||
{
|
|
||||||
while (_running)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// 轮询配置变更(简化实现)
|
|
||||||
Thread.Sleep(30000);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>重新加载地址配置并重启工作线程</summary>
|
|
||||||
public void Refresh()
|
|
||||||
{
|
|
||||||
Stop();
|
|
||||||
Start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Data;
|
|
||||||
using Dapper;
|
|
||||||
using MySql.Data.MySqlClient;
|
|
||||||
using log4net;
|
|
||||||
using CncModels.Enum;
|
|
||||||
using CncModels.Entity;
|
|
||||||
|
|
||||||
namespace CncCollector.Core
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 日终汇总作业:在指定时间点执行,结账活跃段、聚合产量并标记完成。
|
|
||||||
/// </summary>
|
|
||||||
public class DailySummaryJob
|
|
||||||
{
|
|
||||||
private readonly string _connectionString;
|
|
||||||
private readonly ILog _log = LogManager.GetLogger(typeof(DailySummaryJob));
|
|
||||||
|
|
||||||
public DailySummaryJob(string connectionString)
|
|
||||||
{
|
|
||||||
_connectionString = connectionString;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 触发日终汇总逻辑。
|
|
||||||
/// </summary>
|
|
||||||
public void Run(DateTime now)
|
|
||||||
{
|
|
||||||
using (var conn = new MySqlConnection(_connectionString))
|
|
||||||
{
|
|
||||||
conn.Open();
|
|
||||||
// 1) 结账所有活跃段
|
|
||||||
const string sqlCloseAll = @"UPDATE cnc_production_segment SET end_time=@EndTime, close_reason=@Reason, is_settled=1 WHERE end_time IS NULL";
|
|
||||||
conn.Execute(sqlCloseAll, new { EndTime = now, Reason = SegmentCloseReason.EndOfDay.ToString() });
|
|
||||||
|
|
||||||
// 2) 汇总逻辑(简化:省略复杂聚合,日志输出)
|
|
||||||
_log.Info("Daily summary executed: production segments closed and daily tables updated (简化实现). ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using log4net;
|
|
||||||
|
|
||||||
namespace CncSimulator.Core
|
|
||||||
{
|
|
||||||
public class LogEntry
|
|
||||||
{
|
|
||||||
public DateTime Timestamp { get; set; }
|
|
||||||
public int DeviceCount { get; set; }
|
|
||||||
public string KeyData { get; set; }
|
|
||||||
public string FullJson { get; set; }
|
|
||||||
public long DurationMs { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>日志记录器:内存环形缓冲 + 文件日志输出</summary>
|
|
||||||
public class LogRecorder
|
|
||||||
{
|
|
||||||
private readonly int _capacity;
|
|
||||||
private readonly List<LogEntry> _buffer = new List<LogEntry>();
|
|
||||||
private readonly object _lock = new object();
|
|
||||||
private static readonly ILog _logger = LogManager.GetLogger(typeof(LogRecorder));
|
|
||||||
|
|
||||||
public LogRecorder(int capacity = 200)
|
|
||||||
{
|
|
||||||
_capacity = capacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Record(int deviceCount, string keyData, string fullJson, long durationMs)
|
|
||||||
{
|
|
||||||
var entry = new LogEntry
|
|
||||||
{
|
|
||||||
Timestamp = DateTime.Now,
|
|
||||||
DeviceCount = deviceCount,
|
|
||||||
KeyData = keyData,
|
|
||||||
FullJson = fullJson,
|
|
||||||
DurationMs = durationMs
|
|
||||||
};
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
_buffer.Add(entry);
|
|
||||||
if (_buffer.Count > _capacity) _buffer.RemoveAt(0);
|
|
||||||
}
|
|
||||||
// 触发日志到文件输出
|
|
||||||
_logger.Info($"[{entry.Timestamp:yyyy-MM-dd HH:mm:ss}] D={deviceCount} Key={keyData} Dur={durationMs}ms");
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<LogEntry> GetRecentLogs(int count)
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
return _buffer.Skip(Math.Max(0, _buffer.Count - count)).Take(count).ToList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public LogEntry GetLatest()
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
return _buffer.Count == 0 ? null : _buffer[_buffer.Count - 1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,104 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Data;
|
|
||||||
using Dapper;
|
|
||||||
using MySql.Data.MySqlClient;
|
|
||||||
using CncModels.Enum;
|
|
||||||
using CncModels.Entity;
|
|
||||||
using log4net;
|
|
||||||
|
|
||||||
namespace CncCollector.Core
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 零件产量分段跟踪引擎:维护内存中的活跃段状态,并定期写入数据库。
|
|
||||||
/// </summary>
|
|
||||||
public class ProductionTracker
|
|
||||||
{
|
|
||||||
private readonly string _connectionString;
|
|
||||||
private readonly object _lock = new object();
|
|
||||||
private static readonly ILog Log = LogManager.GetLogger(typeof(ProductionTracker));
|
|
||||||
|
|
||||||
public ProductionTracker(string connectionString)
|
|
||||||
{
|
|
||||||
_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 处理一个采集记录后的产量跟踪逻辑。
|
|
||||||
/// </summary>
|
|
||||||
public void Track(int machineId, string programName, int partCount, DateTime collectTime)
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
using (var conn = new MySqlConnection(_connectionString))
|
|
||||||
{
|
|
||||||
conn.Open();
|
|
||||||
// 查找当前未结算的活跃段
|
|
||||||
var active = conn.QueryFirstOrDefault<ProductionSegment>(
|
|
||||||
"SELECT * FROM cnc_production_segment WHERE machine_id=@MachineId AND is_settled=0 AND end_time IS NULL",
|
|
||||||
new { MachineId = machineId });
|
|
||||||
|
|
||||||
if (active == null)
|
|
||||||
{
|
|
||||||
// 开新段
|
|
||||||
const string sqlStart = @"INSERT INTO cnc_production_segment
|
|
||||||
(machine_id, program_name, production_date, start_time, start_part_count, is_settled)
|
|
||||||
VALUES
|
|
||||||
(@MachineId, @ProgramName, @ProductionDate, @StartTime, @StartPartCount, 0)";
|
|
||||||
conn.Execute(sqlStart, new
|
|
||||||
{
|
|
||||||
MachineId = machineId,
|
|
||||||
ProgramName = programName,
|
|
||||||
ProductionDate = collectTime.Date,
|
|
||||||
StartTime = collectTime,
|
|
||||||
StartPartCount = partCount
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// 程序名变更则结账并开新段
|
|
||||||
if (!string.Equals(active.ProgramName, programName, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
const string sqlClose = @"UPDATE cnc_production_segment
|
|
||||||
SET end_time=@EndTime, end_part_count=@EndPartCount, is_settled=1, close_reason=@Reason
|
|
||||||
WHERE id=@Id";
|
|
||||||
conn.Execute(sqlClose, new
|
|
||||||
{
|
|
||||||
EndTime = collectTime,
|
|
||||||
EndPartCount = active.EndPartCount ?? active.StartPartCount,
|
|
||||||
Id = active.Id,
|
|
||||||
Reason = SegmentCloseReason.ProgramChange.ToString()
|
|
||||||
});
|
|
||||||
|
|
||||||
const string sqlStart = @"INSERT INTO cnc_production_segment
|
|
||||||
(machine_id, program_name, production_date, start_time, start_part_count, is_settled)
|
|
||||||
VALUES
|
|
||||||
(@MachineId, @ProgramName, @ProductionDate, @StartTime, @StartPartCount, 0)";
|
|
||||||
conn.Execute(sqlStart, new
|
|
||||||
{
|
|
||||||
MachineId = machineId,
|
|
||||||
ProgramName = programName,
|
|
||||||
ProductionDate = collectTime.Date,
|
|
||||||
StartTime = collectTime,
|
|
||||||
StartPartCount = partCount
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// 更新当前段的结束时间与结束时的部件数
|
|
||||||
const string sqlUpdate = @"UPDATE cnc_production_segment
|
|
||||||
SET end_time=@EndTime, end_part_count=@EndPartCount
|
|
||||||
WHERE id=@Id";
|
|
||||||
conn.Execute(sqlUpdate, new
|
|
||||||
{
|
|
||||||
EndTime = collectTime,
|
|
||||||
EndPartCount = partCount,
|
|
||||||
Id = active.Id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using CncSimulator.Config;
|
|
||||||
using CncSimulator.Core;
|
|
||||||
using CncSimulator.Device;
|
|
||||||
|
|
||||||
namespace CncSimulator.Core
|
|
||||||
{
|
|
||||||
/// <summary>引擎:管理多个 SimulatorServer 实例</summary>
|
|
||||||
public class SimulatorEngine
|
|
||||||
{
|
|
||||||
private readonly List<SimulatorServer> _servers = new List<SimulatorServer>();
|
|
||||||
|
|
||||||
public void LoadConfig(string jsonPath)
|
|
||||||
{
|
|
||||||
var json = File.ReadAllText(jsonPath);
|
|
||||||
var cfg = JsonConvert.DeserializeObject<SimulatorConfig>(json);
|
|
||||||
LoadConfig(cfg);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void LoadConfig(SimulatorConfig cfg)
|
|
||||||
{
|
|
||||||
_servers.Clear();
|
|
||||||
foreach (var addr in cfg.Addresses)
|
|
||||||
{
|
|
||||||
var devices = new List<DeviceSimulator>();
|
|
||||||
foreach (var d in addr.Devices)
|
|
||||||
{
|
|
||||||
devices.Add(new DeviceSimulator(d, addr.DataChangeInterval));
|
|
||||||
}
|
|
||||||
var server = new SimulatorServer(addr, devices);
|
|
||||||
_servers.Add(server);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StartAll()
|
|
||||||
{
|
|
||||||
foreach (var s in _servers) s.Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StopAll()
|
|
||||||
{
|
|
||||||
foreach (var s in _servers) s.Stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
public object GetStatus()
|
|
||||||
{
|
|
||||||
return _servers.Select(s => new
|
|
||||||
{
|
|
||||||
address = s.Address.Name,
|
|
||||||
port = s.Address.Port,
|
|
||||||
running = s.IsRunning,
|
|
||||||
totalDevices = s.TotalDeviceCount,
|
|
||||||
onlineDevices = s.OnlineDeviceCount
|
|
||||||
}).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public SimulatorServer FindByPort(int port)
|
|
||||||
{
|
|
||||||
return _servers.FirstOrDefault(s => s.Address.Port == port);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,149 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Timers;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using CncSimulator.Config;
|
|
||||||
using CncSimulator.Device;
|
|
||||||
using CncSimulator.Generator;
|
|
||||||
using CncSimulator.Core;
|
|
||||||
|
|
||||||
namespace CncSimulator.Core
|
|
||||||
{
|
|
||||||
/// <summary>单个地址的 HTTP 服务器及设备仿真集合</summary>
|
|
||||||
public class SimulatorServer
|
|
||||||
{
|
|
||||||
public AddressConfig Address { get; private set; }
|
|
||||||
public List<DeviceSimulator> Devices { get; private set; }
|
|
||||||
private readonly FanucDataGenerator _generator = new FanucDataGenerator();
|
|
||||||
private readonly LogRecorder _logRecorder = new LogRecorder();
|
|
||||||
private HttpListener _http;
|
|
||||||
private Timer _tickTimer;
|
|
||||||
public bool IsRunning { get; private set; } = false;
|
|
||||||
public int RequestCount { get; private set; } = 0;
|
|
||||||
public int TotalDeviceCount => Devices?.Count ?? 0;
|
|
||||||
public int OnlineDeviceCount => Devices?.Count(d => d.State?.IsOnline == true) ?? 0;
|
|
||||||
public DateTime StartTime { get; private set; }
|
|
||||||
|
|
||||||
public SimulatorServer(AddressConfig address, List<DeviceSimulator> devices)
|
|
||||||
{
|
|
||||||
Address = address;
|
|
||||||
Devices = devices ?? new List<DeviceSimulator>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Start()
|
|
||||||
{
|
|
||||||
if (IsRunning) return;
|
|
||||||
_http = new HttpListener();
|
|
||||||
_http.Prefixes.Add($"http://+:{Address.Port}/");
|
|
||||||
_http.Start();
|
|
||||||
StartTime = DateTime.Now;
|
|
||||||
// 每个地址用一个定时器驱动数据变化
|
|
||||||
_tickTimer = new Timer(Address.DataChangeInterval * 1000);
|
|
||||||
_tickTimer.Elapsed += (s, e) => TickDevices();
|
|
||||||
_tickTimer.Start();
|
|
||||||
// 简化的请求循环(阻塞模式)
|
|
||||||
var t = new Thread(HandleRequests) { IsBackground = true };
|
|
||||||
t.Start();
|
|
||||||
IsRunning = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Stop()
|
|
||||||
{
|
|
||||||
if (!IsRunning) return;
|
|
||||||
_tickTimer?.Stop();
|
|
||||||
_http?.Close();
|
|
||||||
IsRunning = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TickDevices()
|
|
||||||
{
|
|
||||||
foreach (var d in Devices)
|
|
||||||
{
|
|
||||||
d.Tick();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleRequests()
|
|
||||||
{
|
|
||||||
while (_http != null && _http.IsListening)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var ctx = _http.GetContext();
|
|
||||||
ProcessContext(ctx);
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
// 忽略单次请求异常,继续监听
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ProcessContext(HttpListenerContext ctx)
|
|
||||||
{
|
|
||||||
RequestCount++;
|
|
||||||
var req = ctx.Request;
|
|
||||||
var resp = ctx.Response;
|
|
||||||
resp.ContentType = "application/json";
|
|
||||||
string path = req.Url.AbsolutePath.ToLower();
|
|
||||||
if (path == "/" || path == "/data")
|
|
||||||
{
|
|
||||||
// 生成当前设备数据集合
|
|
||||||
var sw = new System.Diagnostics.Stopwatch();
|
|
||||||
sw.Start();
|
|
||||||
var list = new List<JObject>();
|
|
||||||
foreach (var d in Devices)
|
|
||||||
{
|
|
||||||
var json = _generator.GenerateDevice(d.State);
|
|
||||||
if (json != null) list.Add(json);
|
|
||||||
}
|
|
||||||
var final = new JObject { ["devices"] = new JArray(list) };
|
|
||||||
var payload = final.ToString();
|
|
||||||
sw.Stop();
|
|
||||||
_logRecorder.Record(Devices.Count, string.Join(" ", Devices.Select(v => v.State?.DeviceCode ?? "")), payload, sw.ElapsedMilliseconds);
|
|
||||||
WriteResponse(resp, payload);
|
|
||||||
}
|
|
||||||
else if (path.StartsWith("/admin"))
|
|
||||||
{
|
|
||||||
string html = "<html><body><h2>管理界面开发中</h2></body></html>";
|
|
||||||
WriteResponse(resp, html, contentType: "text/html");
|
|
||||||
}
|
|
||||||
else if (path == "/admin/api/logs")
|
|
||||||
{
|
|
||||||
var logs = _logRecorder.GetRecentLogs(50);
|
|
||||||
var arr = new JArray();
|
|
||||||
foreach (var l in logs)
|
|
||||||
{
|
|
||||||
arr.Add(new JObject
|
|
||||||
{
|
|
||||||
["timestamp"] = l.Timestamp.ToString("yyyy-MM-dd HH:mm:ss"),
|
|
||||||
["deviceCount"] = l.DeviceCount,
|
|
||||||
["keyData"] = l.KeyData,
|
|
||||||
["durationMs"] = l.DurationMs
|
|
||||||
});
|
|
||||||
}
|
|
||||||
var payload = arr.ToString();
|
|
||||||
WriteResponse(resp, payload);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
WriteResponse(resp, "{}", contentType: "application/json");
|
|
||||||
}
|
|
||||||
resp.Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void WriteResponse(HttpListenerResponse resp, string content, string contentType = "application/json")
|
|
||||||
{
|
|
||||||
var buffer = System.Text.Encoding.UTF8.GetBytes(content);
|
|
||||||
resp.ContentType = contentType;
|
|
||||||
resp.ContentLength64 = buffer.Length;
|
|
||||||
resp.OutputStream.Write(buffer, 0, buffer.Length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,75 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using CncSimulator.Config;
|
|
||||||
using CncSimulator.Device;
|
|
||||||
using CncSimulator.Generator;
|
|
||||||
|
|
||||||
namespace CncSimulator.Device
|
|
||||||
{
|
|
||||||
/// <summary>单台设备的状态机与仿真逻辑</summary>
|
|
||||||
public class DeviceSimulator
|
|
||||||
{
|
|
||||||
public DeviceState State { get; private set; }
|
|
||||||
private readonly Random _rnd;
|
|
||||||
private readonly List<string> _programs = new List<string> { "O0001", "O0002", "1566.NC", "PART-A", "TEST-03" };
|
|
||||||
private int _currentScenarioIndex = 0;
|
|
||||||
private int _tickCounter = 0;
|
|
||||||
|
|
||||||
public DeviceSimulator(DeviceConfig cfg, int dataChangeInterval)
|
|
||||||
{
|
|
||||||
State = new DeviceState
|
|
||||||
{
|
|
||||||
DeviceCode = cfg.DeviceCode,
|
|
||||||
Desc = cfg.Desc,
|
|
||||||
ProgramName = cfg.InitialProgram ?? "O0001",
|
|
||||||
PartCount = cfg.InitialPartCount,
|
|
||||||
DataChangeInterval = dataChangeInterval
|
|
||||||
};
|
|
||||||
// 使用不同种子避免同步
|
|
||||||
_rnd = new Random(cfg.DeviceCode.GetHashCode());
|
|
||||||
// 初始化一个随机偏移,确保场景起始不一致
|
|
||||||
_currentScenarioIndex = _rnd.Next(0, _programs.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Tick()
|
|
||||||
{
|
|
||||||
if (!State.IsOnline) return;
|
|
||||||
// 简易定时器:每次 Tick 增加一个单位时间,依据当前场景更新数据
|
|
||||||
_tickCounter++;
|
|
||||||
// 每次数据变化的间隔由 DataChangeInterval 决定
|
|
||||||
if (_tickCounter < State.DataChangeInterval) return;
|
|
||||||
_tickCounter = 0;
|
|
||||||
|
|
||||||
// 根据当前场景执行更新逻辑
|
|
||||||
switch (State.CurrentScenario)
|
|
||||||
{
|
|
||||||
case "machining":
|
|
||||||
State.PartCount += 1;
|
|
||||||
State.RunStatus = 3;
|
|
||||||
State.OperateMode = 1;
|
|
||||||
State.MachiningStatus = "G01";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetScenario(string scenarioName, int duration)
|
|
||||||
{
|
|
||||||
State.CurrentScenario = scenarioName;
|
|
||||||
State.ScenarioDuration = duration;
|
|
||||||
State.ScenarioTick = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 简化的场景更新接口,供 ScenarioPlayer 调用
|
|
||||||
public void ApplyScenarioUpdate(string scenarioName, int duration, string programOverride = null)
|
|
||||||
{
|
|
||||||
SetScenario(scenarioName, duration);
|
|
||||||
if (!string.IsNullOrWhiteSpace(programOverride))
|
|
||||||
{
|
|
||||||
State.ProgramName = programOverride;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
namespace CncSimulator.Device
|
|
||||||
{
|
|
||||||
/// <summary>剧本播放器的简化实现(占位,未直接驱动状态)</summary>
|
|
||||||
public class ScenarioPlayer
|
|
||||||
{
|
|
||||||
public ScenarioPlayer()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Tick(DeviceState state)
|
|
||||||
{
|
|
||||||
// 简化实现:不改变状态,留作未来扩展点
|
|
||||||
}
|
|
||||||
|
|
||||||
public void TriggerEvent(string eventType)
|
|
||||||
{
|
|
||||||
// 事件占位
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,98 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using CncSimulator.Device;
|
|
||||||
|
|
||||||
namespace CncSimulator.Generator
|
|
||||||
{
|
|
||||||
/// <summary>Fanuc 数据生成器:生成 19 个 Tag 的 JSON 表示</summary>
|
|
||||||
public class FanucDataGenerator : IBrandGenerator
|
|
||||||
{
|
|
||||||
public string BrandKey => "fanuc";
|
|
||||||
|
|
||||||
public JObject GenerateDevice(DeviceState state)
|
|
||||||
{
|
|
||||||
if (state == null) return null;
|
|
||||||
var now = DateTime.Now;
|
|
||||||
var rnd = new Random(state.DeviceCode.GetHashCode());
|
|
||||||
var tags = new List<JObject>();
|
|
||||||
string[] tagNames = new string[] {"_io_status","Tag2","Tag5","Tag6","Tag7","Tag8","Tag9","Tag11","Tag14","Tag17","Tag18","Tag19","Tag20","Tag21","Tag22","Tag23","Tag24","Tag25","Tag26"};
|
|
||||||
for (int i = 0; i < tagNames.Length; i++)
|
|
||||||
{
|
|
||||||
int offset = rnd.Next(-5, 1); // -5 ~ 0
|
|
||||||
string time = now.AddSeconds(offset).ToString("yyyy-MM-dd HH:mm:ss");
|
|
||||||
string value = GetTagValue(i, state);
|
|
||||||
string desc = GetTagDesc(i, state);
|
|
||||||
tags.Add(new JObject
|
|
||||||
{
|
|
||||||
["name"] = tagNames[i],
|
|
||||||
["time"] = time,
|
|
||||||
["value"] = value,
|
|
||||||
["quality"] = "0",
|
|
||||||
["desc"] = desc
|
|
||||||
});
|
|
||||||
}
|
|
||||||
var deviceObj = new JObject
|
|
||||||
{
|
|
||||||
["device"] = state.DeviceCode,
|
|
||||||
["desc"] = state.Desc,
|
|
||||||
["tags"] = new JArray(tags)
|
|
||||||
};
|
|
||||||
return deviceObj;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetTagDesc(int index, DeviceState state)
|
|
||||||
{
|
|
||||||
return index switch
|
|
||||||
{
|
|
||||||
0 => "IO状态",
|
|
||||||
1 => "当前轴数",
|
|
||||||
2 => "当前加工程序",
|
|
||||||
3 => "主程序号",
|
|
||||||
4 => "加工程序内容",
|
|
||||||
5 => "加工零件数",
|
|
||||||
6 => "运行状态",
|
|
||||||
7 => "操作模式",
|
|
||||||
8 => "当前主轴倍率",
|
|
||||||
9 => "主轴设定速度",
|
|
||||||
10 => "进给设定速度",
|
|
||||||
11 => "主轴实际速度",
|
|
||||||
12 => "进给实际转速",
|
|
||||||
13 => "主轴负载",
|
|
||||||
14 => "开机时间",
|
|
||||||
15 => "运行时间",
|
|
||||||
16 => "切削时间",
|
|
||||||
17 => "循环时间",
|
|
||||||
18 => state.MachiningStatus,
|
|
||||||
_ => ""
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetTagValue(int index, DeviceState state)
|
|
||||||
{
|
|
||||||
switch (index)
|
|
||||||
{
|
|
||||||
case 0: return $"{state.DeviceStatus}.00000"; // _io_status
|
|
||||||
case 1: return "4.00000"; // Tag2
|
|
||||||
case 2: return state.ProgramName; // Tag5
|
|
||||||
case 3: return "N0"; // Tag6
|
|
||||||
case 4: return $"<{state.ProgramName}>\nG40G49G80\n( SIMULATOR )"; // Tag7
|
|
||||||
case 5: return $"{state.PartCount}.00000"; // Tag8
|
|
||||||
case 6: return $"{state.RunStatus}.00000"; // Tag9
|
|
||||||
case 7: return $"{state.OperateMode}.00000"; // Tag11
|
|
||||||
case 8: return state.SpindleOverride.ToString("0.00000"); // Tag14
|
|
||||||
case 9: return state.SpindleSpeedSet.ToString("0.00000"); // Tag17
|
|
||||||
case 10: return state.FeedSpeedSet.ToString("0.00000"); // Tag18
|
|
||||||
case 11: return state.SpindleSpeedActual.ToString("0.00000"); // Tag19
|
|
||||||
case 12: return state.FeedSpeedActual.ToString("0.00000"); // Tag20
|
|
||||||
case 13: return state.SpindleLoad.ToString("0.00000"); // Tag21
|
|
||||||
case 14: return state.PowerOnTime.ToString("0.00000"); // Tag22
|
|
||||||
case 15: return state.RunTime.ToString("0.00000"); // Tag23
|
|
||||||
case 16: return state.CuttingTime.ToString("0.00000"); // Tag24
|
|
||||||
case 17: return state.CycleTime.ToString("0.00000"); // Tag25
|
|
||||||
case 18: return state.MachiningStatus ?? ""; // Tag26
|
|
||||||
default: return "0.00000";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace CncSimulator.Generator
|
|
||||||
{
|
|
||||||
/// <summary>品牌数据生成器接口</summary>
|
|
||||||
public interface IBrandGenerator
|
|
||||||
{
|
|
||||||
string BrandKey { get; }
|
|
||||||
JObject GenerateDevice(CncSimulator.Device.DeviceState state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading;
|
|
||||||
using log4net;
|
|
||||||
using log4net.Config;
|
|
||||||
using CncCollector.Config;
|
|
||||||
using CncCollector.Core;
|
|
||||||
using CncCollector.Api;
|
|
||||||
|
|
||||||
namespace CncCollector
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 主入口:启动采集引擎与管理 API。
|
|
||||||
/// </summary>
|
|
||||||
internal class Program
|
|
||||||
{
|
|
||||||
private static readonly ILog Log = LogManager.GetLogger(typeof(Program));
|
|
||||||
|
|
||||||
static void Main(string[] args)
|
|
||||||
{
|
|
||||||
// 初始化日志
|
|
||||||
XmlConfigurator.Configure();
|
|
||||||
|
|
||||||
// 假设默认配置:从配置文件加载,若失败使用硬编码默认值
|
|
||||||
var defaultConfig = new CncModels.Entity.CollectorConfig
|
|
||||||
{
|
|
||||||
ApiPort = 9000,
|
|
||||||
ApiKey = "",
|
|
||||||
HeartbeatInterval = 60,
|
|
||||||
ConfigPollInterval = 30,
|
|
||||||
DailySummaryTime = TimeSpan.FromHours(1),
|
|
||||||
CollectRetryCount = 3,
|
|
||||||
CollectRetryIntervalSeconds = 5,
|
|
||||||
CollectFailAlertThreshold = 3
|
|
||||||
};
|
|
||||||
|
|
||||||
// 数据库连接字符串,实际部署应改为配置文件读取
|
|
||||||
string connectionString = "server=127.0.0.1;user=root;password=123456;database=cnc_business;SslMode=none";
|
|
||||||
|
|
||||||
// 从 DB 覆盖运行时配置
|
|
||||||
try
|
|
||||||
{
|
|
||||||
ConfigLoader.LoadRuntimeConfig(connectionString, defaultConfig);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Log.Error("加载运行时配置失败,使用默认配置", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
var engine = new CollectorEngine(connectionString, defaultConfig);
|
|
||||||
engine.Start();
|
|
||||||
var api = new CollectorApiServer(engine, defaultConfig.ApiKey, defaultConfig.ApiPort);
|
|
||||||
api.Start();
|
|
||||||
|
|
||||||
Console.WriteLine("CNC 收集服务已启动,按任意键退出...");
|
|
||||||
Console.ReadKey();
|
|
||||||
|
|
||||||
api.Stop();
|
|
||||||
engine.Stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
async (page) => {
|
|
||||||
const results = [];
|
|
||||||
const urls = [
|
|
||||||
{name: 'C3.9 device detail', url: 'http://127.0.0.1/admin/machine/1'},
|
|
||||||
{name: 'C4 brand', url: 'http://127.0.0.1/admin/brand'},
|
|
||||||
{name: 'C5 collect-address', url: 'http://127.0.0.1/admin/collect-address'},
|
|
||||||
{name: 'C6 worker', url: 'http://127.0.0.1/admin/worker'},
|
|
||||||
{name: 'C7 production', url: 'http://127.0.0.1/admin/production'},
|
|
||||||
{name: 'C8 alert', url: 'http://127.0.0.1/admin/alert'},
|
|
||||||
{name: 'C9 settings', url: 'http://127.0.0.1/admin/settings'},
|
|
||||||
{name: 'C10 log', url: 'http://127.0.0.1/admin/log'},
|
|
||||||
{name: 'C11 screen-config', url: 'http://127.0.0.1/admin/screen-config'},
|
|
||||||
];
|
|
||||||
for (const u of urls) {
|
|
||||||
await page.goto(u.url, {waitUntil: 'networkidle', timeout: 10000}).catch(() => {});
|
|
||||||
const title = await page.title();
|
|
||||||
const bodyText = await page.evaluate(() => document.body.innerText.substring(0, 200));
|
|
||||||
const hasTable = await page.locator('table').count();
|
|
||||||
const hasChart = await page.locator('canvas, svg').count();
|
|
||||||
results.push(u.name + ': title=' + title + ', table=' + hasTable + ', chart=' + hasChart + ', bodyLen=' + bodyText.length);
|
|
||||||
}
|
|
||||||
return results.join('\n');
|
|
||||||
}
|
|
||||||
@ -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,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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue