Files
erp/crates/erp-message/src/service/subscription_service.rs
iven 5d6e1dc394 feat(core): implement optimistic locking across all entities
Add VersionMismatch error variant and check_version() helper to erp-core.
All 13 mutable entities now enforce version checking on update/delete:
- erp-auth: user, role, organization, department, position
- erp-config: dictionary, dictionary_item, menu, setting, numbering_rule
- erp-workflow: process_definition, process_instance, task
- erp-message: message, message_subscription

Update DTOs to expose version in responses and require version in update
requests. HTTP 409 Conflict returned on version mismatch.
2026-04-11 23:25:43 +08:00

124 lines
4.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use chrono::Utc;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
use uuid::Uuid;
use crate::dto::{MessageSubscriptionResp, UpdateSubscriptionReq};
use crate::entity::message_subscription;
use crate::error::{MessageError, MessageResult};
use erp_core::error::check_version;
/// 消息订阅偏好服务。
pub struct SubscriptionService;
impl SubscriptionService {
/// 获取用户订阅偏好。
pub async fn get(
tenant_id: Uuid,
user_id: Uuid,
db: &sea_orm::DatabaseConnection,
) -> MessageResult<MessageSubscriptionResp> {
let model = message_subscription::Entity::find()
.filter(message_subscription::Column::TenantId.eq(tenant_id))
.filter(message_subscription::Column::UserId.eq(user_id))
.filter(message_subscription::Column::DeletedAt.is_null())
.one(db)
.await
.map_err(|e| MessageError::Validation(e.to_string()))?
.ok_or_else(|| MessageError::NotFound("订阅偏好不存在".to_string()))?;
Ok(Self::model_to_resp(&model))
}
/// 创建或更新用户订阅偏好upsert
pub async fn upsert(
tenant_id: Uuid,
user_id: Uuid,
req: &UpdateSubscriptionReq,
db: &sea_orm::DatabaseConnection,
) -> MessageResult<MessageSubscriptionResp> {
let existing = message_subscription::Entity::find()
.filter(message_subscription::Column::TenantId.eq(tenant_id))
.filter(message_subscription::Column::UserId.eq(user_id))
.filter(message_subscription::Column::DeletedAt.is_null())
.one(db)
.await
.map_err(|e| MessageError::Validation(e.to_string()))?;
let now = Utc::now();
if let Some(model) = existing {
let current_version = model.version;
let next_ver = check_version(req.version, current_version)
.map_err(|_| MessageError::VersionMismatch)?;
let mut active: message_subscription::ActiveModel = model.into();
if let Some(types) = &req.notification_types {
active.notification_types = Set(Some(types.clone()));
}
if let Some(prefs) = &req.channel_preferences {
active.channel_preferences = Set(Some(prefs.clone()));
}
if let Some(dnd) = req.dnd_enabled {
active.dnd_enabled = Set(dnd);
}
if let Some(ref start) = req.dnd_start {
active.dnd_start = Set(Some(start.clone()));
}
if let Some(ref end) = req.dnd_end {
active.dnd_end = Set(Some(end.clone()));
}
active.updated_at = Set(now);
active.updated_by = Set(user_id);
active.version = Set(next_ver);
let updated = active
.update(db)
.await
.map_err(|e| MessageError::Validation(e.to_string()))?;
Ok(Self::model_to_resp(&updated))
} else {
let id = Uuid::now_v7();
let model = message_subscription::ActiveModel {
id: Set(id),
tenant_id: Set(tenant_id),
user_id: Set(user_id),
notification_types: Set(req.notification_types.clone()),
channel_preferences: Set(req.channel_preferences.clone()),
dnd_enabled: Set(req.dnd_enabled.unwrap_or(false)),
dnd_start: Set(req.dnd_start.clone()),
dnd_end: Set(req.dnd_end.clone()),
created_at: Set(now),
updated_at: Set(now),
created_by: Set(user_id),
updated_by: Set(user_id),
deleted_at: Set(None),
version: Set(1),
};
let inserted = model
.insert(db)
.await
.map_err(|e| MessageError::Validation(e.to_string()))?;
Ok(Self::model_to_resp(&inserted))
}
}
fn model_to_resp(m: &message_subscription::Model) -> MessageSubscriptionResp {
MessageSubscriptionResp {
id: m.id,
tenant_id: m.tenant_id,
user_id: m.user_id,
notification_types: m.notification_types.clone(),
channel_preferences: m.channel_preferences.clone(),
dnd_enabled: m.dnd_enabled,
dnd_start: m.dnd_start.clone(),
dnd_end: m.dnd_end.clone(),
created_at: m.created_at,
updated_at: m.updated_at,
version: m.version,
}
}
}