Phase 5: 报告生成 — ReportGenerator + ReportService, 116测试4秒完成
This commit is contained in:
parent
eb347b3bb0
commit
aea15e6ec6
15
src/CounterDrone.Core/Services/IReportService.cs
Normal file
15
src/CounterDrone.Core/Services/IReportService.cs
Normal file
@ -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<SimEvent> events, string finalDroneStatus, float duration);
|
||||
SimulationReport GetReport(string id);
|
||||
void DeleteReport(string id);
|
||||
PagedResult<SimulationReport> SearchReports(string keyword, string? dateFrom, string? dateTo, int page, int pageSize);
|
||||
}
|
||||
}
|
||||
96
src/CounterDrone.Core/Services/ReportGenerator.cs
Normal file
96
src/CounterDrone.Core/Services/ReportGenerator.cs
Normal file
@ -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
|
||||
{
|
||||
/// <summary>根据仿真结果生成报告内容</summary>
|
||||
public string Generate(TaskFullConfig config, List<SimEvent> 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 => "拦截失败",
|
||||
_ => "未知",
|
||||
};
|
||||
}
|
||||
}
|
||||
78
src/CounterDrone.Core/Services/ReportService.cs
Normal file
78
src/CounterDrone.Core/Services/ReportService.cs
Normal file
@ -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<SimEvent> 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<SimulationReport>(id);
|
||||
}
|
||||
|
||||
public void DeleteReport(string id)
|
||||
{
|
||||
_db.Delete<SimulationReport>(id);
|
||||
}
|
||||
|
||||
public PagedResult<SimulationReport> 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<SimulationReport>().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<SimulationReport>
|
||||
{
|
||||
Items = items, TotalCount = total, Page = page, PageSize = pageSize,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
63
test/unit/CounterDrone.Core.Tests/ReportGeneratorTests.cs
Normal file
63
test/unit/CounterDrone.Core.Tests/ReportGeneratorTests.cs
Normal file
@ -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<TargetConfig> { new TargetConfig { Quantity = 1 } },
|
||||
Equipment = new List<EquipmentDeployment>
|
||||
{
|
||||
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<SimEvent>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
93
test/unit/CounterDrone.Core.Tests/ReportServiceTests.cs
Normal file
93
test/unit/CounterDrone.Core.Tests/ReportServiceTests.cs
Normal file
@ -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<TargetConfig> { new TargetConfig { Quantity = 1 } },
|
||||
Equipment = new List<EquipmentDeployment>(),
|
||||
Cloud = new CloudDispersal(),
|
||||
};
|
||||
|
||||
var report = _service.Generate("task1", config, new List<SimEvent>(), "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<TargetConfig> { new TargetConfig { Quantity = 1 } },
|
||||
Equipment = new List<EquipmentDeployment>(),
|
||||
Cloud = new CloudDispersal(),
|
||||
};
|
||||
|
||||
var report = _service.Generate("task1", config, new List<SimEvent>(), "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<TargetConfig> { new TargetConfig { Quantity = 1 } },
|
||||
Equipment = new List<EquipmentDeployment>(),
|
||||
Cloud = new CloudDispersal(),
|
||||
};
|
||||
|
||||
_service.Generate("t1", cfg, new List<SimEvent>(), "Destroyed", 10f);
|
||||
cfg.Task.Name = "平原拦截";
|
||||
cfg.Task.TaskNumber = "SIM-B";
|
||||
_service.Generate("t2", cfg, new List<SimEvent>(), "Destroyed", 20f);
|
||||
|
||||
var result = _service.SearchReports("城市", null, null, 1, 10);
|
||||
Assert.Equal(1, result.TotalCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user