Files
base/crates/erp-message/src/service/template_service.rs
iven 3772afd987 chore: 干净 ERP 基座 — 删除 health/ai/wechat 业务代码
删除内容:
- 前端: health/(67文件), ai/(2文件), Copilot, MediaPicker, 相关API/Store/Hook
- 后端: wechat_handler, wechat_service, wechat_user entity, analytics handler, ai_workflow_seed
- 配置: WechatConfig, AppConfig.wechat, AuthState wechat 字段
- 启动: 微信凭据检查块, ensure_ai_workflows() 调用
- 迁移: 新增 m20260613_000170_drop_wechat_users.rs
- 脚本: api_test_health_alert.py, api_test_mp.py, mpsync.sh/ps1
- E2E: health-data page, flows/ 目录

保留: erp-core/auth/workflow/message/config/plugin + 基座前端 + 通用组件
2026-06-13 00:32:50 +08:00

297 lines
10 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, 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<MessageTemplateResp>, 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<MessageTemplateResp> {
// 检查编码唯一性
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<MessageTemplateResp> {
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, String>) -> 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);
}
}