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