Phase 1: 核心基础 — 项目骨架、数据模型、Repository、ModelService/GroupService,27个单元测试全部通过

This commit is contained in:
tian 2026-06-11 14:08:37 +08:00
commit b23291c5bf
65 changed files with 4335 additions and 0 deletions

48
.gitignore vendored Normal file
View File

@ -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/

71
AGENTS.md Normal file
View File

@ -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.

34
CHANGELOG.md Normal file
View File

@ -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/

51
CONTRIBUTING.md Normal file
View File

@ -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>(<scope>): <subject>
类型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/` 记录

21
LICENSE Normal file
View File

@ -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.

68
README.md Normal file
View File

@ -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 <repo-url>
cd <project-name>
# [安装 / 构建 / 运行步骤,根据项目补充]
```
## 文档导航
| 你想做的事 | 去哪看 |
|------------|--------|
| 了解项目背景和架构 | `docs/design/architecture/` |
| 查看需求与变更 | `docs/requirements/`(可选) |
| 查看功能设计 | `docs/features/` |
| 查看任务进度 | `docs/implementation/tasks/`(可选) |
| 参与开发 | `CONTRIBUTING.md` |
| 查看版本历史 | `CHANGELOG.md` |
## 维护者
- [@维护者](https://github.com/xxx)
## 许可证
[LICENSE](./LICENSE)

1
VERSION Normal file
View File

@ -0,0 +1 @@
0.1.0

17
data/index.md Normal file
View File

@ -0,0 +1,17 @@
# data — 项目数据
本目录存放非代码的项目数据文件,区别于源代码和文档。
## 用途
- 初始化/种子数据
- 数据库导出快照(用于参考或迁移)
- 静态数据文件(如地理信息、字典数据等)
- 其他非代码的静态项目数据
## 原则
1. 不含可执行代码或业务配置文件
2. 文件应可被版本控制追踪(大文件请使用 Git LFS
3. 按数据类型或用途建立子目录
4. 与 `test/fixtures/` 区分:`data/` 放项目级数据,`test/fixtures/` 放测试专用数据

View File

@ -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/)

File diff suppressed because it is too large Load Diff

26
docs/design/index.md Normal file
View File

@ -0,0 +1,26 @@
# docs/design — 设计方案
本目录存放架构设计和技术方案文档。
## 目录结构
```
design/
├── architecture/ # 架构设计 & 架构决策记录ADR
└── technical/ # 技术方案 / 详细设计
```
## architecture/ — 架构设计
- **总体架构文档**:系统架构图、模块划分、技术选型、核心交互流程
- **ADRArchitecture Decision Record**:记录重大架构决策及其背景
## technical/ — 技术方案
- 各模块的详细技术设计
- 接口设计、数据模型设计
- 关键技术难点的攻关方案
---
> **提示**:架构图和流程图推荐使用 MermaidMarkdown 原生支持)或 PlantUML方便版本控制和 AI 工具解析。

View File

@ -0,0 +1,51 @@
# docs/design/technical — 技术方案
本目录存放各模块的详细技术设计方案。
## 文件命名
```
<模块名>-技术方案.md
```
## 技术方案模板
```markdown
# [模块名] 技术方案
- **版本**v[版本号]
- **作者**[姓名]
- **日期**YYYY-MM-DD
- **状态**:草稿 / 评审中 / 已定稿
## 1. 概述
> 本方案解决什么问题?适用范围是什么?
## 2. 设计目标
- 功能目标
- 非功能目标(性能、安全、可扩展性等)
## 3. 方案描述
### 3.1 整体流程
<!-- 推荐使用 Mermaid 绘制流程图 -->
### 3.2 接口设计
### 3.3 数据模型
### 3.4 关键逻辑
## 4. 风险与应对
| 风险 | 影响 | 应对措施 |
|------|------|----------|
| ... | ... | ... |
## 5. 参考
- [相关文档链接]
```

56
docs/features/index.md Normal file
View File

@ -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`
```

View File

@ -0,0 +1,19 @@
# docs/implementation — 实施与跟踪
> **可选**:如果团队已使用 Jira/TAPD/飞书等工单系统管理任务,可直接删除此目录。
本目录用于管理开发实施计划和任务进度。
## 目录结构
```
implementation/
├── index.md # 本文件
└── tasks/ # 任务拆分与进度跟踪
```
## 使用方式
1. 将功能需求拆分为可执行的任务
2. 每个迭代/功能一个任务文件,记录在 `tasks/`
3. 任务文件中用 Checkbox 标记进度

