diff --git a/src/CounterDrone.Core/Simulation/FrameDataStore.cs b/src/CounterDrone.Core/Simulation/FrameDataStore.cs
index 1c87c1a..53a8737 100644
--- a/src/CounterDrone.Core/Simulation/FrameDataStore.cs
+++ b/src/CounterDrone.Core/Simulation/FrameDataStore.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using System.IO;
using CounterDrone.Core.Models;
@@ -8,6 +9,8 @@ namespace CounterDrone.Core.Simulation
public class FrameDataStore
{
private readonly IPathProvider _paths;
+ /// 帧数据保留天数,默认 180 天(6 个月)
+ public int RetentionDays { get; set; } = 180;
public FrameDataStore(IPathProvider paths)
{
@@ -58,5 +61,28 @@ namespace CounterDrone.Core.Simulation
var dbPath = Path.Combine(_paths.GetFramesDir(), $"{taskId}.db");
if (File.Exists(dbPath)) File.Delete(dbPath);
}
+
+ /// 清理过期的帧数据库文件
+ /// 被删除的文件数量
+ public int CleanupExpired()
+ {
+ var dir = _paths.GetFramesDir();
+ if (!Directory.Exists(dir)) return 0;
+
+ var cutoff = DateTime.Now.AddDays(-RetentionDays);
+ var deleted = 0;
+
+ foreach (var file in Directory.GetFiles(dir, "*.db"))
+ {
+ var creationTime = File.GetCreationTime(file);
+ if (creationTime < cutoff)
+ {
+ File.Delete(file);
+ deleted++;
+ }
+ }
+
+ return deleted;
+ }
}
}
diff --git a/test/reports/喷气拦截_20260611_083136.md b/test/reports/喷气拦截_20260611_083136.md
new file mode 100644
index 0000000..1d24d50
--- /dev/null
+++ b/test/reports/喷气拦截_20260611_083136.md
@@ -0,0 +1,47 @@
+# 仿真评估报告
+
+**任务名称**:喷气拦截
+**任务编号**:SIM-20260611-004
+**仿真耗时**:106.4 秒
+
+## 一、仿真前 — 我方配置
+
+| 配置项 | 值 |
+|--------|-----|
+| 目标数量 | 1 |
+| 发射平台 | 6 台 |
+| 探测设备 | 1 台 |
+| 气溶胶类型 | ActiveMaterial |
+
+## 二、仿真中 — 关键事件时序
+
+| 时间(s) | 事件 | 描述 |
+|---------|------|------|
+| 92.2 | MunitionLaunched | 计划发射 mun_1 |
+| 92.2 | MunitionLaunched | 计划发射 mun_2 |
+| 92.2 | MunitionLaunched | 计划发射 mun_3 |
+| 92.2 | MunitionLaunched | 计划发射 mun_4 |
+| 92.2 | MunitionLaunched | 计划发射 mun_5 |
+| 92.2 | MunitionLaunched | 计划发射 mun_6 |
+| 101.8 | CloudGenerated | 云团生成 |
+| 101.8 | CloudGenerated | 云团生成 |
+| 101.8 | CloudGenerated | 云团生成 |
+| 101.8 | CloudGenerated | 云团生成 |
+| 101.8 | CloudGenerated | 云团生成 |
+| 101.8 | CloudGenerated | 云团生成 |
+| 106.4 | DroneDestroyed | 无人机 drone_1 被摧毁 |
+| 106.4 | SimulationEnd | |
+
+## 三、仿真后 — 数据统计
+
+| 统计项 | 值 |
+|--------|-----|
+| 无人机总数 | 1 |
+| 被摧毁 | 1 |
+| 到达目标 | 0 |
+| 侵入管控区 | 0 |
+| 弹药发射次数 | 6 |
+| 云团生成次数 | 6 |
+
+**对抗结果**:成功拦截
+
diff --git a/test/reports/喷气拦截_20260611_083213.md b/test/reports/喷气拦截_20260611_083213.md
new file mode 100644
index 0000000..e2f757d
--- /dev/null
+++ b/test/reports/喷气拦截_20260611_083213.md
@@ -0,0 +1,47 @@
+# 仿真评估报告
+
+**任务名称**:喷气拦截
+**任务编号**:SIM-20260611-003
+**仿真耗时**:106.4 秒
+
+## 一、仿真前 — 我方配置
+
+| 配置项 | 值 |
+|--------|-----|
+| 目标数量 | 1 |
+| 发射平台 | 6 台 |
+| 探测设备 | 1 台 |
+| 气溶胶类型 | ActiveMaterial |
+
+## 二、仿真中 — 关键事件时序
+
+| 时间(s) | 事件 | 描述 |
+|---------|------|------|
+| 92.2 | MunitionLaunched | 计划发射 mun_1 |
+| 92.2 | MunitionLaunched | 计划发射 mun_2 |
+| 92.2 | MunitionLaunched | 计划发射 mun_3 |
+| 92.2 | MunitionLaunched | 计划发射 mun_4 |
+| 92.2 | MunitionLaunched | 计划发射 mun_5 |
+| 92.2 | MunitionLaunched | 计划发射 mun_6 |
+| 101.8 | CloudGenerated | 云团生成 |
+| 101.8 | CloudGenerated | 云团生成 |
+| 101.8 | CloudGenerated | 云团生成 |
+| 101.8 | CloudGenerated | 云团生成 |
+| 101.8 | CloudGenerated | 云团生成 |
+| 101.8 | CloudGenerated | 云团生成 |
+| 106.4 | DroneDestroyed | 无人机 drone_1 被摧毁 |
+| 106.4 | SimulationEnd | |
+
+## 三、仿真后 — 数据统计
+
+| 统计项 | 值 |
+|--------|-----|
+| 无人机总数 | 1 |
+| 被摧毁 | 1 |
+| 到达目标 | 0 |
+| 侵入管控区 | 0 |
+| 弹药发射次数 | 6 |
+| 云团生成次数 | 6 |
+
+**对抗结果**:成功拦截
+
diff --git a/test/reports/活塞拦截_20260611_083140.md b/test/reports/活塞拦截_20260611_083140.md
new file mode 100644
index 0000000..7bfa144
--- /dev/null
+++ b/test/reports/活塞拦截_20260611_083140.md
@@ -0,0 +1,46 @@
+# 仿真评估报告
+
+**任务名称**:活塞拦截
+**任务编号**:SIM-20260611-041
+**仿真耗时**:185.6 秒
+
+## 一、仿真前 — 我方配置
+
+| 配置项 | 值 |
+|--------|-----|
+| 目标数量 | 1 |
+| 发射平台 | 5 台 |
+| 探测设备 | 1 台 |
+| 气溶胶类型 | InertGas |
+
+## 二、仿真中 — 关键事件时序
+
+| 时间(s) | 事件 | 描述 |
+|---------|------|------|
+| 167.4 | MunitionLaunched | 计划发射 mun_1 |
+| 167.4 | MunitionLaunched | 计划发射 mun_2 |
+| 167.4 | MunitionLaunched | 计划发射 mun_3 |
+| 167.4 | MunitionLaunched | 计划发射 mun_4 |
+| 167.4 | MunitionLaunched | 计划发射 mun_5 |
+| 177.0 | CloudGenerated | 云团生成 |
+| 177.0 | CloudGenerated | 云团生成 |
+| 177.0 | CloudGenerated | 云团生成 |
+| 177.0 | CloudGenerated | 云团生成 |
+| 177.0 | CloudGenerated | 云团生成 |
+| 185.6 | DroneDestroyed | 无人机 drone_1 被摧毁 |
+| 185.6 | DroneDestroyed | 无人机 drone_1 被摧毁 |
+| 185.6 | SimulationEnd | |
+
+## 三、仿真后 — 数据统计
+
+| 统计项 | 值 |
+|--------|-----|
+| 无人机总数 | 1 |
+| 被摧毁 | 2 |
+| 到达目标 | 0 |
+| 侵入管控区 | 0 |
+| 弹药发射次数 | 5 |
+| 云团生成次数 | 5 |
+
+**对抗结果**:成功拦截
+
diff --git a/test/reports/活塞拦截_20260611_083216.md b/test/reports/活塞拦截_20260611_083216.md
new file mode 100644
index 0000000..7bfa144
--- /dev/null
+++ b/test/reports/活塞拦截_20260611_083216.md
@@ -0,0 +1,46 @@
+# 仿真评估报告
+
+**任务名称**:活塞拦截
+**任务编号**:SIM-20260611-041
+**仿真耗时**:185.6 秒
+
+## 一、仿真前 — 我方配置
+
+| 配置项 | 值 |
+|--------|-----|
+| 目标数量 | 1 |
+| 发射平台 | 5 台 |
+| 探测设备 | 1 台 |
+| 气溶胶类型 | InertGas |
+
+## 二、仿真中 — 关键事件时序
+
+| 时间(s) | 事件 | 描述 |
+|---------|------|------|
+| 167.4 | MunitionLaunched | 计划发射 mun_1 |
+| 167.4 | MunitionLaunched | 计划发射 mun_2 |
+| 167.4 | MunitionLaunched | 计划发射 mun_3 |
+| 167.4 | MunitionLaunched | 计划发射 mun_4 |
+| 167.4 | MunitionLaunched | 计划发射 mun_5 |
+| 177.0 | CloudGenerated | 云团生成 |
+| 177.0 | CloudGenerated | 云团生成 |
+| 177.0 | CloudGenerated | 云团生成 |
+| 177.0 | CloudGenerated | 云团生成 |
+| 177.0 | CloudGenerated | 云团生成 |
+| 185.6 | DroneDestroyed | 无人机 drone_1 被摧毁 |
+| 185.6 | DroneDestroyed | 无人机 drone_1 被摧毁 |
+| 185.6 | SimulationEnd | |
+
+## 三、仿真后 — 数据统计
+
+| 统计项 | 值 |
+|--------|-----|
+| 无人机总数 | 1 |
+| 被摧毁 | 2 |
+| 到达目标 | 0 |
+| 侵入管控区 | 0 |
+| 弹药发射次数 | 5 |
+| 云团生成次数 | 5 |
+
+**对抗结果**:成功拦截
+
diff --git a/test/unit/CounterDrone.Core.Tests/EdgeCaseTests.cs b/test/unit/CounterDrone.Core.Tests/EdgeCaseTests.cs
new file mode 100644
index 0000000..9f586bb
--- /dev/null
+++ b/test/unit/CounterDrone.Core.Tests/EdgeCaseTests.cs
@@ -0,0 +1,164 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using CounterDrone.Core;
+using CounterDrone.Core.Algorithms;
+using CounterDrone.Core.Models;
+using CounterDrone.Core.Repository;
+using CounterDrone.Core.Services;
+using CounterDrone.Core.Simulation;
+using Xunit;
+
+namespace CounterDrone.Core.Tests
+{
+ /// 边界场景测试
+ public class EdgeCaseTests : IDisposable
+ {
+ private readonly string _testDir;
+ private readonly IScenarioService _scenario;
+ private readonly SQLite.SQLiteConnection _db;
+ private string _taskId = string.Empty;
+
+ public EdgeCaseTests()
+ {
+ _testDir = Path.Combine(Path.GetTempPath(), $"cd_edge_{Guid.NewGuid():N}");
+ var paths = new TestPathProvider(_testDir);
+ var dbm = new DatabaseManager(paths);
+ _db = dbm.OpenMainDb();
+
+ _db.Insert(new AmmunitionSpec
+ {
+ AerosolType = (int)AerosolType.InertGas, InitialRadius = 50,
+ CoreDensity = 1.0, DispersionRateBase = 5, MaxRadius = 500,
+ MaxDuration = 120, EffectiveConcentration = 0.05,
+ });
+
+ _scenario = new ScenarioService(
+ new SimTaskRepository(_db), new CombatSceneRepository(_db),
+ new ControlZoneRepository(_db), new TargetConfigRepository(_db),
+ new EquipmentDeploymentRepository(_db), new CloudDispersalRepository(_db),
+ new RoutePlanRepository(_db), new WaypointRepository(_db));
+ }
+
+ public void Dispose()
+ {
+ _db?.Close();
+ if (Directory.Exists(_testDir)) Directory.Delete(_testDir, true);
+ }
+
+ [Fact]
+ public void EmptyDeployment_EngineRunsWithoutCrash()
+ {
+ var task = _scenario.CreateTask("空部署", "");
+ _taskId = task.Id;
+
+ _scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
+ _scenario.SaveTarget(_taskId, new TargetConfig
+ {
+ TargetType = (int)TargetType.Piston, Quantity = 1,
+ TypicalSpeed = 100, TypicalAltitude = 300,
+ });
+ _scenario.SaveDeployment(_taskId, new List());
+ _scenario.SaveCloudDispersal(_taskId, new CloudDispersal());
+ _scenario.SaveRoute(_taskId, new RoutePlan { FormationMode = (int)FormationMode.Single },
+ new List
+ {
+ new Waypoint { PosX = 0, PosY = 300, PosZ = 0 },
+ new Waypoint { PosX = 1000, PosY = 300, PosZ = 0 },
+ });
+
+ var engine = new SimulationEngine(_scenario, new FrameDataStore(new TestPathProvider(_testDir)),
+ new DamageModelRouter(), new TestPathProvider(_testDir));
+ engine.Initialize(_taskId);
+ engine.TimeScale = 4f;
+
+ for (int i = 0; i < 500; i++)
+ {
+ var r = engine.Tick(1f / 20f);
+ if (r.State == SimulationState.Completed) break;
+ }
+ engine.Stop();
+
+ Assert.Equal(DroneStatus.ReachedTarget, engine.Drones[0].Status);
+ }
+
+ [Fact]
+ public void ExtremeWind_DroneStillFlies()
+ {
+ var task = _scenario.CreateTask("飓风", "");
+ _taskId = task.Id;
+
+ _scenario.SaveScene(_taskId, new CombatScene
+ {
+ WindSpeed = 30, // 最大风速
+ WindDirection = (int)WindDirection.E,
+ });
+ _scenario.SaveTarget(_taskId, new TargetConfig
+ {
+ TargetType = (int)TargetType.Piston, Quantity = 1,
+ TypicalSpeed = 600, // 高速对抗强风
+ });
+ _scenario.SaveDeployment(_taskId, new List());
+ _scenario.SaveCloudDispersal(_taskId, new CloudDispersal());
+ _scenario.SaveRoute(_taskId, new RoutePlan { FormationMode = (int)FormationMode.Single },
+ new List
+ {
+ new Waypoint { PosX = 0, PosY = 300, PosZ = 0 },
+ new Waypoint { PosX = 5000, PosY = 300, PosZ = 0 },
+ });
+
+ var engine = new SimulationEngine(_scenario, new FrameDataStore(new TestPathProvider(_testDir)),
+ new DamageModelRouter(), new TestPathProvider(_testDir));
+ engine.Initialize(_taskId);
+ engine.TimeScale = 4f;
+
+ for (int i = 0; i < 500; i++)
+ {
+ var r = engine.Tick(1f / 20f);
+ if (r.State == SimulationState.Completed) break;
+ }
+ engine.Stop();
+
+ // 强风下应偏移到东方(+X)
+ Assert.True(engine.Drones[0].PosX > 0);
+ }
+
+ [Fact]
+ public void LongRoute_CompletesWithoutError()
+ {
+ var task = _scenario.CreateTask("超长航路", "");
+ _taskId = task.Id;
+
+ _scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
+ _scenario.SaveTarget(_taskId, new TargetConfig
+ {
+ TargetType = (int)TargetType.HighSpeed, Quantity = 1,
+ TypicalSpeed = 500,
+ });
+ _scenario.SaveDeployment(_taskId, new List());
+ _scenario.SaveCloudDispersal(_taskId, new CloudDispersal());
+
+ // 500 km/h, 100km 航程
+ _scenario.SaveRoute(_taskId, new RoutePlan { FormationMode = (int)FormationMode.Single },
+ new List
+ {
+ new Waypoint { PosX = 0, PosY = 500, PosZ = 0 },
+ new Waypoint { PosX = 100000, PosY = 500, PosZ = 0 },
+ });
+
+ var engine = new SimulationEngine(_scenario, new FrameDataStore(new TestPathProvider(_testDir)),
+ new DamageModelRouter(), new TestPathProvider(_testDir));
+ engine.Initialize(_taskId);
+ engine.TimeScale = 4f;
+
+ for (int i = 0; i < 5000; i++)
+ {
+ var r = engine.Tick(1f / 20f);
+ if (r.State == SimulationState.Completed) break;
+ }
+ engine.Stop();
+
+ Assert.Equal(DroneStatus.ReachedTarget, engine.Drones[0].Status);
+ }
+ }
+}
diff --git a/test/unit/CounterDrone.Core.Tests/FrameDataStoreTests.cs b/test/unit/CounterDrone.Core.Tests/FrameDataStoreTests.cs
new file mode 100644
index 0000000..7dc0b55
--- /dev/null
+++ b/test/unit/CounterDrone.Core.Tests/FrameDataStoreTests.cs
@@ -0,0 +1,72 @@
+using System;
+using System.IO;
+using CounterDrone.Core;
+using CounterDrone.Core.Simulation;
+using Xunit;
+
+namespace CounterDrone.Core.Tests
+{
+ public class FrameDataStoreTests : IDisposable
+ {
+ private readonly string _testDir;
+ private readonly FrameDataStore _store;
+
+ public FrameDataStoreTests()
+ {
+ _testDir = Path.Combine(Path.GetTempPath(), $"cd_frames_{Guid.NewGuid():N}");
+ var paths = new TestPathProvider(_testDir);
+ _store = new FrameDataStore(paths);
+ }
+
+ public void Dispose()
+ {
+ if (Directory.Exists(_testDir)) Directory.Delete(_testDir, true);
+ }
+
+ [Fact]
+ public void CleanupExpired_DeletesOldFiles()
+ {
+ // 设置为 0 天保留,所有文件都是"过期"的
+ _store.RetentionDays = 0;
+
+ // 创建一些假帧数据库
+ var db1 = _store.CreateOrOpen("task_old_1");
+ db1.Close();
+ var db2 = _store.CreateOrOpen("task_old_2");
+ db2.Close();
+
+ var deleted = _store.CleanupExpired();
+ Assert.Equal(2, deleted);
+
+ // 文件应该被删除
+ Assert.False(File.Exists(Path.Combine(_testDir, "frames", "task_old_1.db")));
+ Assert.False(File.Exists(Path.Combine(_testDir, "frames", "task_old_2.db")));
+ }
+
+ [Fact]
+ public void CleanupExpired_KeepsRecentFiles()
+ {
+ _store.RetentionDays = 365; // 保留 1 年
+
+ var db = _store.CreateOrOpen("task_new");
+ db.Close();
+
+ // 手动修改创建时间为"最近"
+ var filePath = Path.Combine(_testDir, "frames", "task_new.db");
+ File.SetCreationTime(filePath, DateTime.Now);
+
+ var deleted = _store.CleanupExpired();
+ Assert.Equal(0, deleted);
+
+ // 文件应该保留
+ Assert.True(File.Exists(filePath));
+ }
+
+ [Fact]
+ public void CleanupExpired_EmptyDir_ReturnsZero()
+ {
+ var deleted = _store.CleanupExpired();
+ Assert.Equal(0, deleted);
+ }
+ }
+}