commit b23291c5bfb28de948ac75227edff7fe403a48f8 Author: tian <11429339@qq.com> Date: Thu Jun 11 14:08:37 2026 +0800 Phase 1: 核心基础 — 项目骨架、数据模型、Repository、ModelService/GroupService,27个单元测试全部通过 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f5ae578 --- /dev/null +++ b/.gitignore @@ -0,0 +1,48 @@ +# === 操作系统生成文件 === +.DS_Store +Thumbs.db + +# === IDE & 编辑器 === +.vscode/ +.idea/ +*.swp +*.swo + +# === .NET 构建产物 === +bin/ +obj/ +*.user +*.suo +*.DotSettings.user + +# === 依赖 === +node_modules/ +vendor/ +__pycache__/ +*.pyc +.venv/ +venv/ +.env + +# === 日志 === +*.log +logs/ + +# === 临时文件 === +*.tmp +*.temp +tmp/ + +# === 测试 === +coverage/ +.nyc_output/ +*.coverage +TestResults/ + +# === 环境变量(敏感信息) === +.env.local +.env.production + +# === 缓存 === +.cache/ +.pytest_cache/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..fdf68a0 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,71 @@ +# CLAUDE.md + +## 1. Think Before Coding + +**Don't assume. Don't hide confusion. Surface tradeoffs.** + +Before implementing: +- State your assumptions explicitly. If uncertain, ask. +- If multiple interpretations exist, present them - don't pick silently. +- If a simpler approach exists, say so. Push back when warranted. +- If something is unclear, stop. Name what's confusing. Ask. + +## 2. Simplicity First + +**Minimum code that solves the problem. Nothing speculative.** + +- No features beyond what was asked. +- No abstractions for single-use code. +- No "flexibility" or "configurability" that wasn't requested. +- No error handling for impossible scenarios. +- If you write 200 lines and it could be 50, rewrite it. + +Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify. + +## 3. Surgical Changes + +**Touch only what you must. Clean up only your own mess.** + +When editing existing code: +- Don't "improve" adjacent code, comments, or formatting. +- Don't refactor things that aren't broken. +- Match existing style, even if you'd do it differently. +- If you notice unrelated dead code, mention it - don't delete it. + +When your changes create orphans: +- Remove imports/variables/functions that YOUR changes made unused. +- Don't remove pre-existing dead code unless asked. + +The test: Every changed line should trace directly to the user's request. + +## 4. Goal-Driven Execution + +**Define success criteria. Loop until verified.** + +Transform tasks into verifiable goals: +- "Add validation" → "Write tests for invalid inputs, then make them pass" +- "Fix the bug" → "Write a test that reproduces it, then make it pass" +- "Refactor X" → "Ensure tests pass before and after" + +For multi-step tasks, state a brief plan: +``` +1. [Step] → verify: [check] +2. [Step] → verify: [check] +3. [Step] → verify: [check] +``` + +Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. + +--- + +## 5. Use PowerShell for Shell Commands + +**All shell commands, scripts, rg/fd calls run through pwsh, never cmd or bash.** + +- Prefix every shell invocation with `pwsh -Command "..."` +- Example: `pwsh -Command "rg -n 'class' src/"` +- Example: `pwsh -Command "./build.bat"` + +--- + +**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..cebd874 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,34 @@ +# Changelog + +本文件记录项目对外发布的变更历史(用户视角)。 +格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/), +版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。 + +格式说明: +- `Added` — 新增功能 +- `Changed` — 现有功能的变更 +- `Deprecated` — 即将移除的功能 +- `Removed` — 已移除的功能 +- `Fixed` — 问题修复 +- `Security` — 安全修复 + +--- + +## [Unreleased] + +### Added +- (待发布的新功能) + +### Changed +- (待发布的变更) + +--- + +## [0.1.0] - YYYY-MM-DD + +### Added +- 项目初始化。 + +--- + +> 更多格式说明见:https://keepachangelog.com/zh-CN/1.1.0/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3901f9e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,51 @@ +# 贡献指南 + +感谢你对本项目的关注!请遵循以下规范参与开发。 + +## 开发流程 + +1. **认领任务** — 从 `docs/implementation/tasks/` 中选取或创建任务 +2. **创建分支** — 分支命名:`feature/<描述>` / `fix/<描述>` / `docs/<描述>` +3. **编写代码** — 遵循项目代码规范,添加必要的测试 +4. **自测** — 确保测试通过,功能正常 +5. **提交代码** — Commit message 遵循 [约定式提交](https://www.conventionalcommits.org/zh-hans/) +6. **发起 Merge Request / Pull Request** — 描述变更内容,关联对应的需求/问题 +7. **代码审查** — 至少一人 Review 通过后合并 + +## Commit Message 规范 + +``` +(): + +类型(type): + feat — 新功能 + fix — 修复 + docs — 文档 + style — 格式(不影响功能) + refactor — 重构 + test — 测试 + chore — 构建/工具 + +示例: + feat(auth): add login by SMS + fix(api): correct user list pagination +``` + +## 代码规范 + +- 保持代码风格与项目一致 +- 公共接口必须有注释 +- 新增功能必须编写测试 +- 修改现有功能需同步更新相关文档 + +## 测试要求 + +- 单元测试覆盖率目标:> [XX]% +- 提交前确保所有测试通过 +- 涉及接口变更需补充集成测试 + +## 文档要求 + +- 功能设计文档放在 `docs/features/` +- 架构变更需在 `docs/design/architecture/` 添加 ADR(架构决策记录) +- 需求变更在 `docs/requirements/changes/` 记录 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4d23f2f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) [2026] [北京泰奥理科技] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0891c7a --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# [项目名称] + +> 一句话描述项目:是什么,解决什么问题,面向谁。 + +## 目录结构 + +``` +├── README.md # 项目概述与快速开始(本文件) +├── CHANGELOG.md # 对外发布变更记录(用户视角) +├── VERSION # 语义化版本号 +├── LICENSE # 许可证 +├── CONTRIBUTING.md # 贡献规范 +├── .gitignore # Git 忽略规则 +├── AGENTS.md # AI 编码助手上下文 +│ +├── src/ # 源代码(所有业务模块) +├── data/ # 项目数据(知识库源、SQL dump 等非代码数据) +├── test/ # 测试代码 +│ ├── unit/ # 单元测试 +│ ├── integration/ # 集成测试 +│ └── e2e/ # 端到端测试 +│ +├── docs/ # 开发文档 +│ ├── design/ # 设计方案(架构 + 技术方案) +│ │ ├── architecture/ # 架构设计 & ADR +│ │ └── technical/ # 技术方案 / 详细设计 +│ ├── features/ # 功能规格说明 +│ ├── requirements/ # 需求与变更管理(可选) +│ ├── implementation/ # 实施计划与任务跟踪(可选) +│ └── verify/ # 人工检查清单(可选) +│ +├── skills/ # AI 辅助技能(pi 专用,可选) +├── scripts/ # 构建、部署、工具脚本 +└── config/ # 项目级配置文件(非代码) +``` + +> - **可选目录**:已有 Jira/TAPD 等工单系统的团队可删除 `requirements/` 和 `implementation/`;不用 pi 编码助手的可删除 `skills/` +> - **`src/` vs 根目录**:业务代码放 `src/`,入口文件(`app.py`、`main.go`)和配置文件(`pytest.ini`、`ruff.toml`、`.env.example`)留根目录 +> - **`data/` vs `test/fixtures/`**:`data/` 放项目级数据(知识库源文件等),`test/fixtures/` 放测试专用 fixture 数据 + +## 快速开始 + +```bash +# 克隆项目 +git clone +cd + +# [安装 / 构建 / 运行步骤,根据项目补充] +``` + +## 文档导航 + +| 你想做的事 | 去哪看 | +|------------|--------| +| 了解项目背景和架构 | `docs/design/architecture/` | +| 查看需求与变更 | `docs/requirements/`(可选) | +| 查看功能设计 | `docs/features/` | +| 查看任务进度 | `docs/implementation/tasks/`(可选) | +| 参与开发 | `CONTRIBUTING.md` | +| 查看版本历史 | `CHANGELOG.md` | + +## 维护者 + +- [@维护者](https://github.com/xxx) + +## 许可证 + +[LICENSE](./LICENSE) diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.0 diff --git a/data/index.md b/data/index.md new file mode 100644 index 0000000..1e00671 --- /dev/null +++ b/data/index.md @@ -0,0 +1,17 @@ +# data — 项目数据 + +本目录存放非代码的项目数据文件,区别于源代码和文档。 + +## 用途 + +- 初始化/种子数据 +- 数据库导出快照(用于参考或迁移) +- 静态数据文件(如地理信息、字典数据等) +- 其他非代码的静态项目数据 + +## 原则 + +1. 不含可执行代码或业务配置文件 +2. 文件应可被版本控制追踪(大文件请使用 Git LFS) +3. 按数据类型或用途建立子目录 +4. 与 `test/fixtures/` 区分:`data/` 放项目级数据,`test/fixtures/` 放测试专用数据 diff --git a/docs/design/architecture/index.md b/docs/design/architecture/index.md new file mode 100644 index 0000000..cf9d6e4 --- /dev/null +++ b/docs/design/architecture/index.md @@ -0,0 +1,53 @@ +# docs/design/architecture — 架构设计 + +存放系统架构文档和架构决策记录(ADR)。 + +## 文档 + +| 文档 | 说明 | +|------|------| +| [总体架构设计](./总体架构设计.md) | 系统整体架构、分层设计、数据模型、仿真引擎、接口设计 | + +## 架构决策记录(ADR) + +当需要做出影响系统结构的重大技术决策时,请创建一条 ADR。 + +### 文件命名 + +``` +ADR-<序号>-<标题>.md +``` + +### ADR 模板 + +```markdown +# ADR-001: [决策标题] + +- **状态**:提议 / 已接受 / 已废弃 / 已替代 +- **日期**:YYYY-MM-DD +- **决策者**:[姓名] +- **替代**:[如替代了之前的 ADR] + +## 背景 + +> 为什么需要做这个决策?面临什么问题? + +## 决策 + +> 我们决定做什么? + +## 备选方案 + +| 方案 | 优点 | 缺点 | +|------|------|------| +| 方案 A | ... | ... | +| 方案 B | ... | ... | + +## 后果 + +> 决策后带来的影响(好的和需要关注的)。 +``` + +--- + +> 关于 ADR 的更多说明,见 [adr.github.io](https://adr.github.io/) diff --git a/docs/design/architecture/总体架构设计.md b/docs/design/architecture/总体架构设计.md new file mode 100644 index 0000000..27eda2d --- /dev/null +++ b/docs/design/architecture/总体架构设计.md @@ -0,0 +1,1384 @@ +# 反无人机仿真系统 — 总体架构设计 + +> **版本**:V7 +> **日期**:2026-06-11 +> **状态**:设计中 +> **Unity 版本**:22.3.62 +> **.NET 版本**:.NET Standard 2.1 +> **变更**:明确 Core 程序集独立于 Unity;新增 IPathProvider 隔离点;新增测试策略(xUnit/确定性测试/CI 兼容) + +--- + +## 一、系统概述 + +本系统对新型气溶胶武器的三种防空毁伤机制(吸入式灭火、爆燃式、吸入式爆炸)进行数字化仿真。通过参数化配置与可视化推演,量化评估新型气溶胶武器对不同类型空中威胁源目标(动力系统)的毁伤效能。 + +系统分为三大模块: + +| 模块 | 核心功能 | +|------|----------| +| **模型管理** | 3D 模型导入/预览/删除,编组管理 | +| **想定管理** | 仿真任务 CRUD,步骤化配置向导,管控区域设置,搜索分页 | +| **仿真报告** | 报告列表,含时序图的完整报告预览,导出 PDF/Word,删除 | + +### 要求来源对照 + +本设计融合两份需求文档: + +| 文档 | 侧重 | +|------|------| +| V1.0 反无人机仿真系统功能需求 | UI 交互、业务流程、业务字段 | +| 技术要求终版 | 毁伤机制、装备体系、可视化效果、性能指标、第三方算法集成 | + +--- + +## 二、架构分层 + +``` +┌──────────────────────────────────────────────────────────┐ +│ Unity UI Layer │ +│ 模型管理 │ 想定管理(含管控区域) │ 仿真运行&回放 │ 仿真报告 │ +├──────────────────────────────────────────────────────────┤ +│ Unity Application Layer │ +│ ModelPanel | ScenarioWizard | SimulationRunner | │ +│ ReplayController | ReportPanel | GroupManager │ +├──────────────────────────────────────────────────────────┤ +│ Service Layer (Pure C#) │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │ +│ │ ModelService │ │ScenarioService│ │ SimulationEngine │ │ +│ │ GroupService │ │ DefenseAdvisor│ │ ├ DroneEntity │ │ +│ └──────────────┘ └──────────────┘ │ ├ PlatformEntity │ │ +│ ┌──────────────┐ ┌──────────────┐ │ ├ MunitionEntity │ │ +│ │ RecordService│ │ReportService │ │ ├ CloudEntity │ │ +│ │ │ │ ├ ReportGen │ │ │ └ GaussianPuff│ │ +│ └──────────────┘ │ └ PDF/Word │ │ ├ ControlZone │ │ +│ └──────────────┘ │ ├ IDamageModel×3 │ │ +│ │ └ EventQueue │ │ +│ ┌──────────────────────────────────────────────────────┐│ +│ │ Algorithm Layer (自主实现) ││ +│ │ DispersionEngine | DamageCalculator ││ +│ │ DefenseAdvisor | SpatioTemporalOptimizer ││ +│ └──────────────────────────────────────────────────────┘│ +├──────────────────────────────────────────────────────────┤ +│ Data Layer │ +│ ┌────────────────────────────┐ ┌────────────────────┐ │ +│ │ main.db (SQLite) │ │ frames/ (SQLite) │ │ +│ │ 11张业务表 + SimEvent │ │ 每任务一个 .db │ │ +│ │ + SimulationReport │ │ SimFrameRecord │ │ +│ │ < 100MB,永久保留 │ │ 6个月 TTL │ │ +│ └────────────────────────────┘ └────────────────────┘ │ +│ ┌────────────────────────────────────────────────────┐ │ +│ │ FileSystem │ │ +│ │ 模型文件(.fbx/.obj/.stl/.glb/.gltf) + 预览图 │ │ +│ └────────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────┘ +``` + +### 各层职责 + +| 层 | 依赖 | 职责 | +|----|------|------| +| **Data Layer** | 主库 SQLite + 任务分库 SQLite + 文件系统 | 主库存元数据/事件/报告(<100MB,永久保留);分库按任务存帧数据(6月TTL);文件系统存模型文件 | +| **Algorithm Layer** | 纯 C#,零外部依赖 | 气溶胶扩散模型、毁伤计算、防御推荐算法(全部自主实现) | +| **Service Layer** | Data Layer + Algorithm Layer | 纯业务逻辑、仿真引擎、录制、报告生成 | +| **Application Layer** | Service Layer + Unity | MonoBehaviour 桥接,生命周期协调,驱动仿真 | +| **UI Layer** | Application Layer | UGUI 页面、弹窗、3D 预览、回放、粒子参数传递 | + +### 2.1 程序集拆分:Core 独立于 Unity + +后端代码拆分为两个程序集,确保 Core 可脱离 Unity 独立编译和测试: + +``` +CounterDrone.Core.dll ← .NET Standard 2.1 类库 + 零 UnityEngine 引用 + 可在任何 .NET 环境运行 + ├── /Models 数据模型(POCO) + ├── /Repository 数据访问(SQLite + 文件存储) + ├── /Services 业务服务(ModelService, ScenarioService 等) + ├── /Simulation 仿真引擎 + 实体(SimulationEngine, DroneEntity, ...) + └── /Algorithms 扩散模型、毁伤模型、防御推荐 + +CounterDrone.Core.Tests.dll ← xUnit 测试项目 + 直接引用 Core.dll + dotnet test 即可运行 + ├── /Unit 单元测试(每个类独立测) + └── /Integration 集成测试(完整仿真流程) + +Unity 项目/Assets/Scripts/ ← Unity 侧 + ├── /Managers MonoBehaviours 桥接层(薄) + └── /UI UGUI 绑定 + 引用 → CounterDrone.Core.dll(放在 Assets/Plugins/) +``` + +### 2.2 Unity 依赖隔离:关键设计 + +| 隔离点 | 做法 | +|------|------| +| **时间** | `engine.Tick(float deltaTime)` — Unity 传 `Time.deltaTime`,测试传固定值 `0.05f` | +| **文件路径** | Core 使用接口 `IPathProvider` → Unity 实现返回 `Application.persistentDataPath`,测试返回 `./testdata/` | +| **日志** | Core 使用 `Microsoft.Extensions.Logging` 抽象 → Unity 桥接到 `Debug.Log`,测试用控制台 | +| **模型加载** | Core 不涉及 3D 渲染,只管理文件路径和元数据 | +| **粒子参数** | Core 输出 `ParticleParams` 数据对象 → Unity 读值设置 ParticleSystem | + +### 2.3 Manager 桥接层示例(极薄) + +```csharp +// Unity 侧 SimulationRunner.cs — 仅负责桥接 +public class SimulationRunner : MonoBehaviour +{ + private ISimulationEngine _engine; + private IRecordService _recorder; + + void Start() + { + // 通过 DI 或工厂获取实例(Core 创建) + _engine = SimulationEngineFactory.Create(taskId); + _recorder = ServiceFactory.CreateRecordService(); + } + + void Update() + { + if (_engine.State != SimulationState.Running) return; + + // 核心只有这一行:把 Unity 时间传入纯 C# 引擎 + var result = _engine.Tick(Time.deltaTime); + + // 更新 Unity 侧的 3D 物体位置(根据 result 中的实体快照) + UpdateVisuals(result.EntitySnapshots); + } + + void UpdateVisuals(List snapshots) + { + foreach (var s in snapshots) + { + // 找到对应 GameObject,设置 transform + _entityMap[s.EntityId].transform.position = s.Position; + } + } +} +``` + +### 2.4 测试策略 + +| 测试类型 | 工具 | 内容 | +|------|------|------| +| **单元测试** | xUnit + Moq | Repository 增删改查、Service 业务逻辑、Algorithm 计算正确性 | +| **仿真确定性测试** | xUnit | 固定输入 + 固定 deltaTime → 断言帧 N 的实体位置 = 预期值 | +| **集成测试** | xUnit + SQLite 文件 | 完整想定配置 → 启动仿真 → 跑 1000 帧 → 断言事件序列和对抗结果 | +| **性能测试** | BenchmarkDotNet | Tick() 单帧耗时 P99 < 5ms | + +> Core 测试无需 Unity,`dotnet test` 即可运行全部测试,适合 CI 流水线。 + +--- + +## 三、枚举定义 + +```csharp +// === 任务 === +enum TaskStatus { Draft = 0, Configuring = 1, Completed = 2 } + +// === 场景 === +enum SceneType { Urban = 0, Plain = 1, Mountain = 2, Coast = 3 } +enum WeatherType { Sunny = 0, Overcast = 1, Fog = 2, Rain = 3, Night = 4 } +enum WindDirection { N = 0, NE = 1, E = 2, SE = 3, S = 4, SW = 5, W = 6, NW = 7 } + +// === 目标 === +enum TargetType { Rotor = 0, FixedWing = 1, Electric = 2, Piston = 3, HighSpeed = 4 } +enum PowerType { Electric = 0, Piston = 1, Jet = 2 } + +// === 装备 & 搭载平台 === +enum PlatformType { AirBased = 0, GroundBased = 1 } // 空基(大型无人机) / 地基(炮弹) +enum EquipmentRole { Detection = 0, LaunchPlatform = 1 } // 装备角色:探测 / 发射平台 + +// === 气溶胶 & 毁伤 === +enum AerosolType { InertGas = 0, ActiveMaterial = 1, ActiveFuel = 2 } +// 对应的毁伤机制名称:吸入式灭火 / 爆燃式 / 吸入式爆炸 +enum ReleaseMode { Single = 0, Continuous = 1, Pulse = 2 } +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 } + +// === 运行时 === +enum EntityType { Drone = 0, Platform = 1, DetectionEquip = 2, Cloud = 3, Munition = 4 } +enum EventType +{ + TargetDetected = 0, + MunitionLaunched = 1, + CloudGenerated = 2, + DroneEnteredCloud = 3, + DroneDestroyed = 4, + DroneReachedTarget = 5, + ZoneIntruded = 6, // 无人机侵入管控区域 + WaypointReached = 7, + SimulationEnd = 8 +} + +// === 报告 === +enum InterceptResult { Success = 0, Partial = 1, Failed = 2 } +enum PositionMode { PointSelect = 0, ManualInput = 1 } +``` + +--- + +## 四、数据模型设计 + +### 4.1 存储架构:主库 + 任务分库 + +``` +CounterDroneBackend_Data/ +├── main.db ← 主库(< 100MB,永久保留) +│ ├── 11 张业务配置表 +│ ├── SimEvent ← 事件表(~30 条/任务) +│ └── SimulationReport +│ +├── frames/ ← 帧数据分库(6 个月 TTL) +│ ├── {taskId_001}.db ← 单任务 ~161 MB(30min 仿真) +│ └── {taskId_002}.db +│ +└── models/ ← 模型文件 + 预览图 +``` + +### 4.2 数据表总览 + +共 11 张业务表(主库) + 1 张运行时表(主库) + 1 张运行时表(任务分库): + +``` +主库(main.db)— 永久保留: + ModelInfo — 模型元数据 + AmmunitionSpec — 弹药基础参数(第三方数据,NEW) + SimTask — 仿真任务主表 + CombatScene — 步骤1:作战场景(含24h时间、天气) + ControlZone — 步骤1+:管控区域/电子围栏(NEW) + TargetConfig — 步骤2:目标配置 + EquipmentDeployment — 步骤3:装备部署(搭载平台+探测设备) + CloudDispersal — 步骤4:云团抛撒配置 + RoutePlan — 步骤5:航路规划 + Waypoint — 航路点 + Group — 编组(独立管理) + SimulationReport — 仿真报告 + SimEvent — 仿真事件(时序图数据源) + +任务分库(frames/{taskId}.db)— 6 个月 TTL: + SimFrameRecord — 逐帧位置记录(回放用) +``` + +### 4.3 业务表详细设计 + +#### ModelInfo — 模型元数据 + +| 字段 | 类型 | 约束 | 说明 | +|------|------|------|------| +| Id | TEXT | PK | GUID | +| Name | TEXT | NOT NULL | 模型名称 | +| ModelType | TEXT | | 分类标签 | +| FilePath | TEXT | | persistentDataPath 下的相对路径 | +| FileSize | REAL | | MB | +| PreviewPath | TEXT | | 预览图路径 | +| HasHighResTexture | INTEGER | DEFAULT 0 | 是否含 2K+ 贴图 | +| CreatedAt | TEXT | | 导入时间 ISO8601 | + +> 支持格式:**.fbx / .obj / .stl / .glb / .gltf**,单文件 ≤ 500MB,支持 2K 及以上分辨率贴图。 + +#### SimTask — 仿真任务主表 + +| 字段 | 类型 | 约束 | 说明 | +|------|------|------|------| +| Id | TEXT | PK | GUID | +| Name | TEXT | NOT NULL | 任务名称 | +| TaskNumber | TEXT | UNIQUE | SIM-yyyyMMdd-xxx | +| Status | INTEGER | DEFAULT 0 | 0=草稿 1=配置中 2=已完成 | +| CurrentStep | INTEGER | DEFAULT 1 | 当前配置步骤 1-5 | +| TickRate | INTEGER | DEFAULT 20 | 仿真 Tick 频率(Hz) | +| CreatedAt | TEXT | | | +| UpdatedAt | TEXT | | | + +#### CombatScene — 步骤1:作战场景 + +| 字段 | 类型 | 约束 | 说明 | +|------|------|------|------| +| TaskId | TEXT | PK, FK | 1:1 SimTask | +| SceneType | INTEGER | | 0城市 1平原 2山区 3海岸 | +| TimeOfDay | TEXT | DEFAULT "12:00" | **NEW** 24h 制时间,如 "14:30" | +| WeatherType | INTEGER | | 0晴 1阴 2雾 3雨 4夜 | +| WindSpeed | REAL | DEFAULT 5 | m/s,范围 0-30 | +| WindDirection | INTEGER | DEFAULT 0 | 八方位 0-7 | +| Temperature | REAL | DEFAULT 20 | ℃ | +| Humidity | REAL | DEFAULT 60 | %RH | +| Pressure | REAL | DEFAULT 1013 | hPa | +| Visibility | REAL | DEFAULT 5000 | m,范围 100-10000 | +| SceneWidth | REAL | DEFAULT 4000 | **NEW** 场景宽 m(≥4m) | +| SceneLength | REAL | DEFAULT 10000 | **NEW** 场景纵深 m(≥100m) | +| SceneHeight | REAL | DEFAULT 1000 | **NEW** 场景高 m(≥10m) | + +> 场景空间范围最低要求:4m × 100m × 10m。默认值 4000m × 10000m × 1000m 可配置。 + +#### ControlZone — 步骤1扩展:管控区域 / 电子围栏(NEW) + +| 字段 | 类型 | 约束 | 说明 | +|------|------|------|------| +| Id | TEXT | PK | GUID | +| TaskId | TEXT | FK | | +| Name | TEXT | | 区域名称 | +| VerticesJson | TEXT | NOT NULL | 多边形顶点 JSON:`[{"x":0,"y":0,"z":0},...]` | +| MinAltitude | REAL | | 区域底高 m | +| MaxAltitude | REAL | | 区域顶高 m | +| OrderIndex | INTEGER | | 多个区域的顺序 | + +> 管控区域在场景设置(地形+天气)之后配置。无人机进入管控区域 → 触发 ZoneIntruded 事件 → 该无人机任务失败。 + +#### TargetConfig — 步骤2:目标配置 + +| 字段 | 类型 | 约束 | 说明 | +|------|------|------|------| +| Id | TEXT | PK | GUID | +| TaskId | TEXT | FK | | +| GroupId | TEXT | FK | 关联无人机编队 Group | +| TargetType | INTEGER | | 0旋翼 1固定翼 2电推 3活塞 4高速 | +| Quantity | INTEGER | DEFAULT 1 | 目标数量 | +| PowerType | INTEGER | | 0电推 1活塞 2喷吸气 | +| Wingspan | REAL | | m | +| TypicalSpeed | REAL | | km/h | +| TypicalAltitude | REAL | | m | + +> 目标类型默认值: + +| 目标类型 | 翼展(m) | 典型速度(km/h) | 典型飞行高度(m) | +|----------|---------|---------------|----------------| +| 旋翼 | 1.2 | 60 | 300 | +| 固定翼 | 3.5 | 150 | 1000 | +| 电推 | 1.8 | 100 | 500 | +| 活塞 | 2.5 | 120 | 800 | +| 高速目标 | 1.5 | 300 | 2000 | + +#### EquipmentDeployment — 步骤3:装备部署(重构) + +| 字段 | 类型 | 约束 | 说明 | +|------|------|------|------| +| Id | TEXT | PK | GUID | +| TaskId | TEXT | FK | | +| EquipmentRole | INTEGER | | 0=探测设备 1=发射平台 | +| Quantity | INTEGER | DEFAULT 1 | 部署数量 | +| GroupId | TEXT | FK | 关联装备编组 Group | +| // 以下为发射平台专用字段 | +| PlatformType | INTEGER | NULLABLE | 0=空基(大型无人机) 1=地基(炮弹),探测设备为 NULL | +| PositionX | REAL | | 部署位置 | +| PositionY | REAL | | | +| PositionZ | REAL | | | +| AerosolType | INTEGER | NULLABLE | 挂载气溶胶类型,探测设备为 NULL | +| MunitionCount | INTEGER | NULLABLE | 挂载弹药数量 | +| MuzzleVelocity | REAL | NULLABLE | **NEW** 弹药初速 m/s(地基炮弹用) | +| ReleaseAltitude | REAL | NULLABLE | **NEW** 弹药释放高度 m(空基平台用) | +| Cooldown | REAL | DEFAULT 5 | 发射冷却时间 s | +| // 以下为探测设备专用字段 | +| DetectionRadius | REAL | NULLABLE | 探测半径 m | + +> **装备体系说明**: +> - **搭载平台**(2 种): +> - 空基(大型无人机):飞到释放点投放弹药 → 弹药自由落体/滑翔至云团生成点 +> - 地基(炮弹):地面发射 → 抛物线弹道 → 到达释放点生成云团 +> - **探测设备**:目标搜索发现与跟踪,输出目标坐标给发射平台 +> - 弹药类型决定生成云团的 AerosolType 和覆盖体积 + +#### CloudDispersal — 步骤4:云团抛撒配置 + +| 字段 | 类型 | 约束 | 说明 | +|------|------|------|------| +| TaskId | TEXT | PK, FK | 1:1 SimTask | +| AerosolType | INTEGER | | 0吸入式灭火 1爆燃式 2吸入式爆炸 | +| DisperseHeight | REAL | DEFAULT 300 | m | +| TriggerMode | INTEGER | | 0时间 1区域 2手动 | +| SpreadAngle | REAL | DEFAULT 0 | 度 | +| ReleaseMode | INTEGER | | 0单次 1连续 2脉冲 | +| InitialScale | REAL | DEFAULT 1000 | m³ | +| Duration | REAL | DEFAULT 60 | s | +| ReleaseInterval | REAL | DEFAULT 5 | s | +| TotalAmount | REAL | DEFAULT 50 | kg | +| PositionX | REAL | | 抛撒坐标 | +| PositionY | REAL | | | +| PositionZ | REAL | | | +| PositionMode | INTEGER | | 0三维点选 1手动输入 2算法推荐(NEW) | +| // 算法推荐结果(NEW) | +| RecommendedPosX | REAL | NULLABLE | 算法推荐最佳抛撒位置 | +| RecommendedPosY | REAL | NULLABLE | | +| RecommendedPosZ | REAL | NULLABLE | | +| RecommendedTiming | REAL | NULLABLE | 算法推荐最佳触发时机(仿真秒) | +| CriticalPosX | REAL | NULLABLE | 最危险值—抛撒位置 | +| CriticalPosY | REAL | NULLABLE | | +| CriticalPosZ | REAL | NULLABLE | | +| CriticalTiming | REAL | NULLABLE | 最危险值—触发时机 | + +#### RoutePlan — 步骤5:航路规划 + +| 字段 | 类型 | 约束 | 说明 | +|------|------|------|------| +| TaskId | TEXT | PK, FK | 1:1 SimTask | +| GroupId | TEXT | FK | 关联无人机编队 Group | +| FormationMode | INTEGER | | 0单机 1编队 2蜂群 | +| FormationSpacing | REAL | DEFAULT 50 | m | +| ETA | TEXT | | 预计到达时间 | + +#### Waypoint — 航路点 + +| 字段 | 类型 | 约束 | 说明 | +|------|------|------|------| +| Id | TEXT | PK | GUID | +| TaskId | TEXT | FK | | +| OrderIndex | INTEGER | NOT NULL | 航路顺序 | +| PosX | REAL | | 坐标(支持三维点选) | +| PosY | REAL | | | +| PosZ | REAL | | | +| Altitude | REAL | | 该航点飞行高度 m | +| Speed | REAL | | 该段速度 km/h | + +#### Group — 编组 + +| 字段 | 类型 | 约束 | 说明 | +|------|------|------|------| +| Id | TEXT | PK | GUID | +| Name | TEXT | NOT NULL | 编组名称 | +| GroupType | INTEGER | | 0=无人机编队 1=装备编组 | +| Description | TEXT | | 备注 | +| CreatedAt | TEXT | | | + +### 4.4 运行时表 + +> **注意**:SimEvent 和 SimulationReport 存储在主库 main.db;SimFrameRecord 存储在各任务的独立分库 frames/{taskId}.db 中。 + +#### SimFrameRecord — 逐帧记录 + +| 字段 | 类型 | 约束 | 说明 | +|------|------|------|------| +| Id | INTEGER | PK AUTOINCREMENT | | +| TaskId | TEXT | FK | | +| FrameIndex | INTEGER | | 帧序号 | +| Timestamp | REAL | | 仿真时间(秒) | +| EntityId | TEXT | | | +| EntityType | INTEGER | | 实体类型枚举 | +| PosX | REAL | | | +| PosY | REAL | | | +| PosZ | REAL | | | +| RotX | REAL | | | +| RotY | REAL | | | +| RotZ | REAL | | | +| StateData | TEXT | | JSON,包含:DestroyStage、CloudRadius、CloudColor 等 | + +> StateData JSON 结构(供 Unity 粒子系统 & 毁伤渲染): +> ```json +> { +> "damageStage": 0, // DamageStage 枚举 +> "hp": 0.75, // 剩余血量 0~1 +> "cloudRadius": 150.0, // 云团当前半径 m(CloudEntity) +> "cloudOpacity": 0.8, // 云团透明度(CloudEntity) +> "cloudColor": "#AABBCC", // 云团颜色(CloudEntity) +> "particleEmitRate": 500 // 粒子发射率(CloudEntity) +> } +> ``` + +#### SimEvent — 仿真事件(时序图数据源) + +| 字段 | 类型 | 约束 | 说明 | +|------|------|------|------| +| Id | TEXT | PK | GUID | +| TaskId | TEXT | FK | | +| OccurredAt | REAL | | 仿真时间(秒) | +| EventType | INTEGER | | 事件类型枚举 | +| SourceId | TEXT | | 源实体 ID | +| TargetId | TEXT | | 目标实体 ID | +| DataJson | TEXT | | 事件携带的结构化数据 | +| Description | TEXT | | 事件描述 | + +--- + +## 五、仿真引擎设计 + +### 5.1 核心实体关系 + +``` +SimulationEngine +│ +├── SimulationState ← 状态(运行/暂停/结束) +├── SceneConfig ← 配置快照(含场景边界、管控区域) +├── List ← 无人机(目标方) +│ └── 按编组组织,共享航路 +├── List ← 搭载平台(发射方)★重构 +│ ├── AirBasedPlatform ← 空基(大型无人机) +│ └── GroundBasedPlatform ← 地基(火炮) +├── List ← 探测设备 +├── List ← 飞行中的弹药 +│ ├── 空基投放:短程自由落体/滑翔 +│ └── 地基发射:抛物线弹道 +├── List ← 气溶胶云团 ★核心毁伤载体★ +│ └── 扩散模型调用第三方 DLL +├── List ← 管控区域(电子围栏)★NEW +├── EventQueue ← 事件队列 +└── IRecordWriter ← 帧数据写入 +``` + +### 5.2 拦截链路(完整流程) + +``` +探测设备扫描 + │ 发现目标 → 输出目标坐标 + ▼ +火控分配(分配给可用搭载平台) + │ + ├── 空基平台:飞至投放点 + │ │ + │ ▼ + │ 投放弹药(自由落体/滑翔至云团生成点) + │ + └── 地基平台:计算射击诸元 + │ + ▼ + 发射炮弹(抛物线弹道飞向目标区域) + │ + ▼ + 弹药到达释放点 → 生成 CloudEntity + MunitionEntity 销毁 + │ + ▼ +云团扩散膨胀(调用第三方 DLL 扩散模型) + │ 持续膨胀 + 消散计时 + ▼ +无人机进入云团范围 + │ 施加毁伤(调用第三方 DLL 毁伤模型) + │ 吸入式灭火 / 爆燃式 / 吸入式爆炸 + ▼ +无人机被摧毁 / 存活 + │ 毁伤阶段状态输出到 StateData + ▼ +Unity 渲染毁伤效果(尾焰异常、姿态失控、坠落) +``` + +### 5.3 仿真 Tick 流程(8 阶段) + +``` +1. 【探测阶段】 + FOR EACH 探测设备: + ├─ 扫描场景内无人机(距离 < DetectionRadius) + └─ 发现目标 → EventQueue.Enqueue(TargetDetected) + +2. 【火控决策阶段】 + 处理 TargetDetected 事件: + ├─ 分配目标给可用搭载平台 + ├─ 空基平台:计算拦截航路,飞向投放点 + └─ 地基平台:计算射击诸元(发射角、方位角) + +3. 【发射阶段】 + FOR EACH 搭载平台: + ├─ 满足发射条件 & 冷却完毕 → 生成 MunitionEntity + │ ├─ 空基:初始速度 = 平台速度,初始位置 = 投放点 + │ └─ 地基:初始速度 = MuzzleVelocity,按抛物线计算 + └─ 进入冷却倒计时 + +4. 【弹药飞行】 + FOR EACH MunitionEntity: + ├─ 空基弹药:自由落体/滑翔至云团生成高度 + ├─ 地基弹药:抛物线位置 = f(初速, 发射角, t) + └─ 到达释放条件 → 生成 CloudEntity → MunitionEntity 销毁 + └─ EventQueue.Enqueue(CloudGenerated) + +5. 【云团演化】(调用第三方 DLL) + FOR EACH CloudEntity: + ├─ 扩散膨胀:radius = DLL.Dispersion.Update(deltaTime, config) + ├─ 更新粒子系统参数(半径、透明度、颜色 → 写入 StateData) + └─ 剩余时间 ≤ 0 → 销毁 + +6. 【毁伤判定】(调用第三方 DLL) + DOUBLE LOOP DroneEntity × CloudEntity: + ├─ 无人机坐标 ∈ 云团球体? + ├─ 是 → damage = DLL.DamageModel.Calculate(DroneType, AerosolType, exposureTime) + ├─ 更新 drone.HP -= damage + ├─ 更新 drone.DamageStage(Normal → EngineAnomaly → AttitudeLoss → Destroyed) + └─ HP ≤ 0 → EventQueue.Enqueue(DroneDestroyed) + +7. 【无人机移动】 + FOR EACH 存活 DroneEntity: + ├─ 沿航路点线性插值移动 + ├─ 实际位移 += 风速矢量 × deltaTime + ├─ 到达航路点 → EventQueue.Enqueue(WaypointReached) + ├─ 到达最后一个航路点 → EventQueue.Enqueue(DroneReachedTarget) + └─ 编队/蜂群模式决定集群排列 + +8. 【管控区域判定】★NEW + FOR EACH 存活 DroneEntity: + ├─ 无人机坐标 ∈ 任一 ControlZone 多边形体积? + └─ 是 → EventQueue.Enqueue(ZoneIntruded) → 标记该无人机任务失败 + +9. 【结束判定】 + ├─ 所有无人机 (被摧毁 OR 到达目标 OR 侵入管控区) → SimulationEnd + ├─ 停止录制 + └─ 更新 SimulationState +``` + +### 5.4 实体行为模型 + +#### DroneEntity(无人机) + +``` +属性: + - 编组 GroupId、航路点队列 Waypoint[] + - 当前位置、速度、朝向 + - HP(0~1 归一化) + - DamageStage(Normal → EngineAnomaly → AttitudeLoss → Destroyed) + - 目标类型 TargetType、动力类型 PowerType + - 存活状态(飞行中/抵达目标/侵入管控区/被摧毁) + +运动模型(简化运动学): + - 两航路点之间线性插值 + - 实际位移 = 航向位移 + 风速矢量 × deltaTime + - 到达判定:||currentPos - waypointPos|| < 1m + +结束条件(任一触发即该无人机结束): + - 被毁伤(HP ≤ 0)→ Destroyed + - 到达攻击目标 → ReachedTarget + - 侵入管控区域 → ZoneIntruded +``` + +#### PlatformEntity(搭载平台)★重构 + +``` +空基平台(AirBasedPlatform): + - 位置(动态,飞向投放点) + - 挂载弹药类型 AerosolType、数量 MunitionCount + - 释放高度 ReleaseAltitude + - 冷却计时器 + - 行为:接收目标坐标 → 计算拦截航路 → 飞至投放点 → 投放弹药 + +地基平台(GroundBasedPlatform): + - 固定位置 + - 弹药初速 MuzzleVelocity + - 挂载弹药类型 AerosolType、数量 MunitionCount + - 冷却计时器 + - 行为:接收目标坐标 → 计算射击诸元 → 发射炮弹 +``` + +#### MunitionEntity(飞行中弹药)★重构 + +``` +属性: + - 弹药类型 AerosolType + - 发射方式 LaunchMode(AirDrop / GroundArtillery) + - 当前位置、速度 + - 目标坐标(云团生成点) + +行为(两种弹道): + - AirDrop(空基投放): + 初始位置 = 投放点,初始速度 = 平台速度 + 运动 = 自由落体 + 水平惯性(简化) + 到达条件 = 当前位置.Z ≤ 云团生成高度 + + - GroundArtillery(地基炮弹): + 初始位置 = 发射点,初始速度 = MuzzleVelocity + 运动 = 抛物线弹道 + x(t) = x0 + v0*cos(θ)*cos(φ)*t + y(t) = y0 + v0*cos(θ)*sin(φ)*t + z(t) = z0 + v0*sin(θ)*t - 0.5*g*t² + 到达条件 = z(t) ≤ 云团生成高度 +``` + +#### CloudEntity(气溶胶云团) + +``` +属性: + - 气溶胶类型 AerosolType + - 中心坐标 + - 当前半径(调用第三方 DLL 扩散模型计算) + - 最大半径 + - 持续时间、剩余时间 + - 粒子系统参数(半径、透明度、颜色 → 输出到 StateData) + +行为(委托第三方 DLL): + - 扩散膨胀:DLL.Dispersion.Update(deltaTime, 环境参数, 释放参数) + - 消散计时 + - 毁伤判定:DLL.DamageModel.Calculate(DroneType, AerosolType, exposureTime) +``` + +--- + +## 六、算法设计 — 接口驱动 & 可替换架构 ★关键 + +### 设计原则 + +所有算法组件通过**接口隔离**,SimulationEngine 只依赖接口,不感知具体实现。每个算法组件可独立替换为第三方实现,无需修改引擎代码。 + +``` + SimulationEngine + │ + ┌─────┼─────┐ + ▼ ▼ ▼ + ICloudDispersionModel ← 扩散模型接口 + IDamageModel ← 毁伤模型接口 + IDefenseAdvisor ← 防御推荐接口 + │ + ┌───────┼───────┐ + ▼ ▼ ▼ + 自主实现 第三方实现 混合(部分自研+部分外购) + (Default) (Future) (Future) +``` + +### 6.1 算法工厂与注册 + +```csharp +/// 算法工厂 — 统一管理所有算法组件的创建与替换 +public static class AlgorithmFactory +{ + // 注册表:每个接口 → 实现类型 + private static readonly Dictionary _registry = new() + { + { typeof(ICloudDispersionModel), typeof(GaussianPuffDispersion) }, + { typeof(IDamageModel), typeof(DamageModelRouter) }, + { typeof(IDefenseAdvisor), typeof(DefaultDefenseAdvisor) }, + }; + + /// 替换某个算法实现(第三方接入点) + public static void Register() where TImpl : TInterface + { + _registry[typeof(TInterface)] = typeof(TImpl); + } + + /// 创建实例,SimulationEngine 统一调用此方法 + public static T Create(params object[] args) + { + var implType = _registry[typeof(T)]; + return (T)Activator.CreateInstance(implType, args); + } +} +``` + +> **替换示例**:将来第三方提供 `ThirdPartyDispersion.dll`,只需一行: +> `AlgorithmFactory.Register();` +> 无需改动 SimulationEngine 任何代码。 + +### 6.2 第三方提供的数据 + +```csharp +/// 第三方提供的弹药基础参数(数据配置,非运行时 DLL) +public class AmmunitionSpec +{ + public AerosolType AerosolType; // 气溶胶类型 + public string Name; // 弹药名称 + + // 初始扩散状态 + public float InitialRadius; // 初始云团半径(m) + public float InitialVolume; // 初始云团体积(m³) + public float CoreDensity; // 核心密度(kg/m³) + public float EdgeDensity; // 边缘密度(kg/m³) + public float InitialTemperature; // 初始温度(K) + public float BuoyancyFactor; // 浮力系数 + + // 弹药基础参数 + public float EffectiveConcentration; // 有效毁伤浓度阈值(kg/m³) + public float DispersionRateBase; // 基础扩散速率(m/s) + public float MaxRadius; // 最大有效半径(m) + public float MaxDuration; // 最大持续时间(s) + public ParticleRenderParams Particles; // 粒子渲染参数 +} +``` + +> 第三方以 JSON 配置文件形式交付 `AmmunitionSpec` 数组,系统启动时加载到 SQLite `AmmunitionSpec` 表。 + +### 6.3 扩散模型设计 — 高斯烟团模型(Gaussian Puff) + +#### 6.3.1 模型选择 + +选用**高斯烟团扩散模型**(Gaussian Puff Model),理由: +- 计算简单,适合实时 20Hz Tick +- 能反映风速、大气稳定度对扩散的影响 +- 工业级大气扩散的经典方法 + +#### 6.3.2 核心公式 + +云团内任意点 (x, y, z) 在时刻 t 的浓度: + +``` +C(x, y, z, t) = ────────────── × exp[ -½( (x-ut)/σx )² ] + (2π)^(3/2) σx σy σz + + × exp[ -½( y/σy )² ] + + × { exp[ -½( (z-H)/σz )² ] + exp[ -½( (z+H)/σz )² ] } + +其中: + Q = 源强(释放总量) + u = 风速(m/s) + H = 有效释放高度(m) + σx, σy, σz = 三轴扩散系数,随时间增大 +``` + +#### 6.3.3 简化方案 + +完整三维浓度场计算量大。实时仿真中采用**简化方案**: + +``` +简化假设: + - 云团近似为球体(σx = σy = σz = σ) + - 半径 R(t) = InitialRadius + DispersionRate × t + - DispersionRate = DispersionRateBase × f(风速, 大气稳定度) + - 中心随风速漂移:Center(t) = Center(0) + WindVector × t + - 密度随半径衰减:Density(t) = CoreDensity × (InitialRadius / R(t))³ + +简化判定: + - 无人机进入球体 → 暴露于云团 + - 密度 ≥ EffectiveConcentration → 有效毁伤 + - 半径 ≥ MaxRadius OR 时间 ≥ MaxDuration → 消散 +``` + +#### 6.3.4 接口 + +```csharp +public interface ICloudDispersionModel +{ + void Initialize(AmmunitionSpec ammo, CombatScene env, Vector3 releasePos, float releaseTime); + void Tick(float deltaTime, float windSpeed, WindDirection windDir); + + Vector3 Center { get; } // 当前中心坐标(随风漂移) + float Radius { get; } // 当前半径(m) + float CoreDensity { get; } // 当前核心密度 + float EffectiveRadius { get; } // 密度 ≥ 有效浓度的半径 + ParticleParams Particles { get; } // Unity 粒子参数 + bool IsDissipated { get; } // 是否已消散 +} +``` + +### 6.4 毁伤模型设计 + +三种气溶胶对应三种毁伤机制,我们自主实现: + +#### 6.4.1 共同接口 + +```csharp +public interface IDamageModel +{ + /// 每 Tick 计算毁伤值 + /// 目标类型 + /// 动力类型(影响毁伤效率) + /// 气溶胶类型 + /// 当前位置云团密度(kg/m³) + /// 累计暴露时间(s) + /// 本次 Tick 时长 + /// 本次毁伤值(0~1 归一化) + float CalculateDamage(TargetType droneType, PowerType powerType, + AerosolType aerosolType, float cloudDensity, + float exposureTime, float deltaTime); + + DamageStage GetDamageStage(float accumulatedDamage); +} +``` + +#### 6.4.2 三种机制的差异 + +| 机制 | 气溶胶类型 | 毁伤原理 | 毁伤曲线特征 | +|------|------|------|------| +| **吸入式灭火** | 惰性气体 | 发动机吸入惰性气体 → 氧气浓度下降 → 熄火 | 阈值型:密度达标后快速生效,延迟短 | +| **爆燃式** | 活性材料 | 材料接触高温发动机表面 → 化学反应爆燃 | 触发型:密度 + 温度双条件,瞬间高伤害 | +| **吸入式爆炸** | 活性燃料 | 燃料被吸入进气道 → 内部爆炸 | 累积型:随暴露时间指数增长 | + +```csharp +// 实现示例伪代码 + +// 吸入式灭火:阈值判定 +class InertGasDamageModel : IDamageModel +{ + float CalculateDamage(...) { + if (cloudDensity < effectiveThreshold) return 0; // 浓度不够,无伤害 + // 超过阈值后,线性累积毁伤 + return damageRate * deltaTime; + } +} + +// 爆燃式:瞬时触发 +class ActiveMaterialDamageModel : IDamageModel +{ + float CalculateDamage(...) { + if (cloudDensity < triggerThreshold) return 0; + if (!triggered) { triggered = true; return burstDamage; } // 一次性爆发 + return residualDamage * deltaTime; // 后续余伤 + } +} + +// 吸入式爆炸:指数累积 +class ActiveFuelDamageModel : IDamageModel +{ + float CalculateDamage(...) { + if (cloudDensity < effectiveThreshold) return 0; + // 累积暴露 → 指数增长 + return baseRate * Mathf.Exp(exposureTime * factor) * deltaTime; + } +} +``` + +#### 6.4.3 毁伤阶段判定 + +``` +accumulatedDamage: 0.0 ─────── 0.25 ─────── 0.6 ─────── 1.0 +DamageStage: Normal EngineAnomaly AttitudeLoss Destroyed + +Normal: 正常运行 +EngineAnomaly: 发动机异常(Unity: 尾焰变色/不稳定) +AttitudeLoss: 姿态失控(Unity: 偏航/滚转) +Destroyed: 被摧毁(Unity: 坠落动画) +``` + +### 6.5 防御推荐算法设计 + +#### 6.5.1 Step A:气溶胶选型 + +基于**规则匹配**,根据目标动力类型推荐最佳毁伤机制: + +| 目标动力 | 推荐气溶胶 | 理由 | +|------|------|------| +| 电推式 | 惰性气体(吸入式灭火) | 电动机无高温,爆燃/爆炸不适用;惰性气体使电调/散热失效 | +| 活塞式 | 惰性气体(吸入式灭火) | 活塞发动机依赖氧气,窒息最有效 | +| 喷吸气式 | 活性材料(爆燃式) | 喷气发动机高温表面触发爆燃 | +| — | 活性燃料(吸入式爆炸) | 适用于高价值高速目标,追求一击必杀 | + +```csharp +// 选型规则表(可配置) +static readonly Dictionary MatchTable = new() +{ + { PowerType.Electric, AerosolType.InertGas }, + { PowerType.Piston, AerosolType.InertGas }, + { PowerType.Jet, AerosolType.ActiveMaterial }, +}; +``` + +#### 6.5.2 Step B:云团参数推荐 — 时空交汇优化 + +``` +问题: + 输入 = 目标航路 R(t)、风速 Vw、搭载平台位置 Pp、弹药飞行时间 Tf + 输出 = 最优抛撒位置 Ps 和触发时机 Ts + +方法: + 1. 将目标航路离散为时间点 t₁, t₂, ..., tₙ + 2. 对每个 tᵢ: + a. 无人机位置 = R(tᵢ) + b. 考虑炮弹飞行时间 Tf,抛撒时刻 = tᵢ - Tf + c. 云团在 [tᵢ - Tf, tᵢ - Tf + Duration] 内的扩散范围 + d. 计算交汇窗口 = 无人机航路 ∩ 云团扩散范围的时空重叠 + 3. 选择交汇窗口最大的 (Ps, Ts) 作为最佳值 + 4. 选择交汇窗口刚好有效的边界作为最危险值 + +简化实现: + - 云团近似为匀速膨胀球体 + - 无人机匀速直线飞行(取平均速度) + - 解析解:计算无人机航迹与云团膨胀球面的两个交点 + - 最佳 = 无人机在云团中时间最长 + - 最危险 = 无人机刚好擦边 +``` + +#### 6.5.3 Step C:反推平台部署 + +``` +根据云团方案反推: + + Q: 需要覆盖多大面积? + → 根据目标数量、编队间距、航路宽度 → 需要的云团数 + + Q: 每个云团需要多少弹药? + → 云团体积 / 单发覆盖体积 → 弹药数 + + Q: 用什么平台? + → 抛撒点距目标 > 平台航程?→ 地基 + → 抛撒点距目标 ≤ 平台航程?→ 空基 + → 根据弹药初速 / 飞行时间校验可行性 + + Q: 探测设备需要多大半径? + → 探测半径 ≥ 从平台发现目标到完成拦截期间目标飞过的距离 +``` + +#### 6.5.4 接口 + +```csharp +public interface IDefenseAdvisor +{ + DefenseRecommendation Recommend(ThreatProfile threat); +} + +// ThreatProfile / DefenseRecommendation / DefenseSolution 等模型见本节上文 +``` + +### 6.6 新增数据表:AmmunitionSpec + +| 字段 | 类型 | 约束 | 说明 | +|------|------|------|------| +| Id | TEXT | PK | GUID | +| AerosolType | INTEGER | | 气溶胶类型 | +| Name | TEXT | | 弹药名称 | +| InitialRadius | REAL | | 初始云团半径 m | +| InitialVolume | REAL | | 初始云团体积 m³ | +| CoreDensity | REAL | | 核心密度 kg/m³ | +| EdgeDensity | REAL | | 边缘密度 kg/m³ | +| InitialTemperature | REAL | | 初始温度 K | +| BuoyancyFactor | REAL | | 浮力系数 | +| EffectiveConcentration | REAL | | 有效毁伤浓度阈值 | +| DispersionRateBase | REAL | | 基础扩散速率 m/s | +| MaxRadius | REAL | | 最大有效半径 m | +| MaxDuration | REAL | | 最大持续时间 s | +| ParticlesJson | TEXT | | 粒子渲染参数 JSON | + +> 第三方以 JSON 文件交付,导入到此表。系统启动时若表为空则从默认配置文件加载。 + +### 6.7 算法替换完整示例 + +以下是将来用第三方 DLL 替换扩散模型的完整流程,演示接口驱动架构的灵活性: + +``` +┌─ 步骤 1:第三方提供实现 ─────────────────────────────┐ +│ │ +│ ThirdPartyDispersion.dll │ +│ └─ class ThirdPartyPuff : ICloudDispersionModel │ +│ { │ +│ void Initialize(AmmunitionSpec, ...) → P/Invoke → native_init() +│ void Tick(float, float, WindDirection) → P/Invoke → native_tick() +│ Vector3 Center → P/Invoke → native_center() +│ float Radius → P/Invoke → native_radius() +│ ... │ +│ } │ +│ │ +└────────────────────────────────────────────────────────┘ + +┌─ 步骤 2:一行注册 ──────────────────────────────────┐ +│ │ +│ AlgorithmFactory.Register │ +│ (); │ +│ │ +└────────────────────────────────────────────────────────┘ + +┌─ 步骤 3:SimulationEngine 无感知切换 ───────────────┐ +│ │ +│ var dispersion = AlgorithmFactory │ +│ .Create(ammo, env, pos, t);│ +│ │ +│ // 后续代码完全不变,无论底层是自主实现还是第三方 DLL │ +│ dispersion.Tick(deltaTime, windSpeed, windDir); │ +│ if (dispersion.IsDissipated) { ... } │ +│ │ +└────────────────────────────────────────────────────────┘ +``` + +**替换粒度**:每个算法组件独立替换,可以实现如下组合: + +``` +组合 A(当前): + ICloudDispersionModel → GaussianPuffDispersion(自主) + IDamageModel → DamageModelRouter(自主) + IDefenseAdvisor → DefaultDefenseAdvisor(自主) + +组合 B(将来可能): + ICloudDispersionModel → ThirdPartyPuff(第三方 DLL) + IDamageModel → DamageModelRouter(自主保留) + IDefenseAdvisor → DefaultDefenseAdvisor(自主保留) + +组合 C(将来可能): + ICloudDispersionModel → ThirdPartyPuff(第三方 DLL) + IDamageModel → ThirdPartyDamage(第三方 DLL) + IDefenseAdvisor → DefaultDefenseAdvisor(自主保留) +``` + +--- + +## 七、数据录制与回放 + +### 7.1 IFrameDataStore 接口 + +```csharp +/// 帧数据存储 — 每任务一个独立 SQLite 文件 +public interface IFrameDataStore +{ + /// 为任务创建帧数据库文件 + void CreateTaskDb(string taskId); + + /// 批量写入一帧所有实体状态 + void WriteFrame(string taskId, int frameIdx, float timestamp, + List entities); + + /// 按帧范围读取(回放用) + List ReadFrames(string taskId, int fromFrame, int toFrame); + + /// 删除任务的帧数据库 + void DeleteTaskDb(string taskId); + + /// 清理过期的帧数据库(定时调用) + void CleanupExpired(DateTime olderThan); +} +``` + +### 7.2 录制 + +- 仿真启动时创建 `frames/{taskId}.db` +- 录制帧率 = 仿真 TickRate(每想定可配置,默认 20Hz) +- 每 Tick 调用 `WriteFrame()` 写入该帧所有实体状态 +- SQLite 事务批量提交(如每 50 帧一批,减少磁盘 IO) +- 仿真事件 `SimEvent` 直接写入主库 `main.db`(每任务 ~30 条,轻量) + +### 7.3 回放 + +- ReplayController 协程驱动 +- 打开 `frames/{taskId}.db`,按 FrameIndex 范围查询 +- WHERE FrameIndex BETWEEN a AND b 命中索引,毫秒级 +- 回放帧率由前端控制(可与录制不同) +- 支持暂停 / 快进 / 后退 / 跳转 + +### 7.4 容量估算 + +以默认 20Hz TickRate,16 实体为例: + +| 仿真时长 | 帧数 | 行数(16实体) | 单任务大小 | +|------|------|------|------| +| 5 min | 6,000 | 96,000 | ~27 MB | +| 15 min | 18,000 | 288,000 | ~80 MB | +| 30 min | 36,000 | 576,000 | ~161 MB | +| 60 min | 72,000 | 1,152,000 | ~322 MB | + +6 个月累计(假设日均 5 次仿真,每次 30min): + +| 项目 | 数据 | +|------|------| +| 仿真总次数 | 5 × 180 = 900 次 | +| 帧数据总大小 | 900 × 161 MB ≈ **145 GB** | +| main.db | < **100 MB** | +| 全部合计 | **~145 GB** | + +### 7.5 TTL 清理策略 + +``` +仿真完成 → 生成 frames/{taskId}.db + │ + ▼ + CleanupExpired() 每天执行一次: + ├─ 扫描 frames/ 目录 + ├─ 检查 {taskId}.db 的创建时间 + └─ 超过 6 个月 → 删除文件 + +可选扩展: + - 归档:移到 archive/ 目录并压缩(.db.gz) + - 降采样:删除前先保留每 N 帧 1 帧的精简版 +``` + +### 7.6 为什么分库而非单库 + +| 对比 | 单库(全部在 main.db) | 分库(每任务一个 .db) | +|------|------|------| +| 帧表行数(6月) | 5.18 亿行 | 每库 57.6 万行 | +| 查询延迟 | 秒级(需精细索引) | 毫秒级 | +| 单文件体积 | > 100 GB | 每文件 ~161 MB | +| 备份/VACUUM | 灾难级 | 无痛 | +| 数据损坏影响 | 全部丢失 | 仅一个任务 | +| 运维(删除/归档) | 复杂 SQL | 直接删文件 | + +--- + +## 八、报告生成 + +### 8.1 报告结构 + +技术要求终版要求报告包含**时序图**和三个分析维度: + +| 阶段 | 内容 | 数据来源 | +|------|------|------| +| **仿真前** | 我方配置(装备类型/数量/编组、弹药类型/数量、部署位置、管控区域) | SimTask + 5 步配置 | +| **仿真中** | 关键事件时序表、各实体轨迹概要、云团演化数据 | SimEvent + SimFrameRecord | +| **仿真后** | 拦截成功率、各阶段耗时、毁伤效果评估、对抗结果判定 | SimEvent 汇总统计 | + +### 8.2 对抗结果判定逻辑 + +``` +成功拦截:所有无人机 被摧毁(未抵达目标、未侵入管控区) +部分拦截:部分无人机被摧毁,其余抵达目标或侵入管控区 +拦截失败:所有无人机抵达目标或侵入管控区(无摧毁) +``` + +### 8.3 导出 + +- 格式:PDF / Word(默认 PDF) +- 模板占位符填充后输出 +- 技术选型:QuestPDF 或其他 .NET Standard 2.1 兼容方案(待调研) + +--- + +## 九、服务接口概要 + +```csharp +// === 模型管理 === +interface IModelService +{ + ModelInfo ImportModel(string filePath, string name); + void DeleteModel(string id); + List GetAllModels(); + ModelInfo GetModel(string id); +} + +// === 编组管理 === +interface IGroupService +{ + Group CreateGroup(string name, GroupType type, string description); + void DeleteGroup(string id); + List GetGroups(GroupType? type); +} + +// === 想定管理 === +interface IScenarioService +{ + SimTask CreateTask(string name, string taskNumber); + void DeleteTask(string id); + PagedResult SearchTasks(string kw, DateTime? from, DateTime? to, int page, int size); + TaskFullConfig GetTaskDetail(string id); + + // 步骤保存 + void SaveScene(string taskId, CombatScene scene); + void SaveControlZones(string taskId, List zones); // NEW + void SaveTarget(string taskId, TargetConfig target); + void SaveDeployment(string taskId, List equips); // 重构 + void SaveCloudDispersal(string taskId, CloudDispersal cloud); + void SaveRoute(string taskId, RoutePlan route, List wps); + void UpdateStep(string taskId, int step); + + // 算法推荐(NEW) + DispersalRecommendation GetDispersalRecommendation(string taskId); +} + +// === 仿真引擎 === +interface ISimulationEngine +{ + void Initialize(string taskId); + SimulationFrameResult Tick(float deltaTime); + void Pause(); + void Resume(); + void Stop(); + SimulationState State { get; } +} + +// === 数据录制 === +interface IRecordService +{ + void StartRecording(string taskId, int tickRate); + void WriteFrame(int frameIdx, float timestamp, List entities); + void WriteEvent(SimEvent evt); + void StopRecording(); + List GetFrames(string taskId, int from, int to); + List GetEvents(string taskId); + void CleanupExpired(DateTime olderThan); // TTL 清理 +} + +// === 仿真报告 === +interface IReportService +{ + SimulationReport Generate(string taskId); + SimulationReport GetReport(string id); + void DeleteReport(string id); + byte[] ExportReport(string id, string format); // "pdf" | "word" + PagedResult SearchReports(string kw, DateTime? from, DateTime? to, int page, int size); +} +``` + +--- + +## 十、配置向导步骤(整合后) + +整合需求 V1.0 和技术要求终版后,配置向导保持 5 步但内容调整: + +``` +┌────────────────────────────────────────────────────────┐ +│ 步骤 1:作战场景设置 │ +│ ├─ 场景类型(城市/平原/山区/海岸) │ +│ ├─ 时间段(24h 制 NEW) │ +│ ├─ 天气 + 风速/风向/温度/湿度/气压/能见度 │ +│ ├─ 场景空间范围(宽/纵深/高 NEW) │ +│ └─ 管控区域(电子围栏 NEW) │ +│ └─ 多边形绘制 / 编辑 / 删除 │ +├────────────────────────────────────────────────────────┤ +│ 步骤 2:目标配置 │ +│ ├─ 编组选择(已有编队/新建编队) │ +│ ├─ 目标类型选择(自动填充默认参数) │ +│ ├─ 数量 / 动力类型 / 翼展 / 速度 / 高度 │ +│ └─ 预设典型目标库(NEW) │ +├────────────────────────────────────────────────────────┤ +│ 步骤 3:装备部署(重构) │ +│ ├─ 装备角色(探测设备 / 发射平台) │ +│ ├─ 发射平台类型(空基 / 地基 NEW) │ +│ ├─ 数量 / 编组 / 部署位置 │ +│ ├─ 挂载气溶胶类型 / 弹药数量 │ +│ ├─ 弹药初速 / 释放高度 / 冷却时间 │ +│ └─ 探测半径(探测设备) │ +├────────────────────────────────────────────────────────┤ +│ 步骤 4:云团抛撒配置 │ +│ ├─ 气溶胶类型(吸入式灭火/爆燃式/吸入式爆炸) │ +│ ├─ 释放参数(高度/触发模式/扩散方向/规模/持续等) │ +│ ├─ 抛撒位置(三维点选 / 手动输入 / 算法推荐 NEW) │ +│ ├─ 算法推荐按钮 → 显示最佳值 & 最危险值(NEW) │ +│ └─ 可点选采纳推荐值 │ +├────────────────────────────────────────────────────────┤ +│ 步骤 5:航路规划 │ +│ ├─ 编队模式(单机/编队/蜂群) │ +│ ├─ 编队间距 / 预计到达时间 │ +│ ├─ 航路点画布(三维点选添加/调整/删除 NEW) │ +│ └─ 航路点列表(坐标/高度/速度) │ +└────────────────────────────────────────────────────────┘ +``` + +--- + +## 十一、关键技术决策 + +| # | 决策 | 选择 | 理由 | +|---|------|------|------| +| 1 | 数据持久化 | SQLite (sqlite-net) | 支持复杂查询、筛选、分页 | +| 2 | 模型文件格式 | .fbx / .obj / .stl / .glb / .gltf | 合并两份需求,≤500MB,2K+贴图 | +| 3 | 模型文件存储 | Application.persistentDataPath | 不与 Unity Assets 耦合 | +| 4 | 序列化方案 | System.Text.Json | Unity 2022+ 内置支持 | +| 5 | Service/Data 层 | 零 UnityEngine 依赖 | 可独立单元测试 | +| 6 | 仿真驱动方式 | 固定 TickRate 帧驱动 | 每想定可配置(默认 20Hz),保证确定性 | +| 7 | 仿真并发 | 单仿真 | 简化状态管理 | +| 8 | 拦截机制 | 多阶段链路 | 探测→火控→发射→弹药飞行→云团→毁伤 | +| 9 | 云团扩散模型 | **高斯烟团模型(自主实现)** | 简化球体膨胀 + 风速漂移 + 密度衰减,适合实时 20Hz | +| 10 | 毁伤模型 | **三种机制自主实现** | 吸入式灭火(阈值型)、爆燃式(触发型)、吸入式爆炸(指数累积型) | +| 11 | 防御方案推荐 | **IDefenseAdvisor(自主实现)** | 规则匹配选型 + 时空交汇优化 + 反推平台部署 | +| 12 | 第三方数据 | **静态配置(JSON → SQLite)** | 仅提供弹药基础参数和初始扩散状态,不提供运行时算法 | +| 13 | 运动学 | 简化运动学 + 风偏 | 航路点线性插值 + 风速矢量 | +| 14 | 弹药弹道 | 抛物线(地基)/ 自由落体(空基) | 基于初速、角度、释放高度计算 | +| 15 | 编组 | Group 独立实体 | 编组独立管理,任务中引用 | +| 16 | 管控区域 | ControlZone 表 + Tick 判定 | 电子围栏,侵入即任务失败 | +| 17 | 配置向导流程 | **威胁驱动三阶段** | Phase1 定义威胁 → Phase2 算法推荐 → Phase3 审阅确认 | +| 18 | 报告导出 | 模板填充 → PDF/Word | QuestPDF 待调研 | +| 19 | 坐标系统 | 右手系 Y-up,米为单位 | 与 Unity 一致 | +| 20 | 空间范围 | 最低 4×100×10m,默认更大 | 可配置 | +| 21 | 帧数据存储 | **主库 + 任务分库** | 主库存元数据+事件(<100MB永久);分库每任务存帧数据(6月TTL),单文件~161MB,直接删文件运维 | +| 22 | 程序集拆分 | **Core 独立于 Unity** | Core 为 .NET Standard 2.1 纯类库,零 UnityEngine 依赖;Unity 仅作桥接;dotnet test 即可全量测试 | + +--- + +## 十二、性能指标(来自技术要求终版) + +| 指标 | 要求 | 说明 | +|------|------|------| +| 最大目标数 | ≥ 5 独立运动实体(3D 显示) | 显示层面,非逻辑上限 | +| 渲染帧率 | ≥ 30 FPS(满载 + 粒子特效) | Unity 前端职责 | +| 仿真空间 | ≥ 4m × 100m × 10m | 默认更大,可配置 | +| 粒子系统 | 云团扩散/流动/色彩 | Unity 根据后端参数渲染 | +| 毁伤效果 | 尾焰异常、姿态失控、坠落 | Unity 根据 DamageStage 渲染 | + +--- + +## 十三、待后续讨论 + +| # | 议题 | 说明 | +|---|------|------| +| 1 | 第三方 DLL 的具体接口规范 | 需与供应商对齐函数签名、数据结构、调用约定 | +| 2 | 3 种毁伤机制的差异化细节 | 吸入式灭火 vs 爆燃式 vs 吸入式爆炸的毁伤曲线差异 | +| 3 | 探测设备融合逻辑 | 多设备发现同一目标时的数据融合策略 | +| 4 | 火控分配策略 | 多目标时的优先级分配规则 | +| 5 | 蜂群模式运动模型 | 随机扰动范围、个体行为差异 | +| 6 | 报告模板具体格式 | 统计维度、图表需求 | +| 7 | 场景类型对仿真的影响 | 山区是否影响探测/运动 | +| 8 | PDF/Word 导出库选型 | QuestPDF 或其他 .NET Standard 2.1 方案 | +| 9 | 预置典型目标库内容 | 具体包含哪些无人机型号及参数 | +| 10 | 扩散模型的大气稳定度参数 | 不同天气条件下 σ 的增长速率如何量化 | +| 11 | 毁伤曲线的具体数值 | 三种机制的 damageRate、threshold、burstDamage 等参数取值 | +| 12 | 时空交汇优化的精度 | 离散采样步长 vs 解析解的选择 | diff --git a/docs/design/index.md b/docs/design/index.md new file mode 100644 index 0000000..0e21946 --- /dev/null +++ b/docs/design/index.md @@ -0,0 +1,26 @@ +# docs/design — 设计方案 + +本目录存放架构设计和技术方案文档。 + +## 目录结构 + +``` +design/ +├── architecture/ # 架构设计 & 架构决策记录(ADR) +└── technical/ # 技术方案 / 详细设计 +``` + +## architecture/ — 架构设计 + +- **总体架构文档**:系统架构图、模块划分、技术选型、核心交互流程 +- **ADR(Architecture Decision Record)**:记录重大架构决策及其背景 + +## technical/ — 技术方案 + +- 各模块的详细技术设计 +- 接口设计、数据模型设计 +- 关键技术难点的攻关方案 + +--- + +> **提示**:架构图和流程图推荐使用 Mermaid(Markdown 原生支持)或 PlantUML,方便版本控制和 AI 工具解析。 diff --git a/docs/design/technical/index.md b/docs/design/technical/index.md new file mode 100644 index 0000000..33b1c70 --- /dev/null +++ b/docs/design/technical/index.md @@ -0,0 +1,51 @@ +# docs/design/technical — 技术方案 + +本目录存放各模块的详细技术设计方案。 + +## 文件命名 + +``` +<模块名>-技术方案.md +``` + +## 技术方案模板 + +```markdown +# [模块名] 技术方案 + +- **版本**:v[版本号] +- **作者**:[姓名] +- **日期**:YYYY-MM-DD +- **状态**:草稿 / 评审中 / 已定稿 + +## 1. 概述 + +> 本方案解决什么问题?适用范围是什么? + +## 2. 设计目标 + +- 功能目标 +- 非功能目标(性能、安全、可扩展性等) + +## 3. 方案描述 + +### 3.1 整体流程 + + + +### 3.2 接口设计 + +### 3.3 数据模型 + +### 3.4 关键逻辑 + +## 4. 风险与应对 + +| 风险 | 影响 | 应对措施 | +|------|------|----------| +| ... | ... | ... | + +## 5. 参考 + +- [相关文档链接] +``` diff --git a/docs/features/index.md b/docs/features/index.md new file mode 100644 index 0000000..beb59da --- /dev/null +++ b/docs/features/index.md @@ -0,0 +1,56 @@ +# docs/features — 功能规格说明 + +每个功能模块对应一个功能规格说明文档,用于描述该功能的完整定义。 + +## 文件命名 + +``` +<功能名称>.md +``` + +## 功能规格模板 + +```markdown +# [功能名称] + +- **版本**:v[版本号] +- **状态**:规划中 / 开发中 / 已完成 +- **负责人**:[姓名] +- **关联需求**:[需求编号或链接] + +## 1. 功能概述 + +> 一句话描述 + 详细说明(用户视角,这个功能能做什么、解决什么问题)。 + +## 2. 用户场景 + +- 作为 [角色],我希望 [做什么],以便 [达到什么目的] + +## 3. 功能详情 + +### 3.1 前置条件 + +### 3.2 主流程 + +### 3.3 异常处理 + +- 异常情况 1:[描述] → [预期行为] +- 异常情况 2:[描述] → [预期行为] + +## 4. 验收标准 + +- [ ] 标准 1 +- [ ] 标准 2 +- [ ] 标准 3 + +## 5. 约束与限制 + +- 性能要求 +- 兼容性要求 +- 其他限制 + +## 6. 关联 + +- 设计方案:`docs/design/technical/<相关方案>.md` +- 任务拆分:`docs/implementation/tasks/<相关任务>.md` +``` diff --git a/docs/implementation/index.md b/docs/implementation/index.md new file mode 100644 index 0000000..e6f504f --- /dev/null +++ b/docs/implementation/index.md @@ -0,0 +1,19 @@ +# docs/implementation — 实施与跟踪 + +> **可选**:如果团队已使用 Jira/TAPD/飞书等工单系统管理任务,可直接删除此目录。 + +本目录用于管理开发实施计划和任务进度。 + +## 目录结构 + +``` +implementation/ +├── index.md # 本文件 +└── tasks/ # 任务拆分与进度跟踪 +``` + +## 使用方式 + +1. 将功能需求拆分为可执行的任务 +2. 每个迭代/功能一个任务文件,记录在 `tasks/` 下 +3. 任务文件中用 Checkbox 标记进度 diff --git a/docs/implementation/tasks/index.md b/docs/implementation/tasks/index.md new file mode 100644 index 0000000..12fac3d --- /dev/null +++ b/docs/implementation/tasks/index.md @@ -0,0 +1,51 @@ +# docs/implementation/tasks — 任务跟踪 + +每个功能或迭代对应一个任务文件。 + +## 文件命名 + +``` +<迭代/功能名称>-任务.md +``` + +## 任务文件模板 + +```markdown +# [迭代/功能名称] 任务跟踪 + +- **开始日期**:YYYY-MM-DD +- **目标日期**:YYYY-MM-DD +- **负责人**:[姓名] + +## 任务列表 + +### 阶段 1:[阶段名称](目标:YYYY-MM-DD) + +- [ ] 任务 1 — @负责人 +- [ ] 任务 2 — @负责人 + - [ ] 子任务 2.1 + - [ ] 子任务 2.2 + +### 阶段 2:[阶段名称](目标:YYYY-MM-DD) + +- [ ] 任务 3 — @负责人 +- [ ] 任务 4 — @负责人 + +## 进度 + +- ✅ 已完成:X / Y +- 🚧 进行中:X +- ⬜ 待开始:X + +## 风险与阻塞 + +| 问题 | 影响 | 责任人 | 状态 | +|------|------|--------|------| +| ... | ... | ... | 解决中 / 已解决 | + +## 变更记录 + +| 日期 | 变更内容 | +|------|----------| +| YYYY-MM-DD | ... | +``` diff --git a/docs/implementation/tasks/实施计划与任务跟踪.md b/docs/implementation/tasks/实施计划与任务跟踪.md new file mode 100644 index 0000000..dd30799 --- /dev/null +++ b/docs/implementation/tasks/实施计划与任务跟踪.md @@ -0,0 +1,281 @@ +# 实施计划与任务跟踪 + +> **项目**:反无人机仿真系统后端 +> **文档版本**:V1.0 +> **创建日期**:2026-06-11 + +--- + +## 一、阶段总览 + +``` +Phase 1 ■■■■■ 核心基础(6天) +Phase 2 ■■■■■ 想定管理(5天) +Phase 3 ■■■■■ 算法层(7天) +Phase 4 ■■■■■ 仿真引擎(7天) +Phase 5 ■■■ 报告生成(3天) +Phase 6 ■■■■ Unity 集成(5天) +Phase 7 ■■ 打磨收尾(3天) +──────────────────────────── +合计 约 36 天(7~8 周) +``` + +> 依赖关系:Phase 1 → Phase 2 → Phase 3+4(可并行) → Phase 5 → Phase 6 → Phase 7 + +--- + +## 二、各阶段详情 + +### Phase 1:核心基础 + +**目标**:搭建项目骨架,数据层可读写,单元测试全绿。 +**时间**:6 天 +**验证**:`dotnet test` 全绿,所有 Repository 和基础 Service 可独立运行。 + +| # | 任务 | 预估 | 状态 | +|---|------|------|------| +| 1.1 | 创建 `CounterDrone.Core` 类库项目(.NET Standard 2.1) | 2h | ⬜ | +| 1.2 | 创建 `CounterDrone.Core.Tests` xUnit 测试项目 | 1h | ⬜ | +| 1.3 | 定义所有枚举(17 个)和数据模型(11 张表 POCO) | 4h | ⬜ | +| 1.4 | 集成 sqlite-net,编写 `DatabaseManager`(建表 / 迁移) | 3h | ⬜ | +| 1.5 | 实现 `IPathProvider` 接口 + 测试用实现 | 1h | ⬜ | +| 1.6 | 实现 `ModelRepository`(CRUD + 文件存储路径管理) | 3h | ⬜ | +| 1.7 | 实现 `AmmunitionSpecRepository`(加载第三方 JSON 配置) | 2h | ⬜ | +| 1.8 | 实现 `GroupRepository` | 2h | ⬜ | +| 1.9 | 实现 `IModelService` + `ModelService` | 3h | ⬜ | +| 1.10 | 实现 `IGroupService` + `GroupService` | 2h | ⬜ | +| 1.11 | 编写 ModelService / GroupService 单元测试 | 4h | ⬜ | +| 1.12 | 编写 Repository 单元测试(SQLite 文件模式) | 4h | ⬜ | + +**Phase 1 验收标准**: +- [ ] 11 张表可在 SQLite 中自动建表 +- [ ] ModelInfo、AmmunitionSpec、Group 的完整 CRUD 通过测试 +- [ ] 第三方 AmmunitionSpec JSON 可成功导入数据库 +- [ ] dotnet test 全部通过 + +--- + +### Phase 2:想定管理 + +**目标**:仿真任务的完整 CRUD + 5 步配置保存/加载 + 搜索分页。 +**时间**:5 天 +**验证**:可创建想定、逐步骤配置保存、草稿恢复、搜索分页。 + +| # | 任务 | 预估 | 状态 | +|---|------|------|------| +| 2.1 | 实现 `SimTaskRepository` + `SimTask` 增删改查 | 2h | ⬜ | +| 2.2 | 实现 `CombatSceneRepository` | 1h | ⬜ | +| 2.3 | 实现 `ControlZoneRepository` | 1h | ⬜ | +| 2.4 | 实现 `TargetConfigRepository` | 1h | ⬜ | +| 2.5 | 实现 `EquipmentDeploymentRepository` | 1h | ⬜ | +| 2.6 | 实现 `CloudDispersalRepository` | 1h | ⬜ | +| 2.7 | 实现 `RoutePlanRepository` + `WaypointRepository` | 2h | ⬜ | +| 2.8 | 实现 `IScenarioService` — CreateTask / DeleteTask | 2h | ⬜ | +| 2.9 | 实现步骤保存方法(SaveScene / SaveTarget / ... / SaveRoute) | 3h | ⬜ | +| 2.10 | 实现 `GetTaskDetail`(聚合 5 步配置) | 2h | ⬜ | +| 2.11 | 实现 `SearchTasks`(关键词 + 时间范围 + 分页) | 3h | ⬜ | +| 2.12 | 实现 UpdateStep(步骤状态管理) | 1h | ⬜ | +| 2.13 | 实现输入校验(必填校验、数值范围校验) | 2h | ⬜ | +| 2.14 | 编写 ScenarioService 单元测试 | 6h | ⬜ | + +**Phase 2 验收标准**: +- [ ] 创建任务 → 5 步逐步骤保存 → 草稿恢复 → 搜索分页 全链路可走通 +- [ ] 按任务名称搜索、按日期筛选、分页浏览通过测试 +- [ ] 必填校验:任务名称为空时拒绝创建 +- [ ] `GetTaskDetail` 返回完整的 5 步配置聚合对象 + +--- + +### Phase 3:算法层 + +**目标**:高斯烟团扩散模型、三种毁伤模型、防御推荐算法全部实现并通过确定性测试。 +**时间**:7 天 +**验证**:固定输入 → 固定输出,可精确断言计算结果。 + +| # | 任务 | 预估 | 状态 | +|---|------|------|------| +| 3.1 | 实现 `GaussianPuffDispersion`(简化球体膨胀 + 风速漂移 + 密度衰减) | 8h | ⬜ | +| 3.2 | 编写扩散模型单元测试(无风/有风/消散/密度衰减 4 场景) | 4h | ⬜ | +| 3.3 | 实现 `InertGasDamageModel`(阈值型吸入式灭火) | 3h | ⬜ | +| 3.4 | 实现 `ActiveMaterialDamageModel`(触发型爆燃式) | 3h | ⬜ | +| 3.5 | 实现 `ActiveFuelDamageModel`(指数累积型吸入式爆炸) | 3h | ⬜ | +| 3.6 | 实现 `DamageModelRouter`(根据 AerosolType 路由到对应模型) | 1h | ⬜ | +| 3.7 | 编写毁伤模型单元测试(每种机制 3 个场景) | 4h | ⬜ | +| 3.8 | 实现 `DefaultDefenseAdvisor` — Step A 气溶胶选型规则 | 3h | ⬜ | +| 3.9 | 实现 `DefaultDefenseAdvisor` — Step B 时空交汇优化 | 6h | ⬜ | +| 3.10 | 实现 `DefaultDefenseAdvisor` — Step C 反推平台部署 | 4h | ⬜ | +| 3.11 | 实现 `AlgorithmFactory`(注册/创建) | 2h | ⬜ | +| 3.12 | 编写 DefenseAdvisor 单元测试(活塞→惰性气体 / 喷气→活性材料) | 4h | ⬜ | + +**Phase 3 验收标准**: +- [ ] 扩散模型:给定初始半径+风速+60s → 断言最终半径和中心偏移 +- [ ] 毁伤模型:活塞发动机 5s 暴露于惰性气体 → 断言 HP ≤ 阈值 +- [ ] 防御推荐:活塞发动机航路 → 推荐惰性气体,拦截概率 > 0 +- [ ] AlgorithmFactory.Register 可替换任一算法实现 +- [ ] 所有测试结果完全可复现(确定性) + +--- + +### Phase 4:仿真引擎 + +**目标**:完整 8 阶段仿真引擎,支持运行/暂停/停止,录制帧数据和事件。 +**时间**:7 天 +**验证**:加载想定 → 运行完整仿真 → 检查事件序列和帧数据。 + +| # | 任务 | 预估 | 状态 | +|---|------|------|------| +| 4.1 | 实现 `DroneEntity`(航路移动 + 风偏 + 编队排列) | 4h | ⬜ | +| 4.2 | 实现 `PlatformEntity`(空基/地基 + 冷却计时) | 3h | ⬜ | +| 4.3 | 实现 `MunitionEntity`(自由落体 + 抛物线弹道) | 4h | ⬜ | +| 4.4 | 实现 `CloudEntity`(集成扩散模型 + 毁伤判定) | 3h | ⬜ | +| 4.5 | 实现 `DetectionEntity`(探测扫描) | 2h | ⬜ | +| 4.6 | 实现 `ControlZone` 多边形判定(Point-in-Polygon) | 2h | ⬜ | +| 4.7 | 实现 `EventQueue` | 2h | ⬜ | +| 4.8 | 实现 `SimulationEngine` — Initialize(加载配置,实例化实体) | 4h | ⬜ | +| 4.9 | 实现 `SimulationEngine` — 8 阶段 Tick 流程 | 6h | ⬜ | +| 4.10 | 实现 `SimulationEngine` — Pause/Resume/Stop + State 管理 | 2h | ⬜ | +| 4.11 | 实现 `IFrameDataStore` — CreateTaskDb / WriteFrame / ReadFrames | 4h | ⬜ | +| 4.12 | 实现 `SimEvent` 写入主库 + 查询 | 2h | ⬜ | +| 4.13 | 编写仿真集成测试(单无人机无拦截 → 到达目标) | 3h | ⬜ | +| 4.14 | 编写仿真集成测试(单无人机 → 拦截成功 → 被摧毁) | 3h | ⬜ | +| 4.15 | 编写仿真集成测试(多无人机 + 编队 + 蜂群) | 3h | ⬜ | +| 4.16 | 编写仿真集成测试(管控区域侵入) | 2h | ⬜ | + +**Phase 4 验收标准**: +- [ ] 加载完整想定配置后可启动仿真 +- [ ] 8 个阶段的 Tick 顺序正确,事件队列正常产出 +- [ ] 集成测试:单无人机无拦截 → 断言 "DroneReachedTarget" 事件 +- [ ] 集成测试:单无人机被拦截 → 断言 "DroneDestroyed" + "CloudGenerated" +- [ ] 帧数据可正确写入 `frames/{taskId}.db`,回读验证帧数一致 +- [ ] Pause/Resume 不影响仿真状态一致性 + +--- + +### Phase 5:报告生成 + +**目标**:仿真结束后自动生成报告,支持 PDF/Word 导出。 +**时间**:3 天 +**验证**:跑完仿真 → 生成报告 → 导出文件存在且内容正确。 + +| # | 任务 | 预估 | 状态 | +|---|------|------|------| +| 5.1 | 实现 `ReportGenerator`(从 SimEvent + 配置聚合统计数据) | 4h | ⬜ | +| 5.2 | 实现对抗结果判定逻辑(成功/部分/失败) | 1h | ⬜ | +| 5.3 | 实现报告模板引擎(占位符填充) | 3h | ⬜ | +| 5.4 | 实现 PDF 导出(QuestPDF 或替代方案调研) | 4h | ⬜ | +| 5.5 | 实现 Word 导出 | 3h | ⬜ | +| 5.6 | 实现 `SimulationReportRepository` | 1h | ⬜ | +| 5.7 | 实现 `IReportService`(Generate / Get / Delete / Search / Export) | 3h | ⬜ | +| 5.8 | 编写报告生成单元测试 | 3h | ⬜ | + +**Phase 5 验收标准**: +- [ ] 仿真完成后报告自动生成,包含仿真前/中/后三阶段数据 +- [ ] 对抗结果判定正确 +- [ ] PDF 导出文件可正常打开 +- [ ] 报告搜索分页功能正常 + +--- + +### Phase 6:Unity 集成 + +**目标**:将 Core.dll 集成到 Unity,实现完整运行链路。 +**时间**:5 天 +**验证**:在 Unity Editor 中完成"创建想定 → 配置 5 步 → 算法推荐 → 启动仿真 → 查看回放"全流程。 + +| # | 任务 | 预估 | 状态 | +|---|------|------|------| +| 6.1 | 创建 Unity 22.3.62 项目,导入 Core.dll 到 Assets/Plugins | 1h | ⬜ | +| 6.2 | 实现 `UnityPathProvider`(返回 persistentDataPath) | 1h | ⬜ | +| 6.3 | 实现 `ModelManager` MonoBehaviour(桥接 IModelService) | 3h | ⬜ | +| 6.4 | 实现 `ScenarioManager` MonoBehaviour(桥接 IScenarioService) | 4h | ⬜ | +| 6.5 | 实现 `SimulationRunner` MonoBehaviour(Update 驱动引擎) | 3h | ⬜ | +| 6.6 | 实现 `ReplayController` MonoBehaviour(协程回放) | 3h | ⬜ | +| 6.7 | 实现 `ReportManager` MonoBehaviour(桥接 IReportService) | 2h | ⬜ | +| 6.8 | 实现 3D 实体可视化(根据 Snapshot 更新 GameObject Transform) | 4h | ⬜ | +| 6.9 | 实现粒子系统参数传递(CloudEntity → ParticleSystem) | 3h | ⬜ | +| 6.10 | 端到端联调 + 问题修复 | 6h | ⬜ | + +**Phase 6 验收标准**: +- [ ] Unity Editor 中创建想定 → 配置保存 → 算法推荐 → 启动仿真全链路可走通 +- [ ] 仿真过程中 3D 实体实时移动 +- [ ] 回放功能正常 +- [ ] 无 Crash,无内存泄漏 + +--- + +### Phase 7:打磨收尾 + +**目标**:性能优化、边界处理、TTL 清理、文档。 +**时间**:3 天 + +| # | 任务 | 预估 | 状态 | +|---|------|------|------| +| 7.1 | 性能优化:Tick() P99 < 5ms | 4h | ⬜ | +| 7.2 | TTL 清理实现 + 测试 | 2h | ⬜ | +| 7.3 | 边界场景测试(0 实体、超大航路、极端风速) | 3h | ⬜ | +| 7.4 | 错误处理加固 | 2h | ⬜ | +| 7.5 | 补充缺失的单元测试(目标覆盖率 > 80%) | 4h | ⬜ | +| 7.6 | 文档更新 + API 注释 | 3h | ⬜ | + +**Phase 7 验收标准**: +- [ ] Tick() 单帧耗时 P99 < 5ms(16 实体场景) +- [ ] 过期帧数据库可自动清理 +- [ ] 单元测试覆盖率 > 80% +- [ ] 所有公开 API 有 XML 文档注释 + +--- + +## 三、依赖关系 + +``` +Phase 1 (核心基础) + │ + ▼ +Phase 2 (想定管理) + │ + ├──────────────────┐ + ▼ ▼ +Phase 3 (算法层) Phase 4 (仿真引擎) + │ │ + └────────┬─────────┘ + ▼ + Phase 5 (报告生成) + │ + ▼ + Phase 6 (Unity 集成) + │ + ▼ + Phase 7 (打磨收尾) +``` + +> Phase 3 和 Phase 4 可部分并行:算法层完成后仿真引擎才能完整串联,但仿真实体模型可先开工。 + +--- + +## 四、里程碑 + +| 里程碑 | 截止 | 标志 | +|------|------|------| +| M1 — 数据层就绪 | Phase 1 结束 | 11 张表 CRUD 通过 | +| M2 — 想定可配置 | Phase 2 结束 | 5 步配置保存/恢复/搜索 | +| M3 — 算法可计算 | Phase 3 结束 | 扩散/毁伤/推荐输出正确 | +| M4 — 仿真可运行 | Phase 4 结束 | 集成测试通过 | +| M5 — 报告可生成 | Phase 5 结束 | PDF/Word 导出 | +| M6 — Unity 可演示 | Phase 6 结束 | 端到端全链路 | +| M7 — 交付就绪 | Phase 7 结束 | 覆盖率 > 80%,P99 < 5ms | + +--- + +## 五、状态图例 + +| 符号 | 含义 | +|------|------| +| ⬜ | 未开始 | +| 🔄 | 进行中 | +| ✅ | 已完成 | +| ⏸️ | 阻塞 | +| ❌ | 放弃 | + +--- + +> **下一步**:确认阶段划分和时间计划后,可以开始 Phase 1,逐项打勾推进。 diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..51f22b6 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,22 @@ +# docs — 开发文档 + +本目录存放项目的全部开发文档,涵盖需求、设计、功能规格、实施跟踪和质量验证。 + +## 目录说明 + +| 子目录 | 用途 | 受众 | 必选 | +|--------|------|------|:--:| +| `requirements/` | 需求与变更管理 | 产品、开发、测试 | 可选 | +| `design/` | 架构设计与技术方案 | 开发、架构师 | ✅ | +| `features/` | 功能规格说明 | 开发、测试、产品 | ✅ | +| `implementation/` | 实施计划与任务跟踪 | 开发、项目管理 | 可选 | +| `verify/` | 人工检查清单(上线/代码审查/安全审计) | 开发、运维 | 可选 | + +> - 标记"可选"的目录:如果团队已使用 Jira/TAPD/飞书等工单系统管理需求和任务,可直接删除,避免双写不同步。 +> - `verify/` 与 `test/` 的区别:`test/` 是**自动化代码测试**,`verify/` 是**人工检查清单和验证流程**。 + +## 使用原则 + +1. 文档**与代码同步更新**,不是一次性交付物 +2. 优先用 Markdown,便于版本控制和 AI 工具阅读 +3. 每个文档文件保持聚焦,一个文件讲清一件事 diff --git a/docs/requirements/V1.0_反无人机仿真系统功能需求260611.txt b/docs/requirements/V1.0_反无人机仿真系统功能需求260611.txt new file mode 100644 index 0000000..ae64b42 --- /dev/null +++ b/docs/requirements/V1.0_反无人机仿真系统功能需求260611.txt @@ -0,0 +1,266 @@ +反无人机仿真系统功能需求 + +一、整体导航需求 + +系统顶部设置固定导航栏,包含三个核心模块入口:模型管理、想定管理、仿真报告。点击对应导航按钮可切换显示对应模块页面,当前激活模块按钮高亮显示。 + + +二、模型管理模块功能需求 + +1. 页面入口 + +点击顶部导航栏「模型管理」,进入模型资源库页面,以网格卡片形式展示所有已导入的模型。 + +2. 导入新模型 + +1.点击页面「导入新模型」按钮,打开导入新模型弹窗 +2.支持两种导入方式: + +•拖拽模型文件到指定拖拽区域 +•点击「从文件选择」按钮选中本地模型文件 + +3.系统自动提取文件名填充模型名称,支持用户手动修改 +4.支持格式:.obj .stl .glb,单文件大小不超过500MB +5.操作: + +•输入模型名称后点击「确认导入」完成导入,导入成功后关闭弹窗并刷新模型列表 +•点击「取消」、弹窗关闭按钮或点击弹窗空白区域,可关闭导入弹窗,清空已填写内容 + +6.必填校验:模型名称为空时无法导入,提示用户输入模型名称 + +3. 模型预览 + +点击模型卡片的「预览」按钮,打开模型预览弹窗,展示模型预览内容,支持拖拽旋转、滚轮缩放操作;点击「关闭」、弹窗关闭按钮或点击弹窗空白区域可关闭预览弹窗。 + +4. 删除模型 + +点击模型卡片的删除按钮,弹出删除确认弹窗,提示「确定要删除该模型吗?删除后无法恢复」;点击「确认删除」完成模型删除并从列表移除,点击「取消」或点击弹窗空白区域取消删除操作。 + + +三、想定管理模块功能需求 + +1. 页面入口 + +点击顶部导航栏「想定管理」,进入仿真任务管理页面,展示已创建的仿真任务列表。 + +2. 任务列表功能 + +1.筛选搜索:支持输入任务名称关键词、选择时间范围,点击「搜索」展示匹配结果;点击「筛选」完成条件设置。 +2.分页浏览:支持点击页码按钮、前后翻页按钮切换分页,浏览不同页的任务。 +3.列表展示内容:任务名称、任务编号、创建时间、任务状态(已完成/草稿/配置中),每个任务提供「继续配置」操作入口。 +4.继续配置:点击任务右侧「继续配置」按钮,进入该任务的步骤化配置页面,保留已配置的内容。 + +3. 新建仿真任务 + +1.点击页面「新增仿真任务」按钮,打开新建仿真任务弹窗 +2.系统根据当前日期自动生成任务编号(格式:SIM-yyyyMMdd-xxx),支持用户手动修改 +3.任务名称为必填项,为空无法创建,提示用户输入 +4.操作: + +•点击「确认创建」,关闭弹窗后进入任务步骤化配置页面,从第一步开始配置 +•点击「取消」、弹窗关闭按钮或点击弹窗空白区域,关闭新建弹窗 + +4. 步骤化配置仿真任务 + +配置分为5个步骤,页面顶部展示步骤进度条,实时展示当前配置进度和已完成步骤;配置过程中返回任务列表时,已配置内容自动保存为草稿。 + +步骤1:作战场景设置 + +•可配置项: +•场景选择:支持城市作战场景、平原野外场景、山区地形场景、海岸边防场景 +•天气类型:支持晴天、阴天、雾天、雨天、夜间 +•风速:滑动条调整,范围0-30m/s,实时展示当前数值,默认值5m/s +•风向:支持8个方向选择 +•温度:输入数值,默认值20℃ +•湿度:输入数值,默认值60%RH +•气压:输入数值,默认值1013hPa +•能见度:滑动条调整,范围100-10000m,步长100m,实时展示当前数值,默认值5000m +•操作: +•点击「返回列表」:确认后返回任务列表,已配置内容自动保存为草稿 +•点击「下一步」:进入步骤2目标配置 + +步骤2:目标配置 + +•可配置项: +•目标类型:选择目标类型后自动填充对应翼展、典型速度、典型飞行高度默认参数,支持手动修改:| 目标类型 | 翼展(m) | 典型速度(km/h) | 典型飞行高度(m) | +| -------- | ------- | -------------- | --------------- | +| 旋翼 | 1.2 | 60 | 300 | +| 固定翼 | 3.5 | 150 | 1000 | +| 电推 | 1.8 | 100 | 500 | +| 活塞 | 2.5 | 120 | 800 | +| 高速目标 | 1.5 | 300 | 2000 | +•目标数量:输入数值,默认值1 +•动力类型:支持电推式、活塞式、喷吸气式选择 +•翼展:可手动修改 +•典型速度:可手动修改 +•典型飞行高度:可手动修改 +•操作: +•点击「上一步」:返回步骤1作战场景设置 +•点击「保存草稿」:保存当前配置,提示保存成功 +•点击「下一步」:进入步骤3装备部署 + +步骤3:装备部署 + +•可配置项: +•部署对象类型:支持防御装备、布撒设备、探测设备选择 +•部署数量:输入数值,默认值3 +•选择编组:支持选择已有编组,也可点击新建编组创建新编组 +•操作: +•点击「上一步」:返回步骤2目标配置 +•点击「保存草稿」:保存当前配置,提示保存成功 +•点击「下一步」:进入步骤4云团抛撒配置 + +步骤4:云团抛撒配置 + +•可配置项: +•气溶胶类型:支持惰性气体、活性材料、活性燃料选择 +•抛撒高度:输入数值,默认值300m +•触发模式:支持时间触发、区域触发、手动触发选择 +•扩散方向:输入角度数值,默认值0度 +•释放方式:支持单次释放、连续释放、脉冲释放选择 +•初始规模:输入数值,默认值1000m³ +•持续时间:输入数值,默认值60s +•释放间隔:输入数值,默认值5s +•释放总量:输入数值,默认值50kg +•抛撒位置定位:支持三维点选、手动输入两种方式 +•操作: +•点击「上一步」:返回步骤3装备部署 +•点击「保存草稿」:保存当前配置,提示保存成功 +•点击「下一步」:进入步骤5航路规划 + +步骤5:航路规划 + +•可配置项: +•航路规划画布:支持添加/调整航路点,支持实时航迹预览,可对已有航路点调整顺序、删除操作,支持放大缩小画布 +•编队模式:支持单机、编队、蜂群选择 +•编队间距:输入数值,默认值50m +•预计到达时间:选择时间 +•航路点列表:展示所有已添加航路点的基础信息,支持调整顺序、删除单个航路点 +•操作: +•点击「上一步」:返回步骤4云团抛撒配置 +•点击「仅保存」:保存当前任务配置,提示保存成功 +•点击「启动仿真」:保存所有配置后,启动仿真流程 +•通用操作:配置过程中任意步骤点击页面顶部「返回任务列表」,确认后返回任务列表,已配置内容自动保存为草稿。 + + + +四、仿真报告模块功能需求 + +1. 页面入口 + +点击顶部导航栏「仿真报告」,进入仿真报告管理页面,展示所有已完成仿真的报告列表。 + +2. 筛选搜索 + +支持输入任务名称关键词、选择开始时间、结束时间范围;点击「重置」清空所有筛选条件,点击「搜索」展示匹配的报告结果。 + +3. 分页浏览 + +支持点击页码按钮、前后翻页按钮切换分页,浏览不同页的报告。 + +4. 报告列表展示 + +展示内容:任务名称、任务编号、完成时间、对抗结果(成功拦截/部分拦截/拦截失败),提供预览、导出、删除三个操作入口。 + +5. 预览报告 + +点击对应报告的「预览」按钮,打开报告详情页面查看完整报告内容。 + +6. 导出报告 + +1.点击对应报告的「导出」按钮,打开导出报告弹窗 +2.支持选择导出格式:PDF、Word,默认选中PDF +3.点击「开始导出」触发报告导出下载,点击「取消」、关闭按钮或点击弹窗空白区域关闭导出弹窗 + +7. 删除报告 + +点击对应报告的删除按钮,弹出删除确认弹窗,提示「确定要删除该仿真报告吗?删除后无法恢复」;点击「确认删除」完成报告删除并从列表移除,点击「取消」或点击弹窗空白区域取消删除操作。 + + +五、业务字段总结 + +1. 模型业务字段 + +字段名 类型 说明 +模型ID 字符串 模型唯一标识 +模型名称 字符串 模型展示名称 +模型类型 字符串 模型分类:旋翼/固定翼/探测设备/防御装备等 +模型文件 文件 上传的模型源文件 +文件大小 数值 模型文件大小,单位MB +模型预览图 图片 模型预览缩略图 +导入时间 日期 模型导入系统的时间 + +2. 仿真任务业务字段 + +字段名 类型 说明 +任务ID 字符串 任务唯一标识 +任务名称 字符串 用户输入的任务名称 +任务编号 字符串 自动生成,支持修改,格式 SIM-yyyyMMdd-xxx +任务状态 枚举 草稿/配置中/已完成 +创建时间 日期 任务创建时间 +更新时间 日期 任务最后修改时间 + +3. 作战场景配置字段 + +字段名 类型 说明 +场景类型 枚举 城市/平原/山区/海岸 +天气类型 枚举 晴天/阴天/雾天/雨天/夜间 +风速 数值 单位m/s,范围0-30 +风向 枚举 八个方位 +温度 数值 单位℃ +湿度 数值 单位%RH +气压 数值 单位hPa +能见度 数值 单位m,范围100-10000 + +4. 目标配置字段 + +字段名 类型 说明 +目标类型 枚举 旋翼/固定翼/电推/活塞/高速目标 +目标数量 数值 目标总数量 +动力类型 枚举 电推式/活塞式/喷吸气式 +翼展 数值 单位m +典型速度 数值 单位km/h +典型飞行高度 数值 单位m + +5. 装备部署配置字段 + +字段名 类型 说明 +部署对象类型 枚举 防御装备/布撒设备/探测设备 +部署数量 数值 部署的装备数量 +所属编组ID 字符串 关联编组唯一标识 + +6. 云团抛撒配置字段 + +字段名 类型 说明 +气溶胶类型 枚举 惰性气体/活性材料/活性燃料 +抛撒高度 数值 单位m +触发模式 枚举 时间触发/区域触发/手动触发 +扩散方向 数值 单位度 +释放方式 枚举 单次释放/连续释放/脉冲释放 +初始规模 数值 单位m³ +持续时间 数值 单位s +释放间隔 数值 单位s +释放总量 数值 单位kg +抛撒位置 坐标 抛撒点经纬度/三维坐标 +定位方式 枚举 三维点选/手动输入 + +7. 航路规划配置字段 + +字段名 类型 说明 +编队模式 枚举 单机/编队/蜂群 +编队间距 数值 单位m +预计到达时间 时间 目标点预计到达时间 +航路点列表 数组 每个航路点包含坐标、高度、速度信息 + +8. 仿真报告业务字段 + +字段名 类型 说明 +报告ID 字符串 报告唯一标识 +关联任务ID 字符串 关联对应仿真任务 +任务名称 字符串 对应仿真任务名称 +任务编号 字符串 对应仿真任务编号 +完成时间 时间 仿真完成生成报告的时间 +目标数量 数值 仿真配置的目标总数量 +装备数量 数值 仿真配置的装备总数量 +对抗结果 枚举 成功拦截/部分拦截/拦截失败 +报告内容 富文本/结构化数据 仿真报告完整内容 diff --git a/docs/requirements/changes/index.md b/docs/requirements/changes/index.md new file mode 100644 index 0000000..72e20e2 --- /dev/null +++ b/docs/requirements/changes/index.md @@ -0,0 +1,11 @@ +# docs/requirements/changes — 需求变更记录 + +每条需求变更一个文件,便于追溯和评审。 + +## 变更记录文件命名 + +``` +YYYY-MM-DD-<简短描述>.md +``` + +示例:`2025-01-15-登录支持短信验证码.md` diff --git a/docs/requirements/index.md b/docs/requirements/index.md new file mode 100644 index 0000000..47f5291 --- /dev/null +++ b/docs/requirements/index.md @@ -0,0 +1,55 @@ +# docs/requirements — 需求与变更管理 + +> **可选**:如果团队已使用 Jira/TAPD/飞书等工单系统,可直接删除此目录,避免双写。 + +本目录用于管理项目需求及其变更记录。 + +## 目录结构 + +``` +requirements/ +├── index.md # 本文件 +└── changes/ # 需求变更记录(每条变更一个文件) +``` + +## 需求变更记录 + +当需求发生变更时,请在 `changes/` 下创建一条记录。 + +### 文件命名规范 + +``` +YYYY-MM-DD-<简短描述>.md +``` + +### 变更记录模板 + +```markdown +# [变更标题] + +- **日期**:YYYY-MM-DD +- **提出人**:[姓名] +- **关联需求**:[原始需求编号或链接] +- **优先级**:高 / 中 / 低 + +## 变更描述 + +> 说明变更内容、变更原因。 + +## 影响范围 + +- [ ] 接口变更 +- [ ] 数据库变更 +- [ ] UI 变更 +- [ ] 文档变更 + +## 审批 + +- [ ] 产品确认 +- [ ] 技术评估 +- [ ] 测试评估 +``` + +--- + +> 每条变更记录对应一个 `.md` 文件,便于版本控制和追溯。 diff --git a/docs/requirements/技术要求终版.txt b/docs/requirements/技术要求终版.txt new file mode 100644 index 0000000..5d8b9cb --- /dev/null +++ b/docs/requirements/技术要求终版.txt @@ -0,0 +1,45 @@ +本文件“新型惰性/活性材料的防空毁伤仿真”系统的相关技术要求 +2.技术要求 +2.1总体要求 +   本系统对新型惰性/活性材料的三种防空毁伤机制[三种威胁源目标(三类无人机): + +吸入式灭火、爆燃式、吸入式爆炸 +哪种型号 +]和效果进行数字化仿真。通过参数化配置与可视化推演,量化评估新型气溶胶武器[我方装备配置: +搭载装备:大型无人机(空基)、炮弹(地基)]对不同类型空中威胁源目标(动力系统)的毁伤效能。 +2.2功能要求 +   1.支持仿真过程前置步骤设置,涵盖装备部署、航迹标绘、管控区域设置[装备部署:我方装备配置 +航迹标绘:威胁源航迹标绘 +管控区域设置:标绘电子围栏,不允许威胁源进入] +   2.支持对于布撒环境、布撒设备、布撒无人机参数设置和动态调整 +   3.系统能够实现云团扩撒效果[三维效果:有数值]、无人机穿云效果、毁伤效果。 +   4.支持仿真过程回放,效果数据分析[仿真评估报告数据分析:, + +1,时序图; +2,仿真前:我方配置情况、 +3,仿真中: +4,仿真后: +] +   5.支持仿真评估报告生成和输出 +   6.支持预定作战场景仿真 +2.3仿真可视化要求 +2.3.1.模型管理 +   (1)支持空中威胁源目标的三维模型的导入和删除,支持FBX、OBJ、GLB、GLTF等格式,支持2K及以上分辨率贴图的三维模型。 +   (2)对三维模型进行预览及详细参数查看,支持模型放大缩小旋转等功能,模型参数支持标签化展示。 +2.3.2仿真配置 +   (1)抛撒配置[我方装备部署: + +类型、数量、挂载气溶胶的类型、数量(一发炮弹打出的云团大概覆盖体积) + +根据算法自动推荐晕图案的抛洒位置、抛洒时机, +最佳值和最危险值 +]:支持自定义云团的抛撒位置、抛撒时机、风速风向参数。 +   (2)目标配置:预置典型目标库,包括多种无人机。 +   (3)航路规划[航迹标绘,以三维形式进行点选 +]:支持设置目标的航路点、飞行速度、高度及航迹。 +   (4)气象条件设置:系统支持预设气象条件,支持24小时制仿真时间段,实现对应可视效果。 +2.4性能要求 +   (1)最大目标数量:针对无人机蜂群场景,单次仿真支持不少于5个独立运动实体。 +   (2)渲染效果:基于粒子系统实现逼真渲染云团扩散、流动、色彩;动态展示目标的毁伤状态,包括尾焰异常、机体姿态失控等物理效果。 +   (3)空间范围:仿真场景有效空域最低要求:4*100*10(。 +   (4)视景帧率:满载运行(含粒子特效与多目标)帧率 ≥ 30FPS。 \ No newline at end of file diff --git a/docs/verify/checklist/index.md b/docs/verify/checklist/index.md new file mode 100644 index 0000000..7813faa --- /dev/null +++ b/docs/verify/checklist/index.md @@ -0,0 +1,36 @@ +# verify/checklist — 检查清单 + +本目录存放各环节的检查清单。 + +## 清单模板 + +以**上线检查清单**为例: + +```markdown +# 上线检查清单 + +## 代码质量 +- [ ] 代码审查已完成 +- [ ] 所有测试通过 +- [ ] 无已知 P0/P1 Bug +- [ ] 代码已合并到主分支 + +## 性能与安全 +- [ ] 性能测试通过 +- [ ] 安全扫描通过 +- [ ] 敏感信息已清理 + +## 运维准备 +- [ ] 数据库变更脚本就绪 +- [ ] 回滚方案已准备 +- [ ] 监控和告警已配置 + +## 文档 +- [ ] CHANGELOG 已更新 +- [ ] 版本号已更新 +- [ ] API 文档已更新(如有) +``` + +--- + +> 根据项目实际情况增删条目。 diff --git a/docs/verify/index.md b/docs/verify/index.md new file mode 100644 index 0000000..7812299 --- /dev/null +++ b/docs/verify/index.md @@ -0,0 +1,20 @@ +# verify — 验证与质量保障 + +本目录存放人工检查清单和验证流程文档。 + +> 已从顶层目录迁移至 `docs/verify/`,以下为历史兼容说明。**新项目请直接使用 `docs/verify/`**。 + +## 目录结构 + +``` +verify/ +├── index.md # 本文件 +└── checklist/ # 检查清单 +``` + +## 用途 + +- 上线前检查清单 +- 发布验收清单 +- 代码审查清单 +- 安全审计清单 diff --git a/scripts/index.md b/scripts/index.md new file mode 100644 index 0000000..79a82c2 --- /dev/null +++ b/scripts/index.md @@ -0,0 +1,9 @@ +# scripts — 工具脚本 + +本目录存放项目的辅助脚本:构建、部署、数据迁移、环境初始化等。 + +## 原则 + +- 每个脚本做好注释,说明用途和运行方式 +- 脚本应尽量幂等(多次运行结果一致) +- 避免将敏感信息(密码、密钥)硬编码在脚本中 diff --git a/skills/index.md b/skills/index.md new file mode 100644 index 0000000..2adb49a --- /dev/null +++ b/skills/index.md @@ -0,0 +1,13 @@ +# skills — AI 辅助技能 + +本目录存放 AI 编码助手的辅助技能定义(遵循 [Agent Skills 标准](https://agentskills.io))。 + +> **注意**:本目录仅适用于使用 pi 编码助手或兼容 Agent Skills 标准的工具。[其他工具可删除此目录。] + +## 使用方式 + +在对话中通过 `/skill:技能名` 调用,或由 AI 自动加载。 + +## 现有技能 + +当前尚无自定义技能。 diff --git a/src/CounterDrone.Core/CounterDrone.Core.csproj b/src/CounterDrone.Core/CounterDrone.Core.csproj new file mode 100644 index 0000000..4cca765 --- /dev/null +++ b/src/CounterDrone.Core/CounterDrone.Core.csproj @@ -0,0 +1,14 @@ + + + + netstandard2.1 + 9.0 + enable + + + + + + + + diff --git a/src/CounterDrone.Core/DatabaseManager.cs b/src/CounterDrone.Core/DatabaseManager.cs new file mode 100644 index 0000000..e9bc076 --- /dev/null +++ b/src/CounterDrone.Core/DatabaseManager.cs @@ -0,0 +1,69 @@ +using System.IO; +using CounterDrone.Core.Models; +using SQLite; + +namespace CounterDrone.Core +{ + /// 数据库管理器 — 建表 / 连接管理 + public class DatabaseManager + { + private readonly IPathProvider _paths; + + public DatabaseManager(IPathProvider paths) + { + _paths = paths; + } + + /// 初始化主数据库(建表) + public SQLiteConnection OpenMainDb() + { + var dbPath = _paths.GetMainDbPath(); + var db = new SQLiteConnection(dbPath); + CreateMainTables(db); + return db; + } + + /// 创建/打开任务帧数据库 + public SQLiteConnection OpenFrameDb(string taskId) + { + var dbPath = Path.Combine(_paths.GetFramesDir(), $"{taskId}.db"); + var db = new SQLiteConnection(dbPath); + db.CreateTable(); + db.CreateIndex("SimFrameRecord", new[] { "TaskId", "FrameIndex" }); + return db; + } + + /// 删除任务帧数据库文件 + public void DeleteFrameDb(string taskId) + { + var dbPath = Path.Combine(_paths.GetFramesDir(), $"{taskId}.db"); + if (File.Exists(dbPath)) + File.Delete(dbPath); + } + + private void CreateMainTables(SQLiteConnection db) + { + db.CreateTable(); + db.CreateTable(); + db.CreateTable(); + db.CreateTable(); + db.CreateTable(); + db.CreateTable(); + db.CreateTable(); + db.CreateTable(); + db.CreateTable(); + db.CreateTable(); + db.CreateTable(); + db.CreateTable(); + db.CreateTable(); + + db.CreateIndex("SimTask", "TaskNumber"); + db.CreateIndex("ControlZone", "TaskId"); + db.CreateIndex("TargetConfig", "TaskId"); + db.CreateIndex("EquipmentDeployment", "TaskId"); + db.CreateIndex("Waypoint", "TaskId"); + db.CreateIndex("SimEvent", "TaskId"); + db.CreateIndex("SimulationReport", "TaskId"); + } + } +} diff --git a/src/CounterDrone.Core/IPathProvider.cs b/src/CounterDrone.Core/IPathProvider.cs new file mode 100644 index 0000000..3186e4f --- /dev/null +++ b/src/CounterDrone.Core/IPathProvider.cs @@ -0,0 +1,11 @@ +namespace CounterDrone.Core +{ + /// 路径提供者接口 — 隔离文件系统依赖 + public interface IPathProvider + { + string GetDataRoot(); + string GetMainDbPath(); + string GetFramesDir(); + string GetModelsDir(); + } +} diff --git a/src/CounterDrone.Core/Models/AmmunitionSpec.cs b/src/CounterDrone.Core/Models/AmmunitionSpec.cs new file mode 100644 index 0000000..fcdee8e --- /dev/null +++ b/src/CounterDrone.Core/Models/AmmunitionSpec.cs @@ -0,0 +1,42 @@ +using System; +using SQLite; + +namespace CounterDrone.Core.Models +{ + /// 弹药基础参数(第三方数据) + [Table("AmmunitionSpec")] + public class AmmunitionSpec + { + [PrimaryKey] + public string Id { get; set; } = Guid.NewGuid().ToString(); + + public int AerosolType { get; set; } + + public string Name { get; set; } = string.Empty; + + // 初始扩散状态 + public double InitialRadius { get; set; } + + public double InitialVolume { get; set; } + + public double CoreDensity { get; set; } + + public double EdgeDensity { get; set; } + + public double InitialTemperature { get; set; } + + public double BuoyancyFactor { get; set; } + + // 弹药基础参数 + public double EffectiveConcentration { get; set; } + + public double DispersionRateBase { get; set; } + + public double MaxRadius { get; set; } + + public double MaxDuration { get; set; } + + /// 粒子渲染参数 JSON + public string ParticlesJson { get; set; } = "{}"; + } +} diff --git a/src/CounterDrone.Core/Models/CloudDispersal.cs b/src/CounterDrone.Core/Models/CloudDispersal.cs new file mode 100644 index 0000000..cf3ccc7 --- /dev/null +++ b/src/CounterDrone.Core/Models/CloudDispersal.cs @@ -0,0 +1,49 @@ +using SQLite; + +namespace CounterDrone.Core.Models +{ + /// 步骤4:云团抛撒配置 + [Table("CloudDispersal")] + public class CloudDispersal + { + [PrimaryKey] + public string TaskId { get; set; } = string.Empty; + + public int AerosolType { get; set; } = (int)Models.AerosolType.InertGas; + + public double DisperseHeight { get; set; } = 300.0; + + public int TriggerMode { get; set; } = (int)Models.TriggerMode.Time; + + public double SpreadAngle { get; set; } = 0.0; + + public int ReleaseMode { get; set; } = (int)Models.ReleaseMode.Single; + + public double InitialScale { get; set; } = 1000.0; + + public double Duration { get; set; } = 60.0; + + public double ReleaseInterval { get; set; } = 5.0; + + public double TotalAmount { get; set; } = 50.0; + + public double PositionX { get; set; } + public double PositionY { get; set; } + public double PositionZ { get; set; } + + public int PositionMode { get; set; } = (int)Models.PositionMode.ManualInput; + + public string Source { get; set; } = "Manual"; + + // 算法推荐结果 + public double? RecommendedPosX { get; set; } + public double? RecommendedPosY { get; set; } + public double? RecommendedPosZ { get; set; } + public double? RecommendedTiming { get; set; } + + public double? CriticalPosX { get; set; } + public double? CriticalPosY { get; set; } + public double? CriticalPosZ { get; set; } + public double? CriticalTiming { get; set; } + } +} diff --git a/src/CounterDrone.Core/Models/CombatScene.cs b/src/CounterDrone.Core/Models/CombatScene.cs new file mode 100644 index 0000000..dace670 --- /dev/null +++ b/src/CounterDrone.Core/Models/CombatScene.cs @@ -0,0 +1,36 @@ +using SQLite; + +namespace CounterDrone.Core.Models +{ + /// 步骤1:作战场景 + [Table("CombatScene")] + public class CombatScene + { + [PrimaryKey] + public string TaskId { get; set; } = string.Empty; + + public int SceneType { get; set; } = (int)Models.SceneType.Plain; + + public string TimeOfDay { get; set; } = "12:00"; + + public int WeatherType { get; set; } = (int)Models.WeatherType.Sunny; + + public double WindSpeed { get; set; } = 5.0; + + public int WindDirection { get; set; } = (int)Models.WindDirection.N; + + public double Temperature { get; set; } = 20.0; + + public double Humidity { get; set; } = 60.0; + + public double Pressure { get; set; } = 1013.0; + + public double Visibility { get; set; } = 5000.0; + + public double SceneWidth { get; set; } = 4000.0; + + public double SceneLength { get; set; } = 10000.0; + + public double SceneHeight { get; set; } = 1000.0; + } +} diff --git a/src/CounterDrone.Core/Models/ControlZone.cs b/src/CounterDrone.Core/Models/ControlZone.cs new file mode 100644 index 0000000..7e8dccc --- /dev/null +++ b/src/CounterDrone.Core/Models/ControlZone.cs @@ -0,0 +1,28 @@ +using System; +using SQLite; + +namespace CounterDrone.Core.Models +{ + /// 管控区域 / 电子围栏 + [Table("ControlZone")] + public class ControlZone + { + [PrimaryKey] + public string Id { get; set; } = Guid.NewGuid().ToString(); + + [Indexed] + public string TaskId { get; set; } = string.Empty; + + public string Name { get; set; } = string.Empty; + + /// 多边形顶点 JSON:[{"x":0,"y":0,"z":0},...] + [NotNull] + public string VerticesJson { get; set; } = "[]"; + + public double MinAltitude { get; set; } + + public double MaxAltitude { get; set; } + + public int OrderIndex { get; set; } + } +} diff --git a/src/CounterDrone.Core/Models/Enums.cs b/src/CounterDrone.Core/Models/Enums.cs new file mode 100644 index 0000000..ba37077 --- /dev/null +++ b/src/CounterDrone.Core/Models/Enums.cs @@ -0,0 +1,152 @@ +namespace CounterDrone.Core.Models +{ + // === 任务 === + public enum TaskStatus + { + Draft = 0, + Configuring = 1, + Completed = 2 + } + + // === 场景 === + public enum SceneType + { + Urban = 0, + Plain = 1, + Mountain = 2, + Coast = 3 + } + + public enum WeatherType + { + Sunny = 0, + Overcast = 1, + Fog = 2, + Rain = 3, + Night = 4 + } + + public enum WindDirection + { + N = 0, NE = 1, E = 2, SE = 3, + S = 4, SW = 5, W = 6, NW = 7 + } + + // === 目标 === + public enum TargetType + { + Rotor = 0, + FixedWing = 1, + Electric = 2, + Piston = 3, + HighSpeed = 4 + } + + public enum PowerType + { + Electric = 0, + Piston = 1, + Jet = 2 + } + + // === 装备 & 搭载平台 === + public enum PlatformType + { + AirBased = 0, + GroundBased = 1 + } + + public enum EquipmentRole + { + Detection = 0, + LaunchPlatform = 1 + } + + // === 气溶胶 & 毁伤 === + public enum AerosolType + { + InertGas = 0, + ActiveMaterial = 1, + ActiveFuel = 2 + } + + public enum ReleaseMode + { + Single = 0, + Continuous = 1, + Pulse = 2 + } + + public enum TriggerMode + { + Time = 0, + Area = 1, + Manual = 2 + } + + public enum DamageStage + { + Normal = 0, + EngineAnomaly = 1, + AttitudeLoss = 2, + Destroyed = 3 + } + + // === 编队 & 编组 === + public enum FormationMode + { + Single = 0, + Formation = 1, + Swarm = 2 + } + + public enum GroupType + { + DroneFleet = 0, + EquipmentGroup = 1 + } + + // === 运行时 === + public enum EntityType + { + Drone = 0, + Platform = 1, + DetectionEquip = 2, + Cloud = 3, + Munition = 4 + } + + public enum SimEventType + { + TargetDetected = 0, + MunitionLaunched = 1, + CloudGenerated = 2, + DroneEnteredCloud = 3, + DroneDestroyed = 4, + DroneReachedTarget = 5, + ZoneIntruded = 6, + WaypointReached = 7, + SimulationEnd = 8 + } + + // === 报告 === + public enum InterceptResult + { + Success = 0, + Partial = 1, + Failed = 2 + } + + public enum PositionMode + { + PointSelect = 0, + ManualInput = 1, + AlgorithmRecommended = 2 + } + + public enum ConfigSource + { + Manual = 0, + Algorithm = 1 + } +} diff --git a/src/CounterDrone.Core/Models/EquipmentDeployment.cs b/src/CounterDrone.Core/Models/EquipmentDeployment.cs new file mode 100644 index 0000000..5046262 --- /dev/null +++ b/src/CounterDrone.Core/Models/EquipmentDeployment.cs @@ -0,0 +1,44 @@ +using System; +using SQLite; + +namespace CounterDrone.Core.Models +{ + /// 步骤3:装备部署 + [Table("EquipmentDeployment")] + public class EquipmentDeployment + { + [PrimaryKey] + public string Id { get; set; } = Guid.NewGuid().ToString(); + + [Indexed] + public string TaskId { get; set; } = string.Empty; + + public int EquipmentRole { get; set; } = (int)Models.EquipmentRole.LaunchPlatform; + + public int Quantity { get; set; } = 1; + + public string GroupId { get; set; } = string.Empty; + + // 发射平台专用 + public int? PlatformType { get; set; } + + public double PositionX { get; set; } + public double PositionY { get; set; } + public double PositionZ { get; set; } + + public int? AerosolType { get; set; } + + public int? MunitionCount { get; set; } + + public int Source { get; set; } = (int)ConfigSource.Manual; + + public double? MuzzleVelocity { get; set; } + + public double? ReleaseAltitude { get; set; } + + public double Cooldown { get; set; } = 5.0; + + // 探测设备专用 + public double? DetectionRadius { get; set; } + } +} diff --git a/src/CounterDrone.Core/Models/Group.cs b/src/CounterDrone.Core/Models/Group.cs new file mode 100644 index 0000000..15155d7 --- /dev/null +++ b/src/CounterDrone.Core/Models/Group.cs @@ -0,0 +1,22 @@ +using System; +using SQLite; + +namespace CounterDrone.Core.Models +{ + /// 编组 + [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"); + } +} diff --git a/src/CounterDrone.Core/Models/ModelInfo.cs b/src/CounterDrone.Core/Models/ModelInfo.cs new file mode 100644 index 0000000..d02e225 --- /dev/null +++ b/src/CounterDrone.Core/Models/ModelInfo.cs @@ -0,0 +1,28 @@ +using System; +using SQLite; + +namespace CounterDrone.Core.Models +{ + /// 模型元数据 + [Table("ModelInfo")] + public class ModelInfo + { + [PrimaryKey] + public string Id { get; set; } = Guid.NewGuid().ToString(); + + [NotNull] + public string Name { get; set; } = string.Empty; + + public string ModelType { get; set; } = string.Empty; + + public string FilePath { get; set; } = string.Empty; + + public double FileSize { get; set; } + + public string PreviewPath { get; set; } = string.Empty; + + public bool HasHighResTexture { get; set; } + + public string CreatedAt { get; set; } = DateTime.UtcNow.ToString("o"); + } +} diff --git a/src/CounterDrone.Core/Models/RoutePlan.cs b/src/CounterDrone.Core/Models/RoutePlan.cs new file mode 100644 index 0000000..cf04618 --- /dev/null +++ b/src/CounterDrone.Core/Models/RoutePlan.cs @@ -0,0 +1,20 @@ +using SQLite; + +namespace CounterDrone.Core.Models +{ + /// 步骤5:航路规划 + [Table("RoutePlan")] + public class RoutePlan + { + [PrimaryKey] + public string TaskId { get; set; } = string.Empty; + + public string GroupId { get; set; } = string.Empty; + + public int FormationMode { get; set; } = (int)Models.FormationMode.Single; + + public double FormationSpacing { get; set; } = 50.0; + + public string ETA { get; set; } = string.Empty; + } +} diff --git a/src/CounterDrone.Core/Models/SimEvent.cs b/src/CounterDrone.Core/Models/SimEvent.cs new file mode 100644 index 0000000..1ce81ea --- /dev/null +++ b/src/CounterDrone.Core/Models/SimEvent.cs @@ -0,0 +1,28 @@ +using System; +using SQLite; + +namespace CounterDrone.Core.Models +{ + /// 仿真事件(时序图数据源)— 存主库 + [Table("SimEvent")] + public class SimEvent + { + [PrimaryKey] + public string Id { get; set; } = Guid.NewGuid().ToString(); + + [Indexed] + public string TaskId { get; set; } = string.Empty; + + public double OccurredAt { get; set; } + + public int EventType { get; set; } + + public string SourceId { get; set; } = string.Empty; + + public string TargetId { get; set; } = string.Empty; + + public string DataJson { get; set; } = "{}"; + + public string Description { get; set; } = string.Empty; + } +} diff --git a/src/CounterDrone.Core/Models/SimFrameRecord.cs b/src/CounterDrone.Core/Models/SimFrameRecord.cs new file mode 100644 index 0000000..6a3998e --- /dev/null +++ b/src/CounterDrone.Core/Models/SimFrameRecord.cs @@ -0,0 +1,34 @@ +using SQLite; + +namespace CounterDrone.Core.Models +{ + /// 逐帧位置记录(回放用)— 存任务分库 + [Table("SimFrameRecord")] + public class SimFrameRecord + { + [PrimaryKey, AutoIncrement] + public int Id { get; set; } + + [Indexed] + public string TaskId { get; set; } = string.Empty; + + public int FrameIndex { get; set; } + + public double Timestamp { get; set; } + + public string EntityId { get; set; } = string.Empty; + + public int EntityType { get; set; } + + public double PosX { get; set; } + public double PosY { get; set; } + public double PosZ { get; set; } + + public double RotX { get; set; } + public double RotY { get; set; } + public double RotZ { get; set; } + + /// JSON:damageStage, cloudRadius, cloudOpacity 等 + public string StateData { get; set; } = "{}"; + } +} diff --git a/src/CounterDrone.Core/Models/SimTask.cs b/src/CounterDrone.Core/Models/SimTask.cs new file mode 100644 index 0000000..f632828 --- /dev/null +++ b/src/CounterDrone.Core/Models/SimTask.cs @@ -0,0 +1,29 @@ +using System; +using SQLite; + +namespace CounterDrone.Core.Models +{ + /// 仿真任务主表 + [Table("SimTask")] + public class SimTask + { + [PrimaryKey] + public string Id { get; set; } = Guid.NewGuid().ToString(); + + [NotNull] + public string Name { get; set; } = string.Empty; + + [Unique] + public string TaskNumber { get; set; } = string.Empty; + + public int Status { get; set; } = (int)TaskStatus.Draft; + + public int CurrentStep { get; set; } = 1; + + public int TickRate { get; set; } = 20; + + public string CreatedAt { get; set; } = DateTime.UtcNow.ToString("o"); + + public string UpdatedAt { get; set; } = DateTime.UtcNow.ToString("o"); + } +} diff --git a/src/CounterDrone.Core/Models/SimulationReport.cs b/src/CounterDrone.Core/Models/SimulationReport.cs new file mode 100644 index 0000000..c2c88f4 --- /dev/null +++ b/src/CounterDrone.Core/Models/SimulationReport.cs @@ -0,0 +1,31 @@ +using System; +using SQLite; + +namespace CounterDrone.Core.Models +{ + /// 仿真报告 + [Table("SimulationReport")] + public class SimulationReport + { + [PrimaryKey] + public string Id { get; set; } = Guid.NewGuid().ToString(); + + [Indexed] + public string TaskId { get; set; } = string.Empty; + + public string TaskName { get; set; } = string.Empty; + + public string TaskNumber { get; set; } = string.Empty; + + public string CompletedAt { get; set; } = string.Empty; + + public int TargetCount { get; set; } + + public int EquipmentCount { get; set; } + + public int InterceptResult { get; set; } + + /// 报告富文本 / 结构化 JSON + public string Content { get; set; } = string.Empty; + } +} diff --git a/src/CounterDrone.Core/Models/TargetConfig.cs b/src/CounterDrone.Core/Models/TargetConfig.cs new file mode 100644 index 0000000..603d9e4 --- /dev/null +++ b/src/CounterDrone.Core/Models/TargetConfig.cs @@ -0,0 +1,30 @@ +using System; +using SQLite; + +namespace CounterDrone.Core.Models +{ + /// 步骤2:目标配置 + [Table("TargetConfig")] + public class TargetConfig + { + [PrimaryKey] + public string Id { get; set; } = Guid.NewGuid().ToString(); + + [Indexed] + public string TaskId { get; set; } = string.Empty; + + public string GroupId { get; set; } = string.Empty; + + public int TargetType { get; set; } = (int)Models.TargetType.Rotor; + + public int Quantity { get; set; } = 1; + + public int PowerType { get; set; } = (int)Models.PowerType.Electric; + + public double Wingspan { get; set; } = 1.2; + + public double TypicalSpeed { get; set; } = 60.0; + + public double TypicalAltitude { get; set; } = 300.0; + } +} diff --git a/src/CounterDrone.Core/Models/TaskFullConfig.cs b/src/CounterDrone.Core/Models/TaskFullConfig.cs new file mode 100644 index 0000000..7b7d9ec --- /dev/null +++ b/src/CounterDrone.Core/Models/TaskFullConfig.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace CounterDrone.Core.Models +{ + /// 想定完整配置(聚合 5 步配置) + public class TaskFullConfig + { + public SimTask Task { get; set; } = new(); + public CombatScene Scene { get; set; } = new(); + public List ControlZones { get; set; } = new(); + public List Targets { get; set; } = new(); + public List Equipment { get; set; } = new(); + public CloudDispersal Cloud { get; set; } = new(); + public RoutePlan Route { get; set; } = new(); + public List Waypoints { get; set; } = new(); + } + + /// 分页结果 + public class PagedResult + { + public List Items { get; set; } = new(); + public int TotalCount { get; set; } + public int Page { get; set; } + public int PageSize { get; set; } + public int TotalPages => (TotalCount + PageSize - 1) / PageSize; + } +} diff --git a/src/CounterDrone.Core/Models/Waypoint.cs b/src/CounterDrone.Core/Models/Waypoint.cs new file mode 100644 index 0000000..bcef32d --- /dev/null +++ b/src/CounterDrone.Core/Models/Waypoint.cs @@ -0,0 +1,27 @@ +using System; +using SQLite; + +namespace CounterDrone.Core.Models +{ + /// 航路点 + [Table("Waypoint")] + public class Waypoint + { + [PrimaryKey] + public string Id { get; set; } = Guid.NewGuid().ToString(); + + [Indexed] + public string TaskId { get; set; } = string.Empty; + + [NotNull] + public int OrderIndex { get; set; } + + public double PosX { get; set; } + public double PosY { get; set; } + public double PosZ { get; set; } + + public double Altitude { get; set; } + + public double Speed { get; set; } + } +} diff --git a/src/CounterDrone.Core/Repository/AmmunitionSpecRepository.cs b/src/CounterDrone.Core/Repository/AmmunitionSpecRepository.cs new file mode 100644 index 0000000..a7eb970 --- /dev/null +++ b/src/CounterDrone.Core/Repository/AmmunitionSpecRepository.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Linq; +using CounterDrone.Core.Models; +using SQLite; + +namespace CounterDrone.Core.Repository +{ + public class AmmunitionSpecRepository : BaseRepository + { + public AmmunitionSpecRepository(SQLiteConnection db) : base(db) { } + + public List GetByAerosolType(int aerosolType) + { + return Db.Table() + .Where(a => a.AerosolType == aerosolType) + .ToList(); + } + } +} diff --git a/src/CounterDrone.Core/Repository/BaseRepository.cs b/src/CounterDrone.Core/Repository/BaseRepository.cs new file mode 100644 index 0000000..d015ad6 --- /dev/null +++ b/src/CounterDrone.Core/Repository/BaseRepository.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using SQLite; + +namespace CounterDrone.Core.Repository +{ + /// 泛型仓储基类 + public abstract class BaseRepository where T : new() + { + protected readonly SQLiteConnection Db; + + protected BaseRepository(SQLiteConnection db) + { + Db = db; + } + + public virtual T GetById(object id) + { + return Db.Find(id); + } + + public virtual List GetAll() + { + return Db.Table().ToList(); + } + + public virtual int Insert(T entity) + { + return Db.Insert(entity); + } + + public virtual int Update(T entity) + { + return Db.Update(entity); + } + + public virtual int Delete(object id) + { + return Db.Delete(id); + } + + public virtual void InsertAll(IEnumerable entities) + { + Db.InsertAll(entities); + } + } +} diff --git a/src/CounterDrone.Core/Repository/GroupRepository.cs b/src/CounterDrone.Core/Repository/GroupRepository.cs new file mode 100644 index 0000000..0a268c9 --- /dev/null +++ b/src/CounterDrone.Core/Repository/GroupRepository.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Linq; +using CounterDrone.Core.Models; +using SQLite; + +namespace CounterDrone.Core.Repository +{ + public class GroupRepository : BaseRepository + { + public GroupRepository(SQLiteConnection db) : base(db) { } + + public List GetByType(int groupType) + { + return Db.Table() + .Where(g => g.GroupType == groupType) + .ToList(); + } + } +} diff --git a/src/CounterDrone.Core/Repository/ModelRepository.cs b/src/CounterDrone.Core/Repository/ModelRepository.cs new file mode 100644 index 0000000..c97e85e --- /dev/null +++ b/src/CounterDrone.Core/Repository/ModelRepository.cs @@ -0,0 +1,10 @@ +using CounterDrone.Core.Models; +using SQLite; + +namespace CounterDrone.Core.Repository +{ + public class ModelRepository : BaseRepository + { + public ModelRepository(SQLiteConnection db) : base(db) { } + } +} diff --git a/src/CounterDrone.Core/Services/GroupService.cs b/src/CounterDrone.Core/Services/GroupService.cs new file mode 100644 index 0000000..fe94156 --- /dev/null +++ b/src/CounterDrone.Core/Services/GroupService.cs @@ -0,0 +1,50 @@ +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 GetGroups(GroupType? type) + { + if (type.HasValue) + return _repo.GetByType((int)type.Value); + return _repo.GetAll(); + } + + public Group GetGroup(string id) + { + return _repo.GetById(id); + } + } +} diff --git a/src/CounterDrone.Core/Services/IGroupService.cs b/src/CounterDrone.Core/Services/IGroupService.cs new file mode 100644 index 0000000..06e18b4 --- /dev/null +++ b/src/CounterDrone.Core/Services/IGroupService.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using CounterDrone.Core.Models; + +namespace CounterDrone.Core.Services +{ + public interface IGroupService + { + Group CreateGroup(string name, GroupType type, string description); + void DeleteGroup(string id); + List GetGroups(GroupType? type); + Group GetGroup(string id); + } +} diff --git a/src/CounterDrone.Core/Services/IModelService.cs b/src/CounterDrone.Core/Services/IModelService.cs new file mode 100644 index 0000000..86a532d --- /dev/null +++ b/src/CounterDrone.Core/Services/IModelService.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using CounterDrone.Core.Models; + +namespace CounterDrone.Core.Services +{ + public interface IModelService + { + ModelInfo ImportModel(string filePath, string name); + void DeleteModel(string id); + List GetAllModels(); + ModelInfo GetModel(string id); + } +} diff --git a/src/CounterDrone.Core/Services/ModelService.cs b/src/CounterDrone.Core/Services/ModelService.cs new file mode 100644 index 0000000..f1da5b4 --- /dev/null +++ b/src/CounterDrone.Core/Services/ModelService.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.IO; +using CounterDrone.Core.Models; +using CounterDrone.Core.Repository; + +namespace CounterDrone.Core.Services +{ + public class ModelService : IModelService + { + private readonly ModelRepository _repo; + private readonly IPathProvider _paths; + + public ModelService(ModelRepository repo, IPathProvider paths) + { + _repo = repo; + _paths = paths; + } + + public ModelInfo ImportModel(string filePath, string name) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentException("模型名称不能为空"); + + var ext = Path.GetExtension(filePath).ToLowerInvariant(); + if (ext != ".fbx" && ext != ".obj" && ext != ".stl" && ext != ".glb" && ext != ".gltf") + throw new ArgumentException($"不支持的模型格式:{ext}"); + + var fileInfo = new FileInfo(filePath); + if (fileInfo.Length > 500 * 1024 * 1024) + throw new ArgumentException("模型文件大小超过 500MB 限制"); + + var model = new ModelInfo + { + Name = name, + FilePath = filePath, + FileSize = fileInfo.Length / (1024.0 * 1024.0), + }; + + _repo.Insert(model); + return model; + } + + public void DeleteModel(string id) + { + _repo.Delete(id); + } + + public List GetAllModels() + { + return _repo.GetAll(); + } + + public ModelInfo GetModel(string id) + { + return _repo.GetById(id); + } + } +} diff --git a/src/CounterDrone.Core/TestPathProvider.cs b/src/CounterDrone.Core/TestPathProvider.cs new file mode 100644 index 0000000..9f8e4c8 --- /dev/null +++ b/src/CounterDrone.Core/TestPathProvider.cs @@ -0,0 +1,35 @@ +using System; +using System.IO; + +namespace CounterDrone.Core +{ + /// IPathProvider 的实现,用于单元测试环境 + public class TestPathProvider : IPathProvider + { + private readonly string _root; + + public TestPathProvider(string rootDir) + { + _root = rootDir; + Directory.CreateDirectory(_root); + } + + public string GetDataRoot() => _root; + + public string GetMainDbPath() => Path.Combine(_root, "main.db"); + + public string GetFramesDir() + { + var dir = Path.Combine(_root, "frames"); + Directory.CreateDirectory(dir); + return dir; + } + + public string GetModelsDir() + { + var dir = Path.Combine(_root, "models"); + Directory.CreateDirectory(dir); + return dir; + } + } +} diff --git a/src/index.md b/src/index.md new file mode 100644 index 0000000..cc693a1 --- /dev/null +++ b/src/index.md @@ -0,0 +1,9 @@ +# src — 源代码 + +本目录存放项目的全部业务源代码。 + +## 原则 + +1. 所有 Python/Go/Java 等业务模块统一收入 `src/` 下 +2. 根目录仅保留入口文件(如 `app.py`、`main.go`)、配置文件和文档 +3. 按功能模块建立子目录,保持扁平层次(不超过 3 层为宜) diff --git a/test/e2e/index.md b/test/e2e/index.md new file mode 100644 index 0000000..64582fe --- /dev/null +++ b/test/e2e/index.md @@ -0,0 +1,14 @@ +# test/e2e — 端到端测试 + +模拟真实用户行为,从系统入口到出口的完整链路测试。 + +## 原则 + +- **用户视角** — 模拟用户操作流程 +- **环境一致** — 尽量在类生产环境中运行 +- **场景驱动** — 聚焦核心用户场景,避免测试琐碎细节 + +## 示例 + +- 用户注册 → 登录 → 操作 → 退出 完整流程 +- 订单创建 → 支付 → 状态变更 完整链路 diff --git a/test/index.md b/test/index.md new file mode 100644 index 0000000..364bcdb --- /dev/null +++ b/test/index.md @@ -0,0 +1,18 @@ +# test — 测试代码 + +本目录存放项目的全部测试代码。测试代码的组织结构应**镜像** `src/` 的结构,便于定位。 + +## 目录说明 + +| 子目录 | 用途 | 特点 | +|--------|------|------| +| `unit/` | 单元测试 | 测试最小单元(函数、方法),不依赖外部系统 | +| `integration/` | 集成测试 | 测试模块间交互,或与外部系统(数据库、API)的集成 | +| `e2e/` | 端到端测试 | 模拟用户行为,从入口到出口的完整链路 | + +## 测试原则 + +1. **先写测试,再写代码**(推荐 TDD) +2. 每个 bug 修复都应伴随对应的回归测试 +3. 测试用例命名应清晰描述测试场景:`<被测方法>_<场景>_<预期结果>` +4. 保持测试独立:单个测试不应依赖其他测试的运行顺序 diff --git a/test/integration/index.md b/test/integration/index.md new file mode 100644 index 0000000..4d0f60b --- /dev/null +++ b/test/integration/index.md @@ -0,0 +1,15 @@ +# test/integration — 集成测试 + +测试多个模块之间的协作,或与外部系统(数据库、消息队列、API)的集成。 + +## 原则 + +- **真实交互** — 连接真实的(或测试专用)外部系统 +- **关键路径** — 优先覆盖核心业务流程 +- **可重复** — 每次运行结果一致(注意数据准备和清理) + +## 示例 + +- 数据库读写操作的正确性 +- API 请求/响应完整链路 +- 消息队列的生产/消费 diff --git a/test/unit/CounterDrone.Core.Tests/AmmunitionSpecRepositoryTests.cs b/test/unit/CounterDrone.Core.Tests/AmmunitionSpecRepositoryTests.cs new file mode 100644 index 0000000..2e27813 --- /dev/null +++ b/test/unit/CounterDrone.Core.Tests/AmmunitionSpecRepositoryTests.cs @@ -0,0 +1,100 @@ +using System; +using System.IO; +using System.Text.Json; +using CounterDrone.Core; +using CounterDrone.Core.Models; +using CounterDrone.Core.Repository; +using SQLite; +using Xunit; + +namespace CounterDrone.Core.Tests +{ + public class AmmunitionSpecRepositoryTests : IDisposable + { + private readonly string _testDir; + private readonly SQLiteConnection _db; + private readonly AmmunitionSpecRepository _repo; + + public AmmunitionSpecRepositoryTests() + { + _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 AmmunitionSpecRepository(_db); + } + + public void Dispose() + { + _db?.Close(); + if (Directory.Exists(_testDir)) + Directory.Delete(_testDir, true); + } + + [Fact] + public void Insert_And_GetById_ReturnsSpec() + { + var spec = new AmmunitionSpec + { + AerosolType = (int)AerosolType.InertGas, + Name = "惰性气体弹-A1", + InitialRadius = 50.0, + InitialVolume = 500000.0, + CoreDensity = 1.2, + EffectiveConcentration = 0.05, + DispersionRateBase = 5.0, + MaxRadius = 200.0, + MaxDuration = 60.0 + }; + + _repo.Insert(spec); + + var result = _repo.GetById(spec.Id); + Assert.NotNull(result); + Assert.Equal("惰性气体弹-A1", result.Name); + Assert.Equal(50.0, result.InitialRadius); + Assert.Equal((int)AerosolType.InertGas, result.AerosolType); + } + + [Fact] + public void GetByAerosolType_FiltersCorrectly() + { + _repo.Insert(new AmmunitionSpec + { + Name = "Inert-A", AerosolType = (int)AerosolType.InertGas + }); + _repo.Insert(new AmmunitionSpec + { + Name = "Inert-B", AerosolType = (int)AerosolType.InertGas + }); + _repo.Insert(new AmmunitionSpec + { + Name = "Active-A", AerosolType = (int)AerosolType.ActiveMaterial + }); + + var inertSpecs = _repo.GetByAerosolType((int)AerosolType.InertGas); + Assert.Equal(2, inertSpecs.Count); + + var activeSpecs = _repo.GetByAerosolType((int)AerosolType.ActiveMaterial); + Assert.Single(activeSpecs); + } + + [Fact] + public void ImportFromJson_LoadsSpecs() + { + var json = @"[ + {""Id"":""spec-1"",""AerosolType"":0,""Name"":""惰性气体弹"",""InitialRadius"":50,""InitialVolume"":500000,""CoreDensity"":1.2,""EdgeDensity"":0.1,""InitialTemperature"":300,""BuoyancyFactor"":0.5,""EffectiveConcentration"":0.05,""DispersionRateBase"":5,""MaxRadius"":200,""MaxDuration"":60,""ParticlesJson"":""{}""}, + {""Id"":""spec-2"",""AerosolType"":1,""Name"":""活性材料弹"",""InitialRadius"":30,""InitialVolume"":100000,""CoreDensity"":2.0,""EdgeDensity"":0.2,""InitialTemperature"":500,""BuoyancyFactor"":0.3,""EffectiveConcentration"":0.1,""DispersionRateBase"":3,""MaxRadius"":150,""MaxDuration"":45,""ParticlesJson"":""{}""} + ]"; + + var specs = JsonSerializer.Deserialize(json); + Assert.NotNull(specs); + _repo.InsertAll(specs); + + var all = _repo.GetAll(); + Assert.Equal(2, all.Count); + Assert.Contains(all, s => s.Name == "惰性气体弹"); + Assert.Contains(all, s => s.Name == "活性材料弹"); + } + } +} diff --git a/test/unit/CounterDrone.Core.Tests/CounterDrone.Core.Tests.csproj b/test/unit/CounterDrone.Core.Tests/CounterDrone.Core.Tests.csproj new file mode 100644 index 0000000..65ecd2a --- /dev/null +++ b/test/unit/CounterDrone.Core.Tests/CounterDrone.Core.Tests.csproj @@ -0,0 +1,25 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/unit/CounterDrone.Core.Tests/DatabaseManagerTests.cs b/test/unit/CounterDrone.Core.Tests/DatabaseManagerTests.cs new file mode 100644 index 0000000..c6b67fb --- /dev/null +++ b/test/unit/CounterDrone.Core.Tests/DatabaseManagerTests.cs @@ -0,0 +1,89 @@ +using System; +using System.IO; +using CounterDrone.Core; +using CounterDrone.Core.Models; +using CounterDrone.Core.Repository; +using Xunit; + +namespace CounterDrone.Core.Tests +{ + public class DatabaseManagerTests : IDisposable + { + private readonly string _testDir; + private readonly IPathProvider _paths; + private readonly DatabaseManager _dbManager; + + public DatabaseManagerTests() + { + _testDir = Path.Combine(Path.GetTempPath(), $"cd_test_{Guid.NewGuid():N}"); + _paths = new TestPathProvider(_testDir); + _dbManager = new DatabaseManager(_paths); + } + + public void Dispose() + { + if (Directory.Exists(_testDir)) + Directory.Delete(_testDir, true); + } + + [Fact] + public void OpenMainDb_CreatesAllTables() + { + var db = _dbManager.OpenMainDb(); + + // 验证所有 13 张表存在 + var tables = new[] + { + "ModelInfo", "AmmunitionSpec", "SimTask", "CombatScene", + "ControlZone", "TargetConfig", "EquipmentDeployment", + "CloudDispersal", "RoutePlan", "Waypoint", "Group", + "SimulationReport", "SimEvent" + }; + + foreach (var table in tables) + { + var count = db.ExecuteScalar( + $"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='{table}'"); + Assert.Equal(1, count); + } + + db.Close(); + } + + [Fact] + public void OpenFrameDb_CreatesFrameRecordTable() + { + var db = _dbManager.OpenFrameDb("task_001"); + + var count = db.ExecuteScalar( + "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='SimFrameRecord'"); + Assert.Equal(1, count); + + db.Close(); + } + + [Fact] + public void DeleteFrameDb_RemovesFile() + { + var taskId = "task_del"; + var db = _dbManager.OpenFrameDb(taskId); + db.Close(); + + _dbManager.DeleteFrameDb(taskId); + + var dbPath = Path.Combine(_paths.GetFramesDir(), $"{taskId}.db"); + Assert.False(File.Exists(dbPath)); + } + + [Fact] + public void OpenMainDb_IsIdempotent() + { + var db1 = _dbManager.OpenMainDb(); + db1.Close(); + + // 第二次打开不抛异常 + var db2 = _dbManager.OpenMainDb(); + db2.Close(); + } + } +} diff --git a/test/unit/CounterDrone.Core.Tests/GroupRepositoryTests.cs b/test/unit/CounterDrone.Core.Tests/GroupRepositoryTests.cs new file mode 100644 index 0000000..9a90feb --- /dev/null +++ b/test/unit/CounterDrone.Core.Tests/GroupRepositoryTests.cs @@ -0,0 +1,75 @@ +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)); + } + } +} diff --git a/test/unit/CounterDrone.Core.Tests/GroupServiceTests.cs b/test/unit/CounterDrone.Core.Tests/GroupServiceTests.cs new file mode 100644 index 0000000..03de66a --- /dev/null +++ b/test/unit/CounterDrone.Core.Tests/GroupServiceTests.cs @@ -0,0 +1,83 @@ +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(() => + _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); + } + } +} diff --git a/test/unit/CounterDrone.Core.Tests/ModelRepositoryTests.cs b/test/unit/CounterDrone.Core.Tests/ModelRepositoryTests.cs new file mode 100644 index 0000000..366fd84 --- /dev/null +++ b/test/unit/CounterDrone.Core.Tests/ModelRepositoryTests.cs @@ -0,0 +1,94 @@ +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 ModelRepositoryTests : IDisposable + { + private readonly string _testDir; + private readonly SQLiteConnection _db; + private readonly ModelRepository _repo; + + public ModelRepositoryTests() + { + _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 ModelRepository(_db); + } + + public void Dispose() + { + _db?.Close(); + if (Directory.Exists(_testDir)) + Directory.Delete(_testDir, true); + } + + [Fact] + public void Insert_And_GetById_ReturnsModel() + { + var model = new ModelInfo + { + Name = "Test Drone", + FilePath = "/models/drone.fbx", + FileSize = 2.5 + }; + + _repo.Insert(model); + + var result = _repo.GetById(model.Id); + Assert.NotNull(result); + Assert.Equal("Test Drone", result.Name); + Assert.Equal(2.5, result.FileSize); + } + + [Fact] + public void GetAll_ReturnsAllModels() + { + _repo.Insert(new ModelInfo { Name = "A" }); + _repo.Insert(new ModelInfo { Name = "B" }); + _repo.Insert(new ModelInfo { Name = "C" }); + + var all = _repo.GetAll(); + Assert.Equal(3, all.Count); + } + + [Fact] + public void Delete_RemovesModel() + { + var model = new ModelInfo { Name = "ToDelete" }; + _repo.Insert(model); + + _repo.Delete(model.Id); + + var result = _repo.GetById(model.Id); + Assert.Null(result); + } + + [Fact] + public void Update_ModifiesModel() + { + var model = new ModelInfo { Name = "Original" }; + _repo.Insert(model); + + model.Name = "Updated"; + _repo.Update(model); + + var result = _repo.GetById(model.Id); + Assert.Equal("Updated", result.Name); + } + + [Fact] + public void GetById_NonExistent_ReturnsNull() + { + var result = _repo.GetById("nonexistent"); + Assert.Null(result); + } + } +} diff --git a/test/unit/CounterDrone.Core.Tests/ModelServiceTests.cs b/test/unit/CounterDrone.Core.Tests/ModelServiceTests.cs new file mode 100644 index 0000000..95faeb3 --- /dev/null +++ b/test/unit/CounterDrone.Core.Tests/ModelServiceTests.cs @@ -0,0 +1,102 @@ +using System; +using System.IO; +using CounterDrone.Core; +using CounterDrone.Core.Services; +using SQLite; +using Xunit; + +namespace CounterDrone.Core.Tests +{ + public class ModelServiceTests : IDisposable + { + private readonly string _testDir; + private readonly SQLiteConnection _db; + private readonly IModelService _service; + private readonly IPathProvider _paths; + + public ModelServiceTests() + { + _testDir = Path.Combine(Path.GetTempPath(), $"cd_test_{Guid.NewGuid():N}"); + _paths = new TestPathProvider(_testDir); + var dbManager = new DatabaseManager(_paths); + _db = dbManager.OpenMainDb(); + _service = new ModelService(new Repository.ModelRepository(_db), _paths); + } + + public void Dispose() + { + _db?.Close(); + if (Directory.Exists(_testDir)) + Directory.Delete(_testDir, true); + } + + private string CreateTempModelFile(string extension) + { + var path = Path.Combine(_testDir, $"test_model{extension}"); + File.WriteAllText(path, "dummy content"); + return path; + } + + [Fact] + public void ImportModel_ValidObj_Succeeds() + { + var filePath = CreateTempModelFile(".obj"); + var model = _service.ImportModel(filePath, "TestModel"); + + Assert.NotNull(model); + Assert.Equal("TestModel", model.Name); + Assert.NotEmpty(model.Id); + } + + [Fact] + public void ImportModel_EmptyName_Throws() + { + var filePath = CreateTempModelFile(".fbx"); + Assert.Throws(() => _service.ImportModel(filePath, "")); + } + + [Fact] + public void ImportModel_UnsupportedFormat_Throws() + { + var filePath = CreateTempModelFile(".xyz"); + Assert.Throws(() => _service.ImportModel(filePath, "BadFormat")); + } + + [Fact] + public void ImportModel_AllSupportedFormats() + { + foreach (var ext in new[] { ".fbx", ".obj", ".stl", ".glb", ".gltf" }) + { + var filePath = CreateTempModelFile(ext); + var model = _service.ImportModel(filePath, $"Model_{ext}"); + Assert.NotNull(model); + } + } + + [Fact] + public void GetAllModels_ReturnsImportedModels() + { + _service.ImportModel(CreateTempModelFile(".fbx"), "A"); + _service.ImportModel(CreateTempModelFile(".obj"), "B"); + + var all = _service.GetAllModels(); + Assert.Equal(2, all.Count); + } + + [Fact] + public void DeleteModel_RemovesModel() + { + var model = _service.ImportModel(CreateTempModelFile(".fbx"), "ToDelete"); + _service.DeleteModel(model.Id); + + var result = _service.GetModel(model.Id); + Assert.Null(result); + } + + [Fact] + public void GetModel_NonExistent_ReturnsNull() + { + Assert.Null(_service.GetModel("not-exist")); + } + } +} diff --git a/test/unit/index.md b/test/unit/index.md new file mode 100644 index 0000000..0eb3857 --- /dev/null +++ b/test/unit/index.md @@ -0,0 +1,17 @@ +# test/unit — 单元测试 + +测试最小单元:单个函数、方法、类。不依赖网络、数据库、文件系统等外部资源。 + +## 原则 + +- **快速** — 毫秒级完成,可频繁运行 +- **隔离** — 不依赖外部系统,使用 Mock / Stub 替代 +- **全覆盖** — 正常路径、异常路径、边界条件 + +## 命名示例 + +``` +UserService.test.ts +loginHandler_test.go +test_calculator.py +```