refactor: 编组概念拆分为批次(WaveId)+火力单元(FireUnit)

Breaking: Group表/枚举/Service/Repository移除; GroupId->WaveId; DroneGroup->DroneWave; DroneGroupId->DroneWaveId; GroupManager删除

Docs: CHANGELOG 0.7.0; 总体架构 V10; DefensePlanner V4; 实施计划 V1.4; 对接文档 V1.2

Tests: 204/204 pass
This commit is contained in:
tian 2026-06-15 15:13:48 +08:00
parent 665747a846
commit 56d345189e
39 changed files with 252 additions and 525 deletions

View File

@ -2,6 +2,47 @@
---
## [0.7.0] - 2026-06-15
### Breaking — 概念升级:编组拆分 + Group 表移除
- **Group 表移除**:原 `Group`DroneFleet / EquipmentGroup不再创建。向后兼容保留旧库中的 Group 表,但不再主动读写
- **GroupType 枚举删除**:不再区分 DroneFleet / EquipmentGroup
- **GroupService / IGroupService / GroupRepository 删除**:不再需要编组管理服务
- **Unity GroupManager 删除**:不再需要编组管理桥接
### Changed — 数据模型重命名
- **`GroupId``WaveId`**TargetConfig、EquipmentDeployment、RoutePlan、Waypoint 四表的编组外键重命名为批次外键
- **RoutePlan 索引重命名**`(TaskId, GroupId)` → `(TaskId, WaveId)`
- **RoutePlanRepository / WaypointRepository**`GetByTaskAndGroup` → `GetByTaskAndWave`
- **IScenarioService.SaveRoute**`groupId` 参数 → `waveId`
### Changed — 算法类型重命名
- **`DroneGroup``DroneWave`**AlgorithmTypes、IDefensePlanner、DefensePlanner、SimulationEngine 全部使用新类型名
- **`DroneGroupId``DroneWaveId`**UnitAssignment 中的编队引用 → 批次引用
- **`BuildDroneGroups()``BuildDroneWaves()`**SimulationEngine 构建方法重命名
### Changed — 文档同步
- 总体架构设计:编组概念升级说明
- DefensePlanner 技术方案DroneGroup → DroneWave
- 实施计划GroupService 移除、多编队 → 多批次
- Unity 前端对接文档DefaultDefenseAdvisor → DefensePlannerSaveRoute 签名更新
- 仿真器实体事件映射BuildDroneGroups → BuildDroneWaves
### Removed
- 7 个文件Group.cs / GroupRepository.cs / IGroupService.cs / GroupService.cs / GroupManager.cs / GroupServiceTests.cs / GroupRepositoryTests.cs
### Metrics
- 测试 208 → **204**4删除 Group 相关测试),全量通过 63s
- 0 编译错误0 编译警告
---
## [0.6.0] - 2026-06-15
### Added — 探测驱动的规划8.1.1

View File

