pcp-mission-planing: Ф4 — валидация-заглушка POST /missions/{id}/validate (контракт конфликтов, пустой diff)

This commit is contained in:
Дмитрий Соловьев
2026-06-03 13:27:31 +03:00
parent f5211fa5a0
commit 8133a5144f
5 changed files with 115 additions and 1 deletions
@@ -252,6 +252,25 @@ class MissionController(
return ResponseEntity.noContent().build()
}
@Operation(
summary = "Проверить миссию-черновик на конфликты",
description = "Считает конфликты/наложения включений плана-черновика и возвращает дифф по " +
"режимам. MVP: валидатор-заглушка — контракт корректен, список конфликтов пуст; " +
"реальные правила (пересечения по времени, видимость цели, ЗРВ, порядок сброса, борт) — " +
"отдельной задачей."
)
@ApiResponses(
value = [
ApiResponse(responseCode = "200", description = "Валидация выполнена, конфликты в теле ответа"),
ApiResponse(responseCode = "404", description = "Миссия не найдена")
]
)
@PostMapping("/{mission_id}/validate")
fun validateMission(
@Parameter(description = "Идентификатор миссии-черновика")
@PathVariable("mission_id") id: UUID
): MissionValidationResponse = missionPlaningService.validateMission(id)
@Operation(summary = "Создать миссию")
@@ -0,0 +1,55 @@
package space.nstart.pcp.pcp_mission_planing_service.controller
import java.util.UUID
/**
* Коды правил валидации. MVP: валидатор-заглушка их **не вычисляет** — список существует как
* стабильный контракт для фронта, чтобы появление реальных правил (раздел 6 дизайн-дока) не ломало схему.
*/
object ConflictRule {
/** Пересечение по времени между включениями (съёмка↔съёмка, съёмка↔сброс, сброс↔сброс). */
const val TIME_OVERLAP = "TIME_OVERLAP"
/** Окно съёмки вне окна видимости цели (баллистика). */
const val TARGET_VISIBILITY = "TARGET_VISIBILITY"
/** Окно сброса вне зоны радиовидимости станции. */
const val COMMS_SESSION = "COMMS_SESSION"
/** Сбрасываемая съёмка не завершается строго до начала зоны сброса. */
const val DROP_ORDER = "DROP_ORDER"
/** Бортовые ограничения: длительность/скважность, энергетика, память. */
const val ONBOARD_LIMIT = "ONBOARD_LIMIT"
}
/** Уровень конфликта: [ERROR] блокирует «Применить», [WARNING] — информативный. */
object ConflictSeverity {
const val ERROR = "error"
const val WARNING = "warning"
}
/**
* Один конфликт на затронутом включении. Возвращается диффом по режимам (а не как весь план),
* чтобы фронт мог точечно подсвечивать проблемные включения.
*
* @param modeId id режима (survey или drop), к которому относится конфликт.
* @param rule код правила, см. [ConflictRule].
* @param severity уровень, см. [ConflictSeverity].
* @param message человекочитаемое описание для оператора.
* @param relatedModeId id второго участника парного правила (например, пересечения по времени), если есть.
*/
data class ModeConflict(
val modeId: Long,
val rule: String,
val severity: String,
val message: String,
val relatedModeId: Long? = null
)
/**
* Результат валидации миссии-черновика.
*
* MVP: [conflicts] всегда пуст — реальные правила ещё не считаются (TODO раздела 6 дизайн-дока).
* Контракт стабилен: фронт уже завязывается на него и не сломается, когда правила появятся.
*/
data class MissionValidationResponse(
val missionId: UUID,
val conflicts: List<ModeConflict>
)
@@ -32,6 +32,7 @@ import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SurveyRouteBuildResp
import space.nstart.pcp.pcp_mission_planing_service.controller.CloneMode
import space.nstart.pcp.pcp_mission_planing_service.controller.MissionCloneRequest
import space.nstart.pcp.pcp_mission_planing_service.controller.MissionCloneResponse
import space.nstart.pcp.pcp_mission_planing_service.controller.MissionValidationResponse
import space.nstart.pcp.pcp_mission_planing_service.entity.DropModeEntity
import space.nstart.pcp.pcp_mission_planing_service.entity.DropModeSurveyEntity
import space.nstart.pcp.pcp_mission_planing_service.entity.MissionEntity
@@ -910,6 +911,21 @@ class MissionPlaningService(
logger.info("Удалён сброс (drop_mode id={}), снято связей со съёмками={}", dropId, links.size)
}
/**
* Валидация миссии-черновика на конфликты/наложения включений.
*
* MVP-заглушка: проверяет существование миссии (404, если нет) и возвращает корректный контракт
* с **пустым** списком конфликтов. Реальные правила (пересечения по времени, видимость цели, ЗРВ
* станции, порядок сброса, бортовые ограничения) — TODO раздела 6 дизайн-дока. Контракт
* ([MissionValidationResponse]/[ModeConflict]) стабилен, чтобы фронт не ломался при их появлении.
*/
fun validateMission(missionId: UUID): MissionValidationResponse {
if (!missionRepository.existsById(missionId)) {
throw notFound(missionId)
}
return MissionValidationResponse(missionId = missionId, conflicts = emptyList())
}
/**
* Строит контур полосы съёмки по параметрам видимости точки.
* Чистый расчёт без персистенции; переиспользуется [buildSurveyRoute] и [saveSurveyRoute].
@@ -199,6 +199,25 @@ class MissionEditModesTest {
assertEquals(HttpStatus.NOT_FOUND, ex.statusCode)
}
// --- validate (заглушка) ---
@Test
fun `validate of an existing mission returns an empty conflict list`() {
val missionId = UUID.randomUUID()
`when`(missionRepository.existsById(missionId)).thenReturn(true)
val result = service.validateMission(missionId)
assertEquals(missionId, result.missionId)
assertTrue(result.conflicts.isEmpty(), "MVP-заглушка не считает конфликтов")
}
@Test
fun `validate of a missing mission yields 404`() {
val ex = assertThrows<ResponseStatusException> { service.validateMission(UUID.randomUUID()) }
assertEquals(HttpStatus.NOT_FOUND, ex.statusCode)
}
// --- helpers ---
private fun survey(): SurveyModeEntity {