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; }
+ }
+}