From c9cca32757e6a2c72c0e8a86403d82651f9425e5 Mon Sep 17 00:00:00 2001
From: haoliang <821644@qq.com>
Date: Tue, 5 May 2026 17:26:19 +0800
Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=20D1-D2=20=E6=95=B0=E6=8D=AE?=
=?UTF-8?q?=E5=9B=9E=E6=94=BE=EF=BC=9A=E6=96=B0=E5=A2=9E=20ReplayService?=
=?UTF-8?q?=E3=80=81ReplayController=E3=80=81ReplayDto=EF=BC=8CDI=20?=
=?UTF-8?q?=E6=B3=A8=E5=86=8C=EF=BC=8CAPI=20=E7=AB=AF=E7=82=B9=EF=BC=8C?=
=?UTF-8?q?=E9=A2=84=E8=A7=88=E4=B8=8E=E6=89=A7=E8=A1=8C=E5=9B=9E=E6=94=BE?=
=?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E5=9F=BA=E4=BA=8E=E7=8E=B0=E6=9C=89?=
=?UTF-8?q?=20SQL=20=E8=BF=81=E7=A7=BB=E3=80=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/CncModels/Dto/CollectLog/ReplayDto.cs | 26 ++
src/CncService/Impl/ReplayService.cs | 224 ++++++++++++++++++
src/CncService/Interface/IReplayService.cs | 17 ++
src/CncWebApi/Controllers/ReplayController.cs | 49 ++++
.../Infrastructure/ServiceResolver.cs | 8 +
5 files changed, 324 insertions(+)
create mode 100644 src/CncModels/Dto/CollectLog/ReplayDto.cs
create mode 100644 src/CncService/Impl/ReplayService.cs
create mode 100644 src/CncService/Interface/IReplayService.cs
create mode 100644 src/CncWebApi/Controllers/ReplayController.cs
diff --git a/src/CncModels/Dto/CollectLog/ReplayDto.cs b/src/CncModels/Dto/CollectLog/ReplayDto.cs
new file mode 100644
index 0000000..23a5557
--- /dev/null
+++ b/src/CncModels/Dto/CollectLog/ReplayDto.cs
@@ -0,0 +1,26 @@
+using System;
+
+namespace CncModels.Dto.CollectLog
+{
+ /// 回放请求参数
+ public class ReplayRequest { public DateTime Date { get; set; } }
+
+ /// 回放预览结果
+ 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; }
+ }
+
+ /// 回放执行结果
+ 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; }
+ }
+}
diff --git a/src/CncService/Impl/ReplayService.cs b/src/CncService/Impl/ReplayService.cs
new file mode 100644
index 0000000..722683a
--- /dev/null
+++ b/src/CncService/Impl/ReplayService.cs
@@ -0,0 +1,224 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Data;
+using MySqlConnector;
+using Dapper;
+using Newtonsoft.Json;
+using CncModels.Entity;
+using CncModels.Dto.CollectLog;
+using CncRepository.Interface;
+using CncRepository.Impl.Log;
+using CncRepository.Impl;
+using CncRepository.Base;
+
+namespace CncService.Impl
+{
+ ///
+ /// 回放服务实现
+ /// 通过日志库日活日志与业务库表实现回放能力
+ /// 注意:此实现尽量复用现有 SQL,避免引入额外依赖
+ ///
+ public class ReplayService : IReplayService
+ {
+ private readonly string _businessConn;
+ private readonly string _logConn;
+
+ public ReplayService(string businessConn, string logConn)
+ {
+ _businessConn = businessConn ?? throw new ArgumentNullException(nameof(businessConn));
+ _logConn = logConn ?? throw new ArgumentNullException(nameof(logConn));
+ }
+
+ /// 预览回放影响范围(不做写操作)
+ public ReplayPreview PreviewReplay(DateTime date)
+ {
+ using (var logConn = new MySqlConnection(_logConn))
+ {
+ logConn.Open();
+ // 原始日志数量
+ var rawCount = logConn.ExecuteScalar(@"SELECT COUNT(1) FROM log_collect_raw WHERE DATE(request_time) = @Date AND is_success = 1", new { Date = date.Date });
+ // 预计影响的记录/机器/段落数量
+ var rebuiltCount = 0; // 预览阶段不写入,返回0
+ using (var b = new MySqlConnection(_businessConn))
+ {
+ b.Open();
+ var machineCount = b.ExecuteScalar(@"SELECT COUNT(DISTINCT machine_id) FROM cnc_collect_record WHERE DATE(collect_time) = @Date", new { Date = date.Date });
+ var segmentCount = b.ExecuteScalar(@"SELECT COUNT(1) FROM cnc_production_segment WHERE production_date = @Date", new { Date = date.Date });
+ var recCount = b.ExecuteScalar(@"SELECT COUNT(1) FROM cnc_collect_record WHERE DATE(collect_time) = @Date", new { Date = date.Date });
+ return new ReplayPreview
+ {
+ Date = date.Date,
+ RawLogCount = rawCount,
+ AffectedMachineCount = machineCount,
+ AffectedRecordCount = recCount,
+ AffectedSegmentCount = segmentCount,
+ };
+ }
+ }
+ }
+
+ /// 执行回放:清空当天数据并重新写入(简化实现)
+ public ReplayResult ExecuteReplay(DateTime date)
+ {
+ int clearedRecordCount = 0;
+ int clearedSegmentCount = 0;
+ int rebuiltRecordCount = 0;
+ try
+ {
+ // 1) 读取当天成功的原始日志
+ List rawLogs;
+ using (var logConn = new MySqlConnection(_logConn))
+ {
+ logConn.Open();
+ string sql = @"SELECT * FROM log_collect_raw WHERE DATE(request_time) = @Date AND is_success = 1 ORDER BY request_time ASC";
+ rawLogs = logConn.Query(sql, new { Date = date.Date }).ToList();
+ }
+
+ // 2) 业务库清空(按外键依赖的顺序)
+ using (var conn = new MySqlConnection(_businessConn))
+ {
+ conn.Open();
+ using (var tran = conn.BeginTransaction())
+ {
+ // 2.1 清空依赖表
+ clearedRecordCount = conn.Execute("DELETE FROM cnc_production_adjustment WHERE DATE(created_at) = @Date", new { Date = date.Date }, tran);
+ // 逐表清空,确保单条语句执行兼容性
+ int c1 = conn.Execute("DELETE FROM cnc_worker_daily_summary WHERE production_date = @Date", new { Date = date.Date }, tran);
+ int c2 = conn.Execute("DELETE FROM cnc_daily_production WHERE production_date = @Date", new { Date = date.Date }, tran);
+ int c3 = conn.Execute("DELETE FROM cnc_machine_daily_status WHERE production_date = @Date", new { Date = date.Date }, tran);
+ int c4 = conn.Execute("DELETE FROM cnc_production_segment WHERE production_date = @Date", new { Date = date.Date }, tran);
+ int c5 = conn.Execute("DELETE FROM cnc_collect_record WHERE DATE(collect_time) = @Date", new { Date = date.Date }, tran);
+ clearedSegmentCount = c1;
+ tran.Commit();
+ }
+ }
+
+ // 3) 逐条 raw 日志解析并写入 cnc_collect_record(简化实现)
+ using (var conn = new MySqlConnection(_businessConn))
+ {
+ conn.Open();
+ foreach (var raw in rawLogs)
+ {
+ // 简单 JSON 解析,提取每个 device 的信息并写入 cnc_collect_record
+ try
+ {
+ var devices = JsonConvert.DeserializeObject>(raw.RawJson);
+ foreach (var d in devices)
+ {
+ string deviceCode = (string)d?.device ?? null;
+ if (string.IsNullOrWhiteSpace(deviceCode)) continue;
+ // 通过设备代码获取机器ID
+ var machine = conn.QuerySingleOrDefault("SELECT id FROM cnc_machine WHERE device_code = @Code", new { Code = deviceCode });
+ if (machine == null) continue;
+ int machineId = machine.Id;
+
+ // 收集 tag 值
+ var programName = (string)ExtractTagValue((IEnumerable)d?.tags, "Tag5");
+ var partCount = (decimal?)ParseDecimal(ExtractTagValue((IEnumerable)d?.tags, "Tag8"));
+ var runStatus = (string)ExtractTagValue((IEnumerable)d?.tags, "Tag9");
+ var operateMode = (string)ExtractTagValue((IEnumerable)d?.tags, "Tag11");
+ var spindleSet = (decimal?)ParseDecimal(ExtractTagValue((IEnumerable)d?.tags, "Tag17"));
+ var spindleActual = (decimal?)ParseDecimal(ExtractTagValue((IEnumerable)d?.tags, "Tag19"));
+ var machiningStatus = (string)ExtractTagValue((IEnumerable)d?.tags, "Tag26");
+
+ var collectTime = raw.RequestTime;
+
+ var rec = new CollectRecord
+ {
+ MachineId = machineId,
+ CollectTime = collectTime,
+ ProgramName = programName,
+ PartCount = partCount,
+ RunStatus = runStatus,
+ OperateMode = operateMode,
+ SpindleSpeedSet = spindleSet,
+ SpindleSpeedActual = spindleActual,
+ MachiningStatus = machiningStatus,
+ CreatedAt = DateTime.Now
+ };
+ string insertSql = @"INSERT INTO cnc_collect_record (machine_id, collect_time, program_name, part_count, run_status, operate_mode, spindle_speed_set, spindle_speed_actual, machining_status, created_at) VALUES (@MachineId, @CollectTime, @ProgramName, @PartCount, @RunStatus, @OperateMode, @SpindleSpeedSet, @SpindleSpeedActual, @MachiningStatus, @CreatedAt)";
+ conn.Execute(insertSql, new
+ {
+ MachineId = rec.MachineId,
+ CollectTime = rec.CollectTime,
+ ProgramName = rec.ProgramName,
+ PartCount = rec.PartCount,
+ RunStatus = rec.RunStatus,
+ OperateMode = rec.OperateMode,
+ SpindleSpeedSet = rec.SpindleSpeedSet,
+ SpindleSpeedActual = rec.SpindleSpeedActual,
+ MachiningStatus = rec.MachiningStatus,
+ CreatedAt = rec.CreatedAt
+ });
+ rebuiltRecordCount++;
+ }
+ }
+ catch
+ {
+ // 忽略单条日志的解析错误,继续处理下一条
+ }
+ }
+ }
+
+ // 4) 重新执行日终汇总(调用同样的 SQL 做聚合)
+ using (var conn = new MySqlConnection(_businessConn))
+ {
+ conn.Open();
+ // 以昨天日期执行日终汇总,使用 DailySummaryJob 风格的实现
+ // 结账活跃段
+ conn.Execute(@"UPDATE cnc_production_segment SET is_settled = 1, close_reason = 'replay' WHERE production_date = @Date AND is_settled = 0", new { Date = date.Date });
+ // 产量汇总(简化:重新计算所有段的 quantity)
+ conn.Execute(@"UPDATE cnc_production_segment SET quantity = GREATEST(0, COALESCE(end_part_count, 0) - start_part_count) WHERE production_date = @Date", new { Date = date.Date });
+ // 汇总日产量(简化版本)
+ conn.Execute(@"DELETE FROM cnc_daily_production WHERE production_date = @Date", new { Date = date.Date });
+ conn.Execute(@"INSERT INTO cnc_daily_production (machine_id, production_date, program_name, total_quantity, segment_count, created_at, updated_at) SELECT machine_id, production_date, program_name, SUM(quantity), COUNT(*), NOW(), NOW() FROM cnc_production_segment WHERE production_date = @Date GROUP BY machine_id, production_date, program_name", new { Date = date.Date });
+ // 更新机床日状态
+ conn.Execute(@"DELETE FROM cnc_machine_daily_status WHERE production_date = @Date", new { Date = date.Date });
+ conn.Execute(@"INSERT INTO cnc_machine_daily_status (machine_id, production_date, data_status, created_at, updated_at) SELECT machine_id, production_date, 'normal', NOW(), NOW() FROM cnc_daily_production WHERE production_date = @Date GROUP BY machine_id", new { Date = date.Date });
+ // 汇总员工日产量(简化)
+ conn.Execute(@"DELETE FROM cnc_worker_daily_summary WHERE production_date = @Date", new { Date = date.Date });
+ conn.Execute(@"INSERT INTO cnc_worker_daily_summary (worker_id, production_date, total_quantity, machine_count, program_count, created_at, updated_at) SELECT 0, production_date, SUM(total_quantity), COUNT(DISTINCT machine_id), COUNT(DISTINCT program_name), NOW(), NOW() FROM cnc_daily_production WHERE production_date = @Date", new { Date = date.Date });
+ }
+
+ return new ReplayResult
+ {
+ Date = date.Date,
+ ClearedRecordCount = clearedRecordCount,
+ ClearedSegmentCount = clearedSegmentCount,
+ RebuiltRecordCount = rebuiltRecordCount,
+ Success = true
+ };
+ }
+ catch (Exception ex)
+ {
+ return new ReplayResult
+ {
+ Date = date.Date,
+ ClearedRecordCount = 0,
+ ClearedSegmentCount = 0,
+ RebuiltRecordCount = 0,
+ Success = false,
+ ErrorMessage = ex.Message
+ };
+ }
+ }
+
+ // helpers
+ private static string ExtractTagValue(IEnumerable tags, string id)
+ {
+ if (tags == null) return null;
+ foreach (var t in tags)
+ {
+ if ((string)t?.id == id) return (string)t?.value;
+ }
+ return null;
+ }
+
+ private static object ParseDecimal(string s)
+ {
+ if (decimal.TryParse(s, out var d)) return d;
+ return null;
+ }
+ }
+}
diff --git a/src/CncService/Interface/IReplayService.cs b/src/CncService/Interface/IReplayService.cs
new file mode 100644
index 0000000..c4572be
--- /dev/null
+++ b/src/CncService/Interface/IReplayService.cs
@@ -0,0 +1,17 @@
+using System;
+using CncModels.Dto.CollectLog;
+
+namespace CncService.Interface
+{
+ ///
+ /// 回放服务接口(D1-D2 数据回放)
+ ///
+ public interface IReplayService
+ {
+ /// 预览回放影响范围
+ ReplayPreview PreviewReplay(DateTime date);
+
+ /// 执行回放,含清空与重建并重新汇总
+ ReplayResult ExecuteReplay(DateTime date);
+ }
+}
diff --git a/src/CncWebApi/Controllers/ReplayController.cs b/src/CncWebApi/Controllers/ReplayController.cs
new file mode 100644
index 0000000..98ffa4a
--- /dev/null
+++ b/src/CncWebApi/Controllers/ReplayController.cs
@@ -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
+{
+ ///
+ /// 数据回放控制器
+ ///
+ [RoutePrefix("api/admin/replay")]
+ [JwtAuthFilter]
+ public class ReplayController : ApiController
+ {
+ private readonly IReplayService _replayService;
+
+ public ReplayController(IReplayService replayService)
+ {
+ _replayService = replayService ?? throw new ArgumentNullException(nameof(replayService));
+ }
+
+ /// 预览回放影响范围
+ [HttpPost]
+ [Route("preview")]
+ [ResponseType(typeof(ApiResponse))]
+ public IHttpActionResult Preview([FromBody] ReplayRequest request)
+ {
+ if (request == null) return BadRequest("请求参数错误");
+ var result = _replayService.PreviewReplay(request.Date);
+ return Ok(ApiResponse.Success(result));
+ }
+
+ /// 执行回放
+ [HttpPost]
+ [Route("execute")]
+ [ResponseType(typeof(ApiResponse))]
+ public IHttpActionResult Execute([FromBody] ReplayRequest request)
+ {
+ if (request == null) return BadRequest("请求参数错误");
+ var result = _replayService.ExecuteReplay(request.Date);
+ return Ok(ApiResponse.Success(result));
+ }
+ }
+}
diff --git a/src/CncWebApi/Infrastructure/ServiceResolver.cs b/src/CncWebApi/Infrastructure/ServiceResolver.cs
index 52cc095..51bcc21 100644
--- a/src/CncWebApi/Infrastructure/ServiceResolver.cs
+++ b/src/CncWebApi/Infrastructure/ServiceResolver.cs
@@ -6,6 +6,7 @@ using System.Web.Http.Dependencies;
using CncRepository.Base;
using CncRepository.Interface;
using CncService.Interface;
+using CncWebApi.Controllers;
namespace CncWebApi.Infrastructure
{
@@ -61,6 +62,8 @@ namespace CncWebApi.Infrastructure
return new Controllers.LogController(
ResolveSystemLogService(),
ResolveProductionAdjustmentRepository());
+ if (serviceType == typeof(Controllers.ReplayController))
+ return new Controllers.ReplayController(ResolveReplayService());
if (serviceType == typeof(Controllers.ScreenConfigController))
return new Controllers.ScreenConfigController(
ResolveScreenService());
@@ -195,6 +198,11 @@ namespace CncWebApi.Infrastructure
new CncRepository.Impl.Log.CollectCycleRepository(_logConn));
}
+ private IReplayService ResolveReplayService()
+ {
+ return new CncService.Impl.ReplayService(_businessConn, _logConn);
+ }
+
#endregion
}
}