配置运行时重载 IConfigService + 防御推荐 + 文档更新

新增:
- IConfigService / ConfigService:读写 defaults.json / planner_config.json
- SavePlannerConfig / SaveDefaults / Reload,无需重启 Unity
- PlannerConfigReloaded 事件 → ScenarioManager 自动重建 planner
- SimulationEngine.UpdatePlanner / ScenarioService.UpdatePlanner
- SimulationRunner.ReloadPlanner 桥接
- DefenseRecommendation / RecommendOption 返回类型
- IScenarioService.GetDefenseRecommendation(配置阶段生成最佳抛撒参数)
- ConfigServiceTests 5 个测试 + DefenseRecommendationTests 3 个测试

修复:
- 云团间距半径基准不一致(间距用 R30、穿越用 R27 → 统一用 effectiveRadius)
- 消除硬编码 30f(DefensePlanner + GaussianPuffDispersion 读 Phase2Duration)
- TryGenerateFireEvents 复用 ComputeEffectiveRadius,消除重复计算
- 概率计算间距加重叠系数
- DatabaseManager 不再 DROP SimulationReport(保留历史报告)
- 清理迁移代码(MetaEntry / 旧表 DROP / 版本检查)

文档:
- 对接文档 V2.1:加 IConfigService API + 配置重载用法 + 修正 DLL 更新说明
- 实施跟踪 V1.8:8.2.1 更新 + 新增 8.2.3/8.2.4 + M14 里程碑
- 270 测试通过,Unity 编译通过
This commit is contained in:
tian 2026-06-25 22:10:22 +08:00
parent a3ceae5c14
commit 36ab4fad7b
12 changed files with 329 additions and 18 deletions

View File

@ -1,8 +1,8 @@
# 实施计划与任务跟踪 # 实施计划与任务跟踪
> **项目**:反无人机仿真系统后端 > **项目**:反无人机仿真系统后端
> **文档版本**V1.7 > **文档版本**V1.8
> **更新日期**2026-06-20 > **更新日期**2026-06-22
--- ---
@ -144,7 +144,7 @@ Phase 13 ✅ 模型重构:分层架构 + 代码审查 + 探测分离
| 指标 | 值 | | 指标 | 值 |
|------|------| |------|------|
| 测试总数 | **262**(全部通过) | | 测试总数 | **270**(全部通过) |
| 行覆盖率 | **95%+** | | 行覆盖率 | **95%+** |
| 分支覆盖率 | **80%+** | | 分支覆盖率 | **80%+** |
| 执行时间 | ~15 秒 | | 执行时间 | ~15 秒 |
@ -190,8 +190,10 @@ Phase 13 ✅ 模型重构:分层架构 + 代码审查 + 探测分离
| # | 功能 | 说明 | | # | 功能 | 说明 |
|---|------|------| |---|------|------|
| 8.2.1 | PDF 导出 | ✅ PdfSharpCorenetstandard2.0,纯托管 Unity 兼容)+ ReportData 结构化模型 + StandardPdfTemplate 内置标准模板 + CJK 字体嵌入。`IReportService.ExportReport(id, "pdf")` 按需导出,仿真后自动生成 MD 到报告目录 | | 8.2.1 | PDF 导出 | ✅ PdfSharpCore + ReportData 结构化模型 + StandardPdfTemplate + CJK 字体嵌入 + IConfigService 运行时重载 |
| 8.2.2 | Word 导出 | ⬜ 调研可行方案 | | 8.2.2 | Word 导出 | ⬜ 调研可行方案 |
| 8.2.3 | 防御推荐方案 | ✅ `IScenarioService.GetDefenseRecommendation`,配置阶段调 planner 生成最佳抛撒参数,前端"一键应用" |
| 8.2.4 | 配置运行时重载 | ✅ `IConfigService` 读写 defaults.json / planner_config.jsonSavePlannerConfig/SaveDefaults/Reload 无需重启 |
### 8.3 第三方对接 ### 8.3 第三方对接
@ -453,6 +455,7 @@ T1数据层→ T2算法层 IsInCoverage
| M11 — 运动学重构 + 实体暴露 + 基础数据 CRUD | ✅ | | M11 — 运动学重构 + 实体暴露 + 基础数据 CRUD | ✅ |
| M12 — 空基/地基统一规划 + 编队轴 + LaneDivider | ✅ | | M12 — 空基/地基统一规划 + 编队轴 + LaneDivider | ✅ |
| M13 — PDF 导出 + 报告模板架构 | ✅ | | M13 — PDF 导出 + 报告模板架构 | ✅ |
| M14 — 防御推荐 GetDefenseRecommendation + 配置运行时重载 | ✅ |
--- ---