@ -1,11 +1,11 @@
# 反无人机仿真系统 — 总体架构设计
> **版本**V9
> **日期**2026-06-13
> **版本**V10
> **日期**2026-06-15
> **状态**:已实现
> **Unity 版本**22.3.62
> **.NET 版本**.NET Standard 2.1
> **变更**DefensePlanner 五步规划引擎FireUnit 通道模型空基平台飞控SQLite domain reload 修复;删除所有 fallback 默认值
> **变更**Group 表移除GroupId → WaveIdDroneGroup → DroneWave编组概念升级为批次+火力单元
---
@ -17,7 +17,7 @@
| 模块 | 核心功能 |
|------|----------|
| **模型管理** | 3D 模型导入/预览/删除,编组管理 |
| **模型管理** | 3D 模型导入/预览/删除 |
| **想定管理** | 仿真任务 CRUD步骤化配置向导管控区域设置搜索分页 |
| **仿真报告** | 报告列表,含时序图的完整报告预览,导出 PDF/Word删除 |
@ -41,12 +41,12 @@
├──────────────────────────────────────────────────────────┤
│ Unity Application Layer │
│ ModelPanel | ScenarioWizard | SimulationRunner | │
│ ReplayController | ReportPanel | GroupManager
│ ReplayController | ReportPanel
├──────────────────────────────────────────────────────────┤
│ Service Layer (Pure C#) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ ModelService │ │ScenarioService│ │ SimulationEngine │ │
│ │ GroupService │ │ DefensePlanner │ │ ├ DroneEntity │ │
│ │ │ │ DefensePlanner │ │ ├ DroneEntity │ │
│ └──────────────┘ └──────────────┘ │ ├ PlatformEntity │ │
│ ┌──────────────┐ ┌──────────────┐ │ ├ MunitionEntity │ │
│ │FrameDataStore│ │ReportService │ │ ├ CloudEntity │ │
@ -199,9 +199,19 @@ enum TriggerMode { Time = 0, Area = 1, Manual = 2 }
// === 毁伤状态阶段(输出到 StateData 供 Unity 渲染)===
enum DamageStage { Normal = 0, EngineAnomaly = 1, AttitudeLoss = 2, Destroyed = 3 }
// === 编队 & 编组 ===
// === 编队 ===
enum FormationMode { Single = 0, Formation = 1, Swarm = 2 }
enum GroupType { DroneFleet = 0, EquipmentGroup = 1 }
// === 核心作战概念 ===
// 火力单元FireUnit能独立完成搜索、跟踪、瞄准并实施打击的最小作战实体。
// 包含探测(雷达/光电/红外)+ 打击(发射架/火炮)的完整闭环系统。
// 配置层 = EquipmentDeployment(LaunchPlatform),含探测字段 + 打击字段。
// 独立探测节点DetectionNode只探测不打击的侦查节点如前沿警戒雷达
// 配置层 = EquipmentDeployment(Detection),仅含探测字段。
// 两者都往统一信息网络送探测信息planner 基于融合后的探测边界规划。
// 无人机批次DroneWave有共同航路的一组无人机攻击方
// 批次关联航路但不独占(多个批次可共享同一条航路)。
// 配置层 = TargetConfig(含 WaveId) + RoutePlan(TaskId, WaveId) + Waypoints。
// === 运行时 ===
enum EntityType { Drone = 0, Platform = 1, DetectionEquip = 2, Cloud = 3, Munition = 4 }
@ -254,12 +264,11 @@ CounterDroneBackend_Data/
SimTask — 仿真任务主表
CombatScene — 步骤1作战场景含24h时间、天气
ControlZone — 步骤1+:管控区域/电子围栏NEW
TargetConfig — 步骤2目标配置
EquipmentDeployment — 步骤3装备部署搭载平台+探测设备
TargetConfig — 步骤2目标配置(含 WaveId 关联批次)
EquipmentDeployment — 步骤3装备部署火力单元含探测+打击,或独立探测节点
CloudDispersal — 步骤4云团抛撒配置
RoutePlan — 步骤5航路规划1:N SimTask编队
RoutePlan — 步骤5航路规划1:N SimTask批次批次关联但不独占航路FK 关联 TargetConfig.WaveId
Waypoint — 航路点
Group — 编组(独立管理)
SimulationReport — 仿真报告
SimEvent — 仿真事件(时序图数据源)
@ -267,6 +276,12 @@ CounterDroneBackend_Data/
SimFrameRecord — 逐帧位置记录(回放用)
```
> **核心作战概念**
> - **火力单元FireUnit**:能独立完成搜索、跟踪、瞄准并实施打击的最小作战实体。包含探测(雷达/光电/红外)+ 打击(发射架/火炮)的完整闭环。配置层 = `EquipmentDeployment(LaunchPlatform)`
> - **独立探测节点DetectionNode**:只探测不打击的侦查节点(如前沿警戒雷达)。配置层 = `EquipmentDeployment(Detection)`
> - **无人机批次DroneWave**:有共同航路的一组无人机(攻击方)。批次关联航路但不独占(多个批次可共享同一条航路)。配置层 = `TargetConfig(WaveId)` + `RoutePlan(TaskId, WaveId)` + `Waypoints`
> - 原来的 `Group`(编组)表已移除:无人机编队概念升级为"批次",装备编组概念不再需要(火力单元是独立作战单位)。
### 4.3 业务表详细设计
#### ModelInfo — 模型元数据
@ -337,7 +352,7 @@ CounterDroneBackend_Data/
|------|------|------|------|
| Id | TEXT | PK | GUID |
| TaskId | TEXT | FK | |
| GroupId | TEXT | FK | 关联无人机编队 Group |
| WaveId | TEXT | FK | 关联无人机批次(同一 WaveId 的无人机共享航路) |
| TargetType | INTEGER | | 0旋翼 1固定翼 2电推 3活塞 4高速 |
| Quantity | INTEGER | DEFAULT 1 | 目标数量 |
| PowerType | INTEGER | | 0电推 1活塞 2喷吸气 |
@ -355,28 +370,31 @@ CounterDroneBackend_Data/
| 活塞 | 2.5 | 120 | 800 |
| 高速目标 | 1.5 | 300 | 2000 |
#### EquipmentDeployment — 步骤3装备部署重构
#### EquipmentDeployment — 步骤3装备部署火力单元 + 独立探测节点
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| Id | TEXT | PK | GUID |
| TaskId | TEXT | FK | |
| EquipmentRole | INTEGER | | 0=探测设备 1=发射平台 |
| EquipmentRole | INTEGER | | 0=独立探测节点 1=火力单元(发射平台) |
| Quantity | INTEGER | DEFAULT 1 | 部署数量 |
| GroupId | TEXT | FK | 关联装备编组 Group火力单元多批次时用于分配 |
| // 以下为发射平台专用字段 |
| PlatformType | INTEGER | NULLABLE | 0=空基(大型无人机) 1=地基(炮弹)探测设备为 NULL |
| WaveId | TEXT | NULLABLE | 可选关联批次(用于 UI 分组,火力单元是独立作战单位,不强依赖批次 |
| // 以下为火力单元(发射平台专用字段 |
| PlatformType | INTEGER | NULLABLE | 0=空基(大型无人机) 1=地基(炮弹)独立探测节点为 NULL |
| PositionX | REAL | | 部署位置 |
| PositionY | REAL | | |
| PositionZ | REAL | | |
| AerosolType | INTEGER | NULLABLE | 挂载气溶胶类型,探测设备为 NULL |
| AerosolType | INTEGER | NULLABLE | 挂载气溶胶类型,独立探测节点为 NULL |
| MunitionCount | INTEGER | NULLABLE | 挂载弹药数量 |
| MuzzleVelocity | REAL | NULLABLE | **NEW** 弹药初速 m/s地基炮弹用 |
| ReleaseAltitude | REAL | NULLABLE | **NEW** 弹药释放高度 m空基平台用 |
| Cooldown | REAL | DEFAULT 5 | 发射冷却时间 s同弹种连发间隔 |
| AmmoChangeTime | REAL | DEFAULT 300 | **NEW** 更换弹种所需时间 s默认 5 分钟) |
| // 以下为探测设备专用字段 |
| DetectionRadius | REAL | NULLABLE | 探测半径 m |
| // 探测能力(火力单元自带 + 独立探测节点均有) |
| RadarRange | REAL | NULLABLE | 雷达探测距离 m雨雾不衰减 |
| EORange | REAL | NULLABLE | 光电探测距离 m受 Visibility 衰减) |
| IRRange | REAL | NULLABLE | 红外探测距离 m不受 Visibility 影响) |
| DetectionAccuracy | REAL | NULLABLE | 探测精度 m位置误差影响抛撒散布范围 |
> **装备体系说明**
> - **搭载平台**2 种):
@ -415,12 +433,12 @@ CounterDroneBackend_Data/
#### RoutePlan — 步骤5航路规划
> **支持多批次**:通过 `TaskId + GroupId` 唯一标识一条航路,一个任务可有多个编队各自独立航路。
> **支持多批次**:通过 `TaskId + WaveId` 唯一标识一条航路,一个任务可有多个批次各自独立航路。
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| TaskId | TEXT | PK(复合) | |
| GroupId | TEXT | PK(复合), FK → Group | 关联编队:同一任务不同编队可有独立航路 |
| WaveId | TEXT | PK(复合) | 关联批次:同一任务不同批次可有独立航路(多个批次可共享同一条航路) |
| FormationMode | INTEGER | | 0单机 1编队 2蜂群 |
| FormationSpacing | REAL | DEFAULT 50 | m |
| ETA | TEXT | | 预计到达时间 |
@ -438,15 +456,13 @@ CounterDroneBackend_Data/
| Altitude | REAL | | 该航点飞行高度 m |
| Speed | REAL | | 该段速度 km/h |
#### Group — 编组
#### ~~Group~~ — 编组(已移除)
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| Id | TEXT | PK | GUID |
| Name | TEXT | NOT NULL | 编组名称 |
| GroupType | INTEGER | | 0=无人机编队 1=装备编组 |
| Description | TEXT | | 备注 |
| CreatedAt | TEXT | | |
> **概念升级**:原 Group 表的两种类型已分别升级:
> - `DroneFleet`(无人机编队)→ **无人机批次DroneWave**:由 `TargetConfig(WaveId)` + `RoutePlan(TaskId, WaveId)` + `Waypoints` 隐式表达。批次关联航路但不独占(多个批次可共享同一条航路)。
> - `EquipmentGroup`(装备编组)→ **不再需要**。火力单元是独立作战的最小单位,不需要上层编组。
>
> 数据层:`Group` 表保留但不再主动使用(向后兼容)。`TargetConfig.WaveId` 语义为"批次 ID"。
### 4.4 运行时表
@ -507,7 +523,7 @@ SimulationEngine
├── SimulationState ← 状态(运行/暂停/结束)
├── SceneConfig ← 配置快照(含场景边界、管控区域)
├── List<DroneEntity> ← 无人机(目标方)
│ └── 按编组组织,共享航路
│ └── 按批次组织,共享航路
├── List<PlatformEntity> ← 搭载平台(发射方)★重构
│ ├── AirBasedPlatform ← 空基(大型无人机)
│ └── GroundBasedPlatform ← 地基(火炮)
@ -613,7 +629,7 @@ Unity 渲染毁伤效果(尾焰异常、姿态失控、坠落)
```
属性:
- 编组 GroupId、航路点队列 Waypoint[]
- 批次 WaveId、航路点队列 Waypoint[]
- 当前位置、速度、朝向
- HP0~1 归一化)
- DamageStageNormal → EngineAnomaly → AttitudeLoss → Destroyed
@ -1080,7 +1096,7 @@ planner 所有策略参数从 `data/planner_config.json` 读取,代码零默
// === 防御规划 ===
public interface IDefensePlanner
{
PlannerResult Plan(List<FireUnit> fireUnits, List<DroneGroup> threats, CombatScene environment);
PlannerResult Plan(List<FireUnit> fireUnits, List<DroneWave> threats, CombatScene environment);
}
// ThreatProfile / DefenseRecommendation / DefenseSolution 等模型见本节上文
@ -1265,7 +1281,7 @@ public interface IFrameDataStore
| 阶段 | 内容 | 数据来源 |
|------|------|------|
| **仿真前** | 我方配置(装备类型/数量/编组、弹药类型/数量、部署位置、管控区域) | SimTask + 5 步配置 |
| **仿真前** | 我方配置(装备类型/数量、火力单元部署、弹药类型/数量、部署位置、管控区域) | SimTask + 5 步配置 |
| **仿真中** | 关键事件时序表、各实体轨迹概要、云团演化数据 | SimEvent + SimFrameRecord |
| **仿真后** | 拦截成功率、各阶段耗时、毁伤效果评估、对抗结果判定 | SimEvent 汇总统计 |
@ -1297,14 +1313,6 @@ interface IModelService
ModelInfo GetModel(string id);
}
// === 编组管理 ===
interface IGroupService
{
Group CreateGroup(string name, GroupType type, string description);
void DeleteGroup(string id);
List<Group> GetGroups(GroupType? type);
}
// === 想定管理 ===
interface IScenarioService
{
@ -1366,7 +1374,7 @@ interface IReportService
### 数据模型
一个仿真任务可包含**多种类型、多批次**的无人机,每批次(编队)有独立的航路:
一个仿真任务可包含**多种类型、多批次**的无人机,每批次有独立的航路:
```
SimTask "城市防御演习"
@ -1374,51 +1382,51 @@ SimTask "城市防御演习"
├── ControlZone[](共享)
├── CloudDispersal共享云团参数具体拦截方案由算法确定
├── 编队 A活塞×3 ← Group.Id
│ ├── TargetConfig类型=活塞,数量=3
│ ├── RoutePlan航路 A北→南
├── 批次 A活塞×3 ← WaveId
│ ├── TargetConfigWaveId=A类型=活塞,数量=3
│ ├── RoutePlanTaskId + WaveId=A航路 A北→南
│ └── Waypoint[]
├── 编队 B喷气×2 ← Group.Id
│ ├── TargetConfig类型=高速,数量=2
│ ├── RoutePlan航路 B西→东
├── 批次 B喷气×2 ← WaveId
│ ├── TargetConfigWaveId=B类型=高速,数量=2
│ ├── RoutePlanTaskId + WaveId=B航路 B西→东
│ └── Waypoint[]
└── 编队 C旋翼×5 ← Group.Id
├── TargetConfig类型=旋翼,数量=5
├── RoutePlan航路 C
└── 批次 C旋翼×5 ← WaveId
├── TargetConfigWaveId=C类型=旋翼,数量=5
├── RoutePlanTaskId + WaveId=C航路 C
└── Waypoint[]
```
**关键变更**`RoutePlan` 主键`TaskId` 改为 `(TaskId, GroupId)` 复合键
**关键变更**`RoutePlan` 主键`(TaskId, WaveId)` 复合键。`Waypoint` 通过 `(TaskId, WaveId)` 关联航路
### 推荐算法
算法需要同时考虑**所有无人机编队**和**可用装备火力单元**,自动匹配分配:
算法需要同时考虑**所有无人机批次**和**可用火力单元**,自动匹配分配:
```csharp
// 输入:所有无人机编队 + 已部署的火力单元
// 输出:每个编队的拦截方案 + 合并的发射计划
var allSolutions = new List<DefenseSolution>();
var usedPlatforms = new HashSet<int>();
var usedFireUnits = new HashSet<string>();
foreach (var droneGroup in threat.DroneGroups)
foreach (var droneWave in threat.DroneWaves)
{
// 从可用装备中选择合适的火力单元(弹药类型匹配目标动力类型)
var availableUnits = fireUnits.Where(u => !usedPlatforms.Contains(u.Id)).ToList();
var solution = advisor.RecommendForGroup(droneGroup, environment, availableUnits);
// 从可用火力单元中选择合适的(弹药类型匹配目标动力类型)
var availableUnits = fireUnits.Where(u => !usedFireUnits.Contains(u.Id)).ToList();
var solution = advisor.RecommendForWave(droneWave, environment, availableUnits);
// 标记占用的平台(一组装备可同时对付一个编队)
foreach (var idx in solution.AssignedPlatformIndices)
usedPlatforms.Add(idx);
// 标记占用的火力单元
foreach (var idx in solution.AssignedFireUnitIds)
usedFireUnits.Add(idx);
allSolutions.Add(solution);
}
// 复用判定(不写死"一对一"或"一对多"
// 同种弹药 → 只需冷却时间(秒级),可直接复用
// 异种弹药 → 需要 AmmoChangeTime默认 5 分钟),检查编队间隔是否够
// 异种弹药 → 需要 AmmoChangeTime默认 5 分钟),检查批次间隔是否够
// 弹药耗尽 → 不可复用
var merged = MergeFireSchedules(allSolutions);
@ -1435,13 +1443,13 @@ engine.SetFireSchedule(merged);
| 表 | 变更 |
|------|------|
| `RoutePlan` | PK 改为 (TaskId, GroupId) 复合键 |
| `TargetConfig` | 不变(已有 GroupId FK |
| `Waypoint` | 关联 `(TaskId, GroupId)` 定位航路 |
| `RoutePlan` | PK 为 (TaskId, WaveId) 复合键 |
| `TargetConfig` | 不变(已有 WaveId |
| `Waypoint` | 关联 `(TaskId, WaveId)` 定位航路 |
`IScenarioService` 新增
`IScenarioService` `SaveRoute` 签名
```csharp
void SaveRoute(string taskId, string groupId, RoutePlan route, List<Waypoint> wps); // groupId 参数
void SaveRoute(string taskId, string waveId, RoutePlan route, List<Waypoint> wps); // waveId 参数
```
### 报告
@ -1450,10 +1458,10 @@ void SaveRoute(string taskId, string groupId, RoutePlan route, List<Waypoint> wp
```markdown
## 编队汇总
| 编队 | 目标 | 数量 | 拦截方案 | 结果 |
| 批次 | 目标 | 数量 | 拦截方案 | 结果 |
|------|------|------|------|------|
| 编队A | 活塞 | 3 | 惰性气体 12发 | ✅ 全毁 |
| 编队B | 喷气 | 2 | 活性材料 8发 | ⚠️ 1逃脱 |
| 批次A | 活塞 | 3 | 惰性气体 12发 | ✅ 全毁 |
| 批次B | 喷气 | 2 | 活性材料 8发 | ⚠️ 1逃脱 |
```
---
@ -1473,7 +1481,7 @@ void SaveRoute(string taskId, string groupId, RoutePlan route, List<Waypoint> wp
│ └─ 多边形绘制 / 编辑 / 删除 │
├────────────────────────────────────────────────────────┤
│ 步骤 2目标配置 │
│ ├─ 编组选择(已有编队/新建编队)
│ ├─ 批次选择(已有批次/新建批次)
│ ├─ 目标类型选择(自动填充默认参数) │
│ ├─ 数量 / 动力类型 / 翼展 / 速度 / 高度 │
│ └─ 预设典型目标库NEW
@ -1481,7 +1489,7 @@ void SaveRoute(string taskId, string groupId, RoutePlan route, List<Waypoint> wp
│ 步骤 3装备部署重构
│ ├─ 装备角色(探测设备 / 发射平台) │
│ ├─ 发射平台类型(空基 / 地基 NEW
│ ├─ 数量 / 编组 / 部署位置 │
│ ├─ 数量 / 部署位置 │
│ ├─ 挂载气溶胶类型 / 弹药数量 │
│ ├─ 弹药初速 / 释放高度 / 冷却时间 │
│ └─ 探测半径(探测设备) │
@ -1521,7 +1529,7 @@ void SaveRoute(string taskId, string groupId, RoutePlan route, List<Waypoint> wp
| 12 | 第三方数据 | **静态配置JSON → SQLite** | 仅提供弹药基础参数和初始扩散状态,不提供运行时算法 |
| 13 | 运动学 | 简化运动学 + 风偏 | 航路点线性插值 + 风速矢量 |
| 14 | 弹药弹道 | 抛物线(地基)/ 自由落体(空基) | 基于初速、角度、释放高度计算 |
| 15 | 编组 | Group 独立实体 | 编组独立管理,任务中引用 |
| 15 | 批次 / 火力单元 | WaveId + 火力单元独立作战 | 批次关联航路但不独占;火力单元是最小独立作战单位,不需要上层编组 |
| 16 | 管控区域 | ControlZone 表 + Tick 判定 | 电子围栏,侵入即任务失败 |
| 17 | 配置向导流程 | **威胁驱动三阶段** | Phase1 定义威胁 → Phase2 算法推荐 → Phase3 审阅确认 |
| 18 | 报告导出 | 模板填充 → Markdown | PDF/Word 待调研 |

View File

@ -1,14 +1,14 @@
# DefensePlanner 防御规划引擎 — 技术方案
- **版本**V3
- **日期**2026-06-13
- **版本**V4
- **日期**2026-06-15
- **状态**:已实现
---
## 1. 概述
DefensePlanner 是防御推荐模块的核心引擎。它接收**可用火力单元池**和**威胁编队列表**,综合考虑弹药匹配、空间可达性、时间约束、资源竞争,输出**最优分配方案**和**临界边际方案**。
DefensePlanner 是防御推荐模块的核心引擎。它接收**可用火力单元池**和**威胁批次列表**,综合考虑弹药匹配、空间可达性、时间约束、资源竞争,输出**最优分配方案**和**临界边际方案**。
**当前问题**
@ -54,11 +54,11 @@ class FireUnit {
}
```
### 2.2 DroneGroup威胁编队
### 2.2 DroneWave威胁批次
```csharp
class DroneGroup {
string GroupId; // 编队 ID
class DroneWave {
string WaveId; // 批次 ID
TargetConfig Target; // 类型、数量、动力、翼展、速度、高度
List<Waypoint> Waypoints; // 航路点
float ArrivalTime; // 预计算:到达防御区域中点的时间 s
@ -78,7 +78,7 @@ class DroneGroup {
```csharp
class UnitAssignment {
string FireUnitId; // 分配到的火力单元
string DroneGroupId; // 对抗的威胁编队
string DroneWaveId; // 对抗的威胁批次
AerosolType AmmoType; // 装填的弹药类型
int RoundsFired; // 本次发射几发
float FirstFireTime; // 首发发射时机 s
@ -113,7 +113,7 @@ class PlannerResult {
## 4. 内部流程(五步法)
```
输入List<FireUnit> + List<DroneGroup> + CombatScene
输入List<FireUnit> + List<DroneWave> + CombatScene
Step 1 — 威胁排序
@ -190,7 +190,7 @@ class InterceptCandidate {
**候选生成逻辑**
对于给定的威胁编队和火力单元:
对于给定的威胁批次和火力单元:
1. **弹药兼容性**`Unit.AmmoTypes` 包含威胁需要的弹药类型
2. **地基可达性**:计算目标与部署点的水平距离,校验 `MuzzleVelocity` 射程
@ -239,7 +239,7 @@ for each threat in pending:
> 作用:给操作员一个置信区间——最优 vs 临界,展示"再少就不够了"的底线。
### 5.5 多编队合并
### 5.5 多批次合并
```
Step 4 的贪心算法天然支持多威胁:
@ -258,7 +258,7 @@ Step 4 的贪心算法天然支持多威胁:
public interface IDefensePlanner
{
/// <summary>为给定火力单元池和威胁列表生成规划方案</summary>
PlannerResult Plan(List<FireUnit> fireUnits, List<DroneGroup> threats, CombatScene environment);
PlannerResult Plan(List<FireUnit> fireUnits, List<DroneWave> threats, CombatScene environment);
}
```
@ -273,16 +273,16 @@ public class DefaultDefensePlanner : IDefensePlanner
public DefaultDefensePlanner(List<AmmunitionSpec> ammoCatalog) { ... }
public PlannerResult Plan(List<FireUnit> fireUnits, List<DroneGroup> threats, CombatScene env)
public PlannerResult Plan(List<FireUnit> fireUnits, List<DroneWave> threats, CombatScene env)
{
// Step 1-5
}
// 内部方法
private List<DroneGroup> Prioritize(List<DroneGroup> threats) { ... }
private List<DroneWave> Prioritize(List<DroneWave> threats) { ... }
private AerosolType MatchAmmo(PowerType power) { ... }
private List<InterceptCandidate> GenerateCandidates(DroneGroup threat, List<FireUnit> units, float now) { ... }
private DefensePlan Solve(List<DroneGroup> threats, List<FireUnit> units) { ... }
private List<InterceptCandidate> GenerateCandidates(DroneWave threat, List<FireUnit> units, float now) { ... }
private DefensePlan Solve(List<DroneWave> threats, List<FireUnit> units) { ... }
private DefensePlan DeriveCritical(DefensePlan best) { ... }
}
```
@ -293,7 +293,7 @@ public class DefaultDefensePlanner : IDefensePlanner
|--------|--------|------|
| `IDefenseAdvisor.Recommend(ThreatProfile)` | `IDefensePlanner.Plan(fireUnits, threats, env)` | ✅ 已替换 |
| `IDefenseAdvisor.GetDefenseRecommendation(taskId)` | 不再需要 | ✅ 已删除 |
| `DefaultDefenseAdvisor.RecommendMultiGroup()` | Planner 原生支持多编队 | ✅ 已删除 |
| `DefaultDefenseAdvisor.RecommendMultiGroup()` | Planner 原生支持多批次 | ✅ 已删除 |
---
@ -304,7 +304,7 @@ SimulationEngine.Initialize(taskId)
│ // 引擎内部组装
├── BuildFireUnits(config) → List<FireUnit>
├── BuildDroneGroups(config) → List<DroneGroup>
├── BuildDroneWaves(config) → List<DroneWave>
└── _scene (CombatScene)

View File

@ -41,7 +41,7 @@
SimulationEngine.Initialize()
├─ BuildFireUnits() → List<FireUnit>
├─ BuildDroneGroups() → List<DroneGroup>
├─ BuildDroneWaves() → List<DroneWave>
IDefensePlanner.Plan(fireUnits, threats, scene)

View File

@ -1,8 +1,8 @@
# 实施计划与任务跟踪
> **项目**:反无人机仿真系统后端
> **文档版本**V1.3
> **更新日期**2026-06-14
> **文档版本**V1.4
> **更新日期**2026-06-15
---
@ -32,7 +32,7 @@ Phase 8 🔄 待开发(天气/物理模型统一已完成)
| # | 任务 | 状态 |
|---|------|------|
| 1.1-1.12 | 项目骨架、枚举、数据模型、Repository、ModelService/GroupService、单元测试 | ✅ |
| 1.1-1.12 | 项目骨架、枚举、数据模型、Repository、ModelService、单元测试 | ✅ |
---
@ -105,7 +105,7 @@ Phase 8 🔄 待开发(天气/物理模型统一已完成)
| 6.1 | 创建 Unity 项目,导入 Core.dll | 1h | ✅ | `src/Unity/` 项目16 个 DLL → `Assets/Plugins/` |
| 6.2 | 实现 `UnityPathProvider` | 1h | ✅ | `Application.persistentDataPath` 桥接 |
| 6.3 | 实现 `ModelManager` | 3h | ✅ | 导入/删除/查询,含 Verify |
| 6.4 | 实现 `ScenarioManager`5 步配置) | 4h | ✅ | 完整 CRUD + 搜索分页 + 多编队 Route |
| 6.4 | 实现 `ScenarioManager`5 步配置) | 4h | ✅ | 完整 CRUD + 搜索分页 + 多批次 Route |
| 6.5 | 实现 `SimulationRunner`Update 驱动) | 3h | ✅ | Tick 驱动 + 事件订阅 + 实体位置同步 + 炮弹轨迹可视化 |
| 6.6 | 实现 `ReplayController`(帧加载) | 3h | ✅ | 从分库加载帧数据TotalFrames/GetFrame |
| 6.7 | 实现 `ReportManager` | 2h | ✅ | 生成 + Markdown 导出 |

