Files
hms/crates/erp-health/src/service/validation.rs
iven 7e66561a5f fix(health): 统一随访类型为 5 种 — phone/outpatient/home_visit/online/wechat
- validation.rs: face_to_face 替换为 outpatient,新增 home_visit/wechat
- FollowUpTaskList.tsx: 新增 online 选项,与后端对齐
- 迁移 078: follow_up_task + follow_up_record face_to_face → outpatient
2026-04-27 11:20:57 +08:00

545 lines
21 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 通用字段校验 — 枚举白名单、输入格式校验
use crate::error::{HealthError, HealthResult};
macro_rules! validate_enum {
($value:expr, $field:expr, [$($allowed:expr),* $(,)?]) => {
{
let v: &str = $value;
let allowed: &[&str] = &[$($allowed),*];
let mut found = false;
let mut _i = 0;
while _i < allowed.len() {
if allowed[_i] == v {
found = true;
break;
}
_i += 1;
}
if !found {
return Err(HealthError::Validation(format!(
"{}: '{}' 不是有效值,允许值: [{}]",
$field, v, allowed.join(", ")
)));
}
}
};
}
/// patient.gender
pub fn validate_gender(value: &str) -> HealthResult<()> {
validate_enum!(value, "gender", ["male", "female", "other"]);
Ok(())
}
/// patient.blood_type
pub fn validate_blood_type(value: &str) -> HealthResult<()> {
validate_enum!(value, "blood_type", [
"A", "B", "AB", "O", "A+", "A-", "B+", "B-", "AB+", "AB-", "O+", "O-",
]);
Ok(())
}
/// appointment.appointment_type
pub fn validate_appointment_type(value: &str) -> HealthResult<()> {
validate_enum!(value, "appointment_type", [
"dialysis", "recheck", "outpatient", "health_checkup", "consultation",
]);
Ok(())
}
/// appointment.status transitions
pub fn validate_appointment_status_transition(current: &str, new: &str) -> HealthResult<()> {
if current == new {
return Ok(());
}
let allowed = match current {
"pending" => matches!(new, "confirmed" | "cancelled"),
"confirmed" => matches!(new, "completed" | "cancelled" | "no_show"),
_ => false,
};
if allowed {
Ok(())
} else {
Err(HealthError::InvalidStatusTransition(format!(
"appointment.status: 不允许从 '{}' 转换到 '{}'", current, new
)))
}
}
/// doctor_schedule.period_type
pub fn validate_period_type(value: &str) -> HealthResult<()> {
validate_enum!(value, "period_type", ["am", "pm", "night", "full_day"]);
Ok(())
}
/// doctor_schedule.status
pub fn validate_schedule_status(value: &str) -> HealthResult<()> {
validate_enum!(value, "schedule.status", ["enabled", "disabled"]);
Ok(())
}
/// follow_up_task.follow_up_type
pub fn validate_follow_up_type(value: &str) -> HealthResult<()> {
validate_enum!(value, "follow_up_type", [
"phone", "outpatient", "home_visit", "online", "wechat",
]);
Ok(())
}
/// consultation.sender_role
pub fn validate_sender_role(value: &str) -> HealthResult<()> {
validate_enum!(value, "sender_role", ["patient", "doctor", "system"]);
Ok(())
}
/// consultation.content_type
pub fn validate_content_type(value: &str) -> HealthResult<()> {
validate_enum!(value, "content_type", ["text", "image", "voice", "file"]);
Ok(())
}
/// consultation.consultation_type
pub fn validate_consultation_type(value: &str) -> HealthResult<()> {
validate_enum!(value, "consultation_type", [
"customer_service", "doctor", "nutritionist", "psychologist",
]);
Ok(())
}
/// health_record.record_type
pub fn validate_record_type(value: &str) -> HealthResult<()> {
validate_enum!(value, "record_type", ["checkup", "outpatient", "inpatient"]);
Ok(())
}
/// patient.status
pub fn validate_patient_status(value: &str) -> HealthResult<()> {
validate_enum!(value, "patient.status", ["active", "inactive", "deceased"]);
Ok(())
}
/// patient.verification_status
pub fn validate_verification_status(value: &str) -> HealthResult<()> {
validate_enum!(value, "verification_status", ["pending", "verified", "rejected"]);
Ok(())
}
/// doctor_profile.online_status
pub fn validate_online_status(value: &str) -> HealthResult<()> {
validate_enum!(value, "online_status", ["online", "offline", "busy"]);
Ok(())
}
/// article.status 枚举白名单
pub fn validate_article_status(value: &str) -> HealthResult<()> {
validate_enum!(value, "article.status", [
"draft", "pending_review", "approved", "rejected", "published",
]);
Ok(())
}
/// article.status 状态转换
/// draft → pending_review, rejected → pending_review
/// pending_review → published / rejected审核通过直接发布或拒绝
/// published → draft (下架)
pub fn validate_article_status_transition(current: &str, new: &str) -> HealthResult<()> {
if current == new {
return Ok(());
}
let allowed = match current {
"draft" => matches!(new, "pending_review"),
"pending_review" => matches!(new, "published" | "rejected"),
"rejected" => matches!(new, "pending_review"),
"published" => matches!(new, "draft"),
_ => false,
};
if allowed {
Ok(())
} else {
Err(HealthError::InvalidStatusTransition(format!(
"article.status: 不允许从 '{}' 转换到 '{}'", current, new
)))
}
}
/// dialysis_record.status 枚举白名单
pub fn validate_dialysis_status(value: &str) -> HealthResult<()> {
validate_enum!(value, "dialysis_record.status", ["draft", "completed", "reviewed"]);
Ok(())
}
/// dialysis_record.status 状态转换
/// draft → completed → reviewed
pub fn validate_dialysis_status_transition(current: &str, new: &str) -> HealthResult<()> {
if current == new {
return Ok(());
}
let allowed = match current {
"draft" => matches!(new, "completed"),
"completed" => matches!(new, "reviewed"),
_ => false,
};
if allowed {
Ok(())
} else {
Err(HealthError::InvalidStatusTransition(format!(
"dialysis_record.status: 不允许从 '{}' 转换到 '{}'", current, new
)))
}
}
/// lab_report.status 状态转换
/// pending → reviewed
pub fn validate_lab_report_status_transition(current: &str, new: &str) -> HealthResult<()> {
if current == new {
return Ok(());
}
let allowed = match current {
"pending" => matches!(new, "reviewed"),
_ => false,
};
if allowed {
Ok(())
} else {
Err(HealthError::InvalidStatusTransition(format!(
"lab_report.status: 不允许从 '{}' 转换到 '{}'", current, new
)))
}
}
/// follow_up_task.status 状态转换(含 overdue 状态)
pub fn validate_follow_up_status_transition(current: &str, new: &str) -> HealthResult<()> {
if current == new {
return Ok(());
}
let allowed = match current {
"pending" => matches!(new, "in_progress" | "cancelled" | "overdue"),
"in_progress" => matches!(new, "completed" | "cancelled"),
"overdue" => matches!(new, "in_progress" | "cancelled"),
_ => false,
};
if allowed {
Ok(())
} else {
Err(HealthError::InvalidStatusTransition(format!(
"follow_up_task.status: 不允许从 '{}' 转换到 '{}'",
current, new
)))
}
}
/// device_reading.device_type
pub fn validate_device_type(value: &str) -> HealthResult<()> {
validate_enum!(value, "device_type", [
"heart_rate", "blood_oxygen", "steps", "sleep", "temperature", "stress",
]);
Ok(())
}
/// alert_rule.condition_type
pub fn validate_condition_type(value: &str) -> HealthResult<()> {
validate_enum!(value, "condition_type", [
"single_threshold", "consecutive", "trend",
]);
Ok(())
}
/// alert.severity
pub fn validate_alert_severity(value: &str) -> HealthResult<()> {
validate_enum!(value, "alert_severity", [
"info", "warning", "critical", "urgent",
]);
Ok(())
}
/// alert.status
pub fn validate_alert_status(value: &str) -> HealthResult<()> {
validate_enum!(value, "alert_status", [
"pending", "acknowledged", "resolved", "dismissed",
]);
Ok(())
}
/// 告警状态转换校验: pending→acknowledged/dismissed, acknowledged→resolved/dismissed
pub fn validate_alert_status_transition(current: &str, next: &str) -> HealthResult<()> {
if current == next {
return Ok(());
}
let allowed = match current {
"pending" => matches!(next, "acknowledged" | "dismissed"),
"acknowledged" => matches!(next, "resolved" | "dismissed"),
_ => false,
};
if allowed {
Ok(())
} else {
Err(HealthError::InvalidStatusTransition(format!(
"alert.status: 不允许从 '{}' 转换到 '{}'", current, next
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
// --- gender ---
#[test]
fn gender_valid() { assert!(validate_gender("male").is_ok()); }
#[test]
fn gender_valid_female() { assert!(validate_gender("female").is_ok()); }
#[test]
fn gender_valid_other() { assert!(validate_gender("other").is_ok()); }
#[test]
fn gender_invalid() { assert!(validate_gender("unknown").is_err()); }
// --- blood_type ---
#[test]
fn blood_type_a() { assert!(validate_blood_type("A").is_ok()); }
#[test]
fn blood_type_o_neg() { assert!(validate_blood_type("O-").is_ok()); }
#[test]
fn blood_type_invalid() { assert!(validate_blood_type("X").is_err()); }
// --- appointment_type ---
#[test]
fn appointment_type_dialysis() { assert!(validate_appointment_type("dialysis").is_ok()); }
#[test]
fn appointment_type_consultation() { assert!(validate_appointment_type("consultation").is_ok()); }
#[test]
fn appointment_type_invalid() { assert!(validate_appointment_type("surgery").is_err()); }
// --- appointment_status_transition ---
#[test]
fn appt_pending_to_confirmed() { assert!(validate_appointment_status_transition("pending", "confirmed").is_ok()); }
#[test]
fn appt_pending_to_cancelled() { assert!(validate_appointment_status_transition("pending", "cancelled").is_ok()); }
#[test]
fn appt_pending_to_completed_fails() { assert!(validate_appointment_status_transition("pending", "completed").is_err()); }
#[test]
fn appt_confirmed_to_completed() { assert!(validate_appointment_status_transition("confirmed", "completed").is_ok()); }
#[test]
fn appt_confirmed_to_no_show() { assert!(validate_appointment_status_transition("confirmed", "no_show").is_ok()); }
#[test]
fn appt_confirmed_to_cancelled() { assert!(validate_appointment_status_transition("confirmed", "cancelled").is_ok()); }
#[test]
fn appt_completed_to_pending_fails() { assert!(validate_appointment_status_transition("completed", "pending").is_err()); }
#[test]
fn appt_same_status_ok() { assert!(validate_appointment_status_transition("pending", "pending").is_ok()); }
// --- period_type ---
#[test]
fn period_am() { assert!(validate_period_type("am").is_ok()); }
#[test]
fn period_night() { assert!(validate_period_type("night").is_ok()); }
#[test]
fn period_invalid() { assert!(validate_period_type("evening").is_err()); }
// --- schedule_status ---
#[test]
fn schedule_enabled() { assert!(validate_schedule_status("enabled").is_ok()); }
#[test]
fn schedule_disabled() { assert!(validate_schedule_status("disabled").is_ok()); }
#[test]
fn schedule_invalid() { assert!(validate_schedule_status("active").is_err()); }
// --- follow_up_type ---
#[test]
fn follow_up_phone() { assert!(validate_follow_up_type("phone").is_ok()); }
#[test]
fn follow_up_online() { assert!(validate_follow_up_type("online").is_ok()); }
#[test]
fn follow_up_invalid() { assert!(validate_follow_up_type("email").is_err()); }
// --- sender_role ---
#[test]
fn sender_patient() { assert!(validate_sender_role("patient").is_ok()); }
#[test]
fn sender_system() { assert!(validate_sender_role("system").is_ok()); }
#[test]
fn sender_invalid() { assert!(validate_sender_role("admin").is_err()); }
// --- content_type ---
#[test]
fn content_text() { assert!(validate_content_type("text").is_ok()); }
#[test]
fn content_image() { assert!(validate_content_type("image").is_ok()); }
#[test]
fn content_invalid() { assert!(validate_content_type("video").is_err()); }
// --- consultation_type ---
#[test]
fn consultation_customer_service() { assert!(validate_consultation_type("customer_service").is_ok()); }
#[test]
fn consultation_psychologist() { assert!(validate_consultation_type("psychologist").is_ok()); }
#[test]
fn consultation_invalid() { assert!(validate_consultation_type("general").is_err()); }
// --- record_type ---
#[test]
fn record_checkup() { assert!(validate_record_type("checkup").is_ok()); }
#[test]
fn record_invalid() { assert!(validate_record_type("emergency").is_err()); }
// --- patient_status ---
#[test]
fn patient_active() { assert!(validate_patient_status("active").is_ok()); }
#[test]
fn patient_deceased() { assert!(validate_patient_status("deceased").is_ok()); }
#[test]
fn patient_invalid() { assert!(validate_patient_status("suspended").is_err()); }
// --- verification_status ---
#[test]
fn verification_pending() { assert!(validate_verification_status("pending").is_ok()); }
#[test]
fn verification_verified() { assert!(validate_verification_status("verified").is_ok()); }
#[test]
fn verification_invalid() { assert!(validate_verification_status("approved").is_err()); }
// --- online_status ---
#[test]
fn online_busy() { assert!(validate_online_status("busy").is_ok()); }
#[test]
fn online_invalid() { assert!(validate_online_status("away").is_err()); }
// --- article_status ---
#[test]
fn article_draft() { assert!(validate_article_status("draft").is_ok()); }
#[test]
fn article_published() { assert!(validate_article_status("published").is_ok()); }
#[test]
fn article_invalid() { assert!(validate_article_status("archived").is_err()); }
// --- article_status_transition ---
#[test]
fn art_draft_to_pending_review() { assert!(validate_article_status_transition("draft", "pending_review").is_ok()); }
#[test]
fn art_draft_to_published_fails() { assert!(validate_article_status_transition("draft", "published").is_err()); }
#[test]
fn art_pending_review_to_published() { assert!(validate_article_status_transition("pending_review", "published").is_ok()); }
#[test]
fn art_pending_review_to_rejected() { assert!(validate_article_status_transition("pending_review", "rejected").is_ok()); }
#[test]
fn art_pending_review_to_draft_fails() { assert!(validate_article_status_transition("pending_review", "draft").is_err()); }
#[test]
fn art_rejected_to_pending_review() { assert!(validate_article_status_transition("rejected", "pending_review").is_ok()); }
#[test]
fn art_published_to_draft() { assert!(validate_article_status_transition("published", "draft").is_ok()); }
#[test]
fn art_published_to_pending_fails() { assert!(validate_article_status_transition("published", "pending_review").is_err()); }
#[test]
fn art_same_status_ok() { assert!(validate_article_status_transition("draft", "draft").is_ok()); }
// --- dialysis_status ---
#[test]
fn dialysis_draft() { assert!(validate_dialysis_status("draft").is_ok()); }
#[test]
fn dialysis_reviewed() { assert!(validate_dialysis_status("reviewed").is_ok()); }
#[test]
fn dialysis_invalid() { assert!(validate_dialysis_status("approved").is_err()); }
// --- dialysis_status_transition ---
#[test]
fn dial_draft_to_completed() { assert!(validate_dialysis_status_transition("draft", "completed").is_ok()); }
#[test]
fn dial_draft_to_reviewed_fails() { assert!(validate_dialysis_status_transition("draft", "reviewed").is_err()); }
#[test]
fn dial_completed_to_reviewed() { assert!(validate_dialysis_status_transition("completed", "reviewed").is_ok()); }
#[test]
fn dial_completed_to_draft_fails() { assert!(validate_dialysis_status_transition("completed", "draft").is_err()); }
#[test]
fn dial_reviewed_to_any_fails() { assert!(validate_dialysis_status_transition("reviewed", "draft").is_err()); }
#[test]
fn dial_same_status_ok() { assert!(validate_dialysis_status_transition("draft", "draft").is_ok()); }
// --- lab_report_status_transition ---
#[test]
fn lab_pending_to_reviewed() { assert!(validate_lab_report_status_transition("pending", "reviewed").is_ok()); }
#[test]
fn lab_pending_to_draft_fails() { assert!(validate_lab_report_status_transition("pending", "draft").is_err()); }
#[test]
fn lab_reviewed_to_any_fails() { assert!(validate_lab_report_status_transition("reviewed", "pending").is_err()); }
#[test]
fn lab_same_status_ok() { assert!(validate_lab_report_status_transition("pending", "pending").is_ok()); }
// --- follow_up_status_transition ---
#[test]
fn fu_pending_to_in_progress() { assert!(validate_follow_up_status_transition("pending", "in_progress").is_ok()); }
#[test]
fn fu_pending_to_overdue() { assert!(validate_follow_up_status_transition("pending", "overdue").is_ok()); }
#[test]
fn fu_pending_to_cancelled() { assert!(validate_follow_up_status_transition("pending", "cancelled").is_ok()); }
#[test]
fn fu_pending_to_completed_fails() { assert!(validate_follow_up_status_transition("pending", "completed").is_err()); }
#[test]
fn fu_in_progress_to_completed() { assert!(validate_follow_up_status_transition("in_progress", "completed").is_ok()); }
#[test]
fn fu_in_progress_to_cancelled() { assert!(validate_follow_up_status_transition("in_progress", "cancelled").is_ok()); }
#[test]
fn fu_overdue_to_in_progress() { assert!(validate_follow_up_status_transition("overdue", "in_progress").is_ok()); }
#[test]
fn fu_overdue_to_cancelled() { assert!(validate_follow_up_status_transition("overdue", "cancelled").is_ok()); }
#[test]
fn fu_overdue_to_completed_fails() { assert!(validate_follow_up_status_transition("overdue", "completed").is_err()); }
#[test]
fn fu_completed_to_any_fails() { assert!(validate_follow_up_status_transition("completed", "pending").is_err()); }
#[test]
fn fu_same_status_ok() { assert!(validate_follow_up_status_transition("pending", "pending").is_ok()); }
// --- device_type ---
#[test]
fn device_type_heart_rate() { assert!(validate_device_type("heart_rate").is_ok()); }
#[test]
fn device_type_blood_oxygen() { assert!(validate_device_type("blood_oxygen").is_ok()); }
#[test]
fn device_type_steps() { assert!(validate_device_type("steps").is_ok()); }
#[test]
fn device_type_invalid() { assert!(validate_device_type("blood_pressure").is_err()); }
// --- condition_type ---
#[test]
fn condition_single_threshold() { assert!(validate_condition_type("single_threshold").is_ok()); }
#[test]
fn condition_consecutive() { assert!(validate_condition_type("consecutive").is_ok()); }
#[test]
fn condition_trend() { assert!(validate_condition_type("trend").is_ok()); }
#[test]
fn condition_invalid() { assert!(validate_condition_type("moving_avg").is_err()); }
// --- alert_severity ---
#[test]
fn severity_info() { assert!(validate_alert_severity("info").is_ok()); }
#[test]
fn severity_urgent() { assert!(validate_alert_severity("urgent").is_ok()); }
#[test]
fn severity_invalid() { assert!(validate_alert_severity("emergency").is_err()); }
// --- alert_status ---
#[test]
fn alert_status_pending() { assert!(validate_alert_status("pending").is_ok()); }
#[test]
fn alert_status_resolved() { assert!(validate_alert_status("resolved").is_ok()); }
#[test]
fn alert_status_invalid() { assert!(validate_alert_status("open").is_err()); }
// --- alert_status_transition ---
#[test]
fn alert_pending_to_acknowledged() { assert!(validate_alert_status_transition("pending", "acknowledged").is_ok()); }
#[test]
fn alert_pending_to_dismissed() { assert!(validate_alert_status_transition("pending", "dismissed").is_ok()); }
#[test]
fn alert_pending_to_resolved_fails() { assert!(validate_alert_status_transition("pending", "resolved").is_err()); }
#[test]
fn alert_acknowledged_to_resolved() { assert!(validate_alert_status_transition("acknowledged", "resolved").is_ok()); }
#[test]
fn alert_acknowledged_to_dismissed() { assert!(validate_alert_status_transition("acknowledged", "dismissed").is_ok()); }
#[test]
fn alert_acknowledged_to_pending_fails() { assert!(validate_alert_status_transition("acknowledged", "pending").is_err()); }
#[test]
fn alert_resolved_to_any_fails() { assert!(validate_alert_status_transition("resolved", "pending").is_err()); }
#[test]
fn alert_same_status_ok() { assert!(validate_alert_status_transition("pending", "pending").is_ok()); }
}