Compare commits

...

2 Commits

Author SHA1 Message Date
haoliang e09fdc1329 feat: 实现数据回放功能(ReplayService + API端点)
- 新增 IReplayService/ReplayService 回放服务(预览+执行)
- 新增 ReplayController(POST preview/execute)
- 新增 ReplayDto 请求/响应DTO
- 回放流程:读取原始日志→清空业务数据→重新解析写入→日终汇总
- ServiceResolver DI注册
- 编译通过 0错误
1 day ago
haoliang c9cca32757 实现 D1-D2 数据回放:新增 ReplayService、ReplayController、ReplayDto,DI 注册,API 端点,预览与执行回放逻辑,基于现有 SQL 迁移。 1 day ago

@ -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,226 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Data;
using MySqlConnector;
using Dapper;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using CncModels.Entity;
using CncModels.Dto.CollectLog;
using CncService.Interface;
using CncRepository.Interface;
using CncRepository.Impl.Log;
using CncRepository.Impl;
using CncRepository.Base;
namespace CncService.Impl
{
/// <summary>
/// 回放服务实现
/// 通过日志库日活日志与业务库表实现回放能力
/// 注意:此实现尽量复用现有 SQL避免引入额外依赖
/// </summary>
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));
}
/// <summary>预览回放影响范围(不做写操作)</summary>
public ReplayPreview PreviewReplay(DateTime date)
{
using (var logConn = new MySqlConnection(_logConn))
{
logConn.Open();
// 原始日志数量
var rawCount = logConn.ExecuteScalar<int>(@"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<int>(@"SELECT COUNT(DISTINCT machine_id) FROM cnc_collect_record WHERE DATE(collect_time) = @Date", new { Date = date.Date });
var segmentCount = b.ExecuteScalar<int>(@"SELECT COUNT(1) FROM cnc_production_segment WHERE production_date = @Date", new { Date = date.Date });
var recCount = b.ExecuteScalar<int>(@"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,
};
}
}
}
/// <summary>执行回放:清空当天数据并重新写入(简化实现)</summary>
public ReplayResult ExecuteReplay(DateTime date)
{
int clearedRecordCount = 0;
int clearedSegmentCount = 0;
int rebuiltRecordCount = 0;
try
{
// 1) 读取当天成功的原始日志
List<CollectRaw> 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<CollectRaw>(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 = JArray.Parse(raw.RawJson);
foreach (JObject d in devices)
{
string deviceCode = d?.Value<string>("device") ?? null;
if (string.IsNullOrWhiteSpace(deviceCode)) continue;
// 通过设备代码获取机器ID
var machineId = conn.QuerySingleOrDefault<int?>("SELECT id FROM cnc_machine WHERE device_code = @Code", new { Code = deviceCode });
if (machineId == null) continue;
// 收集 tag 值
var tags = d?.Value<JArray>("tags");
var programName = ExtractTagValue(tags, "Tag5");
var partCount = ParseDecimal(ExtractTagValue(tags, "Tag8"));
var runStatus = ExtractTagValue(tags, "Tag9");
var operateMode = ExtractTagValue(tags, "Tag11");
var spindleSet = ParseDecimal(ExtractTagValue(tags, "Tag17"));
var spindleActual = ParseDecimal(ExtractTagValue(tags, "Tag19"));
var machiningStatus = ExtractTagValue(tags, "Tag26");
var collectTime = raw.RequestTime;
var rec = new CollectRecord
{
MachineId = machineId.Value,
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(JArray tags, string id)
{
if (tags == null) return null;
foreach (JObject t in tags)
{
if (t?.Value<string>("id") == id) return t?.Value<string>("value");
}
return null;
}
private static decimal? ParseDecimal(string s)
{
if (decimal.TryParse(s, out var d)) return d;
return null;
}
}
}

@ -0,0 +1,17 @@
using System;
using CncModels.Dto.CollectLog;
namespace CncService.Interface
{
/// <summary>
/// 回放服务接口D1-D2 数据回放)
/// </summary>
public interface IReplayService
{
/// <summary>预览回放影响范围</summary>
ReplayPreview PreviewReplay(DateTime date);
/// <summary>执行回放,含清空与重建并重新汇总</summary>
ReplayResult ExecuteReplay(DateTime date);
}
}

@ -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
{
/// <summary>
/// 数据回放控制器
/// </summary>
[RoutePrefix("api/admin/replay")]
[JwtAuthFilter]
public class ReplayController : ApiController
{
private readonly IReplayService _replayService;
public ReplayController(IReplayService replayService)
{
_replayService = replayService ?? throw new ArgumentNullException(nameof(replayService));
}
/// <summary>预览回放影响范围</summary>
[HttpPost]
[Route("preview")]
[ResponseType(typeof(ApiResponse<ReplayPreview>))]
public IHttpActionResult Preview([FromBody] ReplayRequest request)
{
if (request == null) return BadRequest("请求参数错误");
var result = _replayService.PreviewReplay(request.Date);
return Ok(ApiResponse<ReplayPreview>.Success(result));
}
/// <summary>执行回放</summary>
[HttpPost]
[Route("execute")]
[ResponseType(typeof(ApiResponse<ReplayResult>))]
public IHttpActionResult Execute([FromBody] ReplayRequest request)
{
if (request == null) return BadRequest("请求参数错误");
var result = _replayService.ExecuteReplay(request.Date);
return Ok(ApiResponse<ReplayResult>.Success(result));
}
}
}

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

Loading…
Cancel
Save