use chrono::Utc; use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set}; use uuid::Uuid; use crate::dto::{CreateTemplateReq, MessageTemplateResp, UpdateTemplateReq}; use crate::entity::message_template; use crate::error::{MessageError, MessageResult}; /// 消息模板服务。 pub struct TemplateService; impl TemplateService { /// 查询模板列表。 pub async fn list( tenant_id: Uuid, page: u64, page_size: u64, db: &sea_orm::DatabaseConnection, ) -> MessageResult<(Vec, u64)> { let paginator = message_template::Entity::find() .filter(message_template::Column::TenantId.eq(tenant_id)) .filter(message_template::Column::DeletedAt.is_null()) .paginate(db, page_size); let total = paginator .num_items() .await .map_err(|e| MessageError::Validation(e.to_string()))?; let page_index = page.saturating_sub(1); let models = paginator .fetch_page(page_index) .await .map_err(|e| MessageError::Validation(e.to_string()))?; let resps = models.iter().map(Self::model_to_resp).collect(); Ok((resps, total)) } /// 创建消息模板。 pub async fn create( tenant_id: Uuid, operator_id: Uuid, req: &CreateTemplateReq, db: &sea_orm::DatabaseConnection, ) -> MessageResult { // 检查编码唯一性 let existing = message_template::Entity::find() .filter(message_template::Column::TenantId.eq(tenant_id)) .filter(message_template::Column::Code.eq(&req.code)) .filter(message_template::Column::DeletedAt.is_null()) .one(db) .await .map_err(|e| MessageError::Validation(e.to_string()))?; if existing.is_some() { return Err(MessageError::DuplicateTemplateCode(format!( "模板编码已存在: {}", req.code ))); } let id = Uuid::now_v7(); let now = Utc::now(); let model = message_template::ActiveModel { id: Set(id), tenant_id: Set(tenant_id), name: Set(req.name.clone()), code: Set(req.code.clone()), channel: Set(req.channel.clone()), title_template: Set(req.title_template.clone()), body_template: Set(req.body_template.clone()), language: Set(req.language.clone()), created_at: Set(now), updated_at: Set(now), created_by: Set(operator_id), updated_by: Set(operator_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)) } /// 更新消息模板。 pub async fn update( id: Uuid, tenant_id: Uuid, operator_id: Uuid, req: &UpdateTemplateReq, db: &sea_orm::DatabaseConnection, ) -> MessageResult { let model = message_template::Entity::find_by_id(id) .one(db) .await .map_err(|e| MessageError::Validation(e.to_string()))? .filter(|m| m.tenant_id == tenant_id && m.deleted_at.is_none()) .ok_or_else(|| MessageError::NotFound(format!("模板不存在: {id}")))?; let current_version = model.version; let next_ver = erp_core::error::check_version(req.version, current_version) .map_err(|_| MessageError::VersionMismatch)?; let mut active: message_template::ActiveModel = model.into(); if let Some(name) = &req.name { active.name = Set(name.clone()); } if let Some(title) = &req.title_template { active.title_template = Set(title.clone()); } if let Some(body) = &req.body_template { active.body_template = Set(body.clone()); } if let Some(lang) = &req.language { active.language = Set(lang.clone()); } if let Some(channel) = &req.channel { active.channel = Set(channel.clone()); } active.updated_at = Set(Utc::now()); active.updated_by = Set(operator_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)) } /// 软删除消息模板。 pub async fn delete( id: Uuid, tenant_id: Uuid, operator_id: Uuid, db: &sea_orm::DatabaseConnection, ) -> MessageResult<()> { let model = message_template::Entity::find_by_id(id) .one(db) .await .map_err(|e| MessageError::Validation(e.to_string()))? .filter(|m| m.tenant_id == tenant_id && m.deleted_at.is_none()) .ok_or_else(|| MessageError::NotFound(format!("模板不存在: {id}")))?; let current_version = model.version; let mut active: message_template::ActiveModel = model.into(); active.deleted_at = Set(Some(Utc::now())); active.updated_at = Set(Utc::now()); active.updated_by = Set(operator_id); active.version = Set(current_version + 1); active .update(db) .await .map_err(|e| MessageError::Validation(e.to_string()))?; Ok(()) } /// 使用模板渲染消息内容。 /// 支持 {{variable}} 格式的变量插值。 pub fn render(template: &str, variables: &std::collections::HashMap) -> String { let mut result = template.to_string(); for (key, value) in variables { result = result.replace(&format!("{{{{{}}}}}", key), value); } result } pub(crate) fn model_to_resp(m: &message_template::Model) -> MessageTemplateResp { MessageTemplateResp { id: m.id, tenant_id: m.tenant_id, name: m.name.clone(), code: m.code.clone(), channel: m.channel.clone(), title_template: m.title_template.clone(), body_template: m.body_template.clone(), language: m.language.clone(), created_at: m.created_at, updated_at: m.updated_at, version: m.version, } } } #[cfg(test)] mod tests { use super::*; #[test] fn render_replaces_single_variable() { let mut vars = std::collections::HashMap::new(); vars.insert("name".to_string(), "张三".to_string()); let result = TemplateService::render("您好,{{name}}", &vars); assert_eq!(result, "您好,张三"); } #[test] fn render_replaces_multiple_variables() { let mut vars = std::collections::HashMap::new(); vars.insert("name".to_string(), "李四".to_string()); vars.insert("code".to_string(), "ORD-001".to_string()); let result = TemplateService::render("{{name}},您的订单 {{code}} 已发货", &vars); assert_eq!(result, "李四,您的订单 ORD-001 已发货"); } #[test] fn render_no_variables_returns_original() { let vars = std::collections::HashMap::new(); let result = TemplateService::render("没有变量的模板", &vars); assert_eq!(result, "没有变量的模板"); } #[test] fn render_missing_variable_leaves_placeholder() { let vars = std::collections::HashMap::new(); let result = TemplateService::render("您好,{{name}}", &vars); assert_eq!(result, "您好,{{name}}"); } #[test] fn render_same_variable_multiple_times() { let mut vars = std::collections::HashMap::new(); vars.insert("user".to_string(), "王五".to_string()); let result = TemplateService::render("{{user}} 你好,{{user}} 的订单已确认", &vars); assert_eq!(result, "王五 你好,王五 的订单已确认"); } #[test] fn render_empty_template() { let mut vars = std::collections::HashMap::new(); vars.insert("name".to_string(), "test".to_string()); let result = TemplateService::render("", &vars); assert_eq!(result, ""); } #[test] fn render_empty_variable_value() { let mut vars = std::collections::HashMap::new(); vars.insert("name".to_string(), "".to_string()); let result = TemplateService::render("您好,{{name}}!", &vars); assert_eq!(result, "您好,!"); } #[test] fn render_adjacent_variables() { let mut vars = std::collections::HashMap::new(); vars.insert("a".to_string(), "1".to_string()); vars.insert("b".to_string(), "2".to_string()); let result = TemplateService::render("{{a}}{{b}}", &vars); assert_eq!(result, "12"); } #[test] fn render_extra_variables_not_in_template_are_ignored() { let mut vars = std::collections::HashMap::new(); vars.insert("name".to_string(), "赵六".to_string()); vars.insert("unused".to_string(), "ignore".to_string()); let result = TemplateService::render("你好 {{name}}", &vars); assert_eq!(result, "你好 赵六"); } #[test] fn model_to_resp_maps_all_fields() { let m = message_template::Model { id: uuid::Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(), tenant_id: uuid::Uuid::parse_str("00000000-0000-0000-0000-000000000002").unwrap(), name: "欢迎消息".to_string(), code: "WELCOME".to_string(), channel: "in_app".to_string(), title_template: "欢迎 {{name}}".to_string(), body_template: "{{name}},欢迎使用".to_string(), language: "zh-CN".to_string(), created_at: chrono::Utc::now(), updated_at: chrono::Utc::now(), created_by: uuid::Uuid::parse_str("00000000-0000-0000-0000-000000000003").unwrap(), updated_by: uuid::Uuid::parse_str("00000000-0000-0000-0000-000000000003").unwrap(), deleted_at: None, version: 2, }; let resp = TemplateService::model_to_resp(&m); assert_eq!(resp.name, "欢迎消息"); assert_eq!(resp.code, "WELCOME"); assert_eq!(resp.channel, "in_app"); assert_eq!(resp.language, "zh-CN"); assert_eq!(resp.version, 2); } }