diff --git a/docs/implementation/tasks/实施计划与任务跟踪.md b/docs/implementation/tasks/实施计划与任务跟踪.md index c39ff9a..38e7f44 100644 --- a/docs/implementation/tasks/实施计划与任务跟踪.md +++ b/docs/implementation/tasks/实施计划与任务跟踪.md @@ -1,8 +1,8 @@ # 实施计划与任务跟踪 > **项目**:反无人机仿真系统后端 -> **文档版本**:V1.7 -> **更新日期**:2026-06-20 +> **文档版本**:V1.8 +> **更新日期**:2026-06-22 --- @@ -144,7 +144,7 @@ Phase 13 ✅ 模型重构:分层架构 + 代码审查 + 探测分离 | 指标 | 值 | |------|------| -| 测试总数 | **262**(全部通过) | +| 测试总数 | **270**(全部通过) | | 行覆盖率 | **95%+** | | 分支覆盖率 | **80%+** | | 执行时间 | ~15 秒 | @@ -190,8 +190,10 @@ Phase 13 ✅ 模型重构:分层架构 + 代码审查 + 探测分离 | # | 功能 | 说明 | |---|------|------| -| 8.2.1 | PDF 导出 | ✅ PdfSharpCore(netstandard2.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.3 | 防御推荐方案 | ✅ `IScenarioService.GetDefenseRecommendation`,配置阶段调 planner 生成最佳抛撒参数,前端"一键应用" | +| 8.2.4 | 配置运行时重载 | ✅ `IConfigService` 读写 defaults.json / planner_config.json,SavePlannerConfig/SaveDefaults/Reload 无需重启 | ### 8.3 第三方对接 @@ -453,6 +455,7 @@ T1(数据层)→ T2(算法层 IsInCoverage) | M11 — 运动学重构 + 实体暴露 + 基础数据 CRUD | ✅ | | M12 — 空基/地基统一规划 + 编队轴 + LaneDivider | ✅ | | M13 — PDF 导出 + 报告模板架构 | ✅ | +| M14 — 防御推荐 GetDefenseRecommendation + 配置运行时重载 | ✅ | --- diff --git a/docs/对接文档_Unity前端.md b/docs/对接文档_Unity前端.md index 894a774..fb3d429 100644 --- a/docs/对接文档_Unity前端.md +++ b/docs/对接文档_Unity前端.md @@ -1,7 +1,7 @@ # 后端对接文档(Unity 前端) -> **版本**:V2.0 -> **日期**:2026-06-20 +> **版本**:V2.1 +> **日期**:2026-06-22 > **Unity 版本**:2022.3.62f3c1 --- @@ -60,7 +60,7 @@ dotnet publish -c Release -o ../../unity_plugins # 然后将 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 public IScenarioService Service { get; } public IDataService DataService { get; } // 基础数据 CRUD(7 类) +public IConfigService Config { get; } // 配置读写 + 运行时重载 // Scenario CRUD public Scenario CreateScenario(string name, string number); @@ -354,6 +355,47 @@ else { 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().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` 访问) @@ -910,4 +952,4 @@ ScenarioConfig |------|------| | 架构设计 | `docs/design/architecture/总体架构设计.md` | | 实体事件映射 | `docs/design/technical/仿真器实体与事件映射.md` | -| 测试状态 | **262 测试,15 秒(全部通过)** | +| 测试状态 | **270 测试,14 秒(全部通过)** | diff --git a/src/CounterDrone.Core/Services/ConfigService.cs b/src/CounterDrone.Core/Services/ConfigService.cs new file mode 100644 index 0000000..ee7fd84 --- /dev/null +++ b/src/CounterDrone.Core/Services/ConfigService.cs @@ -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 +{ + /// 配置服务实现 — 读写 JSON 配置文件,重载时触发事件通知消费方刷新 + public class ConfigService : IConfigService + { + private readonly IPathProvider _paths; + private readonly SQLiteConnection _db; + + /// planner 配置重载后触发(消费方应重建 DefensePlanner) + public event Action? PlannerConfigReloaded; + + /// 默认数据重载后触发(消费方应刷新缓存) + public event Action? 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); + } + } + } +} diff --git a/src/CounterDrone.Core/Services/IConfigService.cs b/src/CounterDrone.Core/Services/IConfigService.cs new file mode 100644 index 0000000..eab4390 --- /dev/null +++ b/src/CounterDrone.Core/Services/IConfigService.cs @@ -0,0 +1,23 @@ +using CounterDrone.Core.Algorithms; + +namespace CounterDrone.Core.Services +{ + /// 配置服务 — 读写 defaults.json / planner_config.json,支持运行时重载 + public interface IConfigService + { + /// 读取 planner 配置 JSON 原文 + string GetPlannerConfigJson(); + + /// 保存 planner 配置 JSON 并立即重载 + void SavePlannerConfig(string json); + + /// 读取 defaults.json 原文 + string GetDefaultsJson(); + + /// 保存 defaults.json 并重载种子数据到数据库(InsertOrReplace 覆盖同 Id 记录) + void SaveDefaults(string json); + + /// 重新从磁盘加载配置(前端直接改文件后调此方法通知后端刷新) + void Reload(); + } +} diff --git a/src/CounterDrone.Core/Services/IScenarioService.cs b/src/CounterDrone.Core/Services/IScenarioService.cs index 63ab731..a44094d 100644 --- a/src/CounterDrone.Core/Services/IScenarioService.cs +++ b/src/CounterDrone.Core/Services/IScenarioService.cs @@ -60,7 +60,10 @@ namespace CounterDrone.Core.Services /// 返回所有枚举定义(供前端下拉框使用) EnumMetadata GetEnums(); - /// 生成防御推荐方案(配置阶段调用,仿真前基于已配置参数计算最佳/最危险抛撒参数) + /// 生成防御推荐方案(配置阶段调用,仿真前基于已配置参数计算最佳抛撒参数) DefenseRecommendation GetDefenseRecommendation(string scenarioId); + + /// 替换 planner 实例(配置重载后由 ConfigService 触发) + void UpdatePlanner(Algorithms.IDefensePlanner planner); } } diff --git a/src/CounterDrone.Core/Services/ScenarioService.cs b/src/CounterDrone.Core/Services/ScenarioService.cs index 5c855c9..350b8bc 100644 --- a/src/CounterDrone.Core/Services/ScenarioService.cs +++ b/src/CounterDrone.Core/Services/ScenarioService.cs @@ -20,7 +20,7 @@ namespace CounterDrone.Core.Services private readonly RoutePlanRepository _routeRepo; private readonly WaypointRepository _waypointRepo; private readonly IPathProvider? _paths; - private readonly IDefensePlanner? _planner; + private IDefensePlanner? _planner; public ScenarioService( ScenarioRepository scenarioRepo, @@ -46,6 +46,9 @@ namespace CounterDrone.Core.Services _planner = planner; } + /// 替换 planner 实例(配置重载后由 ConfigService 触发) + public void UpdatePlanner(IDefensePlanner planner) => _planner = planner; + // ========== scenario CRUD ========== public Scenario CreateScenario(string name, string ScenarioNumber) diff --git a/src/CounterDrone.Core/Simulation/SimulationEngine.cs b/src/CounterDrone.Core/Simulation/SimulationEngine.cs index 2006f41..3107230 100644 --- a/src/CounterDrone.Core/Simulation/SimulationEngine.cs +++ b/src/CounterDrone.Core/Simulation/SimulationEngine.cs @@ -59,7 +59,7 @@ namespace CounterDrone.Core.Simulation private float _detectionTime; private readonly List _allEvents = new(); private string _scenarioId = string.Empty; - private readonly IDefensePlanner _planner; + private IDefensePlanner _planner; private int _entityCounter; // ── GC 优化:预分配复用池 ── @@ -79,6 +79,9 @@ namespace CounterDrone.Core.Simulation _planner = planner; } + /// 替换 planner 实例(配置重载后调用) + public void UpdatePlanner(IDefensePlanner planner) => _planner = planner; + public void SetFireSchedule(List schedule) { _fireSchedule.Clear(); diff --git a/src/Unity/Assets/Plugins/CounterDrone.Core/CounterDrone.Core.dll b/src/Unity/Assets/Plugins/CounterDrone.Core/CounterDrone.Core.dll index 40b897d..1a30be2 100644 Binary files a/src/Unity/Assets/Plugins/CounterDrone.Core/CounterDrone.Core.dll and b/src/Unity/Assets/Plugins/CounterDrone.Core/CounterDrone.Core.dll differ diff --git a/src/Unity/Assets/Scripts/Managers/ScenarioManager.cs b/src/Unity/Assets/Scripts/Managers/ScenarioManager.cs index 680413a..03e4433 100644 --- a/src/Unity/Assets/Scripts/Managers/ScenarioManager.cs +++ b/src/Unity/Assets/Scripts/Managers/ScenarioManager.cs @@ -14,32 +14,45 @@ namespace CounterDrone.Unity private IScenarioService _service; private SQLite.SQLiteConnection _db; private IDataService _dataService; + private ConfigService _configService; + private IPathProvider _paths; public IScenarioService Service => _service; public IDataService DataService => _dataService; + public IConfigService Config => _configService; public void Awake() { try { - var paths = new UnityPathProvider(); - Debug.Log($"ScenarioManager.Awake: dbPath={paths.GetMainDbPath()}"); - _db = new DatabaseManager(paths).OpenMainDb(); + _paths = new UnityPathProvider(); + Debug.Log($"ScenarioManager.Awake: dbPath={_paths.GetMainDbPath()}"); + _db = new DatabaseManager(_paths).OpenMainDb(); SqliteConnectionTracker.Track(_db); var planner = new DefensePlanner( - DefaultData.Load(paths).Ammunition, - PlannerConfig.Load(paths)); + DefaultData.Load(_paths).Ammunition, + PlannerConfig.Load(_paths)); _service = new ScenarioService( new ScenarioRepository(_db), new CombatSceneRepository(_db), new ControlZoneRepository(_db), new ScenarioDroneRepository(_db), new ScenarioUnitRepository(_db), new CloudDispersalRepository(_db), new RoutePlanRepository(_db), new WaypointRepository(_db), - paths, planner); + _paths, planner); _dataService = new DataService( new AmmunitionSpecRepository(_db), new FireUnitSpecRepository(_db), new DroneSpecRepository(_db), new SensorSpecRepository(_db), new EnvironmentSpecRepository(_db), new FormationTemplateRepository(_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"); } catch (System.Exception ex) diff --git a/src/Unity/Assets/Scripts/Managers/SimulationRunner.cs b/src/Unity/Assets/Scripts/Managers/SimulationRunner.cs index 2422b81..c16cda5 100644 --- a/src/Unity/Assets/Scripts/Managers/SimulationRunner.cs +++ b/src/Unity/Assets/Scripts/Managers/SimulationRunner.cs @@ -25,6 +25,7 @@ namespace CounterDrone.Unity private IPathProvider _paths; private string _scenarioId; private SQLite.SQLiteConnection _db; + private IDefensePlanner _planner; private readonly Dictionary _entityVisuals = new(); private readonly Dictionary _clouds = new(); @@ -49,7 +50,16 @@ namespace CounterDrone.Unity new RoutePlanRepository(_db), new WaypointRepository(_db)); _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); + } + + /// 配置重载后重建 planner(由 ConfigService 触发) + public void ReloadPlanner() + { + _planner = new DefensePlanner(DefaultData.Load(_paths).Ammunition, PlannerConfig.Load(_paths)); + if (_engine != null) + _engine.UpdatePlanner(_planner); } public void LoadAndStart(string scenarioId) diff --git a/test/unit/CounterDrone.Core.Tests/ConfigServiceTests.cs b/test/unit/CounterDrone.Core.Tests/ConfigServiceTests.cs new file mode 100644 index 0000000..d40a88f --- /dev/null +++ b/test/unit/CounterDrone.Core.Tests/ConfigServiceTests.cs @@ -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(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(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(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(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()); + } + } +} diff --git a/unity_plugins/CounterDrone.Core.dll b/unity_plugins/CounterDrone.Core.dll index 40b897d..1a30be2 100644 Binary files a/unity_plugins/CounterDrone.Core.dll and b/unity_plugins/CounterDrone.Core.dll differ