View File

@ -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 | ... |
```

View File

@ -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 6Unity 集成
**目标**:将 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` MonoBehaviourUpdate 驱动引擎) | 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 < 5ms16 实体场景
- [ ] 过期帧数据库可自动清理
- [ ] 单元测试覆盖率 > 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逐项打勾推进。

22
docs/index.md Normal file
View File

@ -0,0 +1,22 @@
# docs — 开发文档
本目录存放项目的全部开发文档,涵盖需求、设计、功能规格、实施跟踪和质量验证。
## 目录说明
| 子目录 | 用途 | 受众 | 必选 |
|--------|------|------|:--:|
| `requirements/` | 需求与变更管理 | 产品、开发、测试 | 可选 |
| `design/` | 架构设计与技术方案 | 开发、架构师 | ✅ |
| `features/` | 功能规格说明 | 开发、测试、产品 | ✅ |
| `implementation/` | 实施计划与任务跟踪 | 开发、项目管理 | 可选 |
| `verify/` | 人工检查清单(上线/代码审查/安全审计) | 开发、运维 | 可选 |
> - 标记"可选"的目录:如果团队已使用 Jira/TAPD/飞书等工单系统管理需求和任务,可直接删除,避免双写不同步。
> - `verify/``test/` 的区别:`test/` 是**自动化代码测试**`verify/` 是**人工检查清单和验证流程**。
## 使用原则
1. 文档**与代码同步更新**,不是一次性交付物
2. 优先用 Markdown便于版本控制和 AI 工具阅读
3. 每个文档文件保持聚焦,一个文件讲清一件事

View File

@ -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 字符串 关联对应仿真任务
任务名称 字符串 对应仿真任务名称
任务编号 字符串 对应仿真任务编号
完成时间 时间 仿真完成生成报告的时间
目标数量 数值 仿真配置的目标总数量
装备数量 数值 仿真配置的装备总数量
对抗结果 枚举 成功拦截/部分拦截/拦截失败
报告内容 富文本/结构化数据 仿真报告完整内容

View File

@ -0,0 +1,11 @@
# docs/requirements/changes — 需求变更记录
每条需求变更一个文件,便于追溯和评审。
## 变更记录文件命名
```
YYYY-MM-DD-<简短描述>.md
```
示例:`2025-01-15-登录支持短信验证码.md`

View File

@ -0,0 +1,55 @@
# docs/requirements — 需求与变更管理
> **可选**:如果团队已使用 Jira/TAPD/飞书等工单系统,可直接删除此目录,避免双写。
本目录用于管理项目需求及其变更记录。
## 目录结构
```
requirements/
├── index.md # 本文件
└── changes/ # 需求变更记录(每条变更一个文件)
```
## 需求变更记录
当需求发生变更时,请在 `changes/` 下创建一条记录。
### 文件命名规范
```
YYYY-MM-DD-<简短描述>.md
```
### 变更记录模板
```markdown
# [变更标题]
- **日期**YYYY-MM-DD
- **提出人**[姓名]
- **关联需求**[原始需求编号或链接]
- **优先级**:高 / 中 / 低
## 变更描述
> 说明变更内容、变更原因。
## 影响范围
- [ ] 接口变更
- [ ] 数据库变更
- [ ] UI 变更
- [ ] 文档变更
## 审批
- [ ] 产品确认
- [ ] 技术评估
- [ ] 测试评估
```
---
> 每条变更记录对应一个 `.md` 文件,便于版本控制和追溯。

View File

@ -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。

View File

@ -0,0 +1,36 @@
# verify/checklist — 检查清单
本目录存放各环节的检查清单。
## 清单模板
以**上线检查清单**为例:
```markdown
# 上线检查清单
## 代码质量
- [ ] 代码审查已完成
- [ ] 所有测试通过
- [ ] 无已知 P0/P1 Bug
- [ ] 代码已合并到主分支
## 性能与安全
- [ ] 性能测试通过
- [ ] 安全扫描通过
- [ ] 敏感信息已清理
## 运维准备
- [ ] 数据库变更脚本就绪
- [ ] 回滚方案已准备
- [ ] 监控和告警已配置
## 文档
- [ ] CHANGELOG 已更新
- [ ] 版本号已更新
- [ ] API 文档已更新(如有)
```
---
> 根据项目实际情况增删条目。

20
docs/verify/index.md Normal file
View File