View File

@ -1,7 +1,7 @@
# 后端对接文档Unity 前端) # 后端对接文档Unity 前端)
> **版本**V2.0 > **版本**V2.1
> **日期**2026-06-20 > **日期**2026-06-22
> **Unity 版本**2022.3.62f3c1 > **Unity 版本**2022.3.62f3c1
--- ---
@ -60,7 +60,7 @@ dotnet publish -c Release -o ../../unity_plugins
# 然后将 unity_plugins/*.dll 全部复制到 src/Unity/Assets/Plugins/CounterDrone.Core/ # 然后将 unity_plugins/*.dll 全部复制到 src/Unity/Assets/Plugins/CounterDrone.Core/
``` ```
> **重要**:更新 DLL 后首次运行 Unity 会重建数据库,**历史仿真报告会丢失**(基础数据和预设想定自动重新种子)。这是预期行为 > **重要**:更新 DLL 后首次运行 Unity 会建表+种子默认数据。**历史仿真报告不再丢失**SimulationReport 表不再被 DROP
--- ---
@ -272,6 +272,7 @@ cam.transform.LookAt(new Vector3(midX, 500, 0)); // 看向 500m 高度
```csharp ```csharp
public IScenarioService Service { get; } public IScenarioService Service { get; }
public IDataService DataService { get; } // 基础数据 CRUD7 类) public IDataService DataService { get; } // 基础数据 CRUD7 类)
public IConfigService Config { get; } // 配置读写 + 运行时重载
// Scenario CRUD // Scenario CRUD
public Scenario CreateScenario(string name, string number); public Scenario CreateScenario(string name, string number);
@ -354,6 +355,47 @@ else
{ {
Debug.Log($"推荐失败: {rec.Summary}"); Debug.Log($"推荐失败: {rec.Summary}");
} }
```
### IConfigService通过 `ScenarioManager.Config` 访问)
配置文件读写 + 运行时重载,**无需重启 Unity**
```csharp
public interface IConfigService
{
string GetPlannerConfigJson(); // 读取 planner_config.json 原文
void SavePlannerConfig(string json); // 保存并立即重载planner 自动重建)
string GetDefaultsJson(); // 读取 defaults.json 原文
void SaveDefaults(string json); // 保存并重载种子数据到数据库
void Reload(); // 从磁盘重新加载(前端直接改文件后通知后端刷新)
}
```
**用法 1通过 API 修改配置**(推荐)
```csharp
var cfg = GetComponent<ScenarioManager>().Config;
// 读当前 planner 配置
string json = cfg.GetPlannerConfigJson();
// 修改后保存 — 自动重载planner 立即用新参数
cfg.SavePlannerConfig(modifiedJson);
// 修改 defaults.json — 自动重载,数据库 InsertOrReplace 覆盖同 Id 记录
cfg.SaveDefaults(modifiedDefaultsJson);
```
**用法 2直接改文件后通知后端重载**
```csharp
// 前端直接修改 persistentDataPath 下的 JSON 文件
// 然后通知后端重新加载
cfg.Reload(); // 重新读磁盘 → 重建 planner → 刷新种子数据
```
> `SavePlannerConfig` 触发后,`ScenarioManager` 内部的 `DefensePlanner` 会自动重建。`SimulationRunner` 中的 planner 在下次 `LoadAndStart` 时自动用新配置。运行中的仿真不受影响planner 在 `Initialize` 时调用,不在运行中调用)。
### IDataService通过 `ScenarioManager.DataService` 访问) ### IDataService通过 `ScenarioManager.DataService` 访问)
@ -910,4 +952,4 @@ ScenarioConfig
|------|------| |------|------|
| 架构设计 | `docs/design/architecture/总体架构设计.md` | | 架构设计 | `docs/design/architecture/总体架构设计.md` |
| 实体事件映射 | `docs/design/technical/仿真器实体与事件映射.md` | | 实体事件映射 | `docs/design/technical/仿真器实体与事件映射.md` |
| 测试状态 | **262 测试15 秒(全部通过)** | | 测试状态 | **270 测试14 秒(全部通过)** |

View File

@ -0,0 +1,93 @@
using System;
using System.IO;
using System.Text.Json;
using CounterDrone.Core.Algorithms;
using CounterDrone.Core.Models;
using SQLite;
namespace CounterDrone.Core.Services
{
/// <summary>配置服务实现 — 读写 JSON 配置文件,重载时触发事件通知消费方刷新</summary>
public class ConfigService : IConfigService
{
private readonly IPathProvider _paths;
private readonly SQLiteConnection _db;
/// <summary>planner 配置重载后触发(消费方应重建 DefensePlanner</summary>
public event Action<PlannerConfig>? PlannerConfigReloaded;
/// <summary>默认数据重载后触发(消费方应刷新缓存)</summary>
public event Action<DefaultData>? DefaultsReloaded;
public ConfigService(IPathProvider paths, SQLiteConnection db)
{
_paths = paths;
_db = db;
}
public string GetPlannerConfigJson()
{
var path = Path.Combine(_paths.GetDataRoot(), "planner_config.json");
if (!File.Exists(path))
throw new FileNotFoundException($"planner 配置文件不存在: {path}");
return File.ReadAllText(path);
}
public void SavePlannerConfig(string json)
{
var path = Path.Combine(_paths.GetDataRoot(), "planner_config.json");
File.WriteAllText(path, json);
ReloadPlannerConfig();
}
public string GetDefaultsJson()
{
var path = Path.Combine(_paths.GetDataRoot(), "defaults.json");
if (!File.Exists(path))
throw new FileNotFoundException($"默认数据文件不存在: {path}");
return File.ReadAllText(path);
}
public void SaveDefaults(string json)
{
var path = Path.Combine(_paths.GetDataRoot(), "defaults.json");
File.WriteAllText(path, json);
ReloadDefaults();
}
public void Reload()
{
ReloadPlannerConfig();
ReloadDefaults();
}
private void ReloadPlannerConfig()
{
var config = PlannerConfig.Load(_paths);
PlannerConfigReloaded?.Invoke(config);
}
private void ReloadDefaults()
{
var defaults = DefaultData.Load(_paths);
ReseedDatabase(defaults);
DefaultsReloaded?.Invoke(defaults);
}
private void ReseedDatabase(DefaultData defaults)
{
foreach (var a in defaults.Ammunition) _db.InsertOrReplace(a);
foreach (var f in defaults.FireUnits) _db.InsertOrReplace(f);
foreach (var l in defaults.LaunchPlatforms) _db.InsertOrReplace(l);
foreach (var d in defaults.Drones) _db.InsertOrReplace(d);
foreach (var s in defaults.Sensors) _db.InsertOrReplace(s);
foreach (var e in defaults.Environments) _db.InsertOrReplace(e);
foreach (var f in defaults.Formations) _db.InsertOrReplace(f);
foreach (var r in defaults.Routes)
{
r.WaypointsJson = JsonSerializer.Serialize(r.Waypoints);
_db.InsertOrReplace(r);
}
}
}
}

View File

@ -0,0 +1,23 @@
using CounterDrone.Core.Algorithms;
namespace CounterDrone.Core.Services
{
/// <summary>配置服务 — 读写 defaults.json / planner_config.json支持运行时重载</summary>
public interface IConfigService
{
/// <summary>读取 planner 配置 JSON 原文</summary>
string GetPlannerConfigJson();
/// <summary>保存 planner 配置 JSON 并立即重载</summary>
void SavePlannerConfig(string json);
/// <summary>读取 defaults.json 原文</summary>
string GetDefaultsJson();
/// <summary>保存 defaults.json 并重载种子数据到数据库InsertOrReplace 覆盖同 Id 记录)</summary>
void SaveDefaults(string json);
/// <summary>重新从磁盘加载配置(前端直接改文件后调此方法通知后端刷新)</summary>
void Reload();
}
}

View File

@ -60,7 +60,10 @@ namespace CounterDrone.Core.Services
/// <summary>返回所有枚举定义(供前端下拉框使用)</summary> /// <summary>返回所有枚举定义(供前端下拉框使用)</summary>
EnumMetadata GetEnums(); EnumMetadata GetEnums();
/// <summary>生成防御推荐方案(配置阶段调用,仿真前基于已配置参数计算最佳/最危险抛撒参数)</summary> /// <summary>生成防御推荐方案(配置阶段调用,仿真前基于已配置参数计算最佳抛撒参数)</summary>
DefenseRecommendation GetDefenseRecommendation(string scenarioId); DefenseRecommendation GetDefenseRecommendation(string scenarioId);
/// <summary>替换 planner 实例(配置重载后由 ConfigService 触发)</summary>
void UpdatePlanner(Algorithms.IDefensePlanner planner);
} }
} }

View File

@ -20,7 +20,7 @@ namespace CounterDrone.Core.Services
private readonly RoutePlanRepository _routeRepo; private readonly RoutePlanRepository _routeRepo;
private readonly WaypointRepository _waypointRepo; private readonly WaypointRepository _waypointRepo;
private readonly IPathProvider? _paths; private readonly IPathProvider? _paths;
private readonly IDefensePlanner? _planner; private IDefensePlanner? _planner;
public ScenarioService( public ScenarioService(
ScenarioRepository scenarioRepo, ScenarioRepository scenarioRepo,
@ -46,6 +46,9 @@ namespace CounterDrone.Core.Services
_planner = planner; _planner = planner;
} }
/// <summary>替换 planner 实例(配置重载后由 ConfigService 触发)</summary>
public void UpdatePlanner(IDefensePlanner planner) => _planner = planner;
// ========== scenario CRUD ========== // ========== scenario CRUD ==========
public Scenario CreateScenario(string name, string ScenarioNumber) public Scenario CreateScenario(string name, string ScenarioNumber)

View File

@ -59,7 +59,7 @@ namespace CounterDrone.Core.Simulation
private float _detectionTime; private float _detectionTime;
private readonly List<SimEvent> _allEvents = new(); private readonly List<SimEvent> _allEvents = new();
private string _scenarioId = string.Empty; private string _scenarioId = string.Empty;
private readonly IDefensePlanner _planner; private IDefensePlanner _planner;
private int _entityCounter; private int _entityCounter;
// ── GC 优化:预分配复用池 ── // ── GC 优化:预分配复用池 ──
@ -79,6 +79,9 @@ namespace CounterDrone.Core.Simulation
_planner = planner; _planner = planner;
} }
/// <summary>替换 planner 实例(配置重载后调用)</summary>
public void UpdatePlanner(IDefensePlanner planner) => _planner = planner;
public void SetFireSchedule(List<FireEvent> schedule) public void SetFireSchedule(List<FireEvent> schedule)
{ {
_fireSchedule.Clear(); _fireSchedule.Clear();

View File

@ -14,32 +14,45 @@ namespace CounterDrone.Unity
private IScenarioService _service; private IScenarioService _service;
private SQLite.SQLiteConnection _db; private SQLite.SQLiteConnection _db;
private IDataService _dataService; private IDataService _dataService;
private ConfigService _configService;
private IPathProvider _paths;
public IScenarioService Service => _service; public IScenarioService Service => _service;
public IDataService DataService => _dataService; public IDataService DataService => _dataService;
public IConfigService Config => _configService;
public void Awake() public void Awake()
{ {
try try
{ {
var paths = new UnityPathProvider(); _paths = new UnityPathProvider();
Debug.Log($"ScenarioManager.Awake: dbPath={paths.GetMainDbPath()}"); Debug.Log($"ScenarioManager.Awake: dbPath={_paths.GetMainDbPath()}");
_db = new DatabaseManager(paths).OpenMainDb(); _db = new DatabaseManager(_paths).OpenMainDb();
SqliteConnectionTracker.Track(_db); SqliteConnectionTracker.Track(_db);
var planner = new DefensePlanner( var planner = new DefensePlanner(
DefaultData.Load(paths).Ammunition, DefaultData.Load(_paths).Ammunition,
PlannerConfig.Load(paths)); PlannerConfig.Load(_paths));
_service = new ScenarioService( _service = new ScenarioService(
new ScenarioRepository(_db), new CombatSceneRepository(_db), new ScenarioRepository(_db), new CombatSceneRepository(_db),
new ControlZoneRepository(_db), new ScenarioDroneRepository(_db), new ControlZoneRepository(_db), new ScenarioDroneRepository(_db),
new ScenarioUnitRepository(_db), new CloudDispersalRepository(_db), new ScenarioUnitRepository(_db), new CloudDispersalRepository(_db),
new RoutePlanRepository(_db), new WaypointRepository(_db), new RoutePlanRepository(_db), new WaypointRepository(_db),
paths, planner); _paths, planner);
_dataService = new DataService( _dataService = new DataService(
new AmmunitionSpecRepository(_db), new FireUnitSpecRepository(_db), new AmmunitionSpecRepository(_db), new FireUnitSpecRepository(_db),
new DroneSpecRepository(_db), new SensorSpecRepository(_db), new DroneSpecRepository(_db), new SensorSpecRepository(_db),
new EnvironmentSpecRepository(_db), new FormationTemplateRepository(_db), new EnvironmentSpecRepository(_db), new FormationTemplateRepository(_db),
new RouteTemplateRepository(_db)); new RouteTemplateRepository(_db));
// 配置服务:订阅重载事件,自动重建 planner
_configService = new ConfigService(_paths, _db);
_configService.PlannerConfigReloaded += cfg =>
{
var newPlanner = new DefensePlanner(DefaultData.Load(_paths).Ammunition, cfg);
_service.UpdatePlanner(newPlanner);
Debug.Log($"ScenarioManager: planner 配置已重载");
};
Debug.Log("ScenarioManager.Awake: OK"); Debug.Log("ScenarioManager.Awake: OK");
} }
catch (System.Exception ex) catch (System.Exception ex)

View File

@ -25,6 +25,7 @@ namespace CounterDrone.Unity
private IPathProvider _paths; private IPathProvider _paths;
private string _scenarioId; private string _scenarioId;
private SQLite.SQLiteConnection _db; private SQLite.SQLiteConnection _db;
private IDefensePlanner _planner;
private readonly Dictionary<string, GameObject> _entityVisuals = new(); private readonly Dictionary<string, GameObject> _entityVisuals = new();
private readonly Dictionary<string, GameObject> _clouds = new(); private readonly Dictionary<string, GameObject> _clouds = new();
@ -49,7 +50,16 @@ namespace CounterDrone.Unity
new RoutePlanRepository(_db), new WaypointRepository(_db)); new RoutePlanRepository(_db), new WaypointRepository(_db));
_frameStore = new FrameDataStore(_paths); _frameStore = new FrameDataStore(_paths);
_engine = new SimulationEngine(_scenario, _frameStore, new DamageModelRouter(), _paths, new DefensePlanner(DefaultData.Load(_paths).Ammunition, PlannerConfig.Load(_paths))); _planner = new DefensePlanner(DefaultData.Load(_paths).Ammunition, PlannerConfig.Load(_paths));
_engine = new SimulationEngine(_scenario, _frameStore, new DamageModelRouter(), _paths, _planner);
}
/// <summary>配置重载后重建 planner由 ConfigService 触发)</summary>
public void ReloadPlanner()
{
_planner = new DefensePlanner(DefaultData.Load(_paths).Ammunition, PlannerConfig.Load(_paths));
if (_engine != null)
_engine.UpdatePlanner(_planner);
} }
public void LoadAndStart(string scenarioId) public void LoadAndStart(string scenarioId)

View File

@ -0,0 +1,118 @@
using System;
using System.IO;
using System.Text.Json;
using CounterDrone.Core;
using CounterDrone.Core.Algorithms;
using CounterDrone.Core.Services;
using Xunit;
namespace CounterDrone.Core.Tests
{
public class ConfigServiceTests : IDisposable
{
private readonly string _testDir;
private readonly IPathProvider _paths;
private readonly ConfigService _config;
private readonly SQLite.SQLiteConnection _db;
public ConfigServiceTests()
{
_testDir = Path.Combine(Path.GetTempPath(), $"cd_cfg_{Guid.NewGuid():N}");
_paths = new TestPathProvider(_testDir);
var dbm = new DatabaseManager(_paths);
_db = dbm.OpenMainDb();
_config = new ConfigService(_paths, _db);
}
public void Dispose()
{
_db?.Close();
if (Directory.Exists(_testDir)) Directory.Delete(_testDir, true);
}
[Fact]
public void GetPlannerConfigJson_ReturnsValidJson()
{
var json = _config.GetPlannerConfigJson();
var cfg = JsonSerializer.Deserialize<PlannerConfig>(json, new JsonSerializerOptions
{
Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() },
PropertyNameCaseInsensitive = true,
});
Assert.NotNull(cfg);
Assert.True(cfg.CloudOverlapRatio >= 0);
}
[Fact]
public void SavePlannerConfig_PersistsAndReloads()
{
var original = _config.GetPlannerConfigJson();
var cfg = JsonSerializer.Deserialize<PlannerConfig>(original, new JsonSerializerOptions
{
Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() },
PropertyNameCaseInsensitive = true,
})!;
cfg.CloudOverlapRatio = 0.3f;
var modifiedJson = JsonSerializer.Serialize(cfg, new JsonSerializerOptions
{
Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() },
});
_config.SavePlannerConfig(modifiedJson);
var reloaded = _config.GetPlannerConfigJson();
var reloadedCfg = JsonSerializer.Deserialize<PlannerConfig>(reloaded, new JsonSerializerOptions
{
Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() },
PropertyNameCaseInsensitive = true,
})!;
Assert.Equal(0.3f, reloadedCfg.CloudOverlapRatio);
}
[Fact]
public void PlannerConfigReloaded_EventFires()
{
PlannerConfig? received = null;
_config.PlannerConfigReloaded += cfg => received = cfg;
var json = _config.GetPlannerConfigJson();
_config.SavePlannerConfig(json);
Assert.NotNull(received);
Assert.True(received.CloudOverlapRatio >= 0);
}
[Fact]
public void Reload_FromDiskRefreshesConfig()
{
// 直接改文件
var path = Path.Combine(_testDir, "planner_config.json");
var json = File.ReadAllText(path);
var cfg = JsonSerializer.Deserialize<PlannerConfig>(json, new JsonSerializerOptions
{
Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() },
PropertyNameCaseInsensitive = true,
})!;
cfg.ExpansionFactor = 0.8f;
File.WriteAllText(path, JsonSerializer.Serialize(cfg, new JsonSerializerOptions
{
Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() },
}));
PlannerConfig? received = null;
_config.PlannerConfigReloaded += cfg => received = cfg;
_config.Reload();
Assert.NotNull(received);
Assert.Equal(0.8f, received.ExpansionFactor);
}
[Fact]
public void GetDefaultsJson_ReturnsValidJson()
{
var json = _config.GetDefaultsJson();
Assert.Contains("ammunition", json.ToLower());
}
}
}

Binary file not shown.