feat(health,ai): 后端服务优化 + 媒体文件处理

- erp-health: article/banner/consultation/media 服务层优化
- erp-ai: analysis/insight/prompt 服务增强
- erp-auth: auth/role/token 服务改进
- erp-workflow: executor 执行引擎修复
- erp-plugin: 服务层改进
- 新增媒体上传文件样例
This commit is contained in:
iven
2026-05-13 23:28:57 +08:00
parent e4e5ef04d4
commit 212c08b7ae
30 changed files with 320 additions and 3 deletions

99
Cargo.lock generated
View File

@@ -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",
]

View File

@@ -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(())
}

View File

@@ -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)

View File

@@ -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?)
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -101,6 +101,12 @@ pub enum HealthError {
#[error("交接记录不存在")]
HandoffLogNotFound,
#[error("媒体文件不存在")]
MediaNotFound,
#[error("媒体文件夹不存在")]
MediaFolderNotFound,
#[error("状态转换无效: {0}")]
InvalidStatusTransition(String),
@@ -148,7 +154,9 @@ impl From<HealthError> 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,

View File

@@ -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<HealthState>,
Path(id): Path<uuid::Uuid>,
) -> Result<Json<ApiResponse<ArticleResp>>, AppError> {
let result = article_service::get_public_article(&state, id).await?;
Ok(Json(ApiResponse::ok(result)))
}
pub async fn get_article<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,

View File

@@ -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<HealthState>,
Path(banner_id): Path<uuid::Uuid>,
) -> Result<axum::response::Response, AppError> {
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())
}

View File

@@ -29,6 +29,13 @@ pub struct MessageListParams {
pub after_id: Option<Uuid>,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct PollMessagesParams {
pub after_id: Option<Uuid>,
/// 超时秒数,默认 25最大 30
pub timeout: Option<u64>,
}
#[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<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(session_id): Path<Uuid>,
Query(params): Query<PollMessagesParams>,
) -> Result<Json<ApiResponse<Vec<MessageResp>>>, AppError>
where
HealthState: FromRef<S>,
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<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,

View File

@@ -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(())

View File

@@ -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))

View File

@@ -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(())
}

View File

@@ -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<String> {
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())
}
// ---------------------------------------------------------------------------
// 内部辅助函数
// ---------------------------------------------------------------------------

View File

@@ -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(())

View File

@@ -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<Uuid>,
timeout_secs: u64,
) -> HealthResult<Vec<MessageResp>> {
// 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,

View File

@@ -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(())
}

View File

@@ -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?;
}

View File

@@ -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)

View File

@@ -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(

View File

@@ -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?;

View File

@@ -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);

View File

@@ -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!(

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -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());