fix(control): validate unit timing configuration

This commit is contained in:
caoqianming 2026-03-26 13:19:10 +08:00
parent 86e651d9ca
commit dbfa673468
2 changed files with 116 additions and 21 deletions

View File

@ -18,6 +18,23 @@ use crate::{
AppState,
};
fn validate_unit_timing_order(
run_time_sec: i32,
acc_time_sec: i32,
) -> Result<(), ApiErr> {
if acc_time_sec <= run_time_sec {
return Err(ApiErr::BadRequest(
"acc_time_sec must be greater than run_time_sec".to_string(),
Some(json!({
"run_time_sec": ["must be less than acc_time_sec"],
"acc_time_sec": ["must be greater than run_time_sec"]
})),
));
}
Ok(())
}
#[derive(Debug, Deserialize, Validate)]
pub struct GetUnitListQuery {
#[validate(length(min = 1, max = 100))]
@ -293,13 +310,13 @@ pub struct CreateUnitReq {
pub name: String,
pub description: Option<String>,
pub enabled: Option<bool>,
#[validate(range(min = 0))]
#[validate(range(min = 1, message = "must be greater than 0"))]
pub run_time_sec: Option<i32>,
#[validate(range(min = 0))]
#[validate(range(min = 1, message = "must be greater than 0"))]
pub stop_time_sec: Option<i32>,
#[validate(range(min = 0))]
#[validate(range(min = 1, message = "must be greater than 0"))]
pub acc_time_sec: Option<i32>,
#[validate(range(min = 0))]
#[validate(range(min = 1, message = "must be greater than 0"))]
pub bl_time_sec: Option<i32>,
pub require_manual_ack_after_fault: Option<bool>,
}
@ -310,6 +327,33 @@ pub async fn create_unit(
) -> Result<impl IntoResponse, ApiErr> {
payload.validate()?;
let run_time_sec = payload.run_time_sec.ok_or_else(|| {
ApiErr::BadRequest(
"run_time_sec is required".to_string(),
Some(json!({ "run_time_sec": ["is required"] })),
)
})?;
let stop_time_sec = payload.stop_time_sec.ok_or_else(|| {
ApiErr::BadRequest(
"stop_time_sec is required".to_string(),
Some(json!({ "stop_time_sec": ["is required"] })),
)
})?;
let acc_time_sec = payload.acc_time_sec.ok_or_else(|| {
ApiErr::BadRequest(
"acc_time_sec is required".to_string(),
Some(json!({ "acc_time_sec": ["is required"] })),
)
})?;
let bl_time_sec = payload.bl_time_sec.ok_or_else(|| {
ApiErr::BadRequest(
"bl_time_sec is required".to_string(),
Some(json!({ "bl_time_sec": ["is required"] })),
)
})?;
validate_unit_timing_order(run_time_sec, acc_time_sec)?;
if crate::service::get_unit_by_code(&state.pool, &payload.code)
.await?
.is_some()
@ -327,10 +371,10 @@ pub async fn create_unit(
name: &payload.name,
description: payload.description.as_deref(),
enabled: payload.enabled.unwrap_or(true),
run_time_sec: payload.run_time_sec.unwrap_or(0),
stop_time_sec: payload.stop_time_sec.unwrap_or(0),
acc_time_sec: payload.acc_time_sec.unwrap_or(0),
bl_time_sec: payload.bl_time_sec.unwrap_or(0),
run_time_sec,
stop_time_sec,
acc_time_sec,
bl_time_sec,
require_manual_ack_after_fault: payload
.require_manual_ack_after_fault
.unwrap_or(true),
@ -355,13 +399,13 @@ pub struct UpdateUnitReq {
pub name: Option<String>,
pub description: Option<String>,
pub enabled: Option<bool>,
#[validate(range(min = 0))]
#[validate(range(min = 1, message = "must be greater than 0"))]
pub run_time_sec: Option<i32>,
#[validate(range(min = 0))]
#[validate(range(min = 1, message = "must be greater than 0"))]
pub stop_time_sec: Option<i32>,
#[validate(range(min = 0))]
#[validate(range(min = 1, message = "must be greater than 0"))]
pub acc_time_sec: Option<i32>,
#[validate(range(min = 0))]
#[validate(range(min = 1, message = "must be greater than 0"))]
pub bl_time_sec: Option<i32>,
pub require_manual_ack_after_fault: Option<bool>,
}
@ -373,12 +417,14 @@ pub async fn update_unit(
) -> Result<impl IntoResponse, ApiErr> {
payload.validate()?;
if crate::service::get_unit_by_id(&state.pool, unit_id)
let existing_unit = crate::service::get_unit_by_id(&state.pool, unit_id)
.await?
.is_none()
{
return Err(ApiErr::NotFound("Unit not found".to_string(), None));
}
.ok_or_else(|| ApiErr::NotFound("Unit not found".to_string(), None))?;
validate_unit_timing_order(
payload.run_time_sec.unwrap_or(existing_unit.run_time_sec),
payload.acc_time_sec.unwrap_or(existing_unit.acc_time_sec),
)?;
if let Some(code) = payload.code.as_deref() {
let duplicate = crate::service::get_unit_by_code(&state.pool, code).await?;
@ -627,3 +673,52 @@ pub async fn get_unit_runtime(
Ok(Json(runtime))
}
#[cfg(test)]
mod tests {
use super::{validate_unit_timing_order, CreateUnitReq, UpdateUnitReq};
use validator::Validate;
#[test]
fn create_unit_req_rejects_zero_second_fields() {
let payload = CreateUnitReq {
code: "U-01".to_string(),
name: "Unit 01".to_string(),
description: None,
enabled: Some(true),
run_time_sec: Some(0),
stop_time_sec: Some(10),
acc_time_sec: Some(20),
bl_time_sec: Some(5),
require_manual_ack_after_fault: Some(true),
};
assert!(payload.validate().is_err());
}
#[test]
fn create_unit_req_rejects_acc_time_not_greater_than_run_time() {
assert!(validate_unit_timing_order(10, 10).is_err());
}
#[test]
fn update_unit_req_rejects_zero_second_fields() {
let payload = UpdateUnitReq {
code: None,
name: None,
description: None,
enabled: None,
run_time_sec: None,
stop_time_sec: Some(0),
acc_time_sec: Some(20),
bl_time_sec: Some(5),
require_manual_ack_after_fault: None,
};
assert!(payload.validate().is_err());
}
#[test]
fn update_unit_req_rejects_acc_time_not_greater_than_run_time_when_both_present() {
assert!(validate_unit_timing_order(20, 15).is_err());
}
}

View File

@ -34,10 +34,10 @@ export function resetUnitForm() {
dom.unitId.value = "";
dom.unitEnabled.checked = true;
dom.unitManualAck.checked = true;
dom.unitRunTimeSec.value = "0";
dom.unitStopTimeSec.value = "0";
dom.unitAccTimeSec.value = "0";
dom.unitBlTimeSec.value = "0";
dom.unitRunTimeSec.value = "10";
dom.unitStopTimeSec.value = "10";
dom.unitAccTimeSec.value = "20";
dom.unitBlTimeSec.value = "10";
}
function openUnitModal() {