@ -0,0 +1,20 @@
# verify — 验证与质量保障
本目录存放人工检查清单和验证流程文档。
> 已从顶层目录迁移至 `docs/verify/`,以下为历史兼容说明。**新项目请直接使用 `docs/verify/`**。
## 目录结构
```
verify/
├── index.md # 本文件
└── checklist/ # 检查清单
```
## 用途
- 上线前检查清单
- 发布验收清单
- 代码审查清单
- 安全审计清单

9
scripts/index.md Normal file
View File

@ -0,0 +1,9 @@
# scripts — 工具脚本
本目录存放项目的辅助脚本:构建、部署、数据迁移、环境初始化等。
## 原则
- 每个脚本做好注释,说明用途和运行方式
- 脚本应尽量幂等(多次运行结果一致)
- 避免将敏感信息(密码、密钥)硬编码在脚本中

13
skills/index.md Normal file
View File

@ -0,0 +1,13 @@
# skills — AI 辅助技能
本目录存放 AI 编码助手的辅助技能定义(遵循 [Agent Skills 标准](https://agentskills.io))。
> **注意**:本目录仅适用于使用 pi 编码助手或兼容 Agent Skills 标准的工具。[其他工具可删除此目录。]
## 使用方式
在对话中通过 `/skill:技能名` 调用,或由 AI 自动加载。
## 现有技能
当前尚无自定义技能。

View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<LangVersion>9.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="sqlite-net-pcl" Version="1.9.172" />
<PackageReference Include="System.Text.Json" Version="9.0.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,69 @@
using System.IO;
using CounterDrone.Core.Models;
using SQLite;
namespace CounterDrone.Core
{
/// <summary>数据库管理器 — 建表 / 连接管理</summary>
public class DatabaseManager
{
private readonly IPathProvider _paths;
public DatabaseManager(IPathProvider paths)
{
_paths = paths;
}
/// <summary>初始化主数据库(建表)</summary>
public SQLiteConnection OpenMainDb()
{
var dbPath = _paths.GetMainDbPath();
var db = new SQLiteConnection(dbPath);
CreateMainTables(db);
return db;
}
/// <summary>创建/打开任务帧数据库</summary>
public SQLiteConnection OpenFrameDb(string taskId)
{
var dbPath = Path.Combine(_paths.GetFramesDir(), $"{taskId}.db");
var db = new SQLiteConnection(dbPath);
db.CreateTable<SimFrameRecord>();
db.CreateIndex("SimFrameRecord", new[] { "TaskId", "FrameIndex" });
return db;
}
/// <summary>删除任务帧数据库文件</summary>
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<ModelInfo>();
db.CreateTable<AmmunitionSpec>();
db.CreateTable<SimTask>();
db.CreateTable<CombatScene>();
db.CreateTable<ControlZone>();
db.CreateTable<TargetConfig>();
db.CreateTable<EquipmentDeployment>();
db.CreateTable<CloudDispersal>();
db.CreateTable<RoutePlan>();
db.CreateTable<Waypoint>();
db.CreateTable<Group>();
db.CreateTable<SimulationReport>();
db.CreateTable<SimEvent>();
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");
}
}
}

View File

@ -0,0 +1,11 @@
namespace CounterDrone.Core
{
/// <summary>路径提供者接口 — 隔离文件系统依赖</summary>
public interface IPathProvider
{
string GetDataRoot();
string GetMainDbPath();
string GetFramesDir();
string GetModelsDir();
}
}

View File

@ -0,0 +1,42 @@
using System;
using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>弹药基础参数(第三方数据)</summary>
[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; }
/// <summary>粒子渲染参数 JSON</summary>
public string ParticlesJson { get; set; } = "{}";
}
}

View File

@ -0,0 +1,49 @@
using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>步骤4云团抛撒配置</summary>
[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; }
}
}

View File

@ -0,0 +1,36 @@
using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>步骤1作战场景</summary>
[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;
}
}

View File

@ -0,0 +1,28 @@
using System;
using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>管控区域 / 电子围栏</summary>
[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;
/// <summary>多边形顶点 JSON[{"x":0,"y":0,"z":0},...]</summary>
[NotNull]
public string VerticesJson { get; set; } = "[]";
public double MinAltitude { get; set; }
public double MaxAltitude { get; set; }
public int OrderIndex { get; set; }
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,44 @@
using System;
using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>步骤3装备部署</summary>
[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; }
}
}

View File

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

View File