View File

@ -1,7 +1,7 @@
# 后端对接文档Unity 前端)
> **版本**V1.1
> **日期**2026-06-12
> **版本**V1.2
> **日期**2026-06-15
> **Unity 版本**2022.3.62f3c1
---
@ -35,17 +35,21 @@ data/default_ammo.json ← 默认弹药参数(放到 persisten
var scenario = GetComponent<ScenarioManager>();
var task = scenario.CreateTask("拦截活塞式无人机", "");
scenario.SaveScene(task.Id, new CombatScene { WindSpeed = 3 });
scenario.SaveTarget(task.Id, new TargetConfig { PowerType = (int)PowerType.Piston, ... });
scenario.SaveRoute(task.Id, routePlan, waypoints);
scenario.SaveTarget(task.Id, new TargetConfig { WaveId = "default", PowerType = (int)PowerType.Piston, ... });
scenario.SaveRoute(task.Id, "default", routePlan, waypoints);
// 推荐方案(自动从数据库读弹药规格
// 推荐方案(DefensePlanner 五步流水线
var detail = scenario.GetDetail(task.Id);
var ammoCatalog = db.Table<AmmunitionSpec>().ToList(); // DatabaseManager 已种子
var rec = new DefaultDefenseAdvisor(ammoCatalog).Recommend(new ThreatProfile { ... });
var fireUnits = new List<FireUnit> { /* 从 EquipmentDeployment 构建 */ };
var threats = new List<DroneWave> { new DroneWave { WaveId = "default", Target = detail.Targets[0], Route = detail.Routes[0], Waypoints = detail.WaypointGroups["default"] } };
var plan = new DefensePlanner(ammoCatalog, PlannerConfig.Load(paths)).Plan(fireUnits, threats, detail.Scene, new List<DetectionSource>());
// 应用到想定
scenario.SaveCloud(task.Id, rec.Best.RecommendedCloud);
scenario.SaveDeployment(task.Id, rec.Best.Platforms.Select(p => new EquipmentDeployment { ... }).ToList());
// 应用到想定(发射计划传给引擎,部署方案保存到配置)
var mergedSchedule = plan.Best.MergedSchedule;
// 部署火力单元到 EquipmentDeployment...
scenario.SaveCloud(task.Id, new CloudDispersal { /* 从 plan.Best.Assignments 推算云团参数 */ });
scenario.SaveDeployment(task.Id, equips);
```
### 3.2 启动仿真(事件驱动)
@ -54,7 +58,7 @@ scenario.SaveDeployment(task.Id, rec.Best.Platforms.Select(p => new EquipmentDep
var runner = GetComponent<SimulationRunner>();
// 推荐方案的 FireSchedule 直接传给引擎
runner.Engine.SetFireSchedule(rec.Best.FireSchedule);
runner.Engine.SetFireSchedule(plan.Best.MergedSchedule);
runner.LoadAndStart(task.Id);
runner.Engine.TimeScale = 4f; // 加速
@ -93,7 +97,7 @@ mgr.SaveScene(id, combatScene);
mgr.SaveTarget(id, targetConfig);
mgr.SaveDeployment(id, equipmentList);
mgr.SaveCloud(id, cloudDispersal);
mgr.SaveRoute(id, routePlan, waypoints);
mgr.SaveRoute(id, "default", routePlan, waypoints);
TaskFullConfig detail = mgr.GetDetail(id);
PagedResult<SimTask> result = mgr.Search("关键词", from, to, page, pageSize);
```
@ -163,4 +167,4 @@ dotnet publish -c Release -o ../../unity_plugins
| 实体事件映射 | `docs/design/technical/仿真器实体与事件映射.md` |
| 任务跟踪 | `docs/implementation/tasks/实施计划与任务跟踪.md` |
| 测试报告 | `test/reports/`(每次集成测试自动生成) |
| 测试状态 | **129 测试95.4% 行覆盖率12 秒** |
| 测试状态 | **204 测试95.4% 行覆盖率63 秒** |

View File

@ -46,10 +46,10 @@ namespace CounterDrone.Core.Algorithms
public float MuzzleVelocity { get; set; } = 800f;
}
/// <summary>无人机编队(规划器输入)</summary>
public class DroneGroup
/// <summary>无人机批次(规划器输入)</summary>
public class DroneWave
{
public string GroupId { get; set; } = string.Empty;
public string WaveId { get; set; } = string.Empty;
public TargetConfig Target { get; set; } = new();
public RoutePlan Route { get; set; } = new();
public List<Waypoint> Waypoints { get; set; } = new();
@ -120,7 +120,7 @@ namespace CounterDrone.Core.Algorithms
public class UnitAssignment
{
public string FireUnitId { get; set; } = string.Empty;
public string DroneGroupId { get; set; } = string.Empty;
public string DroneWaveId { get; set; } = string.Empty;
public AerosolType AmmoType { get; set; }
public int RoundsFired { get; set; }
public float FirstFireTime { get; set; }

View File

@ -26,7 +26,7 @@ namespace CounterDrone.Core.Algorithms
// 五步流水线
// ═══════════════════════════════════════════════
public PlannerResult Plan(List<FireUnit> fireUnits, List<DroneGroup> threats,
public PlannerResult Plan(List<FireUnit> fireUnits, List<DroneWave> threats,
CombatScene environment, List<DetectionSource> detectionSources)
{
var result = new PlannerResult();
@ -108,7 +108,7 @@ namespace CounterDrone.Core.Algorithms
// Step 3-4: 候选生成 → 贪心分配
// ═══════════════════════════════════════════════
private DefensePlan Solve(List<DroneGroup> sortedThreats,
private DefensePlan Solve(List<DroneWave> sortedThreats,
List<FireUnit> fireUnits, CombatScene env)
{
var plan = new DefensePlan();
@ -215,7 +215,7 @@ namespace CounterDrone.Core.Algorithms
plan.Assignments.Add(new UnitAssignment
{
FireUnitId = unit.Id,
DroneGroupId = threat.GroupId,
DroneWaveId = threat.WaveId,
AmmoType = neededAmmo,
RoundsFired = rounds,
FirstFireTime = fireEvents.Count > 0 ? fireEvents[0].FireTime : 0,
@ -236,7 +236,7 @@ namespace CounterDrone.Core.Algorithms
var a2 = _ammoCatalog.FirstOrDefault(s =>
s.AerosolType == (int)MatchAmmo(_config, (PowerType)threat.Target.PowerType));
int totalRounds = plan.Assignments
.Where(a => a.DroneGroupId == threat.GroupId)
.Where(a => a.DroneWaveId == threat.WaveId)
.Sum(a => a.RoundsFired);
if (totalRounds > 0)
threatProbs.Add(ComputeInterceptProbability(threat, a2, totalRounds, env));
@ -253,7 +253,7 @@ namespace CounterDrone.Core.Algorithms
// 候选生成(使用真实物理)
// ═══════════════════════════════════════════════
private List<InterceptCandidate> GenerateCandidates(DroneGroup threat,
private List<InterceptCandidate> GenerateCandidates(DroneWave threat,
List<FireUnit> availableUnits, CombatScene env)
{
var candidates = new List<InterceptCandidate>();
@ -281,7 +281,7 @@ namespace CounterDrone.Core.Algorithms
return candidates;
}
private InterceptCandidate? BuildGroundBasedCandidate(DroneGroup threat,
private InterceptCandidate? BuildGroundBasedCandidate(DroneWave threat,
FireUnit unit, AerosolType ammoType, AmmunitionSpec ammo,
float effectiveR, CombatScene env)
{
@ -311,7 +311,7 @@ namespace CounterDrone.Core.Algorithms
};
}
private InterceptCandidate? BuildAirBasedCandidate(DroneGroup threat,
private InterceptCandidate? BuildAirBasedCandidate(DroneWave threat,
FireUnit unit, AerosolType ammoType, AmmunitionSpec ammo,
float effectiveR, CombatScene env)
{
@ -358,7 +358,7 @@ namespace CounterDrone.Core.Algorithms
return (effectiveR, expansionTime, rPhase2);
}
private int CalcRoundsNeeded(DroneGroup threat, AmmunitionSpec ammo,
private int CalcRoundsNeeded(DroneWave threat, AmmunitionSpec ammo,
CombatScene env, bool isAirBased, float turbulentRadius)
{
var cloudModel = new CloudExpansionModel(ammo, env);
@ -371,7 +371,7 @@ namespace CounterDrone.Core.Algorithms
return cloudModel.RoundsNeeded(requiredCoverage, spacing);
}
private float ComputeInterceptProbability(DroneGroup threat,
private float ComputeInterceptProbability(DroneWave threat,
AmmunitionSpec ammo, int rounds, CombatScene env)
{
float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f;
@ -390,7 +390,7 @@ namespace CounterDrone.Core.Algorithms
// 发射事件生成(真实物理)
// ═══════════════════════════════════════════════
private List<FireEvent> GenerateFireEventsAt(DroneGroup threat, FireUnit unit,
private List<FireEvent> GenerateFireEventsAt(DroneWave threat, FireUnit unit,
AerosolType ammoType, AmmunitionSpec ammo, CombatScene env, float targetOffset,
int yLane, int yLanes, float formationWidth)
{
@ -517,7 +517,7 @@ namespace CounterDrone.Core.Algorithms
var reduced = new UnitAssignment
{
FireUnitId = assignment.FireUnitId,
DroneGroupId = assignment.DroneGroupId,
DroneWaveId = assignment.DroneWaveId,
AmmoType = assignment.AmmoType,
RoundsFired = rounds,
FirstFireTime = assignment.FirstFireTime,
@ -537,7 +537,7 @@ namespace CounterDrone.Core.Algorithms
// 辅助
// ═══════════════════════════════════════════════
private static Vector3 ThreatMidpoint(DroneGroup threat)
private static Vector3 ThreatMidpoint(DroneWave threat)
{
if (threat.Waypoints.Count < 2)
return new Vector3(0, (float)threat.Target.TypicalAltitude, 0);

View File

@ -8,10 +8,10 @@ namespace CounterDrone.Core.Algorithms
public interface IDefensePlanner
{
/// <param name="fireUnits">可用的火力单元(空基/地基统一表达)</param>
/// <param name="threats">威胁编队列表</param>
/// <param name="threats">威胁批次列表</param>
/// <param name="environment">作战环境(含天气,影响光电探测距离)</param>
/// <param name="detectionSources">统一信息网络的探测源列表(独立探测设备 + 火力单元自带探测)。可为空列表。</param>
PlannerResult Plan(List<FireUnit> fireUnits, List<DroneGroup> threats,
PlannerResult Plan(List<FireUnit> fireUnits, List<DroneWave> threats,
CombatScene environment, List<DetectionSource> detectionSources);
}
}

View File

@ -68,9 +68,8 @@ namespace CounterDrone.Core
db.CreateTable<EquipmentDeployment>();
db.CreateTable<CloudDispersal>();
db.CreateTable<RoutePlan>();
db.CreateIndex("RoutePlan", new[] { "TaskId", "GroupId" }, true);
db.CreateIndex("RoutePlan", new[] { "TaskId", "WaveId" }, true);
db.CreateTable<Waypoint>();
db.CreateTable<Group>();
db.CreateTable<SimulationReport>();
db.CreateIndex("SimTask", "TaskNumber");

View File

@ -92,7 +92,7 @@ namespace CounterDrone.Core.Models
Destroyed = 3
}
// === 编队 & 编组 ===
// === 编队 & 批次 ===
public enum FormationMode
{
Single = 0,
@ -100,12 +100,6 @@ namespace CounterDrone.Core.Models
Swarm = 2
}
public enum GroupType
{
DroneFleet = 0,
EquipmentGroup = 1
}
// === 运行时 ===
public enum EntityType
{

View File

@ -17,7 +17,7 @@ namespace CounterDrone.Core.Models
public int Quantity { get; set; } = 1;
public string GroupId { get; set; } = string.Empty;
public string WaveId { get; set; } = string.Empty;
// 发射平台专用
public int? PlatformType { get; set; }

View File

@ -1,22 +0,0 @@
using System;
using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>编组</summary>
[Table("Group")]
public class Group
{
[PrimaryKey]
public string Id { get; set; } = Guid.NewGuid().ToString();
[NotNull]
public string Name { get; set; } = string.Empty;
public int GroupType { get; set; } = (int)Models.GroupType.DroneFleet;
public string Description { get; set; } = string.Empty;
public string CreatedAt { get; set; } = DateTime.UtcNow.ToString("o");
}
}

View File

@ -3,7 +3,7 @@ using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>步骤5航路规划 — 多编队支持:每个(任务,编队)一条航路</summary>
/// <summary>步骤5航路规划 — 多批次支持:每个(任务,批次)一条航路</summary>
[Table("RoutePlan")]
public class RoutePlan
{
@ -14,7 +14,7 @@ namespace CounterDrone.Core.Models
public string TaskId { get; set; } = string.Empty;
[Indexed]
public string GroupId { get; set; } = string.Empty;
public string WaveId { get; set; } = string.Empty;
public int FormationMode { get; set; } = (int)Models.FormationMode.Single;

View File

@ -13,7 +13,7 @@ namespace CounterDrone.Core.Models
[Indexed]
public string TaskId { get; set; } = string.Empty;
public string GroupId { get; set; } = string.Empty;
public string WaveId { get; set; } = string.Empty;
public int TargetType { get; set; } = (int)Models.TargetType.Rotor;

View File

@ -11,9 +11,9 @@ namespace CounterDrone.Core.Models
public List<TargetConfig> Targets { get; set; } = new();
public List<EquipmentDeployment> Equipment { get; set; } = new();
public CloudDispersal Cloud { get; set; } = new();
/// <summary>多编队航路(多批次支持)</summary>
/// <summary>多批次航路(多批次支持)</summary>
public List<RoutePlan> Routes { get; set; } = new();
/// <summary>按 GroupId 分组的航路点</summary>
/// <summary>按 WaveId 分组的航路点</summary>
public Dictionary<string, List<Waypoint>> WaypointGroups { get; set; } = new();
}

View File

@ -13,8 +13,8 @@ namespace CounterDrone.Core.Models
[Indexed]
public string TaskId { get; set; } = string.Empty;
/// <summary>关联的编队 ID多编队支持)</summary>
public string GroupId { get; set; } = string.Empty;
/// <summary>关联的批次 ID多批次支持)</summary>
public string WaveId { get; set; } = string.Empty;
[NotNull]
public int OrderIndex { get; set; }

View File

@ -1,19 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using CounterDrone.Core.Models;
using SQLite;
namespace CounterDrone.Core.Repository
{
public class GroupRepository : BaseRepository<Group>
{
public GroupRepository(SQLiteConnection db) : base(db) { }
public List<Group> GetByType(int groupType)
{
return Db.Table<Group>()
.Where(g => g.GroupType == groupType)
.ToList();
}
}
}

View File

@ -14,10 +14,10 @@ namespace CounterDrone.Core.Repository
return Db.Table<RoutePlan>().Where(r => r.TaskId == taskId).ToList();
}
public RoutePlan GetByTaskAndGroup(string taskId, string groupId)
public RoutePlan GetByTaskAndWave(string taskId, string waveId)
{
return Db.Table<RoutePlan>()
.FirstOrDefault(r => r.TaskId == taskId && r.GroupId == groupId);
.FirstOrDefault(r => r.TaskId == taskId && r.WaveId == waveId);
}
}
}

View File

@ -17,10 +17,10 @@ namespace CounterDrone.Core.Repository
.ToList();
}
public List<Waypoint> GetByTaskAndGroup(string taskId, string groupId)
public List<Waypoint> GetByTaskAndWave(string taskId, string waveId)
{
return Db.Table<Waypoint>()
.Where(w => w.TaskId == taskId && w.GroupId == groupId)
.Where(w => w.TaskId == taskId && w.WaveId == waveId)
.OrderBy(w => w.OrderIndex)
.ToList();
}

View File

@ -1,50 +0,0 @@
using System;
using System.Collections.Generic;
using CounterDrone.Core.Models;
using CounterDrone.Core.Repository;
namespace CounterDrone.Core.Services
{
public class GroupService : IGroupService
{
private readonly GroupRepository _repo;
public GroupService(GroupRepository repo)
{
_repo = repo;
}
public Group CreateGroup(string name, GroupType type, string description)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("编组名称不能为空");
var group = new Group
{
Name = name,
GroupType = (int)type,
Description = description,
};
_repo.Insert(group);
return group;
}
public void DeleteGroup(string id)
{
_repo.Delete(id);
}
public List<Group> GetGroups(GroupType? type)
{
if (type.HasValue)
return _repo.GetByType((int)type.Value);
return _repo.GetAll();
}
public Group GetGroup(string id)
{
return _repo.GetById(id);
}
}
}

View File

@ -1,14 +0,0 @@
using System.Collections.Generic;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Services
{
/// <summary>编组管理服务</summary>
public interface IGroupService
{
Group CreateGroup(string name, GroupType type, string description);
void DeleteGroup(string id);
List<Group> GetGroups(GroupType? type);
Group GetGroup(string id);
}
}

View File

@ -24,7 +24,7 @@ namespace CounterDrone.Core.Services
List<EquipmentDeployment> GetDetections(string taskId);
void SaveCloudDispersal(string taskId, CloudDispersal cloud);
void SaveRoute(string taskId, string groupId, RoutePlan route, List<Waypoint> waypoints);
void SaveRoute(string taskId, string waveId, RoutePlan route, List<Waypoint> waypoints);
void UpdateStep(string taskId, int step);
}
}

View File

@ -101,7 +101,7 @@ namespace CounterDrone.Core.Services
Cloud = _cloudRepo.GetById(id) ?? new CloudDispersal { TaskId = id },
Routes = _routeRepo.GetByTaskId(id),
WaypointGroups = _waypointRepo.GetByTaskId(id)
.GroupBy(w => w.GroupId)
.GroupBy(w => w.WaveId)
.ToDictionary(g => g.Key, g => g.OrderBy(w => w.OrderIndex).ToList()),
};
}
@ -189,11 +189,11 @@ namespace CounterDrone.Core.Services
TouchTask(taskId);
}
public void SaveRoute(string taskId, string groupId, RoutePlan route, List<Waypoint> waypoints)
public void SaveRoute(string taskId, string waveId, RoutePlan route, List<Waypoint> waypoints)
{
route.TaskId = taskId;
route.GroupId = groupId;
var existing = _routeRepo.GetByTaskAndGroup(taskId, groupId);
route.WaveId = waveId;
var existing = _routeRepo.GetByTaskAndWave(taskId, waveId);
if (existing != null)
{
route.Id = existing.Id;
@ -202,13 +202,13 @@ namespace CounterDrone.Core.Services
else
_routeRepo.Insert(route);
// 删除该编队的旧航路点,插入新的
var oldWps = _waypointRepo.GetByTaskAndGroup(taskId, groupId);
// 删除该批次的旧航路点,插入新的
var oldWps = _waypointRepo.GetByTaskAndWave(taskId, waveId);
foreach (var w in oldWps) _waypointRepo.Delete(w.Id);
for (int i = 0; i < waypoints.Count; i++)
{
waypoints[i].TaskId = taskId;
waypoints[i].GroupId = groupId;
waypoints[i].WaveId = waveId;
waypoints[i].OrderIndex = i;
if (string.IsNullOrEmpty(waypoints[i].Id))
waypoints[i].Id = Guid.NewGuid().ToString();

View File

@ -8,7 +8,7 @@ namespace CounterDrone.Core.Simulation
public class DroneEntity
{
public string Id { get; }
public string GroupId { get; }
public string WaveId { get; }
public TargetType TargetType { get; }
public PowerType PowerType { get; }
public float Wingspan { get; }
@ -27,11 +27,11 @@ namespace CounterDrone.Core.Simulation
/// <summary>沿航路已飞行弧长(米),由 RouteGeometry 驱动</summary>
private float _traveledArc;
public DroneEntity(string id, string groupId, TargetConfig config, List<Waypoint> route,
public DroneEntity(string id, string waveId, TargetConfig config, List<Waypoint> route,
int formationIndex, float lateralSpacing, int longitudinalIndex, float longitudinalSpacing, FormationMode mode)
{
Id = id;
GroupId = groupId;
WaveId = waveId;
TargetType = (TargetType)config.TargetType;
PowerType = (PowerType)config.PowerType;
Wingspan = (float)config.Wingspan;

View File

@ -88,8 +88,8 @@ namespace CounterDrone.Core.Simulation
_drones.Clear();
foreach (var target in config.Targets)
{
var route = config.Routes.FirstOrDefault(r => r.GroupId == target.GroupId);
var wps = config.WaypointGroups.GetValueOrDefault(target.GroupId, new List<Waypoint>());
var route = config.Routes.FirstOrDefault(r => r.WaveId == target.WaveId);
var wps = config.WaypointGroups.GetValueOrDefault(target.WaveId, new List<Waypoint>());
if (route == null || wps.Count == 0) continue;
var mode = (FormationMode)route.FormationMode;
var lateralSpacing = (float)route.LateralSpacing;
@ -100,7 +100,7 @@ namespace CounterDrone.Core.Simulation
{
int latIdx = i % latCount;
int longIdx = i / latCount;
_drones.Add(new DroneEntity($"drone_{++_entityCounter}", target.GroupId,
_drones.Add(new DroneEntity($"drone_{++_entityCounter}", target.WaveId,
target, wps, latIdx, lateralSpacing, longIdx, longSpacing, mode));
}
}
@ -137,7 +137,7 @@ namespace CounterDrone.Core.Simulation
if (_fireSchedule.Count == 0)
{
var fireUnits = BuildFireUnits(config);
var threats = BuildDroneGroups(config);
var threats = BuildDroneWaves(config);
if (fireUnits.Count > 0 && threats.Count > 0)
{
var detectionSources = BuildDetectionSources(config);
@ -378,16 +378,16 @@ namespace CounterDrone.Core.Simulation
return sources;
}
private static List<DroneGroup> BuildDroneGroups(TaskFullConfig config)
private static List<DroneWave> BuildDroneWaves(TaskFullConfig config)
{
var groups = new List<DroneGroup>();
var groups = new List<DroneWave>();
foreach (var t in config.Targets)
{
var route = config.Routes.FirstOrDefault(r => r.GroupId == t.GroupId);
var wps = config.WaypointGroups.GetValueOrDefault(t.GroupId, new List<Waypoint>());
groups.Add(new DroneGroup
var route = config.Routes.FirstOrDefault(r => r.WaveId == t.WaveId);
var wps = config.WaypointGroups.GetValueOrDefault(t.WaveId, new List<Waypoint>());
groups.Add(new DroneWave
{
GroupId = t.GroupId,
WaveId = t.WaveId,
Target = t,
Route = route ?? new RoutePlan(),
Waypoints = wps,

View File

@ -1,45 +0,0 @@
using System.Collections.Generic;
using CounterDrone.Core;
using CounterDrone.Core.Models;
using CounterDrone.Core.Repository;
using CounterDrone.Core.Services;
using UnityEngine;
namespace CounterDrone.Unity
{
/// <summary>编组管理桥接 — 无人机编队和装备编组的 CRUD</summary>
public class GroupManager : MonoBehaviour
{
private IGroupService _service;
private SQLite.SQLiteConnection _db;
public IGroupService Service => _service;
public void Awake()
{
var paths = new UnityPathProvider();
_db = new DatabaseManager(paths).OpenMainDb();
SqliteConnectionTracker.Track(_db);
_service = new GroupService(new GroupRepository(_db));
}
void OnDisable() { SqliteConnectionTracker.Untrack(_db); _db?.Dispose(); _db = null; }
public Group Create(string name, GroupType type, string desc = "")
=> _service.CreateGroup(name, type, desc);
public void Delete(string id) => _service.DeleteGroup(id);
public List<Group> GetByType(GroupType? type = null) => _service.GetGroups(type);
public Group Get(string id) => _service.GetGroup(id);
[ContextMenu("Verify")]
void Verify()
{
Awake();
var g = Create("测试编队", GroupType.DroneFleet, "自动验证");
Debug.Log($"GroupManager OK. Created: {g.Name} ({g.Id})");
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 24c080ea3d6cbcf45aa804fd10269e4a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -35,7 +35,7 @@ namespace CounterDrone.Unity
scenarioMgr.SaveScene(taskId, new CombatScene { WindSpeed = 0 });
scenarioMgr.SaveTarget(taskId, new TargetConfig
{
GroupId = "default",
WaveId = "default",
TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston,
Quantity = 1, TypicalSpeed = 200, TypicalAltitude = 500,
});
@ -54,9 +54,9 @@ namespace CounterDrone.Unity
var ammoCatalog = db.Table<AmmunitionSpec>().ToList();
var detail = scenarioMgr.GetDetail(taskId);
var droneGroup = new DroneGroup
var droneGroup = new DroneWave
{
GroupId = "default",
WaveId = "default",
Target = detail.Targets[0],
Route = detail.Routes[0],
Waypoints = detail.WaypointGroups["default"],
@ -72,7 +72,7 @@ namespace CounterDrone.Unity
TotalMunitions = 3,
AmmoTypes = new() { AerosolType.InertGas, AerosolType.ActiveMaterial, AerosolType.ActiveFuel },
});
var result = new DefensePlanner(ammoCatalog, PlannerConfig.Load(paths)).Plan(fireUnits, new List<DroneGroup> { droneGroup }, detail.Scene, new List<DetectionSource>());
var result = new DefensePlanner(ammoCatalog, PlannerConfig.Load(paths)).Plan(fireUnits, new List<DroneWave> { droneGroup }, detail.Scene, new List<DetectionSource>());
scenarioMgr.SaveCloud(taskId, new CloudDispersal
{
PositionX = (droneGroup.Waypoints[0].PosX + droneGroup.Waypoints[^1].PosX) / 2,

View File

@ -53,7 +53,7 @@ namespace CounterDrone.Unity
});
scenario.SaveTarget(taskId, new TargetConfig
{
GroupId = "default",
WaveId = "default",
TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston,
Quantity = 1, TypicalSpeed = _droneSpeed, TypicalAltitude = 500,
});
@ -68,9 +68,9 @@ namespace CounterDrone.Unity
var detail = scenario.GetTaskDetail(taskId);
var ammoCatalog = db.Table<AmmunitionSpec>().ToList();
var planner = new DefensePlanner(ammoCatalog, PlannerConfig.Load(paths));
var droneGroup = new DroneGroup
var droneGroup = new DroneWave
{
GroupId = "default",
WaveId = "default",
Target = detail.Targets[0],
Route = detail.Routes[0],
Waypoints = detail.WaypointGroups["default"],
@ -86,7 +86,7 @@ namespace CounterDrone.Unity
TotalMunitions = 3,
AmmoTypes = new() { AerosolType.InertGas, AerosolType.ActiveMaterial, AerosolType.ActiveFuel },
});
var result = planner.Plan(fireUnits, new List<DroneGroup> { droneGroup }, detail.Scene, new List<DetectionSource>());
var result = planner.Plan(fireUnits, new List<DroneWave> { droneGroup }, detail.Scene, new List<DetectionSource>());
scenario.SaveCloudDispersal(taskId, new CloudDispersal
{
PositionX = (droneGroup.Waypoints[0].PosX + droneGroup.Waypoints[^1].PosX) / 2,

View File

@ -31,12 +31,12 @@ namespace CounterDrone.Core.Tests
{
var db = _dbManager.OpenMainDb();
// 验证所有 12 张表存在
// 验证所有 11 张表存在
var tables = new[]
{
"ModelInfo", "AmmunitionSpec", "SimTask", "CombatScene",
"ControlZone", "TargetConfig", "EquipmentDeployment",
"CloudDispersal", "RoutePlan", "Waypoint", "Group",
"CloudDispersal", "RoutePlan", "Waypoint",
"SimulationReport"
};

View File

@ -37,12 +37,12 @@ namespace CounterDrone.Core.Tests
AmmoTypes = new() { AerosolType.InertGas },
};
private static DroneGroup MakeThreat(PowerType power = PowerType.Piston, float speed = 120f,
private static DroneWave MakeThreat(PowerType power = PowerType.Piston, float speed = 120f,
float startX = 0, float endX = 10000, float alt = 500)
{
var t = new DroneGroup
var t = new DroneWave
{
GroupId = "default",
WaveId = "default",
Target = new TargetConfig
{
TargetType = (int)TargetType.Piston, PowerType = (int)power,
@ -57,7 +57,7 @@ namespace CounterDrone.Core.Tests
return t;
}
private PlannerResult Plan(List<FireUnit> units, DroneGroup threat,
private PlannerResult Plan(List<FireUnit> units, DroneWave threat,
CombatScene? env = null)
=> new DefensePlanner(TestAmmo, TestPlannerConfig.Instance).Plan(units, new() { threat }, env ?? new CombatScene(), new List<DetectionSource>());
@ -291,8 +291,8 @@ namespace CounterDrone.Core.Tests
[Fact]
public void MultiThreat_BothEngaged()
{
var a = MakeThreat(PowerType.Piston, 120); a.GroupId = "g0";
var b = MakeThreat(PowerType.Jet, 300); b.GroupId = "g1";
var a = MakeThreat(PowerType.Piston, 120); a.WaveId = "g0";
var b = MakeThreat(PowerType.Jet, 300); b.WaveId = "g1";
var result = new DefensePlanner(TestAmmo, TestPlannerConfig.Instance).Plan(
new() { MakeGroundUnit("u0", 5000), MakeGroundUnit("u1", 5100) },
new() { a, b }, new CombatScene(), new List<DetectionSource>());
@ -405,9 +405,9 @@ namespace CounterDrone.Core.Tests
// Z 向航路:从 (5000,0) 飞向 (5000,10000),即沿 +Z 方向
// 之前 offset 写死 X 轴时,多发云团会沿 X 发散到 5000±N×spacing偏离航路
// 改用 RouteGeometry 后offset 沿航路切向(+Z云团应集中在 X=5000
var threat = new DroneGroup
var threat = new DroneWave
{
GroupId = "default",
WaveId = "default",
Target = new TargetConfig
{
TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston,

View File

@ -49,7 +49,7 @@ namespace CounterDrone.Core.Tests
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
_scenario.SaveTarget(_taskId, new TargetConfig
{
GroupId = "default",
WaveId = "default",
TargetType = (int)TargetType.Piston,
Quantity = 1,
TypicalSpeed = 100,
@ -92,7 +92,7 @@ namespace CounterDrone.Core.Tests
});
_scenario.SaveTarget(_taskId, new TargetConfig
{
GroupId = "default",
WaveId = "default",
TargetType = (int)TargetType.Piston,
Quantity = 1,
TypicalSpeed = 600, // 高速对抗强风
@ -131,7 +131,7 @@ namespace CounterDrone.Core.Tests
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
_scenario.SaveTarget(_taskId, new TargetConfig
{
GroupId = "default",
WaveId = "default",
TargetType = (int)TargetType.HighSpeed,
Quantity = 1,
TypicalSpeed = 500,

View File

@ -114,7 +114,7 @@ namespace CounterDrone.Core.Tests
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
_scenario.SaveTarget(_taskId, new TargetConfig
{
GroupId = "default",
WaveId = "default",
TargetType = (int)TargetType.FixedWing,
PowerType = (int)PowerType.Jet,
Quantity = 1,
@ -147,7 +147,7 @@ namespace CounterDrone.Core.Tests
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
_scenario.SaveTarget(_taskId, new TargetConfig
{
GroupId = "default",
WaveId = "default",
TargetType = (int)TargetType.Electric,
PowerType = (int)PowerType.Electric,
Quantity = 1,
@ -189,7 +189,7 @@ namespace CounterDrone.Core.Tests
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
_scenario.SaveTarget(_taskId, new TargetConfig
{
GroupId = "default", TargetType = (int)TargetType.Piston,
WaveId = "default", TargetType = (int)TargetType.Piston,
PowerType = (int)PowerType.Piston,
Quantity = 1,
TypicalSpeed = 200, TypicalAltitude = 500,
@ -273,7 +273,7 @@ namespace CounterDrone.Core.Tests
});
_scenario.SaveTarget(_taskId, new TargetConfig
{
GroupId = "default", TargetType = (int)TargetType.Piston,
WaveId = "default", TargetType = (int)TargetType.Piston,
PowerType = (int)PowerType.Piston,
Quantity = 1,
TypicalSpeed = 200, TypicalAltitude = 500,
@ -333,7 +333,7 @@ namespace CounterDrone.Core.Tests
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
_scenario.SaveTarget(_taskId, new TargetConfig
{
GroupId = "default",
WaveId = "default",
TargetType = (int)TargetType.HighSpeed,
PowerType = (int)PowerType.Jet,
Quantity = 1,
@ -377,7 +377,7 @@ namespace CounterDrone.Core.Tests
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
_scenario.SaveTarget(_taskId, new TargetConfig
{
GroupId = "default",
WaveId = "default",
TargetType = (int)TargetType.Piston,
PowerType = (int)PowerType.Piston,
Quantity = 1,
@ -443,7 +443,7 @@ namespace CounterDrone.Core.Tests
});
_scenario.SaveTarget(_taskId, new TargetConfig
{
GroupId = "default",
WaveId = "default",
TargetType = (int)TargetType.Piston,
PowerType = (int)PowerType.Piston,
Quantity = 1,
@ -502,7 +502,7 @@ namespace CounterDrone.Core.Tests
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
_scenario.SaveTarget(_taskId, new TargetConfig
{
GroupId = "default", TargetType = (int)TargetType.Piston,
WaveId = "default", TargetType = (int)TargetType.Piston,
PowerType = (int)PowerType.Piston,
Quantity = 3,
TypicalSpeed = 200, TypicalAltitude = 500,
@ -557,7 +557,7 @@ namespace CounterDrone.Core.Tests
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
_scenario.SaveTarget(_taskId, new TargetConfig
{
GroupId = "default", TargetType = (int)TargetType.Piston,
WaveId = "default", TargetType = (int)TargetType.Piston,
PowerType = (int)PowerType.Piston,
Quantity = 3,
TypicalSpeed = 150, TypicalAltitude = 500,
@ -619,7 +619,7 @@ namespace CounterDrone.Core.Tests
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0, Visibility = 10000 });
_scenario.SaveTarget(_taskId, new TargetConfig
{
GroupId = "default", TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston,
WaveId = "default", TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston,
Quantity = 1, TypicalSpeed = 200, TypicalAltitude = 500,
});
_scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
@ -644,7 +644,7 @@ namespace CounterDrone.Core.Tests
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0, Visibility = 10000 });
_scenario.SaveTarget(_taskId, new TargetConfig
{
GroupId = "default", TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston,
WaveId = "default", TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston,
Quantity = 1, TypicalSpeed = 200, TypicalAltitude = 500,
});
_scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },

View File

@ -1,75 +0,0 @@
using System;
using System.IO;
using CounterDrone.Core;
using CounterDrone.Core.Models;
using CounterDrone.Core.Repository;
using SQLite;
using Xunit;
namespace CounterDrone.Core.Tests
{
public class GroupRepositoryTests : IDisposable
{
private readonly string _testDir;
private readonly SQLiteConnection _db;
private readonly GroupRepository _repo;
public GroupRepositoryTests()
{
_testDir = Path.Combine(Path.GetTempPath(), $"cd_test_{Guid.NewGuid():N}");
var paths = new TestPathProvider(_testDir);
var dbManager = new DatabaseManager(paths);
_db = dbManager.OpenMainDb();
_repo = new GroupRepository(_db);
}
public void Dispose()
{
_db?.Close();
if (Directory.Exists(_testDir))
Directory.Delete(_testDir, true);
}
[Fact]
public void Insert_And_GetById_ReturnsGroup()
{
var group = new Group
{
Name = "Alpha Fleet",
GroupType = (int)GroupType.DroneFleet,
Description = "Test drone formation"
};
_repo.Insert(group);
var result = _repo.GetById(group.Id);
Assert.NotNull(result);
Assert.Equal("Alpha Fleet", result.Name);
Assert.Equal((int)GroupType.DroneFleet, result.GroupType);
}
[Fact]
public void GetByType_FiltersCorrectly()
{
_repo.Insert(new Group { Name = "Fleet A", GroupType = (int)GroupType.DroneFleet });
_repo.Insert(new Group { Name = "Fleet B", GroupType = (int)GroupType.DroneFleet });
_repo.Insert(new Group { Name = "Equip G1", GroupType = (int)GroupType.EquipmentGroup });
var fleets = _repo.GetByType((int)GroupType.DroneFleet);
Assert.Equal(2, fleets.Count);
var equipGroups = _repo.GetByType((int)GroupType.EquipmentGroup);
Assert.Single(equipGroups);
}
[Fact]
public void Delete_RemovesGroup()
{
var group = new Group { Name = "ToDelete" };
_repo.Insert(group);
_repo.Delete(group.Id);
Assert.Null(_repo.GetById(group.Id));
}
}
}

View File

@ -1,83 +0,0 @@
using System;
using System.IO;
using CounterDrone.Core;
using CounterDrone.Core.Models;
using CounterDrone.Core.Services;
using SQLite;
using Xunit;
namespace CounterDrone.Core.Tests
{
public class GroupServiceTests : IDisposable
{
private readonly string _testDir;
private readonly SQLiteConnection _db;
private readonly IGroupService _service;
public GroupServiceTests()
{
_testDir = Path.Combine(Path.GetTempPath(), $"cd_test_{Guid.NewGuid():N}");
var paths = new TestPathProvider(_testDir);
var dbManager = new DatabaseManager(paths);
_db = dbManager.OpenMainDb();
_service = new GroupService(new Repository.GroupRepository(_db));
}
public void Dispose()
{
_db?.Close();
if (Directory.Exists(_testDir))
Directory.Delete(_testDir, true);
}
[Fact]
public void CreateGroup_Valid_ReturnsGroup()
{
var group = _service.CreateGroup("Alpha", GroupType.DroneFleet, "Test fleet");
Assert.NotNull(group);
Assert.Equal("Alpha", group.Name);
Assert.NotEmpty(group.Id);
}
[Fact]
public void CreateGroup_EmptyName_Throws()
{
Assert.Throws<ArgumentException>(() =>
_service.CreateGroup("", GroupType.DroneFleet, ""));
}
[Fact]
public void GetGroups_WithTypeFilter()
{
_service.CreateGroup("Fleet A", GroupType.DroneFleet, "");
_service.CreateGroup("Fleet B", GroupType.DroneFleet, "");
_service.CreateGroup("Equip X", GroupType.EquipmentGroup, "");
var fleets = _service.GetGroups(GroupType.DroneFleet);
Assert.Equal(2, fleets.Count);
var all = _service.GetGroups(null);
Assert.Equal(3, all.Count);
}
[Fact]
public void DeleteGroup_RemovesGroup()
{
var group = _service.CreateGroup("DeleteMe", GroupType.DroneFleet, "");
_service.DeleteGroup(group.Id);
Assert.Null(_service.GetGroup(group.Id));
}
[Fact]
public void GetGroup_ById_ReturnsCorrectGroup()
{
var created = _service.CreateGroup("Target", GroupType.EquipmentGroup, "desc");
var fetched = _service.GetGroup(created.Id);
Assert.Equal("Target", fetched.Name);
Assert.Equal("desc", fetched.Description);
}
}
}

View File

@ -103,7 +103,7 @@ namespace CounterDrone.Core.Tests
_service.SaveTarget(task.Id, new TargetConfig
{
GroupId = "default",
WaveId = "default",
TargetType = (int)TargetType.FixedWing,
Quantity = 3,
});
@ -201,7 +201,7 @@ namespace CounterDrone.Core.Tests
var task = _service.CreateTask("Target Test", "");
_service.SaveTarget(task.Id, new TargetConfig
{
GroupId = "default",
WaveId = "default",
TargetType = (int)TargetType.Piston,
Quantity = 5,
PowerType = (int)PowerType.Piston,
@ -394,7 +394,7 @@ namespace CounterDrone.Core.Tests
// 4. 步骤2目标配置
_service.SaveTarget(task.Id, new TargetConfig
{
GroupId = "default",
WaveId = "default",
TargetType = (int)TargetType.HighSpeed,
Quantity = 2,
PowerType = (int)PowerType.Jet,

View File

@ -53,7 +53,7 @@ namespace CounterDrone.Core.Tests
_scenarioService.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
_scenarioService.SaveTarget(_taskId, new TargetConfig
{
GroupId = "default",
WaveId = "default",
TargetType = (int)TargetType.Piston,
Quantity = 1,
PowerType = (int)PowerType.Piston,
@ -147,7 +147,7 @@ namespace CounterDrone.Core.Tests
{
var task = _scenarioService.CreateTask("t", "");
_scenarioService.SaveScene(task.Id, new CombatScene());
_scenarioService.SaveTarget(task.Id, new TargetConfig { GroupId = "d", Quantity = 1, TypicalSpeed = 60, TypicalAltitude = 300 });
_scenarioService.SaveTarget(task.Id, new TargetConfig { WaveId = "d", Quantity = 1, TypicalSpeed = 60, TypicalAltitude = 300 });
_scenarioService.SaveDeployment(task.Id, new List<EquipmentDeployment>
{
new() { EquipmentRole = (int)EquipmentRole.LaunchPlatform, PlatformType = (int)PlatformType.AirBased,