From b23291c5bfb28de948ac75227edff7fe403a48f8 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Thu, 11 Jun 2026 14:08:37 +0800 Subject: [PATCH] =?UTF-8?q?Phase=201:=20=E6=A0=B8=E5=BF=83=E5=9F=BA?= =?UTF-8?q?=E7=A1=80=20=E2=80=94=20=E9=A1=B9=E7=9B=AE=E9=AA=A8=E6=9E=B6?= =?UTF-8?q?=E3=80=81=E6=95=B0=E6=8D=AE=E6=A8=A1=E5=9E=8B=E3=80=81Repositor?= =?UTF-8?q?y=E3=80=81ModelService/GroupService=EF=BC=8C27=E4=B8=AA?= =?UTF-8?q?=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95=E5=85=A8=E9=83=A8=E9=80=9A?= =?UTF-8?q?=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 48 + AGENTS.md | 71 + CHANGELOG.md | 34 + CONTRIBUTING.md | 51 + LICENSE | 21 + README.md | 68 + VERSION | 1 + data/index.md | 17 + docs/design/architecture/index.md | 53 + docs/design/architecture/总体架构设计.md | 1384 +++++++++++++++++ docs/design/index.md | 26 + docs/design/technical/index.md | 51 + docs/features/index.md | 56 + docs/implementation/index.md | 19 + docs/implementation/tasks/index.md | 51 + .../tasks/实施计划与任务跟踪.md | 281 ++++ docs/index.md | 22 + .../V1.0_反无人机仿真系统功能需求260611.txt | 266 ++++ docs/requirements/changes/index.md | 11 + docs/requirements/index.md | 55 + docs/requirements/技术要求终版.txt | 45 + docs/verify/checklist/index.md | 36 + docs/verify/index.md | 20 + scripts/index.md | 9 + skills/index.md | 13 + .../CounterDrone.Core.csproj | 14 + src/CounterDrone.Core/DatabaseManager.cs | 69 + src/CounterDrone.Core/IPathProvider.cs | 11 + .../Models/AmmunitionSpec.cs | 42 + .../Models/CloudDispersal.cs | 49 + src/CounterDrone.Core/Models/CombatScene.cs | 36 + src/CounterDrone.Core/Models/ControlZone.cs | 28 + src/CounterDrone.Core/Models/Enums.cs | 152 ++ .../Models/EquipmentDeployment.cs | 44 + src/CounterDrone.Core/Models/Group.cs | 22 + src/CounterDrone.Core/Models/ModelInfo.cs | 28 + src/CounterDrone.Core/Models/RoutePlan.cs | 20 + src/CounterDrone.Core/Models/SimEvent.cs | 28 + .../Models/SimFrameRecord.cs | 34 + src/CounterDrone.Core/Models/SimTask.cs | 29 + .../Models/SimulationReport.cs | 31 + src/CounterDrone.Core/Models/TargetConfig.cs | 30 + .../Models/TaskFullConfig.cs | 27 + src/CounterDrone.Core/Models/Waypoint.cs | 27 + .../Repository/AmmunitionSpecRepository.cs | 19 + .../Repository/BaseRepository.cs | 46 + .../Repository/GroupRepository.cs | 19 + .../Repository/ModelRepository.cs | 10 + .../Services/GroupService.cs | 50 + .../Services/IGroupService.cs | 13 + .../Services/IModelService.cs | 13 + .../Services/ModelService.cs | 59 + src/CounterDrone.Core/TestPathProvider.cs | 35 + src/index.md | 9 + test/e2e/index.md | 14 + test/index.md | 18 + test/integration/index.md | 15 + .../AmmunitionSpecRepositoryTests.cs | 100 ++ .../CounterDrone.Core.Tests.csproj | 25 + .../DatabaseManagerTests.cs | 89 ++ .../GroupRepositoryTests.cs | 75 + .../GroupServiceTests.cs | 83 + .../ModelRepositoryTests.cs | 94 ++ .../ModelServiceTests.cs | 102 ++ test/unit/index.md | 17 + 65 files changed, 4335 insertions(+) create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 VERSION create mode 100644 data/index.md create mode 100644 docs/design/architecture/index.md create mode 100644 docs/design/architecture/总体架构设计.md create mode 100644 docs/design/index.md create mode 100644 docs/design/technical/index.md create mode 100644 docs/features/index.md create mode 100644 docs/implementation/index.md create mode 100644 docs/implementation/tasks/index.md create mode 100644 docs/implementation/tasks/实施计划与任务跟踪.md create mode 100644 docs/index.md create mode 100644 docs/requirements/V1.0_反无人机仿真系统功能需求260611.txt create mode 100644 docs/requirements/changes/index.md create mode 100644 docs/requirements/index.md create mode 100644 docs/requirements/技术要求终版.txt create mode 100644 docs/verify/checklist/index.md create mode 100644 docs/verify/index.md create mode 100644 scripts/index.md create mode 100644 skills/index.md create mode 100644 src/CounterDrone.Core/CounterDrone.Core.csproj create mode 100644 src/CounterDrone.Core/DatabaseManager.cs create mode 100644 src/CounterDrone.Core/IPathProvider.cs create mode 100644 src/CounterDrone.Core/Models/AmmunitionSpec.cs create mode 100644 src/CounterDrone.Core/Models/CloudDispersal.cs create mode 100644 src/CounterDrone.Core/Models/CombatScene.cs create mode 100644 src/CounterDrone.Core/Models/ControlZone.cs create mode 100644 src/CounterDrone.Core/Models/Enums.cs create mode 100644 src/CounterDrone.Core/Models/EquipmentDeployment.cs create mode 100644 src/CounterDrone.Core/Models/Group.cs create mode 100644 src/CounterDrone.Core/Models/ModelInfo.cs create mode 100644 src/CounterDrone.Core/Models/RoutePlan.cs create mode 100644 src/CounterDrone.Core/Models/SimEvent.cs create mode 100644 src/CounterDrone.Core/Models/SimFrameRecord.cs create mode 100644 src/CounterDrone.Core/Models/SimTask.cs create mode 100644 src/CounterDrone.Core/Models/SimulationReport.cs create mode 100644 src/CounterDrone.Core/Models/TargetConfig.cs create mode 100644 src/CounterDrone.Core/Models/TaskFullConfig.cs create mode 100644 src/CounterDrone.Core/Models/Waypoint.cs create mode 100644 src/CounterDrone.Core/Repository/AmmunitionSpecRepository.cs create mode 100644 src/CounterDrone.Core/Repository/BaseRepository.cs create mode 100644 src/CounterDrone.Core/Repository/GroupRepository.cs create mode 100644 src/CounterDrone.Core/Repository/ModelRepository.cs create mode 100644 src/CounterDrone.Core/Services/GroupService.cs create mode 100644 src/CounterDrone.Core/Services/IGroupService.cs create mode 100644 src/CounterDrone.Core/Services/IModelService.cs create mode 100644 src/CounterDrone.Core/Services/ModelService.cs create mode 100644 src/CounterDrone.Core/TestPathProvider.cs create mode 100644 src/index.md create mode 100644 test/e2e/index.md create mode 100644 test/index.md create mode 100644 test/integration/index.md create mode 100644 test/unit/CounterDrone.Core.Tests/AmmunitionSpecRepositoryTests.cs create mode 100644 test/unit/CounterDrone.Core.Tests/CounterDrone.Core.Tests.csproj create mode 100644 test/unit/CounterDrone.Core.Tests/DatabaseManagerTests.cs create mode 100644 test/unit/CounterDrone.Core.Tests/GroupRepositoryTests.cs create mode 100644 test/unit/CounterDrone.Core.Tests/GroupServiceTests.cs create mode 100644 test/unit/CounterDrone.Core.Tests/ModelRepositoryTests.cs create mode 100644 test/unit/CounterDrone.Core.Tests/ModelServiceTests.cs create mode 100644 test/unit/index.md 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 +```