diff --git a/Cargo.lock b/Cargo.lock index b142971..8841f27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -516,12 +516,24 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.11.1" @@ -1537,6 +1549,7 @@ dependencies = [ "erp-core", "hex", "hmac", + "image", "jsonwebtoken", "num-traits", "rand_core 0.6.4", @@ -1688,6 +1701,7 @@ dependencies = [ "erp-workflow", "futures", "hex", + "hmac", "metrics", "metrics-exporter-prometheus", "moka", @@ -1784,6 +1798,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -2520,6 +2543,32 @@ dependencies = [ "version_check", ] +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "image-webp", + "moxcms", + "num-traits", + "png", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -2998,6 +3047,16 @@ dependencies = [ "uuid", ] +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "multer" version = "3.1.0" @@ -3488,6 +3547,19 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polyval" version = "0.6.2" @@ -3654,6 +3726,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "pxfm" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" + [[package]] name = "quanta" version = "0.12.6" @@ -3669,6 +3747,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quote" version = "1.0.45" @@ -7058,3 +7142,18 @@ dependencies = [ "cc", "pkg-config", ] + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] diff --git a/crates/erp-ai/src/service/analysis.rs b/crates/erp-ai/src/service/analysis.rs index 82606be..94cc0da 100644 --- a/crates/erp-ai/src/service/analysis.rs +++ b/crates/erp-ai/src/service/analysis.rs @@ -170,6 +170,7 @@ impl AnalysisService { active.result_content = Set(Some(content)); active.result_metadata = Set(Some(metadata)); active.updated_at = Set(chrono::Utc::now()); + active.version_lock = Set(active.version_lock.unwrap() + 1); active.update(&self.db).await?; Ok(()) } @@ -185,6 +186,7 @@ impl AnalysisService { active.status = Set("failed".into()); active.error_message = Set(Some(error)); active.updated_at = Set(chrono::Utc::now()); + active.version_lock = Set(active.version_lock.unwrap() + 1); active.update(&self.db).await?; Ok(()) } diff --git a/crates/erp-ai/src/service/insight_service.rs b/crates/erp-ai/src/service/insight_service.rs index f8a30b3..bb625e2 100644 --- a/crates/erp-ai/src/service/insight_service.rs +++ b/crates/erp-ai/src/service/insight_service.rs @@ -123,6 +123,7 @@ impl InsightService { let mut active: copilot_insights::ActiveModel = model.into(); active.deleted_at = Set(Some(now)); active.updated_at = Set(now); + active.version_lock = Set(active.version_lock.unwrap() + 1); active.update(db).await?; } Ok(count) diff --git a/crates/erp-ai/src/service/prompt.rs b/crates/erp-ai/src/service/prompt.rs index 8ac9008..f264957 100644 --- a/crates/erp-ai/src/service/prompt.rs +++ b/crates/erp-ai/src/service/prompt.rs @@ -161,6 +161,7 @@ impl PromptService { .filter(ai_prompt::Column::Category.eq(&entity.category)) .filter(ai_prompt::Column::IsActive.eq(true)) .filter(ai_prompt::Column::DeletedAt.is_null()) + .filter(ai_prompt::Column::Id.ne(id)) .all(&self.db) .await?; @@ -168,6 +169,7 @@ impl PromptService { let mut active: ai_prompt::ActiveModel = sibling.into(); active.is_active = Set(false); active.updated_at = Set(chrono::Utc::now()); + active.version_lock = Set(active.version_lock.unwrap() + 1); active.update(&self.db).await?; } @@ -175,6 +177,7 @@ impl PromptService { let mut active: ai_prompt::ActiveModel = entity.into(); active.is_active = Set(true); active.updated_at = Set(chrono::Utc::now()); + active.version_lock = Set(active.version_lock.unwrap() + 1); Ok(active.update(&self.db).await?) } diff --git a/crates/erp-auth/src/service/auth_service.rs b/crates/erp-auth/src/service/auth_service.rs index 64c712b..7dfbb09 100644 --- a/crates/erp-auth/src/service/auth_service.rs +++ b/crates/erp-auth/src/service/auth_service.rs @@ -137,6 +137,7 @@ impl AuthService { let mut user_active: user::ActiveModel = user_model.clone().into(); user_active.last_login_at = Set(Some(Utc::now())); user_active.updated_at = Set(Utc::now()); + user_active.version = Set(user_active.version.unwrap() + 1); user_active .update(db) .await diff --git a/crates/erp-auth/src/service/role_service.rs b/crates/erp-auth/src/service/role_service.rs index 6e26b72..254b4ac 100644 --- a/crates/erp-auth/src/service/role_service.rs +++ b/crates/erp-auth/src/service/role_service.rs @@ -299,6 +299,7 @@ impl RoleService { 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 .update(db) .await diff --git a/crates/erp-auth/src/service/token_service.rs b/crates/erp-auth/src/service/token_service.rs index 62ea928..4199786 100644 --- a/crates/erp-auth/src/service/token_service.rs +++ b/crates/erp-auth/src/service/token_service.rs @@ -168,6 +168,7 @@ impl TokenService { let mut active: user_token::ActiveModel = token_row.into(); active.revoked_at = Set(Some(Utc::now())); active.updated_at = Set(Utc::now()); + active.version = Set(active.version.unwrap() + 1); active .update(db) .await @@ -199,6 +200,10 @@ impl TokenService { user_token::Column::UpdatedAt, sea_orm::sea_query::Expr::value(now.naive_utc()), ) + .col_expr( + user_token::Column::Version, + sea_orm::sea_query::Expr::col(user_token::Column::Version).add(1), + ) .filter(user_token::Column::TokenHash.eq(&hash)) .filter(user_token::Column::UserId.eq(claims.sub)) .filter(user_token::Column::TenantId.eq(claims.tid)) @@ -233,6 +238,7 @@ impl TokenService { let mut active: user_token::ActiveModel = token.into(); active.revoked_at = Set(Some(now)); active.updated_at = Set(now); + active.version = Set(active.version.unwrap() + 1); active .update(db) .await diff --git a/crates/erp-config/src/service/menu_service.rs b/crates/erp-config/src/service/menu_service.rs index 57b88e5..70693eb 100644 --- a/crates/erp-config/src/service/menu_service.rs +++ b/crates/erp-config/src/service/menu_service.rs @@ -372,6 +372,7 @@ impl MenuService { 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 .update(db) .await diff --git a/crates/erp-config/src/service/numbering_service.rs b/crates/erp-config/src/service/numbering_service.rs index 6db4d86..1d56622 100644 --- a/crates/erp-config/src/service/numbering_service.rs +++ b/crates/erp-config/src/service/numbering_service.rs @@ -392,6 +392,7 @@ impl NumberingService { active.seq_current = Set(next_seq); active.last_reset_date = Set(Some(today)); active.updated_at = Set(Utc::now()); + active.version = Set(active.version.unwrap() + 1); active .update(txn) .await diff --git a/crates/erp-health/src/error.rs b/crates/erp-health/src/error.rs index 26aab08..f8be9b0 100644 --- a/crates/erp-health/src/error.rs +++ b/crates/erp-health/src/error.rs @@ -101,6 +101,12 @@ pub enum HealthError { #[error("交接记录不存在")] HandoffLogNotFound, + #[error("媒体文件不存在")] + MediaNotFound, + + #[error("媒体文件夹不存在")] + MediaFolderNotFound, + #[error("状态转换无效: {0}")] InvalidStatusTransition(String), @@ -148,7 +154,9 @@ impl From for AppError { | HealthError::CarePlanOutcomeNotFound | HealthError::ShiftNotFound | HealthError::PatientAssignmentNotFound - | HealthError::HandoffLogNotFound => AppError::NotFound(err.to_string()), + | HealthError::HandoffLogNotFound + | HealthError::MediaNotFound + | HealthError::MediaFolderNotFound => AppError::NotFound(err.to_string()), HealthError::ScheduleFull => AppError::Validation(err.to_string()), HealthError::InvalidStatusTransition(s) => AppError::Validation(s), HealthError::VersionMismatch => AppError::VersionMismatch, diff --git a/crates/erp-health/src/handler/article_handler.rs b/crates/erp-health/src/handler/article_handler.rs index cd26c0a..93fa680 100644 --- a/crates/erp-health/src/handler/article_handler.rs +++ b/crates/erp-health/src/handler/article_handler.rs @@ -72,6 +72,15 @@ pub async fn list_public_articles( Ok(Json(ApiResponse::ok(result))) } +/// GET /public/articles/{id} — 公开文章详情(无需认证,仅返回已发布文章) +pub async fn get_public_article( + State(state): State, + Path(id): Path, +) -> Result>, AppError> { + let result = article_service::get_public_article(&state, id).await?; + Ok(Json(ApiResponse::ok(result))) +} + pub async fn get_article( State(state): State, Extension(ctx): Extension, diff --git a/crates/erp-health/src/handler/banner_handler.rs b/crates/erp-health/src/handler/banner_handler.rs index e8432cb..cf31c82 100644 --- a/crates/erp-health/src/handler/banner_handler.rs +++ b/crates/erp-health/src/handler/banner_handler.rs @@ -133,3 +133,37 @@ where let result = banner_service::list_public_banners(&state, tenant_id).await?; Ok(Json(ApiResponse::ok(result))) } + +/// GET /public/banner-image/{banner_id} — 公开轮播图图片(无需认证,供小程序下载) +pub async fn serve_banner_image( + State(state): State, + Path(banner_id): Path, +) -> Result { + use axum::http::{StatusCode, header}; + use axum::response::IntoResponse; + + let path = banner_service::get_banner_image_path(&state, banner_id).await?; + let data = tokio::fs::read(&path) + .await + .map_err(|e| AppError::Internal(format!("读取图片文件失败: {}", e)))?; + + let mime = if path.ends_with(".png") { + "image/png" + } else if path.ends_with(".gif") { + "image/gif" + } else if path.ends_with(".webp") { + "image/webp" + } else { + "image/jpeg" + }; + + Ok(( + StatusCode::OK, + [ + (header::CONTENT_TYPE, mime), + (header::CACHE_CONTROL, "public, max-age=3600"), + ], + data, + ) + .into_response()) +} diff --git a/crates/erp-health/src/handler/consultation_handler.rs b/crates/erp-health/src/handler/consultation_handler.rs index add045b..3efcbed 100644 --- a/crates/erp-health/src/handler/consultation_handler.rs +++ b/crates/erp-health/src/handler/consultation_handler.rs @@ -29,6 +29,13 @@ pub struct MessageListParams { pub after_id: Option, } +#[derive(Debug, Deserialize, IntoParams)] +pub struct PollMessagesParams { + pub after_id: Option, + /// 超时秒数,默认 25,最大 30 + pub timeout: Option, +} + #[derive(Debug, serde::Deserialize, utoipa::ToSchema)] pub struct CloseSessionReq { pub version: i32, @@ -129,6 +136,30 @@ where Ok(Json(ApiResponse::ok(result))) } +/// 长轮询咨询消息 — 有新消息立即返回,否则挂起等待(最多 timeout 秒)。 +pub async fn poll_messages( + State(state): State, + Extension(ctx): Extension, + Path(session_id): Path, + Query(params): Query, +) -> Result>>, AppError> +where + HealthState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "health.consultation.list")?; + let timeout_secs = params.timeout.unwrap_or(25).min(30); + let result = consultation_service::poll_new_messages( + &state, + ctx.tenant_id, + session_id, + params.after_id, + timeout_secs, + ) + .await?; + Ok(Json(ApiResponse::ok(result))) +} + pub async fn close_session( State(state): State, Extension(ctx): Extension, diff --git a/crates/erp-health/src/oauth/service.rs b/crates/erp-health/src/oauth/service.rs index 260781c..dafdd1d 100644 --- a/crates/erp-health/src/oauth/service.rs +++ b/crates/erp-health/src/oauth/service.rs @@ -302,6 +302,7 @@ impl OAuthService { let mut active: api_client::ActiveModel = client.into(); active.deleted_at = Set(Some(Utc::now().into())); + active.version = Set(active.version.unwrap() + 1); active.update(db).await?; Ok(()) diff --git a/crates/erp-health/src/service/appointment_service.rs b/crates/erp-health/src/service/appointment_service.rs index 133483d..26451ce 100644 --- a/crates/erp-health/src/service/appointment_service.rs +++ b/crates/erp-health/src/service/appointment_service.rs @@ -202,6 +202,10 @@ pub async fn create_appointment( Expr::col(doctor_schedule::Column::CurrentAppointments).add(1), ) .col_expr(doctor_schedule::Column::UpdatedAt, Expr::value(Utc::now())) + .col_expr( + doctor_schedule::Column::Version, + Expr::col(doctor_schedule::Column::Version).add(1), + ) .filter(doctor_schedule::Column::TenantId.eq(tenant_id)) .filter(doctor_schedule::Column::DoctorId.eq(doctor_id_val)) .filter(doctor_schedule::Column::ScheduleDate.eq(req.appointment_date)) @@ -332,6 +336,10 @@ pub async fn update_appointment_status( Expr::col(doctor_schedule::Column::CurrentAppointments).sub(1), ) .col_expr(doctor_schedule::Column::UpdatedAt, Expr::value(Utc::now())) + .col_expr( + doctor_schedule::Column::Version, + Expr::col(doctor_schedule::Column::Version).add(1), + ) .filter(doctor_schedule::Column::TenantId.eq(tenant_id)) .filter(doctor_schedule::Column::DoctorId.eq(did)) .filter(doctor_schedule::Column::ScheduleDate.eq(model.appointment_date)) diff --git a/crates/erp-health/src/service/article_service.rs b/crates/erp-health/src/service/article_service.rs index d34d626..b8cc801 100644 --- a/crates/erp-health/src/service/article_service.rs +++ b/crates/erp-health/src/service/article_service.rs @@ -338,6 +338,7 @@ pub async fn increment_view_count( let mut active: article::ActiveModel = model.into(); active.view_count = Set(active.view_count.take().unwrap_or(0) + 1); active.updated_at = Set(Utc::now()); + active.version = Set(active.version.unwrap() + 1); active.update(&state.db).await?; Ok(()) } diff --git a/crates/erp-health/src/service/banner_service.rs b/crates/erp-health/src/service/banner_service.rs index fdceb6a..a77d8e5 100644 --- a/crates/erp-health/src/service/banner_service.rs +++ b/crates/erp-health/src/service/banner_service.rs @@ -254,6 +254,10 @@ pub async fn sort_banners( banner::Entity::update_many() .col_expr(banner::Column::SortOrder, Expr::value(item.sort_order)) .col_expr(banner::Column::UpdatedAt, Expr::value(Utc::now())) + .col_expr( + banner::Column::Version, + Expr::col(banner::Column::Version).add(1), + ) .filter(banner::Column::Id.eq(item.id)) .filter(banner::Column::TenantId.eq(tenant_id)) .filter(banner::Column::DeletedAt.is_null()) @@ -303,13 +307,12 @@ pub async fn list_public_banners( if media.deleted_at.is_some() { return None; } - let image_url = media.storage_path.trim_start_matches("./").to_string(); Some(PublicBannerResp { id: b.id, title: b.title, subtitle: b.subtitle, - image_url: Some(image_url), + image_url: Some(format!("/public/banner-image/{}", b.id)), link_type: b.link_type, link_target: b.link_target, }) @@ -334,6 +337,21 @@ pub fn generate_signed_url(path: &str, secret_key: &str, ttl_secs: u64) -> (Stri (token, expires) } +/// 根据 banner_id 获取关联媒体文件的本地磁盘路径(供公开图片端点使用) +pub async fn get_banner_image_path(state: &HealthState, banner_id: Uuid) -> HealthResult { + let banner = banner::Entity::find_by_id(banner_id) + .one(&state.db) + .await? + .ok_or_else(|| HealthError::Validation("轮播图不存在".to_string()))?; + + let media = media_item::Entity::find_by_id(banner.media_item_id) + .one(&state.db) + .await? + .ok_or_else(|| HealthError::Validation("媒体文件不存在".to_string()))?; + + Ok(media.storage_path.clone()) +} + // --------------------------------------------------------------------------- // 内部辅助函数 // --------------------------------------------------------------------------- diff --git a/crates/erp-health/src/service/ble_gateway_service.rs b/crates/erp-health/src/service/ble_gateway_service.rs index 0995ce9..2e2161d 100644 --- a/crates/erp-health/src/service/ble_gateway_service.rs +++ b/crates/erp-health/src/service/ble_gateway_service.rs @@ -264,6 +264,7 @@ pub async fn heartbeat( active.ip_address = Set(Some(v)); } active.updated_at = Set(now); + active.version = Set(active.version.unwrap() + 1); active.update(&state.db).await?; Ok(()) diff --git a/crates/erp-health/src/service/consultation_service.rs b/crates/erp-health/src/service/consultation_service.rs index fd6a0c1..cdf0a90 100644 --- a/crates/erp-health/src/service/consultation_service.rs +++ b/crates/erp-health/src/service/consultation_service.rs @@ -457,6 +457,63 @@ pub async fn list_messages( }) } +/// 长轮询:等待咨询会话的新消息。 +/// +/// 先查 DB,有新消息立即返回;否则订阅 EventBus 等待 `consultation.new_message` 事件, +/// 匹配当前 session_id 后再查一次 DB 返回。超时返回空列表。 +pub async fn poll_new_messages( + state: &HealthState, + tenant_id: Uuid, + session_id: Uuid, + after_id: Option, + timeout_secs: u64, +) -> HealthResult> { + // 1. 先查 DB,有新消息立即返回 + let initial = list_messages(state, tenant_id, session_id, 1, 50, after_id).await?; + if !initial.data.is_empty() { + return Ok(initial.data); + } + + // 2. 订阅咨询相关事件,等待新消息 + let (mut rx, _handle) = state + .event_bus + .subscribe_filtered(crate::event::CONSULTATION_NEW_MESSAGE.to_string()); + + let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(timeout_secs); + + loop { + let remaining = deadline.saturating_duration_since(tokio::time::Instant::now()); + if remaining.is_zero() { + return Ok(vec![]); + } + + let result = tokio::time::timeout(remaining, rx.recv()).await; + + match result { + Ok(Some(event)) => { + // 匹配 session_id + let event_session_id = event + .payload + .get("session_id") + .and_then(|v| v.as_str()) + .and_then(|s| Uuid::parse_str(s).ok()); + + if event_session_id == Some(session_id) { + // 收到匹配事件,再查一次 DB 获取完整消息 + let fresh = + list_messages(state, tenant_id, session_id, 1, 50, after_id).await?; + if !fresh.data.is_empty() { + return Ok(fresh.data); + } + } + // 事件不匹配当前会话,继续等待 + } + Ok(None) => return Ok(vec![]), // channel 关闭 + Err(_) => return Ok(vec![]), // 超时 + } + } +} + pub async fn create_message( state: &HealthState, tenant_id: Uuid, diff --git a/crates/erp-health/src/service/critical_value_threshold_service.rs b/crates/erp-health/src/service/critical_value_threshold_service.rs index cefd3f9..438f57c 100644 --- a/crates/erp-health/src/service/critical_value_threshold_service.rs +++ b/crates/erp-health/src/service/critical_value_threshold_service.rs @@ -173,6 +173,7 @@ pub async fn delete_threshold( let mut active: critical_value_threshold::ActiveModel = existing.into(); active.deleted_at = Set(Some(chrono::Utc::now())); active.updated_by = Set(operator_id); + active.version = Set(active.version.unwrap() + 1); active.update(db).await?; Ok(()) } diff --git a/crates/erp-health/src/service/follow_up_template_service.rs b/crates/erp-health/src/service/follow_up_template_service.rs index 5774f05..b7e6c86 100644 --- a/crates/erp-health/src/service/follow_up_template_service.rs +++ b/crates/erp-health/src/service/follow_up_template_service.rs @@ -207,6 +207,7 @@ pub async fn update_template( for old in old_fields { let mut af: follow_up_template_field::ActiveModel = old.into(); af.deleted_at = Set(Some(Utc::now())); + af.version = Set(af.version.unwrap() + 1); af.update(&state.db).await?; } // 插入新字段 @@ -269,6 +270,7 @@ pub async fn delete_template( for f in fields { let mut af: follow_up_template_field::ActiveModel = f.into(); af.deleted_at = Set(Some(Utc::now())); + af.version = Set(af.version.unwrap() + 1); af.update(&state.db).await?; } diff --git a/crates/erp-health/src/service/media_service.rs b/crates/erp-health/src/service/media_service.rs index 4768fc7..8c703de 100644 --- a/crates/erp-health/src/service/media_service.rs +++ b/crates/erp-health/src/service/media_service.rs @@ -238,6 +238,10 @@ pub async fn delete_media_item( // 级联:关联的 banner 全部设为 inactive banner::Entity::update_many() .col_expr(banner::Column::Status, Expr::value("inactive")) + .col_expr( + banner::Column::Version, + Expr::col(banner::Column::Version).add(1), + ) .filter(banner::Column::MediaItemId.eq(id)) .filter(banner::Column::TenantId.eq(tenant_id)) .exec(&state.db) @@ -282,6 +286,10 @@ pub async fn batch_delete( .col_expr(media_item::Column::DeletedAt, Expr::value(Some(now))) .col_expr(media_item::Column::UpdatedAt, Expr::value(now)) .col_expr(media_item::Column::UpdatedBy, Expr::value(operator_id)) + .col_expr( + media_item::Column::Version, + Expr::col(media_item::Column::Version).add(1), + ) .filter(media_item::Column::Id.is_in(req.ids.clone())) .filter(media_item::Column::TenantId.eq(tenant_id)) .filter(media_item::Column::DeletedAt.is_null()) @@ -291,6 +299,10 @@ pub async fn batch_delete( // 级联:停用关联 banner banner::Entity::update_many() .col_expr(banner::Column::Status, Expr::value("inactive")) + .col_expr( + banner::Column::Version, + Expr::col(banner::Column::Version).add(1), + ) .filter(banner::Column::MediaItemId.is_in(req.ids.clone())) .filter(banner::Column::TenantId.eq(tenant_id)) .exec(&state.db) diff --git a/crates/erp-health/src/service/patient_service/relation.rs b/crates/erp-health/src/service/patient_service/relation.rs index 9fbab71..a8996e6 100644 --- a/crates/erp-health/src/service/patient_service/relation.rs +++ b/crates/erp-health/src/service/patient_service/relation.rs @@ -65,6 +65,10 @@ pub async fn manage_patient_tags( Expr::value(Some(now)), ) .col_expr(patient_tag_relation::Column::UpdatedAt, Expr::value(now)) + .col_expr( + patient_tag_relation::Column::Version, + Expr::col(patient_tag_relation::Column::Version).add(1), + ) .filter(patient_tag_relation::Column::TenantId.eq(tenant_id)) .filter(patient_tag_relation::Column::PatientId.eq(patient_id)) .filter(patient_tag_relation::Column::DeletedAt.is_null()) @@ -526,6 +530,7 @@ pub async fn remove_doctor( active.deleted_at = Set(Some(Utc::now())); active.updated_at = Set(Utc::now()); active.updated_by = Set(operator_id); + active.version = Set(active.version.unwrap() + 1); active.update(&state.db).await?; audit_service::record( diff --git a/crates/erp-health/src/service/points_service/product.rs b/crates/erp-health/src/service/points_service/product.rs index 3fdc62f..7e4dc9b 100644 --- a/crates/erp-health/src/service/points_service/product.rs +++ b/crates/erp-health/src/service/points_service/product.rs @@ -542,6 +542,8 @@ pub async fn exchange_product( // 关联消费流水的 order_id let mut spend_active: points_transaction::ActiveModel = spend.into(); spend_active.order_id = Set(Some(inserted_order.id)); + spend_active.version = Set(spend_active.version.unwrap() + 1); + spend_active.updated_at = Set(Utc::now()); spend_active.update(&txn).await?; txn.commit().await?; diff --git a/crates/erp-plugin/src/service.rs b/crates/erp-plugin/src/service.rs index 8784254..a78c5dd 100644 --- a/crates/erp-plugin/src/service.rs +++ b/crates/erp-plugin/src/service.rs @@ -193,6 +193,7 @@ impl PluginService { // 更新状态 let mut active: plugin::ActiveModel = model.into(); + active.version = Set(bump_version(&active.version)); active.status = Set("installed".to_string()); active.installed_at = Set(Some(now)); active.updated_at = Set(now); @@ -246,6 +247,7 @@ impl PluginService { let now = Utc::now(); let mut active: plugin::ActiveModel = model.into(); + active.version = Set(bump_version(&active.version)); active.status = Set("running".to_string()); active.enabled_at = Set(Some(now)); active.updated_at = Set(now); @@ -276,6 +278,7 @@ impl PluginService { let now = Utc::now(); let mut active: plugin::ActiveModel = model.into(); + active.version = Set(bump_version(&active.version)); active.status = Set("disabled".to_string()); active.updated_at = Set(now); active.updated_by = Set(Some(operator_id)); @@ -313,6 +316,7 @@ impl PluginService { for entity in &tenant_entities { let mut active: plugin_entity::ActiveModel = entity.clone().into(); + active.version = Set(bump_version(&active.version)); active.deleted_at = Set(Some(now)); active.updated_at = Set(now); active.updated_by = Set(Some(operator_id)); @@ -346,6 +350,7 @@ impl PluginService { unregister_plugin_permissions(db, tenant_id, &manifest.metadata.id).await?; let mut active: plugin::ActiveModel = model.into(); + active.version = Set(bump_version(&active.version)); active.status = Set("uninstalled".to_string()); active.updated_at = Set(now); active.updated_by = Set(Some(operator_id)); @@ -459,6 +464,7 @@ impl PluginService { let now = Utc::now(); let manifest_id = manifest.metadata.id.clone(); let mut active: plugin::ActiveModel = model.into(); + active.version = Set(bump_version(&active.version)); active.config_json = Set(config); active.updated_at = Set(now); active.updated_by = Set(Some(operator_id)); @@ -563,6 +569,7 @@ impl PluginService { validate_status_any(&model.status, &["uninstalled", "uploaded"])?; let now = Utc::now(); let mut active: plugin::ActiveModel = model.into(); + active.version = Set(bump_version(&active.version)); active.deleted_at = Set(Some(now)); active.updated_at = Set(now); active.updated_by = Set(Some(operator_id)); @@ -693,6 +700,7 @@ impl PluginService { if let Some(em) = entity_model { let mut active: plugin_entity::ActiveModel = em.into(); + active.version = Set(bump_version(&active.version)); active.schema_json = Set(serde_json::to_value(entity) .map_err(|e| PluginError::InvalidManifest(e.to_string()))?); active.updated_at = Set(now); diff --git a/crates/erp-server/src/dialysis_workflow.rs b/crates/erp-server/src/dialysis_workflow.rs index fa4613a..4eaad98 100644 --- a/crates/erp-server/src/dialysis_workflow.rs +++ b/crates/erp-server/src/dialysis_workflow.rs @@ -108,7 +108,9 @@ async fn handle_dialysis_record_created( .ok_or("透析记录不存在")?; let mut active: erp_dialysis::entity::dialysis_record::ActiveModel = record.into(); + active.version = Set(active.version.unwrap() + 1); active.workflow_instance_id = Set(Some(result.id)); + active.updated_at = Set(chrono::Utc::now()); active.update(db).await?; tracing::info!( diff --git a/crates/erp-server/uploads/019d80da-7a2c-7820-b0a3-3d5266a3a324/019e1659-c40f-7ca1-9e1a-c2316c1c85dc.bin b/crates/erp-server/uploads/019d80da-7a2c-7820-b0a3-3d5266a3a324/019e1659-c40f-7ca1-9e1a-c2316c1c85dc.bin new file mode 100644 index 0000000..97fa03f Binary files /dev/null and b/crates/erp-server/uploads/019d80da-7a2c-7820-b0a3-3d5266a3a324/019e1659-c40f-7ca1-9e1a-c2316c1c85dc.bin differ diff --git a/crates/erp-server/uploads/019d80da-7a2c-7820-b0a3-3d5266a3a324/019e16e7-866c-7651-b7cc-d2320a3ca585.jpg b/crates/erp-server/uploads/019d80da-7a2c-7820-b0a3-3d5266a3a324/019e16e7-866c-7651-b7cc-d2320a3ca585.jpg new file mode 100644 index 0000000..97fa03f Binary files /dev/null and b/crates/erp-server/uploads/019d80da-7a2c-7820-b0a3-3d5266a3a324/019e16e7-866c-7651-b7cc-d2320a3ca585.jpg differ diff --git a/crates/erp-server/uploads/019d80da-7a2c-7820-b0a3-3d5266a3a324/019e16e7-866c-7651-b7cc-d2320a3ca585_thumb.jpg b/crates/erp-server/uploads/019d80da-7a2c-7820-b0a3-3d5266a3a324/019e16e7-866c-7651-b7cc-d2320a3ca585_thumb.jpg new file mode 100644 index 0000000..d46ffac Binary files /dev/null and b/crates/erp-server/uploads/019d80da-7a2c-7820-b0a3-3d5266a3a324/019e16e7-866c-7651-b7cc-d2320a3ca585_thumb.jpg differ diff --git a/crates/erp-workflow/src/engine/executor.rs b/crates/erp-workflow/src/engine/executor.rs index af44047..5a61824 100644 --- a/crates/erp-workflow/src/engine/executor.rs +++ b/crates/erp-workflow/src/engine/executor.rs @@ -89,6 +89,7 @@ impl FlowExecutor { // 消费当前 token let mut active: token::ActiveModel = current_token.into(); + active.version = Set(active.version.unwrap() + 1); active.status = Set("consumed".to_string()); active.consumed_at = Set(Some(Utc::now())); active @@ -599,6 +600,7 @@ impl FlowExecutor { .ok_or_else(|| WorkflowError::NotFound(format!("流程实例不存在: {instance_id}")))?; let mut active: process_instance::ActiveModel = instance.into(); + active.version = Set(active.version.unwrap() + 1); active.status = Set("completed".to_string()); active.completed_at = Set(Some(Utc::now())); active.updated_at = Set(Utc::now());