From aea15e6ec6d08fe801b200377999d12236322b16 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Thu, 11 Jun 2026 16:00:13 +0800 Subject: [PATCH] =?UTF-8?q?Phase=205:=20=E6=8A=A5=E5=91=8A=E7=94=9F?= =?UTF-8?q?=E6=88=90=20=E2=80=94=20ReportGenerator=20+=20ReportService,=20?= =?UTF-8?q?116=E6=B5=8B=E8=AF=954=E7=A7=92=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/IReportService.cs | 15 +++ .../Services/ReportGenerator.cs | 96 +++++++++++++++++++ .../Services/ReportService.cs | 78 +++++++++++++++ .../ReportGeneratorTests.cs | 63 ++++++++++++ .../ReportServiceTests.cs | 93 ++++++++++++++++++ 5 files changed, 345 insertions(+) create mode 100644 src/CounterDrone.Core/Services/IReportService.cs create mode 100644 src/CounterDrone.Core/Services/ReportGenerator.cs create mode 100644 src/CounterDrone.Core/Services/ReportService.cs create mode 100644 test/unit/CounterDrone.Core.Tests/ReportGeneratorTests.cs create mode 100644 test/unit/CounterDrone.Core.Tests/ReportServiceTests.cs diff --git a/src/CounterDrone.Core/Services/IReportService.cs b/src/CounterDrone.Core/Services/IReportService.cs new file mode 100644 index 0000000..b43f160 --- /dev/null +++ b/src/CounterDrone.Core/Services/IReportService.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using CounterDrone.Core.Models; +using SimEvent = CounterDrone.Core.Simulation.SimEvent; + +namespace CounterDrone.Core.Services +{ + public interface IReportService + { + SimulationReport Generate(string taskId, TaskFullConfig config, + List events, string finalDroneStatus, float duration); + SimulationReport GetReport(string id); + void DeleteReport(string id); + PagedResult SearchReports(string keyword, string? dateFrom, string? dateTo, int page, int pageSize); + } +} diff --git a/src/CounterDrone.Core/Services/ReportGenerator.cs b/src/CounterDrone.Core/Services/ReportGenerator.cs new file mode 100644 index 0000000..ad4584f --- /dev/null +++ b/src/CounterDrone.Core/Services/ReportGenerator.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using CounterDrone.Core.Models; +using SimEvent = CounterDrone.Core.Simulation.SimEvent; + +namespace CounterDrone.Core.Services +{ + public class ReportGenerator + { + /// 根据仿真结果生成报告内容 + public string Generate(TaskFullConfig config, List events, + Simulation.DroneStatus finalDroneStatus, float simulationDuration) + { + var sb = new StringBuilder(); + + // === 仿真前:我方配置 === + sb.AppendLine("# 仿真评估报告"); + sb.AppendLine(); + sb.AppendLine($"**任务名称**:{config.Task.Name}"); + sb.AppendLine($"**任务编号**:{config.Task.TaskNumber}"); + sb.AppendLine($"**仿真耗时**:{simulationDuration:F1} 秒"); + sb.AppendLine(); + + sb.AppendLine("## 一、仿真前 — 我方配置"); + sb.AppendLine(); + var deployment = config.Equipment; + var platforms = deployment.Where(e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform).ToList(); + var detections = deployment.Where(e => e.EquipmentRole == (int)EquipmentRole.Detection).ToList(); + + sb.AppendLine($"| 配置项 | 值 |"); + sb.AppendLine($"|--------|-----|"); + sb.AppendLine($"| 目标数量 | {config.Targets.Sum(t => t.Quantity)} |"); + sb.AppendLine($"| 发射平台 | {platforms.Sum(p => p.Quantity)} 台 |"); + sb.AppendLine($"| 探测设备 | {detections.Sum(d => d.Quantity)} 台 |"); + sb.AppendLine($"| 气溶胶类型 | {(AerosolType)config.Cloud.AerosolType} |"); + sb.AppendLine(); + + // === 仿真中:关键事件时序 === + sb.AppendLine("## 二、仿真中 — 关键事件时序"); + sb.AppendLine(); + sb.AppendLine("| 时间(s) | 事件 | 描述 |"); + sb.AppendLine("|---------|------|------|"); + foreach (var e in events.OrderBy(e => e.OccurredAt)) + { + sb.AppendLine($"| {e.OccurredAt:F1} | {e.Type} | {e.Description} |"); + } + sb.AppendLine(); + + // === 仿真后:统计分析 === + sb.AppendLine("## 三、仿真后 — 数据统计"); + sb.AppendLine(); + + var destroyed = events.Count(e => e.Type == SimEventType.DroneDestroyed); + var reached = events.Count(e => e.Type == SimEventType.DroneReachedTarget); + var intruded = events.Count(e => e.Type == SimEventType.ZoneIntruded); + var cloudCount = events.Count(e => e.Type == SimEventType.CloudGenerated); + var launchCount = events.Count(e => e.Type == SimEventType.MunitionLaunched); + var totalDrones = config.Targets.Sum(t => t.Quantity); + + sb.AppendLine($"| 统计项 | 值 |"); + sb.AppendLine($"|--------|-----|"); + sb.AppendLine($"| 无人机总数 | {totalDrones} |"); + sb.AppendLine($"| 被摧毁 | {destroyed} |"); + sb.AppendLine($"| 到达目标 | {reached} |"); + sb.AppendLine($"| 侵入管控区 | {intruded} |"); + sb.AppendLine($"| 弹药发射次数 | {launchCount} |"); + sb.AppendLine($"| 云团生成次数 | {cloudCount} |"); + sb.AppendLine(); + + // 对抗结果 + var result = DetermineInterceptResult(destroyed, reached + intruded, totalDrones); + sb.AppendLine($"**对抗结果**:{TranslateResult(result)}"); + sb.AppendLine(); + + return sb.ToString(); + } + + public InterceptResult DetermineInterceptResult(int destroyed, int reachedOrIntruded, int total) + { + if (total == 0) return InterceptResult.Failed; + if (destroyed >= total) return InterceptResult.Success; + if (destroyed > 0) return InterceptResult.Partial; + return InterceptResult.Failed; + } + + private static string TranslateResult(InterceptResult r) => r switch + { + InterceptResult.Success => "成功拦截", + InterceptResult.Partial => "部分拦截", + InterceptResult.Failed => "拦截失败", + _ => "未知", + }; + } +} diff --git a/src/CounterDrone.Core/Services/ReportService.cs b/src/CounterDrone.Core/Services/ReportService.cs new file mode 100644 index 0000000..eaf0b9b --- /dev/null +++ b/src/CounterDrone.Core/Services/ReportService.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using CounterDrone.Core.Models; +using SQLite; +using SimEvent = CounterDrone.Core.Simulation.SimEvent; + +namespace CounterDrone.Core.Services +{ + public class ReportService : IReportService + { + private readonly SQLiteConnection _db; + private readonly ReportGenerator _generator = new(); + + public ReportService(SQLiteConnection db) + { + _db = db; + } + + public SimulationReport Generate(string taskId, TaskFullConfig config, + List events, string finalDroneStatus, float duration) + { + var destroyed = events.Count(e => e.Type == SimEventType.DroneDestroyed); + var reached = events.Count(e => e.Type == SimEventType.DroneReachedTarget); + var intruded = events.Count(e => e.Type == SimEventType.ZoneIntruded); + var total = config.Targets.Sum(t => t.Quantity); + var result = _generator.DetermineInterceptResult(destroyed, reached + intruded, total); + + var report = new SimulationReport + { + TaskId = taskId, + TaskName = config.Task.Name, + TaskNumber = config.Task.TaskNumber, + CompletedAt = DateTime.UtcNow.ToString("o"), + TargetCount = total, + EquipmentCount = config.Equipment.Count, + InterceptResult = (int)result, + Content = _generator.Generate(config, events, Simulation.DroneStatus.Flying, duration), + }; + + _db.Insert(report); + return report; + } + + public SimulationReport GetReport(string id) + { + return _db.Find(id); + } + + public void DeleteReport(string id) + { + _db.Delete(id); + } + + public PagedResult SearchReports(string keyword, string? dateFrom, string? dateTo, int page, int pageSize) + { + if (page < 1) page = 1; + if (pageSize < 1) pageSize = 10; + var offset = (page - 1) * pageSize; + + var query = _db.Table().AsQueryable(); + if (!string.IsNullOrWhiteSpace(keyword)) + query = query.Where(r => r.TaskName.Contains(keyword) || r.TaskNumber.Contains(keyword)); + if (!string.IsNullOrWhiteSpace(dateFrom)) + query = query.Where(r => r.CompletedAt.CompareTo(dateFrom) >= 0); + if (!string.IsNullOrWhiteSpace(dateTo)) + query = query.Where(r => r.CompletedAt.CompareTo(dateTo) <= 0); + + var total = query.Count(); + var items = query.OrderByDescending(r => r.CompletedAt).Skip(offset).Take(pageSize).ToList(); + + return new PagedResult + { + Items = items, TotalCount = total, Page = page, PageSize = pageSize, + }; + } + } +} diff --git a/test/unit/CounterDrone.Core.Tests/ReportGeneratorTests.cs b/test/unit/CounterDrone.Core.Tests/ReportGeneratorTests.cs new file mode 100644 index 0000000..f5defac --- /dev/null +++ b/test/unit/CounterDrone.Core.Tests/ReportGeneratorTests.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using CounterDrone.Core.Models; +using CounterDrone.Core.Services; +using CounterDrone.Core.Simulation; +using Xunit; +using SimEvent = CounterDrone.Core.Simulation.SimEvent; + +namespace CounterDrone.Core.Tests +{ + public class ReportGeneratorTests + { + [Fact] + public void DetermineInterceptResult_AllDestroyed_Success() + { + var gen = new ReportGenerator(); + Assert.Equal(InterceptResult.Success, gen.DetermineInterceptResult(3, 0, 3)); + } + + [Fact] + public void DetermineInterceptResult_Partial() + { + var gen = new ReportGenerator(); + Assert.Equal(InterceptResult.Partial, gen.DetermineInterceptResult(2, 1, 3)); + } + + [Fact] + public void DetermineInterceptResult_AllFailed() + { + var gen = new ReportGenerator(); + Assert.Equal(InterceptResult.Failed, gen.DetermineInterceptResult(0, 3, 3)); + } + + [Fact] + public void Generate_ProducesReportContent() + { + var gen = new ReportGenerator(); + var config = new TaskFullConfig + { + Task = new SimTask { Name = "测试任务", TaskNumber = "SIM-001" }, + Targets = new List { new TargetConfig { Quantity = 1 } }, + Equipment = new List + { + new EquipmentDeployment { EquipmentRole = (int)EquipmentRole.LaunchPlatform, Quantity = 3 }, + new EquipmentDeployment { EquipmentRole = (int)EquipmentRole.Detection, Quantity = 1 }, + }, + Cloud = new CloudDispersal { AerosolType = (int)AerosolType.InertGas }, + }; + var events = new List + { + new SimEvent { Type = SimEventType.MunitionLaunched, OccurredAt = 5f, Description = "发射" }, + new SimEvent { Type = SimEventType.CloudGenerated, OccurredAt = 10f, Description = "云团" }, + new SimEvent { Type = SimEventType.DroneDestroyed, OccurredAt = 15f, Description = "摧毁" }, + }; + + var content = gen.Generate(config, events, DroneStatus.Destroyed, 30f); + + Assert.Contains("仿真评估报告", content); + Assert.Contains("测试任务", content); + Assert.Contains("成功拦截", content); + Assert.Contains("发射", content); + } + } +} diff --git a/test/unit/CounterDrone.Core.Tests/ReportServiceTests.cs b/test/unit/CounterDrone.Core.Tests/ReportServiceTests.cs new file mode 100644 index 0000000..592afed --- /dev/null +++ b/test/unit/CounterDrone.Core.Tests/ReportServiceTests.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.IO; +using CounterDrone.Core; +using CounterDrone.Core.Models; +using CounterDrone.Core.Services; +using CounterDrone.Core.Simulation; +using SQLite; +using Xunit; +using SimEvent = CounterDrone.Core.Simulation.SimEvent; + +namespace CounterDrone.Core.Tests +{ + public class ReportServiceTests : IDisposable + { + private readonly string _testDir; + private readonly IReportService _service; + private readonly SQLiteConnection _db; + + public ReportServiceTests() + { + _testDir = Path.Combine(Path.GetTempPath(), $"cd_rep_{Guid.NewGuid():N}"); + var paths = new TestPathProvider(_testDir); + var dbm = new DatabaseManager(paths); + _db = dbm.OpenMainDb(); + _service = new ReportService(_db); + } + + public void Dispose() + { + _db?.Close(); + if (Directory.Exists(_testDir)) Directory.Delete(_testDir, true); + } + + [Fact] + public void Generate_And_Get_ReturnsReport() + { + var config = new TaskFullConfig + { + Task = new SimTask { Name = "Test", TaskNumber = "SIM-001" }, + Targets = new List { new TargetConfig { Quantity = 1 } }, + Equipment = new List(), + Cloud = new CloudDispersal(), + }; + + var report = _service.Generate("task1", config, new List(), "Destroyed", 45f); + + Assert.NotNull(report); + Assert.NotEmpty(report.Id); + Assert.Equal("Test", report.TaskName); + Assert.Contains("仿真评估报告", report.Content); + + var fetched = _service.GetReport(report.Id); + Assert.NotNull(fetched); + } + + [Fact] + public void Delete_RemovesReport() + { + var config = new TaskFullConfig + { + Task = new SimTask { Name = "Del", TaskNumber = "SIM-DEL" }, + Targets = new List { new TargetConfig { Quantity = 1 } }, + Equipment = new List(), + Cloud = new CloudDispersal(), + }; + + var report = _service.Generate("task1", config, new List(), "Destroyed", 10f); + _service.DeleteReport(report.Id); + Assert.Null(_service.GetReport(report.Id)); + } + + [Fact] + public void SearchReports_Keyword() + { + var cfg = new TaskFullConfig + { + Task = new SimTask { Name = "城市防御", TaskNumber = "SIM-A" }, + Targets = new List { new TargetConfig { Quantity = 1 } }, + Equipment = new List(), + Cloud = new CloudDispersal(), + }; + + _service.Generate("t1", cfg, new List(), "Destroyed", 10f); + cfg.Task.Name = "平原拦截"; + cfg.Task.TaskNumber = "SIM-B"; + _service.Generate("t2", cfg, new List(), "Destroyed", 20f); + + var result = _service.SearchReports("城市", null, null, 1, 10); + Assert.Equal(1, result.TotalCount); + } + } +}