@ -0,0 +1,28 @@
using System;
using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>模型元数据</summary>
[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");
}
}

View File

@ -0,0 +1,20 @@
using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>步骤5航路规划</summary>
[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;
}
}

View File

@ -0,0 +1,28 @@
using System;
using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>仿真事件(时序图数据源)— 存主库</summary>
[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;
}
}

View File

@ -0,0 +1,34 @@
using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>逐帧位置记录(回放用)— 存任务分库</summary>
[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; }
/// <summary>JSONdamageStage, cloudRadius, cloudOpacity 等</summary>
public string StateData { get; set; } = "{}";
}
}

View File

@ -0,0 +1,29 @@
using System;
using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>仿真任务主表</summary>
[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");
}
}

View File

@ -0,0 +1,31 @@
using System;
using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>仿真报告</summary>
[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; }
/// <summary>报告富文本 / 结构化 JSON</summary>
public string Content { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,30 @@
using System;
using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>步骤2目标配置</summary>
[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;
}
}

View File

@ -0,0 +1,27 @@
using System.Collections.Generic;
namespace CounterDrone.Core.Models
{
/// <summary>想定完整配置(聚合 5 步配置)</summary>
public class TaskFullConfig
{
public SimTask Task { get; set; } = new();
public CombatScene Scene { get; set; } = new();
public List<ControlZone> ControlZones { get; set; } = new();
public List<TargetConfig> Targets { get; set; } = new();
public List<EquipmentDeployment> Equipment { get; set; } = new();
public CloudDispersal Cloud { get; set; } = new();
public RoutePlan Route { get; set; } = new();
public List<Waypoint> Waypoints { get; set; } = new();
}
/// <summary>分页结果</summary>
public class PagedResult<T>
{
public List<T> 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;
}
}

View File

@ -0,0 +1,27 @@
using System;
using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>航路点</summary>
[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; }
}
}

View File

@ -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<AmmunitionSpec>
{
public AmmunitionSpecRepository(SQLiteConnection db) : base(db) { }
public List<AmmunitionSpec> GetByAerosolType(int aerosolType)
{
return Db.Table<AmmunitionSpec>()
.Where(a => a.AerosolType == aerosolType)
.ToList();
}
}
}

View File

@ -0,0 +1,46 @@
using System.Collections.Generic;
using SQLite;
namespace CounterDrone.Core.Repository
{
/// <summary>泛型仓储基类</summary>
public abstract class BaseRepository<T> where T : new()
{
protected readonly SQLiteConnection Db;
protected BaseRepository(SQLiteConnection db)
{
Db = db;
}
public virtual T GetById(object id)
{
return Db.Find<T>(id);
}
public virtual List<T> GetAll()
{
return Db.Table<T>().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<T>(id);
}
public virtual void InsertAll(IEnumerable<T> entities)
{
Db.InsertAll(entities);
}
}
}

View File

@ -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<Group>
{
public GroupRepository(SQLiteConnection db) : base(db) { }
public List<Group> GetByType(int groupType)
{
return Db.Table<Group>()
.Where(g => g.GroupType == groupType)
.ToList();
}
}
}

View File

@ -0,0 +1,10 @@
using CounterDrone.Core.Models;
using SQLite;
namespace CounterDrone.Core.Repository
{
public class ModelRepository : BaseRepository<ModelInfo>
{
public ModelRepository(SQLiteConnection db) : base(db) { }
}
}

View File

@ -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<Group> GetGroups(GroupType? type)
{
if (type.HasValue)
return _repo.GetByType((int)type.Value);
return _repo.GetAll();
}
public Group GetGroup(string id)
{
return _repo.GetById(id);
}
}
}

View File

@ -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<Group> GetGroups(GroupType? type);
Group GetGroup(string id);
}
}

View File

@ -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<ModelInfo> GetAllModels();
ModelInfo GetModel(string id);
}
}

View File

@ -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<ModelInfo> GetAllModels()
{
return _repo.GetAll();
}
public ModelInfo GetModel(string id)
{
return _repo.GetById(id);
}
}
}

View File

@ -0,0 +1,35 @@
using System;
using System.IO;
namespace CounterDrone.Core
{
/// <summary>IPathProvider 的实现,用于单元测试环境</summary>
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;
}
}
}

9
src/index.md Normal file
View File

