From c13ed696aa7f88c14c8a8d7a54649a3df681fe50 Mon Sep 17 00:00:00 2001 From: haoliang <821644@qq.com> Date: Fri, 15 May 2026 22:13:48 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9ECncReplay=E9=87=8D=E6=94=BE?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=EF=BC=9A=E6=8C=89=E6=97=B6=E9=97=B4=E9=A1=BA?= =?UTF-8?q?=E5=BA=8F=E9=87=8D=E6=94=BE=E9=87=87=E9=9B=86=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E9=87=8D=E6=96=B0=E8=AE=A1=E7=AE=97=E4=BA=A7=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/CncReplay/App.config | 8 ++ src/CncReplay/CncReplay.csproj | 23 ++++ src/CncReplay/Program.cs | 240 +++++++++++++++++++++++++++++++++ 3 files changed, 271 insertions(+) create mode 100644 src/CncReplay/App.config create mode 100644 src/CncReplay/CncReplay.csproj create mode 100644 src/CncReplay/Program.cs diff --git a/src/CncReplay/App.config b/src/CncReplay/App.config new file mode 100644 index 0000000..f84bf73 --- /dev/null +++ b/src/CncReplay/App.config @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/CncReplay/CncReplay.csproj b/src/CncReplay/CncReplay.csproj new file mode 100644 index 0000000..cc068bf --- /dev/null +++ b/src/CncReplay/CncReplay.csproj @@ -0,0 +1,23 @@ + + + net472 + x64 + CncReplay + CncReplay + Exe + bin\ + false + false + + + + + + + + + + + + + diff --git a/src/CncReplay/Program.cs b/src/CncReplay/Program.cs new file mode 100644 index 0000000..6427500 --- /dev/null +++ b/src/CncReplay/Program.cs @@ -0,0 +1,240 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Diagnostics; +using System.Linq; +using Dapper; +using MySqlConnector; +using Newtonsoft.Json.Linq; +using CncCollector.Core; +using CncModels.Entity; + +namespace CncReplay +{ + /// + /// 采集日志重放工具:按时间顺序重放 log_collect_raw 中的采集日志, + /// 用新的产量跟踪逻辑重新计算产量。 + /// + class Program + { + static void Main(string[] args) + { + Console.OutputEncoding = System.Text.Encoding.UTF8; + Console.WriteLine("========== CNC采集日志重放工具 =========="); + Console.WriteLine(); + + // 1. 读取连接串 + var businessConnStr = ConfigurationManager.ConnectionStrings["BusinessConnection"]?.ConnectionString; + var logConnStr = ConfigurationManager.ConnectionStrings["LogConnection"]?.ConnectionString; + + if (string.IsNullOrEmpty(businessConnStr) || string.IsNullOrEmpty(logConnStr)) + { + Console.WriteLine("错误:未找到数据库连接串配置(BusinessConnection / LogConnection)。"); + Console.WriteLine("请检查 App.config 中的 connectionStrings 配置。"); + return; + } + + // 2. 查询待重放日志(按时间顺序,取最近的N条成功记录) + List logs; + using (var conn = new MySqlConnection(logConnStr)) + { + conn.Open(); + logs = conn.Query( + "SELECT id AS Id, request_time AS RequestTime, raw_json AS RawJson, collect_address_id AS CollectAddressId " + + "FROM log_collect_raw WHERE is_success = 1 ORDER BY request_time ASC LIMIT 10").AsList(); + } + + if (logs.Count == 0) + { + Console.WriteLine("未找到待重放的采集日志(is_success=1 的记录)。"); + return; + } + + Console.WriteLine($"找到 {logs.Count} 条待重放日志。"); + Console.WriteLine(); + + // 3. 加载品牌配置 + 字段映射 + 机床列表 + Brand brand; + List mappings; + List machines; + + using (var conn = new MySqlConnection(businessConnStr)) + { + conn.Open(); + + // 加载 FANUC 品牌配置(id=1) + brand = conn.QueryFirstOrDefault( + "SELECT id AS Id, brand_name AS BrandName, device_field AS DeviceField, tags_path AS TagsPath, " + + "is_enabled AS IsEnabled, created_at AS CreatedAt, updated_at AS UpdatedAt " + + "FROM cnc_brand WHERE id = 1"); + + if (brand == null) + { + Console.WriteLine("错误:未找到品牌配置(cnc_brand.id=1),请先配置品牌。"); + return; + } + + // 加载品牌字段映射 + mappings = conn.Query( + "SELECT id AS Id, brand_id AS BrandId, standard_field AS StandardField, field_name AS FieldName, " + + "match_by AS MatchBy, data_type AS DataType, is_required AS IsRequired, is_enabled AS IsEnabled, " + + "created_at AS CreatedAt FROM cnc_brand_field_mapping WHERE brand_id = @BrandId", + new { BrandId = brand.Id }).AsList(); + + if (mappings == null || mappings.Count == 0) + { + Console.WriteLine("警告:未找到品牌字段映射,解析将无法提取数据。"); + mappings = new List(); + } + + // 加载所有启用的机床 + machines = conn.Query( + "SELECT id AS Id, device_code AS DeviceCode, name AS Name, workshop_id AS WorkshopId, " + + "collect_address_id AS CollectAddressId, ip_address AS IpAddress, brand_id AS BrandId, " + + "is_enabled AS IsEnabled, last_ping_time AS LastPingTime, last_collect_time AS LastCollectTime, " + + "last_device_status AS LastDeviceStatus, last_run_status AS LastRunStatus, " + + "last_program_name AS LastProgramName, last_part_count AS LastPartCount, " + + "last_operate_mode AS LastOperateMode, last_machining_status AS LastMachiningStatus, " + + "created_at AS CreatedAt, updated_at AS UpdatedAt " + + "FROM cnc_machine WHERE is_enabled = 1").AsList(); + + if (machines == null || machines.Count == 0) + { + Console.WriteLine("警告:未找到启用的机床配置。"); + machines = new List(); + } + } + + Console.WriteLine($"品牌: {brand.BrandName},字段映射: {mappings.Count}条,机床: {machines.Count}台"); + Console.WriteLine(); + + // 构建 device_code → machine 的查找字典 + var machineDict = machines + .Where(m => !string.IsNullOrEmpty(m.DeviceCode)) + .ToDictionary(m => m.DeviceCode, StringComparer.OrdinalIgnoreCase); + + // 4. 创建产量跟踪器 + using (var tracker = new ProductionTracker(businessConnStr)) + { + var swTotal = Stopwatch.StartNew(); + int processed = 0; + + // 5. 逐条重放 + foreach (var log in logs) + { + var startTime = DateTime.Now; + var analysisLogs = new List(); + analysisLogs.Add("========== CNC采集产量分析日志(重放) =========="); + analysisLogs.Add("原始采集时间:" + log.RequestTime.ToString("yyyy-MM-dd HH:mm:ss")); + + JArray devices; + try + { + devices = JArray.Parse(log.RawJson); + } + catch (Exception ex) + { + Console.WriteLine($" [警告] 日志 #{log.Id} JSON解析失败: {ex.Message},跳过。"); + continue; + } + + var records = new List(); + + foreach (var deviceToken in devices) + { + var deviceObj = deviceToken as JObject; + if (deviceObj == null) continue; + + // 提取 device 字段值 + string deviceCode = DataParser.ExtractDeviceCode(deviceObj, brand.DeviceField ?? "device"); + if (string.IsNullOrEmpty(deviceCode)) continue; + + // 匹配机床 + Machine machine; + if (!machineDict.TryGetValue(deviceCode, out machine)) + { + analysisLogs.Add($"---未知设备: device={deviceCode}---"); + continue; + } + + // 解析字段 + var parsed = DataParser.ParseDevice(deviceObj, brand, mappings); + + var programName = GetStringValue(parsed, "program_name"); + var partCount = GetDecimalValue(parsed, "part_count"); + var totalPartCount = GetDecimalValue(parsed, "total_part_count"); + + // 跳过无效数据(断电设备) + if (string.IsNullOrEmpty(programName) && !partCount.HasValue) + continue; + + var record = new CollectRecord + { + MachineId = machine.Id, + CollectTime = log.RequestTime, + ProgramName = programName, + PartCount = partCount, + TotalPartCount = totalPartCount, + DeviceStatus = GetStringValue(parsed, "device_status"), + RunStatus = GetStringValue(parsed, "run_status"), + OperateMode = GetStringValue(parsed, "operate_mode"), + PowerOnTime = GetDecimalValue(parsed, "power_on_time"), + RunTime = GetDecimalValue(parsed, "run_time") + }; + records.Add(record); + + // 调用产量跟踪 + var (logText, changed, todayTotal) = tracker.Track( + machine.Id, programName, totalPartCount, log.RequestTime); + analysisLogs.Add("---机床:" + machine.Name + "---"); + analysisLogs.Add("程序名=" + (programName ?? "空") + ",加工零件总数=" + (totalPartCount?.ToString() ?? "空")); + analysisLogs.Add("结果:" + logText + ",当日产量=" + todayTotal.ToString("F0")); + } + + // 写入采集记录 + 更新分析日志 + analysisLogs.Add("=========================================="); + CollectRecordWriter.WriteBatchReplay(businessConnStr, logConnStr, + records, log.Id, log.CollectAddressId, log.RequestTime, + string.Join("\n", analysisLogs)); + + processed++; + Console.WriteLine($"[{processed}/{logs.Count}] {log.RequestTime:yyyy-MM-dd HH:mm:ss} → " + + $"{records.Count}台设备,耗时{(DateTime.Now - startTime).TotalMilliseconds:F0}ms"); + } + + swTotal.Stop(); + Console.WriteLine(); + Console.WriteLine($"重放完成:{processed}条日志,总耗时{swTotal.Elapsed.TotalSeconds:F0}秒"); + } + } + + /// + /// 从解析结果中获取字段的字符串值 + /// + static string GetStringValue(Dictionary parsed, string field) + { + DataParser.ParsedField pf; + return parsed.TryGetValue(field, out pf) ? pf.StringValue : null; + } + + /// + /// 从解析结果中获取字段的数值 + /// + static decimal? GetDecimalValue(Dictionary parsed, string field) + { + DataParser.ParsedField pf; + return parsed.TryGetValue(field, out pf) ? pf.NumericValue : null; + } + } + + /// + /// log_collect_raw 表的查询结果映射 + /// + class LogEntry + { + public long Id { get; set; } + public DateTime RequestTime { get; set; } + public string RawJson { get; set; } + public int CollectAddressId { get; set; } + } +}