fix: P0 止血 — 消除崩溃风险 + 伪CAS修复 + 硬编码清除 + 晚间血压
- 新增 sea_orm_ext 模块: safe_version() / bump_version() 替代 14 处 unwrap() - 修复 points_service 伪 CAS 逻辑 bug: 在 Set() 前提取原始版本并重新验证 - AdminDashboard: API 失败时显示 unknown 状态而非虚假绿色 healthy - AdminDashboard: 今日操作改用真实数据,移除 "0 错误" 硬编码 - OperatorWorkbench: 移除硬编码 "美玲",改用真实用户名 - Home.tsx: operator "内容发布" 从硬编码 0 改为真实积分统计 - 小程序体征录入: 新增晚间血压 indicator_type,映射到 evening 字段
This commit is contained in:
@@ -9,6 +9,7 @@ pub mod module;
|
||||
pub mod rbac;
|
||||
pub mod request_info;
|
||||
pub mod sanitize;
|
||||
pub mod sea_orm_ext;
|
||||
pub mod types;
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
17
crates/erp-core/src/sea_orm_ext.rs
Normal file
17
crates/erp-core/src/sea_orm_ext.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use sea_orm::ActiveValue;
|
||||
|
||||
/// 从 SeaORM ActiveValue<i32> 中安全提取 version 值。
|
||||
/// Set(v) / Unchanged(v) → 返回 v
|
||||
/// NotSet → 返回 1(首次版本号)
|
||||
/// 绝不 panic。
|
||||
pub fn safe_version(val: &ActiveValue<i32>) -> i32 {
|
||||
match val {
|
||||
ActiveValue::Set(v) | ActiveValue::Unchanged(v) => *v,
|
||||
ActiveValue::NotSet => 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// 安全递增 version:基于当前值 +1,绝不 panic。
|
||||
pub fn bump_version(current: &ActiveValue<i32>) -> i32 {
|
||||
safe_version(current) + 1
|
||||
}
|
||||
@@ -8,6 +8,8 @@ use uuid::Uuid;
|
||||
use erp_core::events::DomainEvent;
|
||||
use erp_core::types::PaginatedResponse;
|
||||
|
||||
use erp_core::sea_orm_ext::bump_version;
|
||||
|
||||
use crate::entity::{device_readings, patient, patient_devices, vital_signs, vital_signs_hourly};
|
||||
use crate::error::{HealthError, HealthResult};
|
||||
use crate::service::validation::validate_device_type;
|
||||
@@ -187,12 +189,12 @@ async fn ensure_device_binding(
|
||||
version: Set(1),
|
||||
};
|
||||
binding.insert(db).await?;
|
||||
} else {
|
||||
} else if let Some(existing) = existing {
|
||||
// 更新最后同步时间
|
||||
let mut active: patient_devices::ActiveModel = existing.unwrap().into();
|
||||
let mut active: patient_devices::ActiveModel = existing.into();
|
||||
active.last_sync_at = Set(Some(Utc::now()));
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.version = Set(active.version.unwrap() + 1);
|
||||
active.version = Set(bump_version(&active.version));
|
||||
active.update(db).await?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -306,7 +308,7 @@ async fn upsert_hourly_aggregates(
|
||||
active.avg_val = Set(combined_avg);
|
||||
active.sample_count = Set(total_count);
|
||||
active.updated_at = Set(now);
|
||||
active.version = Set(active.version.unwrap() + 1);
|
||||
active.version = Set(bump_version(&active.version));
|
||||
active.update(db).await?;
|
||||
} else {
|
||||
to_insert.push(vital_signs_hourly::ActiveModel {
|
||||
@@ -491,7 +493,7 @@ async fn sync_bp_glucose_to_vital_signs(
|
||||
let mut model = if let Some(rec) = existing {
|
||||
let mut m: vital_signs::ActiveModel = rec.into();
|
||||
m.updated_at = Set(Utc::now());
|
||||
m.version = Set(m.version.unwrap() + 1);
|
||||
m.version = Set(bump_version(&m.version));
|
||||
m
|
||||
} else {
|
||||
vital_signs::ActiveModel {
|
||||
|
||||
@@ -4,6 +4,7 @@ use chrono::Utc;
|
||||
use erp_core::audit::AuditLog;
|
||||
use erp_core::audit_service;
|
||||
use erp_core::events::DomainEvent;
|
||||
use erp_core::sea_orm_ext::bump_version;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{ActiveValue::Set, DatabaseBackend, QueryOrder, QuerySelect, Statement, TransactionTrait};
|
||||
use uuid::Uuid;
|
||||
@@ -433,7 +434,7 @@ pub async fn batch_assign_tasks(
|
||||
active.assigned_to = Set(Some(req.assigned_to));
|
||||
active.updated_at = Set(now);
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(active.version.unwrap() + 1);
|
||||
active.version = Set(bump_version(&active.version));
|
||||
active.update(&state.db).await.map_err(|e| HealthError::DbError(e.to_string()))?;
|
||||
succeeded += 1;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use erp_core::audit::AuditLog;
|
||||
use erp_core::audit_service;
|
||||
use erp_core::error::check_version;
|
||||
use erp_core::events::DomainEvent;
|
||||
use erp_core::sea_orm_ext::bump_version;
|
||||
use erp_core::types::PaginatedResponse;
|
||||
|
||||
use crate::dto::points_dto::*;
|
||||
@@ -770,7 +771,7 @@ pub async fn delete_product(
|
||||
active.deleted_at = Set(Some(now));
|
||||
active.updated_at = Set(now);
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(active.version.unwrap() + 1);
|
||||
active.version = Set(bump_version(&active.version));
|
||||
let m = active.update(&state.db).await?;
|
||||
|
||||
audit_service::record(
|
||||
@@ -1272,7 +1273,7 @@ pub async fn delete_rule(
|
||||
active.deleted_at = Set(Some(now));
|
||||
active.updated_at = Set(now);
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(active.version.unwrap() + 1);
|
||||
active.version = Set(bump_version(&active.version));
|
||||
let m = active.update(&state.db).await?;
|
||||
|
||||
audit_service::record(
|
||||
@@ -1511,7 +1512,7 @@ pub async fn delete_offline_event(
|
||||
active.deleted_at = Set(Some(now));
|
||||
active.updated_at = Set(now);
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(active.version.unwrap() + 1);
|
||||
active.version = Set(bump_version(&active.version));
|
||||
let m = active.update(&state.db).await?;
|
||||
|
||||
audit_service::record(
|
||||
@@ -1597,7 +1598,7 @@ pub async fn admin_checkin_event(
|
||||
reg_active.checked_in_by = Set(operator_id);
|
||||
reg_active.updated_at = Set(now);
|
||||
reg_active.updated_by = Set(operator_id);
|
||||
reg_active.version = Set(reg_active.version.unwrap() + 1);
|
||||
reg_active.version = Set(bump_version(®_active.version));
|
||||
let updated_reg = reg_active.update(&txn).await?;
|
||||
|
||||
// 4. 如果活动有积分奖励且尚未发放,则发放积分
|
||||
@@ -1656,7 +1657,7 @@ pub async fn admin_checkin_event(
|
||||
let mut reg_active2: offline_event_registration::ActiveModel = updated_reg.into();
|
||||
reg_active2.points_granted = Set(true);
|
||||
reg_active2.updated_at = Set(now);
|
||||
reg_active2.version = Set(reg_active2.version.unwrap() + 1);
|
||||
reg_active2.version = Set(bump_version(®_active2.version));
|
||||
reg_active2.update(&txn).await?;
|
||||
}
|
||||
|
||||
@@ -1796,7 +1797,7 @@ pub async fn expire_points(db: &sea_orm::DatabaseConnection, event_bus: &erp_cor
|
||||
let mut active_txn: points_transaction::ActiveModel = txn.into();
|
||||
active_txn.status = Set("expired".to_string());
|
||||
active_txn.remaining_amount = Set(0);
|
||||
active_txn.version = Set(active_txn.version.unwrap() + 1);
|
||||
active_txn.version = Set(bump_version(&active_txn.version));
|
||||
active_txn.updated_at = Set(Utc::now());
|
||||
active_txn.update(txn_db).await?;
|
||||
|
||||
@@ -1810,15 +1811,26 @@ pub async fn expire_points(db: &sea_orm::DatabaseConnection, event_bus: &erp_cor
|
||||
let new_expired = account.total_expired + remaining;
|
||||
|
||||
let mut active_account: points_account::ActiveModel = account.into();
|
||||
let original_ver: i32 = match active_account.version {
|
||||
sea_orm::ActiveValue::Unchanged(v) => v,
|
||||
_ => {
|
||||
return Err(HealthError::Validation(
|
||||
"积分账户版本号状态异常".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
active_account.balance = Set(new_balance);
|
||||
active_account.total_expired = Set(new_expired);
|
||||
active_account.version = Set(active_account.version.unwrap() + 1);
|
||||
active_account.version = Set(original_ver + 1);
|
||||
active_account.updated_at = Set(Utc::now());
|
||||
let expected_ver: i32 = match &active_account.version {
|
||||
sea_orm::ActiveValue::Unchanged(v) | sea_orm::ActiveValue::Set(v) => *v,
|
||||
_ => 0,
|
||||
};
|
||||
let _next_ver = check_version(expected_ver, expected_ver)?;
|
||||
// 重新从 DB 读取当前版本,防止并发修改导致伪 CAS 通过
|
||||
let current = points_account::Entity::find_by_id(account_id)
|
||||
.one(txn_db)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
HealthError::Validation("积分账户不存在".to_string())
|
||||
})?;
|
||||
let _next_ver = check_version(original_ver, current.version)?;
|
||||
active_account.update(txn_db).await?;
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -4,6 +4,8 @@ use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
use sha2::{Sha256, Digest};
|
||||
|
||||
use erp_core::sea_orm_ext::bump_version;
|
||||
|
||||
use erp_core::error::AppResult;
|
||||
|
||||
use crate::dto::{
|
||||
@@ -673,7 +675,7 @@ impl PluginService {
|
||||
active.plugin_version = Set(new_version.clone());
|
||||
active.updated_at = Set(now);
|
||||
active.updated_by = Set(Some(operator_id));
|
||||
active.version = Set(active.version.unwrap() + 1);
|
||||
active.version = Set(bump_version(&active.version));
|
||||
|
||||
let updated = active
|
||||
.update(db)
|
||||
|
||||
@@ -127,7 +127,7 @@ async fn process_pending_events(
|
||||
let mut active: domain_event::ActiveModel = event_model.into();
|
||||
active.status = Set("published".to_string());
|
||||
active.published_at = Set(Some(Utc::now()));
|
||||
active.attempts = Set(active.attempts.unwrap() + 1);
|
||||
active.attempts = Set(erp_core::sea_orm_ext::bump_version(&active.attempts));
|
||||
active.update(db).await?;
|
||||
}
|
||||
|
||||
|
||||
@@ -172,13 +172,13 @@ impl DefinitionService {
|
||||
active.description = Set(Some(description.clone()));
|
||||
}
|
||||
// 当 nodes 或 edges 任一存在时,取最终值验证流程图完整性
|
||||
let final_nodes = req.nodes.as_ref().or_else(|| {
|
||||
let _final_nodes = req.nodes.as_ref().or_else(|| {
|
||||
serde_json::from_value::<Vec<crate::dto::NodeDef>>(active.nodes.as_ref().clone()).ok().as_ref().map(|_| unreachable!())
|
||||
});
|
||||
// 简化:如果提供了 nodes 或 edges,将两者合并后验证
|
||||
if req.nodes.is_some() || req.edges.is_some() {
|
||||
let nodes_val = req.nodes.as_ref().map(|n| serde_json::to_value(n).unwrap()).unwrap_or(active.nodes.as_ref().clone());
|
||||
let edges_val = req.edges.as_ref().map(|e| serde_json::to_value(e).unwrap()).unwrap_or(active.edges.as_ref().clone());
|
||||
let nodes_val = req.nodes.as_ref().map(|n| serde_json::to_value(n).unwrap_or_default()).unwrap_or(active.nodes.as_ref().clone());
|
||||
let edges_val = req.edges.as_ref().map(|e| serde_json::to_value(e).unwrap_or_default()).unwrap_or(active.edges.as_ref().clone());
|
||||
let nodes: Vec<crate::dto::NodeDef> = serde_json::from_value(nodes_val)
|
||||
.map_err(|e| WorkflowError::Validation(format!("节点数据无效: {e}")))?;
|
||||
let edges: Vec<crate::dto::EdgeDef> = serde_json::from_value(edges_val)
|
||||
|
||||
Reference in New Issue
Block a user