Phase 2: 想定管理 — 8个Repository + ScenarioService(CRUD/5步保存/搜索分页),47个测试全部通过
This commit is contained in:
parent
b23291c5bf
commit
b33a6a87e1
10
src/CounterDrone.Core/Repository/CloudDispersalRepository.cs
Normal file
10
src/CounterDrone.Core/Repository/CloudDispersalRepository.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using CounterDrone.Core.Models;
|
||||
using SQLite;
|
||||
|
||||
namespace CounterDrone.Core.Repository
|
||||
{
|
||||
public class CloudDispersalRepository : BaseRepository<CloudDispersal>
|
||||
{
|
||||
public CloudDispersalRepository(SQLiteConnection db) : base(db) { }
|
||||
}
|
||||
}
|
||||
10
src/CounterDrone.Core/Repository/CombatSceneRepository.cs
Normal file
10
src/CounterDrone.Core/Repository/CombatSceneRepository.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using CounterDrone.Core.Models;
|
||||
using SQLite;
|
||||
|
||||
namespace CounterDrone.Core.Repository
|
||||
{
|
||||
public class CombatSceneRepository : BaseRepository<CombatScene>
|
||||
{
|
||||
public CombatSceneRepository(SQLiteConnection db) : base(db) { }
|
||||
}
|
||||
}
|
||||
27
src/CounterDrone.Core/Repository/ControlZoneRepository.cs
Normal file
27
src/CounterDrone.Core/Repository/ControlZoneRepository.cs
Normal file
@ -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<ControlZone>
|
||||
{
|
||||
public ControlZoneRepository(SQLiteConnection db) : base(db) { }
|
||||
|
||||
public List<ControlZone> GetByTaskId(string taskId)
|
||||
{
|
||||
return Db.Table<ControlZone>()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<EquipmentDeployment>
|
||||
{
|
||||
public EquipmentDeploymentRepository(SQLiteConnection db) : base(db) { }
|
||||
|
||||
public List<EquipmentDeployment> GetByTaskId(string taskId)
|
||||
{
|
||||
return Db.Table<EquipmentDeployment>().Where(e => e.TaskId == taskId).ToList();
|
||||
}
|
||||
|
||||
public void DeleteByTaskId(string taskId)
|
||||
{
|
||||
var equips = GetByTaskId(taskId);
|
||||
foreach (var e in equips)
|
||||
Db.Delete(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/CounterDrone.Core/Repository/RoutePlanRepository.cs
Normal file
10
src/CounterDrone.Core/Repository/RoutePlanRepository.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using CounterDrone.Core.Models;
|
||||
using SQLite;
|
||||
|
||||
namespace CounterDrone.Core.Repository
|
||||
{
|
||||
public class RoutePlanRepository : BaseRepository<RoutePlan>
|
||||
{
|
||||
public RoutePlanRepository(SQLiteConnection db) : base(db) { }
|
||||
}
|
||||
}
|
||||
34
src/CounterDrone.Core/Repository/SimTaskRepository.cs
Normal file
34
src/CounterDrone.Core/Repository/SimTaskRepository.cs
Normal file
@ -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<SimTask>
|
||||
{
|
||||
public SimTaskRepository(SQLiteConnection db) : base(db) { }
|
||||
|
||||
public SimTask GetByTaskNumber(string taskNumber)
|
||||
{
|
||||
return Db.Table<SimTask>().FirstOrDefault(t => t.TaskNumber == taskNumber);
|
||||
}
|
||||
|
||||
public List<SimTask> Search(string keyword, string? dateFrom, string? dateTo, int offset, int limit, out int totalCount)
|
||||
{
|
||||
var query = Db.Table<SimTask>().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();
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/CounterDrone.Core/Repository/TargetConfigRepository.cs
Normal file
24
src/CounterDrone.Core/Repository/TargetConfigRepository.cs
Normal file
@ -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<TargetConfig>
|
||||
{
|
||||
public TargetConfigRepository(SQLiteConnection db) : base(db) { }
|
||||
|
||||
public List<TargetConfig> GetByTaskId(string taskId)
|
||||
{
|
||||
return Db.Table<TargetConfig>().Where(t => t.TaskId == taskId).ToList();
|
||||
}
|
||||
|
||||
public void DeleteByTaskId(string taskId)
|
||||
{
|
||||
var targets = GetByTaskId(taskId);
|
||||
foreach (var t in targets)
|
||||
Db.Delete(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/CounterDrone.Core/Repository/WaypointRepository.cs
Normal file
27
src/CounterDrone.Core/Repository/WaypointRepository.cs
Normal file
@ -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<Waypoint>
|
||||
{
|
||||
public WaypointRepository(SQLiteConnection db) : base(db) { }
|
||||
|
||||
public List<Waypoint> GetByTaskId(string taskId)
|
||||
{
|
||||
return Db.Table<Waypoint>()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
21
src/CounterDrone.Core/Services/IScenarioService.cs
Normal file
21
src/CounterDrone.Core/Services/IScenarioService.cs
Normal file
@ -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<SimTask> 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<ControlZone> zones);
|
||||
void SaveTarget(string taskId, TargetConfig target);
|
||||
void SaveDeployment(string taskId, List<EquipmentDeployment> equips);
|
||||
void SaveCloudDispersal(string taskId, CloudDispersal cloud);
|
||||
void SaveRoute(string taskId, RoutePlan route, List<Waypoint> waypoints);
|
||||
void UpdateStep(string taskId, int step);
|
||||
}
|
||||
}
|
||||
222
src/CounterDrone.Core/Services/ScenarioService.cs
Normal file
222
src/CounterDrone.Core/Services/ScenarioService.cs
Normal file
@ -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<SimTask> 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<SimTask>
|
||||
{
|
||||
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<ControlZone> 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<EquipmentDeployment> 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<Waypoint> 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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
483
test/unit/CounterDrone.Core.Tests/ScenarioServiceTests.cs
Normal file
483
test/unit/CounterDrone.Core.Tests/ScenarioServiceTests.cs
Normal file
@ -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<ArgumentException>(() =>
|
||||
_service.CreateTask("", "SIM-001"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateTask_DuplicateNumber_Throws()
|
||||
{
|
||||
_service.CreateTask("Task A", "SIM-001");
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
_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<ControlZone>
|
||||
{
|
||||
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<ControlZone>
|
||||
{
|
||||
new ControlZone { Name = "Old" }
|
||||
});
|
||||
_service.SaveControlZones(task.Id, new List<ControlZone>
|
||||
{
|
||||
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<EquipmentDeployment>
|
||||
{
|
||||
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<Waypoint>
|
||||
{
|
||||
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<ControlZone>
|
||||
{
|
||||
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<EquipmentDeployment>
|
||||
{
|
||||
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<Waypoint>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user