@ -0,0 +1,9 @@
# src — 源代码
本目录存放项目的全部业务源代码。
## 原则
1. 所有 Python/Go/Java 等业务模块统一收入 `src/`
2. 根目录仅保留入口文件(如 `app.py`、`main.go`)、配置文件和文档
3. 按功能模块建立子目录,保持扁平层次(不超过 3 层为宜)

14
test/e2e/index.md Normal file
View File

@ -0,0 +1,14 @@
# test/e2e — 端到端测试
模拟真实用户行为,从系统入口到出口的完整链路测试。
## 原则
- **用户视角** — 模拟用户操作流程
- **环境一致** — 尽量在类生产环境中运行
- **场景驱动** — 聚焦核心用户场景,避免测试琐碎细节
## 示例
- 用户注册 → 登录 → 操作 → 退出 完整流程
- 订单创建 → 支付 → 状态变更 完整链路

18
test/index.md Normal file
View File

@ -0,0 +1,18 @@
# test — 测试代码
本目录存放项目的全部测试代码。测试代码的组织结构应**镜像** `src/` 的结构,便于定位。
## 目录说明
| 子目录 | 用途 | 特点 |
|--------|------|------|
| `unit/` | 单元测试 | 测试最小单元(函数、方法),不依赖外部系统 |
| `integration/` | 集成测试 | 测试模块间交互或与外部系统数据库、API的集成 |
| `e2e/` | 端到端测试 | 模拟用户行为,从入口到出口的完整链路 |
## 测试原则
1. **先写测试,再写代码**(推荐 TDD
2. 每个 bug 修复都应伴随对应的回归测试
3. 测试用例命名应清晰描述测试场景:`<被测方法>_<场景>_<预期结果>`
4. 保持测试独立:单个测试不应依赖其他测试的运行顺序

15
test/integration/index.md Normal file
View File

@ -0,0 +1,15 @@
# test/integration — 集成测试
测试多个模块之间的协作或与外部系统数据库、消息队列、API的集成。
## 原则
- **真实交互** — 连接真实的(或测试专用)外部系统
- **关键路径** — 优先覆盖核心业务流程
- **可重复** — 每次运行结果一致(注意数据准备和清理)
## 示例
- 数据库读写操作的正确性
- API 请求/响应完整链路
- 消息队列的生产/消费

View File

@ -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<AmmunitionSpec[]>(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 == "活性材料弹");
}
}
}

View File

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\CounterDrone.Core\CounterDrone.Core.csproj" />
</ItemGroup>
</Project>

View File

@ -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<int>(
$"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<int>(
"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();
}
}
}

View File

@ -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));
}
}
}

View File

@ -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<ArgumentException>(() =>
_service.CreateGroup("", GroupType.DroneFleet, ""));
}
[Fact]
public void GetGroups_WithTypeFilter()
{
_service.CreateGroup("Fleet A", GroupType.DroneFleet, "");
_service.CreateGroup("Fleet B", GroupType.DroneFleet, "");
_service.CreateGroup("Equip X", GroupType.EquipmentGroup, "");
var fleets = _service.GetGroups(GroupType.DroneFleet);
Assert.Equal(2, fleets.Count);
var all = _service.GetGroups(null);
Assert.Equal(3, all.Count);
}
[Fact]
public void DeleteGroup_RemovesGroup()
{
var group = _service.CreateGroup("DeleteMe", GroupType.DroneFleet, "");
_service.DeleteGroup(group.Id);
Assert.Null(_service.GetGroup(group.Id));
}
[Fact]
public void GetGroup_ById_ReturnsCorrectGroup()
{
var created = _service.CreateGroup("Target", GroupType.EquipmentGroup, "desc");
var fetched = _service.GetGroup(created.Id);
Assert.Equal("Target", fetched.Name);
Assert.Equal("desc", fetched.Description);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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<ArgumentException>(() => _service.ImportModel(filePath, ""));
}
[Fact]
public void ImportModel_UnsupportedFormat_Throws()
{
var filePath = CreateTempModelFile(".xyz");
Assert.Throws<ArgumentException>(() => _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"));
}
}
}

17
test/unit/index.md Normal file
View File

@ -0,0 +1,17 @@
# test/unit — 单元测试
测试最小单元:单个函数、方法、类。不依赖网络、数据库、文件系统等外部资源。
## 原则
- **快速** — 毫秒级完成,可频繁运行
- **隔离** — 不依赖外部系统,使用 Mock / Stub 替代
- **全覆盖** — 正常路径、异常路径、边界条件
## 命名示例
```
UserService.test.ts
loginHandler_test.go
test_calculator.py
```