From b33a6a87e11750addcea0a65e83186b61acc9768 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Thu, 11 Jun 2026 14:13:35 +0800 Subject: [PATCH] =?UTF-8?q?Phase=202:=20=E6=83=B3=E5=AE=9A=E7=AE=A1?= =?UTF-8?q?=E7=90=86=20=E2=80=94=208=E4=B8=AARepository=20+=20ScenarioServ?= =?UTF-8?q?ice=EF=BC=88CRUD/5=E6=AD=A5=E4=BF=9D=E5=AD=98/=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E5=88=86=E9=A1=B5=EF=BC=89=EF=BC=8C47=E4=B8=AA?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E5=85=A8=E9=83=A8=E9=80=9A=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Repository/CloudDispersalRepository.cs | 10 + .../Repository/CombatSceneRepository.cs | 10 + .../Repository/ControlZoneRepository.cs | 27 + .../EquipmentDeploymentRepository.cs | 24 + .../Repository/RoutePlanRepository.cs | 10 + .../Repository/SimTaskRepository.cs | 34 ++ .../Repository/TargetConfigRepository.cs | 24 + .../Repository/WaypointRepository.cs | 27 + .../Services/IScenarioService.cs | 21 + .../Services/ScenarioService.cs | 222 ++++++++ .../ScenarioServiceTests.cs | 483 ++++++++++++++++++ 11 files changed, 892 insertions(+) create mode 100644 src/CounterDrone.Core/Repository/CloudDispersalRepository.cs create mode 100644 src/CounterDrone.Core/Repository/CombatSceneRepository.cs create mode 100644 src/CounterDrone.Core/Repository/ControlZoneRepository.cs create mode 100644 src/CounterDrone.Core/Repository/EquipmentDeploymentRepository.cs create mode 100644 src/CounterDrone.Core/Repository/RoutePlanRepository.cs create mode 100644 src/CounterDrone.Core/Repository/SimTaskRepository.cs create mode 100644 src/CounterDrone.Core/Repository/TargetConfigRepository.cs create mode 100644 src/CounterDrone.Core/Repository/WaypointRepository.cs create mode 100644 src/CounterDrone.Core/Services/IScenarioService.cs create mode 100644 src/CounterDrone.Core/Services/ScenarioService.cs create mode 100644 test/unit/CounterDrone.Core.Tests/ScenarioServiceTests.cs diff --git a/src/CounterDrone.Core/Repository/CloudDispersalRepository.cs b/src/CounterDrone.Core/Repository/CloudDispersalRepository.cs new file mode 100644 index 0000000..71e37ba --- /dev/null +++ b/src/CounterDrone.Core/Repository/CloudDispersalRepository.cs @@ -0,0 +1,10 @@ +using CounterDrone.Core.Models; +using SQLite; + +namespace CounterDrone.Core.Repository +{ + public class CloudDispersalRepository : BaseRepository + { + public CloudDispersalRepository(SQLiteConnection db) : base(db) { } + } +} diff --git a/src/CounterDrone.Core/Repository/CombatSceneRepository.cs b/src/CounterDrone.Core/Repository/CombatSceneRepository.cs new file mode 100644 index 0000000..9ee52ec --- /dev/null +++ b/src/CounterDrone.Core/Repository/CombatSceneRepository.cs @@ -0,0 +1,10 @@ +using CounterDrone.Core.Models; +using SQLite; + +namespace CounterDrone.Core.Repository +{ + public class CombatSceneRepository : BaseRepository + { + public CombatSceneRepository(SQLiteConnection db) : base(db) { } + } +} diff --git a/src/CounterDrone.Core/Repository/ControlZoneRepository.cs b/src/CounterDrone.Core/Repository/ControlZoneRepository.cs new file mode 100644 index 0000000..ab5dcf3 --- /dev/null +++ b/src/CounterDrone.Core/Repository/ControlZoneRepository.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Linq; +using CounterDrone.Core.Models; +using SQLite; + +namespace CounterDrone.Core.Repository +{ + public class ControlZoneRepository : BaseRepository + { + public ControlZoneRepository(SQLiteConnection db) : base(db) { } + + public List GetByTaskId(string taskId) + { + return Db.Table() + .Where(z => z.TaskId == taskId) + .OrderBy(z => z.OrderIndex) + .ToList(); + } + + public void DeleteByTaskId(string taskId) + { + var zones = GetByTaskId(taskId); + foreach (var z in zones) + Db.Delete(z); + } + } +} diff --git a/src/CounterDrone.Core/Repository/EquipmentDeploymentRepository.cs b/src/CounterDrone.Core/Repository/EquipmentDeploymentRepository.cs new file mode 100644 index 0000000..555f124 --- /dev/null +++ b/src/CounterDrone.Core/Repository/EquipmentDeploymentRepository.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Linq; +using CounterDrone.Core.Models; +using SQLite; + +namespace CounterDrone.Core.Repository +{ + public class EquipmentDeploymentRepository : BaseRepository + { + public EquipmentDeploymentRepository(SQLiteConnection db) : base(db) { } + + public List GetByTaskId(string taskId) + { + return Db.Table().Where(e => e.TaskId == taskId).ToList(); + } + + public void DeleteByTaskId(string taskId) + { + var equips = GetByTaskId(taskId); + foreach (var e in equips) + Db.Delete(e); + } + } +} diff --git a/src/CounterDrone.Core/Repository/RoutePlanRepository.cs b/src/CounterDrone.Core/Repository/RoutePlanRepository.cs new file mode 100644 index 0000000..a09cf2d --- /dev/null +++ b/src/CounterDrone.Core/Repository/RoutePlanRepository.cs @@ -0,0 +1,10 @@ +using CounterDrone.Core.Models; +using SQLite; + +namespace CounterDrone.Core.Repository +{ + public class RoutePlanRepository : BaseRepository + { + public RoutePlanRepository(SQLiteConnection db) : base(db) { } + } +} diff --git a/src/CounterDrone.Core/Repository/SimTaskRepository.cs b/src/CounterDrone.Core/Repository/SimTaskRepository.cs new file mode 100644 index 0000000..65822cd --- /dev/null +++ b/src/CounterDrone.Core/Repository/SimTaskRepository.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Linq; +using CounterDrone.Core.Models; +using SQLite; + +namespace CounterDrone.Core.Repository +{ + public class SimTaskRepository : BaseRepository + { + public SimTaskRepository(SQLiteConnection db) : base(db) { } + + public SimTask GetByTaskNumber(string taskNumber) + { + return Db.Table().FirstOrDefault(t => t.TaskNumber == taskNumber); + } + + public List Search(string keyword, string? dateFrom, string? dateTo, int offset, int limit, out int totalCount) + { + var query = Db.Table().AsQueryable(); + + if (!string.IsNullOrWhiteSpace(keyword)) + query = query.Where(t => t.Name.Contains(keyword) || t.TaskNumber.Contains(keyword)); + + if (!string.IsNullOrWhiteSpace(dateFrom)) + query = query.Where(t => t.CreatedAt.CompareTo(dateFrom) >= 0); + + if (!string.IsNullOrWhiteSpace(dateTo)) + query = query.Where(t => t.CreatedAt.CompareTo(dateTo) <= 0); + + totalCount = query.Count(); + return query.OrderByDescending(t => t.CreatedAt).Skip(offset).Take(limit).ToList(); + } + } +} diff --git a/src/CounterDrone.Core/Repository/TargetConfigRepository.cs b/src/CounterDrone.Core/Repository/TargetConfigRepository.cs new file mode 100644 index 0000000..0129183 --- /dev/null +++ b/src/CounterDrone.Core/Repository/TargetConfigRepository.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Linq; +using CounterDrone.Core.Models; +using SQLite; + +namespace CounterDrone.Core.Repository +{ + public class TargetConfigRepository : BaseRepository + { + public TargetConfigRepository(SQLiteConnection db) : base(db) { } + + public List GetByTaskId(string taskId) + { + return Db.Table().Where(t => t.TaskId == taskId).ToList(); + } + + public void DeleteByTaskId(string taskId) + { + var targets = GetByTaskId(taskId); + foreach (var t in targets) + Db.Delete(t); + } + } +} diff --git a/src/CounterDrone.Core/Repository/WaypointRepository.cs b/src/CounterDrone.Core/Repository/WaypointRepository.cs new file mode 100644 index 0000000..7ca00cf --- /dev/null +++ b/src/CounterDrone.Core/Repository/WaypointRepository.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Linq; +using CounterDrone.Core.Models; +using SQLite; + +namespace CounterDrone.Core.Repository +{ + public class WaypointRepository : BaseRepository + { + public WaypointRepository(SQLiteConnection db) : base(db) { } + + public List GetByTaskId(string taskId) + { + return Db.Table() + .Where(w => w.TaskId == taskId) + .OrderBy(w => w.OrderIndex) + .ToList(); + } + + public void DeleteByTaskId(string taskId) + { + var wps = GetByTaskId(taskId); + foreach (var w in wps) + Db.Delete(w); + } + } +} diff --git a/src/CounterDrone.Core/Services/IScenarioService.cs b/src/CounterDrone.Core/Services/IScenarioService.cs new file mode 100644 index 0000000..3386f1c --- /dev/null +++ b/src/CounterDrone.Core/Services/IScenarioService.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using CounterDrone.Core.Models; + +namespace CounterDrone.Core.Services +{ + public interface IScenarioService + { + SimTask CreateTask(string name, string taskNumber); + void DeleteTask(string id); + PagedResult SearchTasks(string keyword, string? dateFrom, string? dateTo, int page, int pageSize); + TaskFullConfig? GetTaskDetail(string id); + + void SaveScene(string taskId, CombatScene scene); + void SaveControlZones(string taskId, List zones); + 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 waypoints); + void UpdateStep(string taskId, int step); + } +} diff --git a/src/CounterDrone.Core/Services/ScenarioService.cs b/src/CounterDrone.Core/Services/ScenarioService.cs new file mode 100644 index 0000000..e64cd14 --- /dev/null +++ b/src/CounterDrone.Core/Services/ScenarioService.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections.Generic; +using CounterDrone.Core.Models; +using CounterDrone.Core.Repository; + +namespace CounterDrone.Core.Services +{ + public class ScenarioService : IScenarioService + { + private readonly SimTaskRepository _taskRepo; + private readonly CombatSceneRepository _sceneRepo; + private readonly ControlZoneRepository _zoneRepo; + private readonly TargetConfigRepository _targetRepo; + private readonly EquipmentDeploymentRepository _equipRepo; + private readonly CloudDispersalRepository _cloudRepo; + private readonly RoutePlanRepository _routeRepo; + private readonly WaypointRepository _waypointRepo; + + public ScenarioService( + SimTaskRepository taskRepo, + CombatSceneRepository sceneRepo, + ControlZoneRepository zoneRepo, + TargetConfigRepository targetRepo, + EquipmentDeploymentRepository equipRepo, + CloudDispersalRepository cloudRepo, + RoutePlanRepository routeRepo, + WaypointRepository waypointRepo) + { + _taskRepo = taskRepo; + _sceneRepo = sceneRepo; + _zoneRepo = zoneRepo; + _targetRepo = targetRepo; + _equipRepo = equipRepo; + _cloudRepo = cloudRepo; + _routeRepo = routeRepo; + _waypointRepo = waypointRepo; + } + + // ========== Task CRUD ========== + + public SimTask CreateTask(string name, string taskNumber) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentException("任务名称不能为空"); + + if (string.IsNullOrWhiteSpace(taskNumber)) + taskNumber = GenerateTaskNumber(); + + var existing = _taskRepo.GetByTaskNumber(taskNumber); + if (existing != null) + throw new ArgumentException($"任务编号 {taskNumber} 已存在"); + + var task = new SimTask + { + Name = name, + TaskNumber = taskNumber, + Status = (int)TaskStatus.Configuring, + CurrentStep = 1, + }; + + _taskRepo.Insert(task); + return task; + } + + public void DeleteTask(string id) + { + _taskRepo.Delete(id); + } + + public PagedResult SearchTasks(string keyword, string? dateFrom, string? dateTo, int page, int pageSize) + { + if (page < 1) page = 1; + if (pageSize < 1) pageSize = 10; + var offset = (page - 1) * pageSize; + + var items = _taskRepo.Search(keyword, dateFrom, dateTo, offset, pageSize, out var total); + + return new PagedResult + { + Items = items, + TotalCount = total, + Page = page, + PageSize = pageSize, + }; + } + + public TaskFullConfig? GetTaskDetail(string id) + { + var task = _taskRepo.GetById(id); + if (task == null) return null; + + return new TaskFullConfig + { + Task = task, + Scene = _sceneRepo.GetById(id) ?? new CombatScene { TaskId = id }, + ControlZones = _zoneRepo.GetByTaskId(id), + Targets = _targetRepo.GetByTaskId(id), + Equipment = _equipRepo.GetByTaskId(id), + Cloud = _cloudRepo.GetById(id) ?? new CloudDispersal { TaskId = id }, + Route = _routeRepo.GetById(id) ?? new RoutePlan { TaskId = id }, + Waypoints = _waypointRepo.GetByTaskId(id), + }; + } + + // ========== Step Save ========== + + public void SaveScene(string taskId, CombatScene scene) + { + scene.TaskId = taskId; + var existing = _sceneRepo.GetById(taskId); + if (existing != null) + _sceneRepo.Update(scene); + else + _sceneRepo.Insert(scene); + TouchTask(taskId); + } + + public void SaveControlZones(string taskId, List zones) + { + _zoneRepo.DeleteByTaskId(taskId); + for (int i = 0; i < zones.Count; i++) + { + zones[i].TaskId = taskId; + zones[i].OrderIndex = i; + } + _zoneRepo.InsertAll(zones); + TouchTask(taskId); + } + + public void SaveTarget(string taskId, TargetConfig target) + { + target.TaskId = taskId; + if (string.IsNullOrEmpty(target.Id)) + target.Id = Guid.NewGuid().ToString(); + + var existing = _targetRepo.GetById(target.Id); + if (existing != null) + _targetRepo.Update(target); + else + _targetRepo.Insert(target); + TouchTask(taskId); + } + + public void SaveDeployment(string taskId, List equips) + { + _equipRepo.DeleteByTaskId(taskId); + foreach (var e in equips) + { + e.TaskId = taskId; + if (string.IsNullOrEmpty(e.Id)) + e.Id = Guid.NewGuid().ToString(); + } + _equipRepo.InsertAll(equips); + TouchTask(taskId); + } + + public void SaveCloudDispersal(string taskId, CloudDispersal cloud) + { + cloud.TaskId = taskId; + var existing = _cloudRepo.GetById(taskId); + if (existing != null) + _cloudRepo.Update(cloud); + else + _cloudRepo.Insert(cloud); + TouchTask(taskId); + } + + public void SaveRoute(string taskId, RoutePlan route, List waypoints) + { + route.TaskId = taskId; + var existingRoute = _routeRepo.GetById(taskId); + if (existingRoute != null) + _routeRepo.Update(route); + else + _routeRepo.Insert(route); + + _waypointRepo.DeleteByTaskId(taskId); + for (int i = 0; i < waypoints.Count; i++) + { + waypoints[i].TaskId = taskId; + waypoints[i].OrderIndex = i; + if (string.IsNullOrEmpty(waypoints[i].Id)) + waypoints[i].Id = Guid.NewGuid().ToString(); + } + _waypointRepo.InsertAll(waypoints); + TouchTask(taskId); + } + + public void UpdateStep(string taskId, int step) + { + var task = _taskRepo.GetById(taskId); + if (task == null) return; + + if (step < 1) step = 1; + if (step > 5) step = 5; + + task.CurrentStep = step; + _taskRepo.Update(task); + } + + // ========== Helpers ========== + + private void TouchTask(string taskId) + { + var task = _taskRepo.GetById(taskId); + if (task != null) + { + task.UpdatedAt = DateTime.UtcNow.ToString("o"); + _taskRepo.Update(task); + } + } + + public static string GenerateTaskNumber() + { + var now = DateTime.Now; + var datePart = now.ToString("yyyyMMdd"); + var random = new Random(); + var seq = random.Next(1, 1000); + return $"SIM-{datePart}-{seq:D3}"; + } + } +} diff --git a/test/unit/CounterDrone.Core.Tests/ScenarioServiceTests.cs b/test/unit/CounterDrone.Core.Tests/ScenarioServiceTests.cs new file mode 100644 index 0000000..599fbf5 --- /dev/null +++ b/test/unit/CounterDrone.Core.Tests/ScenarioServiceTests.cs @@ -0,0 +1,483 @@ +using System; +using System.Collections.Generic; +using System.IO; +using CounterDrone.Core; +using CounterDrone.Core.Models; +using CounterDrone.Core.Repository; +using CounterDrone.Core.Services; +using SQLite; +using Xunit; +using TaskStatus = CounterDrone.Core.Models.TaskStatus; + +namespace CounterDrone.Core.Tests +{ + public class ScenarioServiceTests : IDisposable + { + private readonly string _testDir; + private readonly SQLiteConnection _db; + private readonly IScenarioService _service; + + public ScenarioServiceTests() + { + _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 ScenarioService( + new SimTaskRepository(_db), + new CombatSceneRepository(_db), + new ControlZoneRepository(_db), + new TargetConfigRepository(_db), + new EquipmentDeploymentRepository(_db), + new CloudDispersalRepository(_db), + new RoutePlanRepository(_db), + new WaypointRepository(_db)); + } + + public void Dispose() + { + _db?.Close(); + if (Directory.Exists(_testDir)) + Directory.Delete(_testDir, true); + } + + // ========== Task CRUD ========== + + [Fact] + public void CreateTask_Valid_ReturnsTask() + { + var task = _service.CreateTask("测试任务", "SIM-20260611-001"); + + Assert.NotNull(task); + Assert.Equal("测试任务", task.Name); + Assert.Equal("SIM-20260611-001", task.TaskNumber); + Assert.Equal((int)TaskStatus.Configuring, task.Status); + Assert.Equal(1, task.CurrentStep); + Assert.NotEmpty(task.Id); + } + + [Fact] + public void CreateTask_AutoGeneratesTaskNumber() + { + var task = _service.CreateTask("自动编号", ""); + Assert.StartsWith("SIM-", task.TaskNumber); + } + + [Fact] + public void CreateTask_EmptyName_Throws() + { + Assert.Throws(() => + _service.CreateTask("", "SIM-001")); + } + + [Fact] + public void CreateTask_DuplicateNumber_Throws() + { + _service.CreateTask("Task A", "SIM-001"); + Assert.Throws(() => + _service.CreateTask("Task B", "SIM-001")); + } + + [Fact] + public void DeleteTask_RemovesTask() + { + var task = _service.CreateTask("ToDelete", ""); + _service.DeleteTask(task.Id); + + var detail = _service.GetTaskDetail(task.Id); + Assert.Null(detail); + } + + [Fact] + public void GetTaskDetail_ReturnsFullConfig() + { + var task = _service.CreateTask("Full Config", ""); + + // 保存各步骤配置 + _service.SaveScene(task.Id, new CombatScene + { + SceneType = (int)SceneType.Mountain, + WindSpeed = 10.0, + Temperature = 25.0, + }); + + _service.SaveTarget(task.Id, new TargetConfig + { + TargetType = (int)TargetType.FixedWing, + Quantity = 3, + }); + + _service.SaveCloudDispersal(task.Id, new CloudDispersal + { + AerosolType = (int)AerosolType.ActiveMaterial, + Duration = 45.0, + }); + + _service.UpdateStep(task.Id, 4); + + // 读取完整配置 + var detail = _service.GetTaskDetail(task.Id); + + Assert.Equal("Full Config", detail.Task.Name); + Assert.Equal(4, detail.Task.CurrentStep); + Assert.Equal((int)SceneType.Mountain, detail.Scene.SceneType); + Assert.Equal(10.0, detail.Scene.WindSpeed); + Assert.Equal(3, detail.Targets[0].Quantity); + Assert.Equal((int)AerosolType.ActiveMaterial, detail.Cloud.AerosolType); + } + + // ========== Step Save ========== + + [Fact] + public void SaveScene_FirstTime_Inserts() + { + var task = _service.CreateTask("Scene Test", ""); + _service.SaveScene(task.Id, new CombatScene + { + WeatherType = (int)WeatherType.Fog, + Visibility = 2000.0, + TimeOfDay = "03:00", + }); + + var detail = _service.GetTaskDetail(task.Id); + Assert.Equal((int)WeatherType.Fog, detail.Scene.WeatherType); + Assert.Equal(2000.0, detail.Scene.Visibility); + Assert.Equal("03:00", detail.Scene.TimeOfDay); + } + + [Fact] + public void SaveScene_SecondTime_Updates() + { + var task = _service.CreateTask("Update Scene", ""); + _service.SaveScene(task.Id, new CombatScene { WindSpeed = 5.0 }); + _service.SaveScene(task.Id, new CombatScene { WindSpeed = 15.0 }); + + var detail = _service.GetTaskDetail(task.Id); + Assert.Equal(15.0, detail.Scene.WindSpeed); + } + + [Fact] + public void SaveControlZones_SavesAndRetrieves() + { + var task = _service.CreateTask("Zone Test", ""); + var zones = new List + { + new ControlZone { Name = "禁区A", VerticesJson = "[{\"x\":0,\"y\":0,\"z\":0},{\"x\":100,\"y\":0,\"z\":0},{\"x\":100,\"y\":100,\"z\":0}]", MinAltitude = 0, MaxAltitude = 500 }, + new ControlZone { Name = "禁区B", VerticesJson = "[{\"x\":200,\"y\":200,\"z\":0}]", MinAltitude = 100, MaxAltitude = 300 }, + }; + + _service.SaveControlZones(task.Id, zones); + + var detail = _service.GetTaskDetail(task.Id); + Assert.Equal(2, detail.ControlZones.Count); + Assert.Equal("禁区A", detail.ControlZones[0].Name); + Assert.Equal(0, detail.ControlZones[0].OrderIndex); + Assert.Equal("禁区B", detail.ControlZones[1].Name); + Assert.Equal(1, detail.ControlZones[1].OrderIndex); + } + + [Fact] + public void SaveControlZones_ReplaceExisting() + { + var task = _service.CreateTask("Zone Replace", ""); + _service.SaveControlZones(task.Id, new List + { + new ControlZone { Name = "Old" } + }); + _service.SaveControlZones(task.Id, new List + { + new ControlZone { Name = "New1" }, + new ControlZone { Name = "New2" }, + }); + + var detail = _service.GetTaskDetail(task.Id); + Assert.Equal(2, detail.ControlZones.Count); + } + + [Fact] + public void SaveTarget_SavesCorrectly() + { + var task = _service.CreateTask("Target Test", ""); + _service.SaveTarget(task.Id, new TargetConfig + { + TargetType = (int)TargetType.Piston, + Quantity = 5, + PowerType = (int)PowerType.Piston, + Wingspan = 2.5, + TypicalSpeed = 120.0, + TypicalAltitude = 800.0, + }); + + var detail = _service.GetTaskDetail(task.Id); + var target = detail.Targets[0]; + Assert.Equal((int)TargetType.Piston, target.TargetType); + Assert.Equal(5, target.Quantity); + Assert.Equal(120.0, target.TypicalSpeed); + } + + [Fact] + public void SaveDeployment_SavesMultiple() + { + var task = _service.CreateTask("Equip Test", ""); + var equips = new List + { + new EquipmentDeployment + { + EquipmentRole = (int)EquipmentRole.LaunchPlatform, + PlatformType = (int)PlatformType.GroundBased, + Quantity = 2, + PositionX = 1000, PositionY = 0, PositionZ = 50, + MuzzleVelocity = 800.0, + MunitionCount = 3, + }, + new EquipmentDeployment + { + EquipmentRole = (int)EquipmentRole.Detection, + Quantity = 1, + DetectionRadius = 5000.0, + }, + }; + + _service.SaveDeployment(task.Id, equips); + + var detail = _service.GetTaskDetail(task.Id); + Assert.Equal(2, detail.Equipment.Count); + + var platform = detail.Equipment.Find(e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform); + Assert.NotNull(platform); + Assert.Equal(2, platform.Quantity); + Assert.Equal(800.0, platform.MuzzleVelocity); + } + + [Fact] + public void SaveRoute_SavesWaypoints() + { + var task = _service.CreateTask("Route Test", ""); + var route = new RoutePlan + { + FormationMode = (int)FormationMode.Formation, + FormationSpacing = 60.0, + }; + var waypoints = new List + { + new Waypoint { PosX = 0, PosY = 0, PosZ = 100, Altitude = 500, Speed = 100 }, + new Waypoint { PosX = 5000, PosY = 0, PosZ = 100, Altitude = 500, Speed = 100 }, + new Waypoint { PosX = 10000, PosY = 0, PosZ = 100, Altitude = 300, Speed = 80 }, + }; + + _service.SaveRoute(task.Id, route, waypoints); + + var detail = _service.GetTaskDetail(task.Id); + Assert.Equal((int)FormationMode.Formation, detail.Route.FormationMode); + Assert.Equal(3, detail.Waypoints.Count); + Assert.Equal(0, detail.Waypoints[0].OrderIndex); + Assert.Equal(2, detail.Waypoints[2].OrderIndex); + Assert.Equal(5000.0, detail.Waypoints[1].PosX); + } + + // ========== Search & Pagination ========== + + [Fact] + public void SearchTasks_KeywordFilter() + { + _service.CreateTask("城市防御演习", ""); + _service.CreateTask("平原拦截测试", ""); + _service.CreateTask("海岸边防演练", ""); + + var result = _service.SearchTasks("城市", null, null, 1, 10); + Assert.Equal(1, result.TotalCount); + Assert.Equal("城市防御演习", result.Items[0].Name); + } + + [Fact] + public void SearchTasks_NoResults() + { + _service.CreateTask("Task A", ""); + var result = _service.SearchTasks("不存在的任务", null, null, 1, 10); + Assert.Equal(0, result.TotalCount); + } + + [Fact] + public void SearchTasks_Pagination() + { + for (int i = 0; i < 15; i++) + _service.CreateTask($"Task {i:D2}", ""); + + var page1 = _service.SearchTasks(null, null, null, 1, 5); + Assert.Equal(15, page1.TotalCount); + Assert.Equal(5, page1.Items.Count); + Assert.Equal(3, page1.TotalPages); + + var page3 = _service.SearchTasks(null, null, null, 3, 5); + Assert.Equal(5, page3.Items.Count); + } + + [Fact] + public void SearchTasks_DateRangeFilter() + { + // All tasks created today, so date filter should include them + _service.CreateTask("Recent Task", ""); + + var yesterday = DateTime.UtcNow.AddDays(-1).ToString("yyyy-MM-dd"); + var tomorrow = DateTime.UtcNow.AddDays(1).ToString("yyyy-MM-dd"); + + var result = _service.SearchTasks(null, yesterday, tomorrow, 1, 10); + Assert.Equal(1, result.TotalCount); + + var oldResult = _service.SearchTasks(null, "2020-01-01", "2020-12-31", 1, 10); + Assert.Equal(0, oldResult.TotalCount); + } + + // ========== UpdateStep ========== + + [Fact] + public void UpdateStep_ValidSteps() + { + var task = _service.CreateTask("Step Test", ""); + Assert.Equal(1, task.CurrentStep); + + _service.UpdateStep(task.Id, 3); + var detail = _service.GetTaskDetail(task.Id); + Assert.Equal(3, detail.Task.CurrentStep); + + _service.UpdateStep(task.Id, 5); + detail = _service.GetTaskDetail(task.Id); + Assert.Equal(5, detail.Task.CurrentStep); + } + + [Fact] + public void UpdateStep_ClampedToRange() + { + var task = _service.CreateTask("Clamp Test", ""); + + _service.UpdateStep(task.Id, 0); + Assert.Equal(1, _service.GetTaskDetail(task.Id).Task.CurrentStep); + + _service.UpdateStep(task.Id, 10); + Assert.Equal(5, _service.GetTaskDetail(task.Id).Task.CurrentStep); + } + + // ========== Full Workflow ========== + + [Fact] + public void FullWorkflow_CreateConfigureSearch() + { + // 1. 创建任务 + var task = _service.CreateTask("完整流程测试", ""); + Assert.NotNull(task); + + // 2. 步骤1:作战场景 + _service.SaveScene(task.Id, new CombatScene + { + SceneType = (int)SceneType.Urban, + WeatherType = (int)WeatherType.Rain, + WindSpeed = 8.0, + WindDirection = (int)WindDirection.SE, + Temperature = 18.0, + TimeOfDay = "14:00", + }); + _service.UpdateStep(task.Id, 1); + + // 3. 步骤1扩展:管控区域 + _service.SaveControlZones(task.Id, new List + { + new ControlZone + { + Name = "核心区", + VerticesJson = "[{\"x\":0,\"y\":0,\"z\":0},{\"x\":500,\"y\":0,\"z\":0},{\"x\":500,\"y\":500,\"z\":0},{\"x\":0,\"y\":500,\"z\":0}]", + MinAltitude = 0, MaxAltitude = 1000, + } + }); + + // 4. 步骤2:目标配置 + _service.SaveTarget(task.Id, new TargetConfig + { + TargetType = (int)TargetType.HighSpeed, + Quantity = 2, + PowerType = (int)PowerType.Jet, + TypicalSpeed = 300.0, + }); + _service.UpdateStep(task.Id, 2); + + // 5. 步骤3:装备部署 + _service.SaveDeployment(task.Id, new List + { + new EquipmentDeployment + { + EquipmentRole = (int)EquipmentRole.Detection, + Quantity = 1, + DetectionRadius = 6000.0, + }, + new EquipmentDeployment + { + EquipmentRole = (int)EquipmentRole.LaunchPlatform, + PlatformType = (int)PlatformType.GroundBased, + Quantity = 3, + PositionX = 800, PositionY = 0, PositionZ = 50, + MuzzleVelocity = 850.0, + MunitionCount = 2, + }, + }); + _service.UpdateStep(task.Id, 3); + + // 6. 步骤4:云团抛撒 + _service.SaveCloudDispersal(task.Id, new CloudDispersal + { + AerosolType = (int)AerosolType.ActiveMaterial, + TriggerMode = (int)TriggerMode.Area, + Duration = 50.0, + PositionX = 3000, PositionY = 0, PositionZ = 300, + }); + _service.UpdateStep(task.Id, 4); + + // 7. 步骤5:航路规划 + _service.SaveRoute(task.Id, new RoutePlan + { + FormationMode = (int)FormationMode.Swarm, + FormationSpacing = 30.0, + }, new List + { + new Waypoint { PosX = 0, PosY = 100, PosZ = 200, Altitude = 600, Speed = 250 }, + new Waypoint { PosX = 10000, PosY = 100, PosZ = 200, Altitude = 600, Speed = 250 }, + }); + _service.UpdateStep(task.Id, 5); + + // 8. 验证完整配置 + var detail = _service.GetTaskDetail(task.Id); + + Assert.Equal("完整流程测试", detail.Task.Name); + Assert.Equal(5, detail.Task.CurrentStep); + Assert.Equal((int)TaskStatus.Configuring, detail.Task.Status); + + // 场景 + Assert.Equal((int)SceneType.Urban, detail.Scene.SceneType); + Assert.Equal((int)WeatherType.Rain, detail.Scene.WeatherType); + Assert.Equal("14:00", detail.Scene.TimeOfDay); + + // 管控区域 + Assert.Single(detail.ControlZones); + Assert.Equal("核心区", detail.ControlZones[0].Name); + + // 目标 + Assert.Single(detail.Targets); + Assert.Equal(2, detail.Targets[0].Quantity); + Assert.Equal(300.0, detail.Targets[0].TypicalSpeed); + + // 装备 + Assert.Equal(2, detail.Equipment.Count); + + // 云团 + Assert.Equal((int)AerosolType.ActiveMaterial, detail.Cloud.AerosolType); + Assert.Equal(50.0, detail.Cloud.Duration); + + // 航路 + Assert.Equal((int)FormationMode.Swarm, detail.Route.FormationMode); + Assert.Equal(2, detail.Waypoints.Count); + Assert.Equal(250.0, detail.Waypoints[0].Speed); + + // 9. 搜索验证 + var searchResult = _service.SearchTasks("完整流程", null, null, 1, 10); + Assert.Equal(1, searchResult.TotalCount); + } + } +}