fix(health): 二次审计修复 — 状态机/枚举校验/归属验证/事件补全
- 状态机验证: patient.status (active→inactive/deceased/inactive→active), patient.verification_status (pending→verified/rejected), follow_up_task.status (pending→in_progress/cancelled, in_progress→completed/cancelled) - 枚举白名单: gender/blood_type/appointment_type/period_type/schedule_status/ follow_up_type/sender_role/content_type/consultation_type - 归属验证: family_member update/delete 校验 patient_id 匹配 - 事件补全: patient.deceased/verified 条件事件, consultation close 允许 waiting - 默认值修正: appointment_type "regular"→"outpatient", period_type "morning"→"am", consultation_type "text"→"customer_service" - 新增 validation.rs 通用校验模块
This commit is contained in:
336
Cargo.lock
generated
336
Cargo.lock
generated
@@ -714,6 +714,26 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
@@ -1202,6 +1222,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"erp-core",
|
||||
"jsonwebtoken",
|
||||
"reqwest",
|
||||
"sea-orm",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1531,6 +1552,21 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||
dependencies = [
|
||||
"foreign-types-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
@@ -1736,6 +1772,25 @@ version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"http",
|
||||
"indexmap",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
@@ -1901,6 +1956,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
@@ -1909,6 +1965,38 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.27.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
|
||||
dependencies = [
|
||||
"http",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"rustls",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-tls"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1917,13 +2005,23 @@ version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"ipnet",
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.6.3",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2124,6 +2222,16 @@ version = "2.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
|
||||
|
||||
[[package]]
|
||||
name = "iri-string"
|
||||
version = "0.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
@@ -2190,6 +2298,8 @@ version = "0.3.94"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
@@ -2461,6 +2571,23 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
@@ -2579,6 +2706,50 @@ version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.78"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered-float"
|
||||
version = "4.6.0"
|
||||
@@ -3135,6 +3306,46 @@ dependencies = [
|
||||
"bytecheck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-tls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"native-tls",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
@@ -3349,6 +3560,15 @@ version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
@@ -3526,6 +3746,29 @@ version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
@@ -4061,6 +4304,9 @@ name = "sync_wrapper"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
@@ -4073,6 +4319,27 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"core-foundation 0.9.4",
|
||||
"system-configuration-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration-sys"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-interface"
|
||||
version = "0.27.3"
|
||||
@@ -4271,6 +4538,26 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.18"
|
||||
@@ -4422,11 +4709,14 @@ dependencies = [
|
||||
"bitflags",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"iri-string",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -4519,6 +4809,12 @@ dependencies = [
|
||||
"tracing-serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.19.0"
|
||||
@@ -4697,6 +4993,15 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
|
||||
dependencies = [
|
||||
"try-lock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
@@ -4741,6 +5046,16 @@ dependencies = [
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.67"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.117"
|
||||
@@ -5227,6 +5542,16 @@ dependencies = [
|
||||
"wast 246.0.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.94"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.11"
|
||||
@@ -5386,6 +5711,17 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
|
||||
@@ -31,6 +31,8 @@ pub struct UpdatePatientReq {
|
||||
pub emergency_contact_phone: Option<String>,
|
||||
pub source: Option<String>,
|
||||
pub notes: Option<String>,
|
||||
pub status: Option<String>,
|
||||
pub verification_status: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
|
||||
@@ -17,6 +17,21 @@ pub enum HealthError {
|
||||
#[error("排班不存在")]
|
||||
ScheduleNotFound,
|
||||
|
||||
#[error("体征记录不存在")]
|
||||
VitalSignsNotFound,
|
||||
|
||||
#[error("化验报告不存在")]
|
||||
LabReportNotFound,
|
||||
|
||||
#[error("健康档案不存在")]
|
||||
HealthRecordNotFound,
|
||||
|
||||
#[error("家庭成员不存在")]
|
||||
FamilyMemberNotFound,
|
||||
|
||||
#[error("标签不存在")]
|
||||
TagNotFound,
|
||||
|
||||
#[error("排班已满,无法预约")]
|
||||
ScheduleFull,
|
||||
|
||||
@@ -44,6 +59,11 @@ impl From<HealthError> for AppError {
|
||||
| HealthError::DoctorNotFound
|
||||
| HealthError::AppointmentNotFound
|
||||
| HealthError::ScheduleNotFound
|
||||
| HealthError::VitalSignsNotFound
|
||||
| HealthError::LabReportNotFound
|
||||
| HealthError::HealthRecordNotFound
|
||||
| HealthError::FamilyMemberNotFound
|
||||
| HealthError::TagNotFound
|
||||
| HealthError::FollowUpTaskNotFound
|
||||
| HealthError::ConsultationNotFound => AppError::NotFound(err.to_string()),
|
||||
HealthError::ScheduleFull => AppError::Validation(err.to_string()),
|
||||
|
||||
@@ -104,6 +104,8 @@ where
|
||||
emergency_contact_phone: req.emergency_contact_phone,
|
||||
source: req.source,
|
||||
notes: req.notes,
|
||||
status: req.status,
|
||||
verification_status: req.verification_status,
|
||||
};
|
||||
let result = patient_service::update_patient(
|
||||
&state, ctx.tenant_id, id, Some(ctx.user_id), update, version,
|
||||
@@ -281,6 +283,8 @@ pub struct UpdatePatientWithVersion {
|
||||
pub emergency_contact_phone: Option<String>,
|
||||
pub source: Option<String>,
|
||||
pub notes: Option<String>,
|
||||
pub status: Option<String>,
|
||||
pub verification_status: Option<String>,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,12 @@ use erp_core::error::check_version;
|
||||
use erp_core::types::PaginatedResponse;
|
||||
|
||||
use crate::dto::appointment_dto::*;
|
||||
use crate::entity::{appointment, doctor_schedule};
|
||||
use crate::entity::{appointment, doctor_schedule, patient};
|
||||
use crate::error::{HealthError, HealthResult};
|
||||
use crate::service::validation::{
|
||||
validate_appointment_type,
|
||||
validate_period_type, validate_schedule_status,
|
||||
};
|
||||
use crate::state::HealthState;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -66,6 +70,17 @@ pub async fn create_appointment(
|
||||
operator_id: Option<Uuid>,
|
||||
req: CreateAppointmentReq,
|
||||
) -> HealthResult<AppointmentResp> {
|
||||
// 校验患者存在
|
||||
patient::Entity::find()
|
||||
.filter(patient::Column::Id.eq(req.patient_id))
|
||||
.filter(patient::Column::TenantId.eq(tenant_id))
|
||||
.filter(patient::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::PatientNotFound)?;
|
||||
|
||||
if let Some(ref at) = req.appointment_type { validate_appointment_type(at)?; }
|
||||
|
||||
// 原子 CAS: 排班名额 +1
|
||||
// 使用 raw SQL 实现 CAS 防止超额预约
|
||||
let cas_result = doctor_schedule::Entity::update_many()
|
||||
@@ -99,7 +114,7 @@ pub async fn create_appointment(
|
||||
tenant_id: Set(tenant_id),
|
||||
patient_id: Set(req.patient_id),
|
||||
doctor_id: Set(req.doctor_id),
|
||||
appointment_type: Set(req.appointment_type.unwrap_or_else(|| "regular".to_string())),
|
||||
appointment_type: Set(req.appointment_type.unwrap_or_else(|| "outpatient".to_string())),
|
||||
appointment_date: Set(req.appointment_date),
|
||||
start_time: Set(req.start_time),
|
||||
end_time: Set(req.end_time),
|
||||
@@ -254,12 +269,14 @@ pub async fn create_schedule(
|
||||
req: CreateScheduleReq,
|
||||
) -> HealthResult<ScheduleResp> {
|
||||
let now = Utc::now();
|
||||
let period_type = req.period_type.unwrap_or_else(|| "am".to_string());
|
||||
validate_period_type(&period_type)?;
|
||||
let active = doctor_schedule::ActiveModel {
|
||||
id: Set(Uuid::now_v7()),
|
||||
tenant_id: Set(tenant_id),
|
||||
doctor_id: Set(req.doctor_id),
|
||||
schedule_date: Set(req.schedule_date),
|
||||
period_type: Set(req.period_type.unwrap_or_else(|| "morning".to_string())),
|
||||
period_type: Set(period_type),
|
||||
start_time: Set(req.start_time),
|
||||
end_time: Set(req.end_time),
|
||||
max_appointments: Set(req.max_appointments),
|
||||
@@ -300,6 +317,8 @@ pub async fn update_schedule(
|
||||
let next_ver = check_version(expected_version, model.version)
|
||||
.map_err(|_| HealthError::VersionMismatch)?;
|
||||
|
||||
if let Some(ref s) = req.status { validate_schedule_status(s)?; }
|
||||
|
||||
let mut active: doctor_schedule::ActiveModel = model.into();
|
||||
if let Some(v) = req.start_time { active.start_time = Set(v); }
|
||||
if let Some(v) = req.end_time { active.end_time = Set(v); }
|
||||
|
||||
@@ -10,8 +10,9 @@ use erp_core::error::check_version;
|
||||
use erp_core::types::PaginatedResponse;
|
||||
|
||||
use crate::dto::consultation_dto::*;
|
||||
use crate::entity::{consultation_message, consultation_session};
|
||||
use crate::entity::{consultation_message, consultation_session, patient};
|
||||
use crate::error::{HealthError, HealthResult};
|
||||
use crate::service::validation::{validate_sender_role, validate_content_type, validate_consultation_type};
|
||||
use crate::state::HealthState;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -25,12 +26,25 @@ pub async fn create_session(
|
||||
req: CreateSessionReq,
|
||||
) -> HealthResult<SessionResp> {
|
||||
let now = Utc::now();
|
||||
|
||||
// 校验患者存在
|
||||
patient::Entity::find()
|
||||
.filter(patient::Column::Id.eq(req.patient_id))
|
||||
.filter(patient::Column::TenantId.eq(tenant_id))
|
||||
.filter(patient::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::PatientNotFound)?;
|
||||
|
||||
let consultation_type = req.consultation_type.unwrap_or_else(|| "customer_service".to_string());
|
||||
validate_consultation_type(&consultation_type)?;
|
||||
|
||||
let active = consultation_session::ActiveModel {
|
||||
id: Set(Uuid::now_v7()),
|
||||
tenant_id: Set(tenant_id),
|
||||
patient_id: Set(req.patient_id),
|
||||
doctor_id: Set(req.doctor_id),
|
||||
consultation_type: Set(req.consultation_type.unwrap_or_else(|| "text".to_string())),
|
||||
consultation_type: Set(consultation_type),
|
||||
status: Set("waiting".to_string()),
|
||||
last_message_at: Set(None),
|
||||
unread_count_patient: Set(0),
|
||||
@@ -113,7 +127,11 @@ pub async fn close_session(
|
||||
.filter(consultation_session::Column::Id.eq(session_id))
|
||||
.filter(consultation_session::Column::TenantId.eq(tenant_id))
|
||||
.filter(consultation_session::Column::DeletedAt.is_null())
|
||||
.filter(consultation_session::Column::Status.eq("active"))
|
||||
.filter(
|
||||
Condition::any()
|
||||
.add(consultation_session::Column::Status.eq("active"))
|
||||
.add(consultation_session::Column::Status.eq("waiting")),
|
||||
)
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::ConsultationNotFound)?;
|
||||
@@ -234,6 +252,9 @@ pub async fn create_message(
|
||||
.ok_or(HealthError::ConsultationNotFound)?;
|
||||
|
||||
let now = Utc::now();
|
||||
validate_sender_role(&req.sender_role)?;
|
||||
let content_type = req.content_type.unwrap_or_else(|| "text".to_string());
|
||||
validate_content_type(&content_type)?;
|
||||
let is_patient = req.sender_role == "patient";
|
||||
let should_activate = session.status == "waiting";
|
||||
|
||||
@@ -244,7 +265,7 @@ pub async fn create_message(
|
||||
session_id: Set(req.session_id),
|
||||
sender_id: Set(req.sender_id),
|
||||
sender_role: Set(req.sender_role),
|
||||
content_type: Set(req.content_type.unwrap_or_else(|| "text".to_string())),
|
||||
content_type: Set(content_type),
|
||||
content: Set(req.content),
|
||||
is_read: Set(false),
|
||||
created_at: Set(now),
|
||||
|
||||
@@ -10,8 +10,9 @@ use erp_core::error::check_version;
|
||||
use erp_core::types::PaginatedResponse;
|
||||
|
||||
use crate::dto::follow_up_dto::*;
|
||||
use crate::entity::{follow_up_record, follow_up_task};
|
||||
use crate::entity::{follow_up_record, follow_up_task, patient};
|
||||
use crate::error::{HealthError, HealthResult};
|
||||
use crate::service::validation::validate_follow_up_type;
|
||||
use crate::state::HealthState;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -65,6 +66,18 @@ pub async fn create_task(
|
||||
req: CreateFollowUpTaskReq,
|
||||
) -> HealthResult<FollowUpTaskResp> {
|
||||
let now = Utc::now();
|
||||
|
||||
validate_follow_up_type(&req.follow_up_type)?;
|
||||
|
||||
// 校验患者存在
|
||||
patient::Entity::find()
|
||||
.filter(patient::Column::Id.eq(req.patient_id))
|
||||
.filter(patient::Column::TenantId.eq(tenant_id))
|
||||
.filter(patient::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::PatientNotFound)?;
|
||||
|
||||
let active = follow_up_task::ActiveModel {
|
||||
id: Set(Uuid::now_v7()),
|
||||
tenant_id: Set(tenant_id),
|
||||
@@ -119,6 +132,13 @@ pub async fn update_task(
|
||||
let next_ver = check_version(expected_version, model.version)
|
||||
.map_err(|_| HealthError::VersionMismatch)?;
|
||||
|
||||
if let Some(ref ft) = req.follow_up_type { validate_follow_up_type(ft)?; }
|
||||
|
||||
// 状态机验证: follow_up_task.status
|
||||
if let Some(ref new_status) = req.status {
|
||||
validate_follow_up_status_transition(&model.status, new_status)?;
|
||||
}
|
||||
|
||||
let mut active: follow_up_task::ActiveModel = model.into();
|
||||
if let Some(v) = req.assigned_to { active.assigned_to = Set(Some(v)); }
|
||||
if let Some(v) = req.follow_up_type { active.follow_up_type = Set(v); }
|
||||
@@ -301,3 +321,22 @@ pub async fn list_records(
|
||||
|
||||
Ok(PaginatedResponse { data, total, page, page_size: limit, total_pages })
|
||||
}
|
||||
|
||||
/// 随访任务状态机: pending → in_progress/cancelled, in_progress → completed/cancelled
|
||||
fn validate_follow_up_status_transition(current: &str, new_status: &str) -> HealthResult<()> {
|
||||
if current == new_status {
|
||||
return Ok(());
|
||||
}
|
||||
let allowed = match current {
|
||||
"pending" => matches!(new_status, "in_progress" | "cancelled"),
|
||||
"in_progress" => matches!(new_status, "completed" | "cancelled"),
|
||||
_ => false,
|
||||
};
|
||||
if allowed {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(HealthError::InvalidStatusTransition(format!(
|
||||
"follow_up_task.status: 不允许从 '{}' 转换到 '{}'", current, new_status
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use erp_core::error::check_version;
|
||||
use erp_core::types::PaginatedResponse;
|
||||
|
||||
use crate::dto::health_data_dto::*;
|
||||
use crate::entity::{health_record, health_trend, lab_report, vital_signs};
|
||||
use crate::entity::{health_record, health_trend, lab_report, patient, vital_signs};
|
||||
use crate::error::{HealthError, HealthResult};
|
||||
use crate::state::HealthState;
|
||||
|
||||
@@ -71,6 +71,15 @@ pub async fn create_vital_signs(
|
||||
operator_id: Option<Uuid>,
|
||||
req: CreateVitalSignsReq,
|
||||
) -> HealthResult<VitalSignsResp> {
|
||||
// 校验患者存在
|
||||
patient::Entity::find()
|
||||
.filter(patient::Column::Id.eq(patient_id))
|
||||
.filter(patient::Column::TenantId.eq(tenant_id))
|
||||
.filter(patient::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::PatientNotFound)?;
|
||||
|
||||
let now = Utc::now();
|
||||
let active = vital_signs::ActiveModel {
|
||||
id: Set(Uuid::now_v7()),
|
||||
@@ -122,7 +131,7 @@ pub async fn update_vital_signs(
|
||||
.filter(vital_signs::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::PatientNotFound)?;
|
||||
.ok_or(HealthError::VitalSignsNotFound)?;
|
||||
let next_ver = check_version(expected_version, model.version)
|
||||
.map_err(|_| HealthError::VersionMismatch)?;
|
||||
|
||||
@@ -167,7 +176,7 @@ pub async fn delete_vital_signs(
|
||||
.filter(vital_signs::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::PatientNotFound)?;
|
||||
.ok_or(HealthError::VitalSignsNotFound)?;
|
||||
|
||||
let mut active: vital_signs::ActiveModel = model.into();
|
||||
active.deleted_at = Set(Some(Utc::now()));
|
||||
@@ -271,7 +280,7 @@ pub async fn update_lab_report(
|
||||
.filter(lab_report::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::PatientNotFound)?;
|
||||
.ok_or(HealthError::LabReportNotFound)?;
|
||||
let next_ver = check_version(expected_version, model.version)
|
||||
.map_err(|_| HealthError::VersionMismatch)?;
|
||||
|
||||
@@ -306,7 +315,7 @@ pub async fn delete_lab_report(
|
||||
.filter(lab_report::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::PatientNotFound)?;
|
||||
.ok_or(HealthError::LabReportNotFound)?;
|
||||
|
||||
let mut active: lab_report::ActiveModel = model.into();
|
||||
active.deleted_at = Set(Some(Utc::now()));
|
||||
@@ -403,7 +412,7 @@ pub async fn update_health_record(
|
||||
.filter(health_record::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::PatientNotFound)?;
|
||||
.ok_or(HealthError::HealthRecordNotFound)?;
|
||||
let next_ver = check_version(expected_version, model.version)
|
||||
.map_err(|_| HealthError::VersionMismatch)?;
|
||||
|
||||
@@ -439,7 +448,7 @@ pub async fn delete_health_record(
|
||||
.filter(health_record::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::PatientNotFound)?;
|
||||
.ok_or(HealthError::HealthRecordNotFound)?;
|
||||
|
||||
let mut active: health_record::ActiveModel = model.into();
|
||||
active.deleted_at = Set(Some(Utc::now()));
|
||||
|
||||
@@ -5,3 +5,4 @@ pub mod follow_up_service;
|
||||
pub mod health_data_service;
|
||||
pub mod patient_service;
|
||||
pub mod seed;
|
||||
pub mod validation;
|
||||
|
||||
@@ -15,6 +15,7 @@ use crate::entity::patient_family_member;
|
||||
use crate::entity::patient_tag_relation;
|
||||
use crate::entity::patient_doctor_relation;
|
||||
use crate::error::{HealthError, HealthResult};
|
||||
use crate::service::validation::{validate_gender, validate_blood_type};
|
||||
use crate::state::HealthState;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -97,6 +98,9 @@ pub async fn create_patient(
|
||||
let now = Utc::now();
|
||||
let id = Uuid::now_v7();
|
||||
|
||||
if let Some(ref g) = req.gender { validate_gender(g)?; }
|
||||
if let Some(ref bt) = req.blood_type { validate_blood_type(bt)?; }
|
||||
|
||||
let active = patient::ActiveModel {
|
||||
id: Set(id),
|
||||
tenant_id: Set(tenant_id),
|
||||
@@ -157,6 +161,26 @@ pub async fn update_patient(
|
||||
let next_ver = check_version(expected_version, model.version)
|
||||
.map_err(|_| HealthError::VersionMismatch)?;
|
||||
|
||||
if let Some(ref g) = req.gender { validate_gender(g)?; }
|
||||
if let Some(ref bt) = req.blood_type { validate_blood_type(bt)?; }
|
||||
|
||||
// 状态机验证: patient.status
|
||||
if let Some(ref new_status) = req.status {
|
||||
validate_status_transition("patient.status", &model.status, new_status, &[
|
||||
("active", "inactive"),
|
||||
("active", "deceased"),
|
||||
("inactive", "active"),
|
||||
])?;
|
||||
}
|
||||
// 状态机验证: patient.verification_status
|
||||
if let Some(ref new_vs) = req.verification_status {
|
||||
validate_status_transition("patient.verification_status", &model.verification_status, new_vs, &[
|
||||
("pending", "verified"),
|
||||
("pending", "rejected"),
|
||||
("rejected", "pending"),
|
||||
])?;
|
||||
}
|
||||
|
||||
let mut active: patient::ActiveModel = model.into();
|
||||
|
||||
if let Some(v) = req.name { active.name = Set(v); }
|
||||
@@ -170,6 +194,8 @@ pub async fn update_patient(
|
||||
if let Some(v) = req.emergency_contact_phone { active.emergency_contact_phone = Set(Some(v)); }
|
||||
if let Some(v) = req.source { active.source = Set(Some(v)); }
|
||||
if let Some(v) = req.notes { active.notes = Set(Some(v)); }
|
||||
if let Some(ref v) = req.status { active.status = Set(v.clone()); }
|
||||
if let Some(ref v) = req.verification_status { active.verification_status = Set(v.clone()); }
|
||||
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.updated_by = Set(operator_id);
|
||||
@@ -177,8 +203,16 @@ pub async fn update_patient(
|
||||
|
||||
let updated = active.update(&state.db).await?;
|
||||
|
||||
// 根据状态变更发布不同事件
|
||||
let event_type = if req.status.as_deref() == Some("deceased") {
|
||||
"patient.deceased"
|
||||
} else if req.verification_status.as_deref() == Some("verified") {
|
||||
"patient.verified"
|
||||
} else {
|
||||
"patient.updated"
|
||||
};
|
||||
let event = DomainEvent::new(
|
||||
"patient.updated",
|
||||
event_type,
|
||||
tenant_id,
|
||||
serde_json::json!({ "patient_id": updated.id }),
|
||||
);
|
||||
@@ -398,7 +432,7 @@ pub async fn create_family_member(
|
||||
pub async fn update_family_member(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
_patient_id: Uuid,
|
||||
patient_id: Uuid,
|
||||
family_member_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
req: FamilyMemberReq,
|
||||
@@ -406,11 +440,12 @@ pub async fn update_family_member(
|
||||
) -> HealthResult<FamilyMemberResp> {
|
||||
let model = patient_family_member::Entity::find()
|
||||
.filter(patient_family_member::Column::Id.eq(family_member_id))
|
||||
.filter(patient_family_member::Column::PatientId.eq(patient_id))
|
||||
.filter(patient_family_member::Column::TenantId.eq(tenant_id))
|
||||
.filter(patient_family_member::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::PatientNotFound)?;
|
||||
.ok_or(HealthError::FamilyMemberNotFound)?;
|
||||
|
||||
let next_ver = check_version(expected_version, model.version)
|
||||
.map_err(|_| HealthError::VersionMismatch)?;
|
||||
@@ -444,17 +479,18 @@ pub async fn update_family_member(
|
||||
pub async fn delete_family_member(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
_patient_id: Uuid,
|
||||
patient_id: Uuid,
|
||||
family_member_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
) -> HealthResult<()> {
|
||||
let model = patient_family_member::Entity::find()
|
||||
.filter(patient_family_member::Column::Id.eq(family_member_id))
|
||||
.filter(patient_family_member::Column::PatientId.eq(patient_id))
|
||||
.filter(patient_family_member::Column::TenantId.eq(tenant_id))
|
||||
.filter(patient_family_member::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::PatientNotFound)?;
|
||||
.ok_or(HealthError::FamilyMemberNotFound)?;
|
||||
|
||||
let mut active: patient_family_member::ActiveModel = model.into();
|
||||
active.deleted_at = Set(Some(Utc::now()));
|
||||
@@ -563,3 +599,23 @@ fn model_to_resp(m: patient::Model) -> PatientResp {
|
||||
version: m.version,
|
||||
}
|
||||
}
|
||||
|
||||
/// 状态机转换校验: 检查 (current → new) 是否在 allowed_transitions 中
|
||||
fn validate_status_transition(
|
||||
field_name: &str,
|
||||
current: &str,
|
||||
new_status: &str,
|
||||
allowed_transitions: &[(&str, &str)],
|
||||
) -> HealthResult<()> {
|
||||
if current == new_status {
|
||||
return Ok(());
|
||||
}
|
||||
if allowed_transitions.iter().any(|(from, to)| *from == current && *to == new_status) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(HealthError::InvalidStatusTransition(format!(
|
||||
"{}: 不允许从 '{}' 转换到 '{}'",
|
||||
field_name, current, new_status
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
108
crates/erp-health/src/service/validation.rs
Normal file
108
crates/erp-health/src/service/validation.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
//! 通用字段校验 — 枚举白名单、输入格式校验
|
||||
|
||||
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", "wechat", "visit", "sms", "other",
|
||||
]);
|
||||
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(())
|
||||
}
|
||||
Reference in New Issue
Block a user