npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kne/fastify-message

v2.1.1

Published

管理消息发送,支持邮件、短信等消息,模板基于 Liquid(@kne/message-template)

Readme

fastify-message

描述

管理消息发送,支持邮件、短信等消息,模板基于 Liquid(@kne/message-template)

安装

npm i --save @kne/fastify-message

概述

项目概述

fastify-message 是一个基于 Fastify 框架的消息管理插件,提供模板管理和消息发送功能,支持邮件,并通过 senders 扩展短信等渠道。

模板语法基于 @kne/message-template(Liquid 白名单子集),与前端 components-thirdparty:TemplateEditor 编辑器、预览共用同一套语法与渲染规则。

主要特性

  • 邮件 / 短信模板管理:新建、修改、删除、复制、启用 / 禁用,系统模板只读
  • Liquid 模板语法:变量 {{ name }}、原样输出 {{ name | raw }}、条件 {% if %}…{% else %}…{% endif %}
  • 保存与导入前校验语法白名单,非法模板拒绝保存
  • 邮件 HTML 模式自动转义变量,短信文本模式不转义
  • 模板变量声明(variables),预览时使用示例值
  • 系统级(目录导入)和业务级(接口创建)模板分类;业务模板启用后覆盖同编码系统模板
  • 发送记录追踪(成功 / 失败 / 待发送及失败原因)与发送统计,失败记录可重发
  • 支持异步发送与失败重试,SMTP 连接池复用
  • 租户模板与租户 SMTP:tenantId 由宿主项目自定义;mailSettingTemplateCodes 白名单 ∩ 平台开关决定哪些系统模板开放给租户定制;services.mailSetting 自带 SMTP 存储与租户模板定制,方法集合固定挂载、不随插件参数增删,只有发送取值会随 getEmailConfig 改用宿主实现(如 fastify-tenant)
  • 兼容 PostgreSQL、MySQL、SQLite,启动时自动升级表结构

安装

npm install @kne/fastify-message

基础配置

const fastify = require('fastify')();

fastify.register(require('@kne/fastify-message'), {
  name: 'message', // 插件注册名称
  templateDir: path.resolve(__dirname, './messageTemplate'), // 系统模板目录
  emailConfig: {
    host: 'smtp.example.com',
    port: 465,
    secure: true,
    user: '[email protected]',
    pass: 'your-password',
    defaultSubject: '系统通知' // 默认邮件主题
  },
  retry: { times: 2, delay: 1000 }, // 发送失败重试次数与间隔(毫秒),默认不重试
  // 可选:按租户返回 SMTP 配置 { host, port, secure, user, pass, fromName, fromAddress },返回空时使用 emailConfig
  // 配合 fastify-tenant 时传 fastify.tenant.services.mail.resolveEmailConfig;不传时由 services.mailSetting 读包内表 t_message_tenant_email_config(mailSetting 的方法始终完整挂载)
  getEmailConfig: async ({ tenantId }) => fastify.tenant.services.mail.resolveEmailConfig({ tenantId }),
  // 可选:允许开放给租户定制的系统模板编码(白名单),也可以是 ({ tenantId, type }) => codes;不传时为全部系统邮件与短信模板
  mailSettingTemplateCodes: ['welcome'],
  autoUpgrade: true, // 启动时自动补齐表字段与索引、迁移 1.x 模板,默认开启;关闭后需自行调用 services.upgradeSchema()
  isTest: false // 测试模式下不会真实发送邮件
});

模板格式

模板文件扩展名为 .liquid(也支持 .html、.txt),用注释标记分段:

<!-- subject -->
欢迎加入 {{ appName }}
<!-- description -->
新用户注册成功后发送
<!-- variables -->
[{ "name": "username", "label": "用户名", "example": "张三" }, { "name": "appName", "label": "应用名", "example": "LeapIn" }]
<!-- html -->
<h1>你好,{{ username }}!</h1>
{% if vip %}<p>尊敬的会员,欢迎回来</p>{% else %}<p>欢迎使用 {{ appName }}</p>{% endif %}

| 分段 | 说明 | |----|----| | subject | 邮件主题(文本模式渲染) | | description | 模板描述 | | variables | 变量声明 JSON 数组 [{ name, label, example }],name 支持 a.b 路径 | | html | 邮件正文(HTML 模式渲染,变量自动转义) | | text | 短信正文;邮件中作为纯文本备用正文,缺省时由 html 自动转换 | | 其它 | 进入 extra,按文本模式渲染后传给发送器,如短信服务商的 templateCode |

没有任何分段标记时,整个文件作为正文。语法不在白名单内(如过滤器、for 循环、<%= %>)的文件会被跳过并记录错误日志。

模板命名规则

模板文件命名格式:code_type_name.liquid

  • code: 模板编码
  • type: 模板类型(可选,默认为0)
    • 0: 邮件
    • 1: 短信
  • name: 模板名称(可选,默认使用code)

示例:welcome_0_欢迎邮件.liquid、verify_1_验证码.liquid

模板选择规则

sendMessage 未传 level 时,先使用启用中的同编码业务模板(level 1),没有再使用系统模板(level 0)。因此可以在后台把系统模板复制为业务模板(复制后保留编码、默认停用),修改后启用即可替换线上内容;停用业务模板后自动回退到系统模板。停用的模板不能发送。

租户模板与租户 SMTP

tenantId 是由宿主项目定义的租户标识(字符串),可以是 fastify-tenant 的租户,也可以是业务自己的"客户"、"组织"等实体 id;插件不校验租户是否存在,也不依赖任何租户插件。模板带 tenantId 字段(空字符串为全局模板),唯一索引为 code + type + level + tenantId。

  • sendMessage 传 tenantId 时,先在该租户启用的模板中按上面的规则选择,没有再回退到全局模板;停用租户模板即恢复全局内容
  • 邮件发送时统一调用 services.mailSetting.getEmailConfig({ tenantId }):传了插件配置 getEmailConfig 时内部转调宿主实现,没传时读包内 t_message_tenant_email_config;返回配置则用租户 SMTP 发送,发件人为 fromName / fromAddress(缺省为 user),不再走 senders[0];返回空则沿用全局 emailConfig / senders
  • 发送记录保存 tenantId,重发与启动补发同样使用租户模板和 SMTP
  • template.copy({ id, tenantId }) 把全局模板复制为租户模板;模板列表接口不传 filter[tenantId] 时只返回全局模板
  • verifyEmailConfig({ config, to }) 校验 SMTP 能否登录,传 to 时再发一封测试邮件

租户消息设置(mailSetting)

插件自带租户消息设置的默认实现(2.1.1 起),开箱即用;用 @kne/fastify-tenant 时可继续用它的实现,两者通过 getEmailConfig 切换:

  • 内置:不传 getEmailConfig 时,services.mailSetting 用内置表 t_message_tenant_email_config 存租户 SMTP,并提供 smtpDetail / smtpSave / smtpRemove / smtpTest 与 templateList / templateDetail / templateCustomize / templateSave / templateSetStatus / templateReset / templatePreview;发送时自动读取该租户已启用的配置
  • 外置:传了 getEmailConfig(如 fastify.tenant.services.mail.resolveEmailConfig)时,services.mailSetting.getEmailConfig 内部转调宿主实现,内置表不再参与发送;其余方法读写内置表,行为不变
  • 方法集合固定:services.mailSetting 在任何插件参数下都完整挂载同一套方法(含 getEmailConfig / smtpSave / smtpRemove / smtpTest 与全部 template*),增删参数不会让某个方法变成 undefined
  • 只按 getEmailConfig 有无决定发送取值来源,没有额外开关;租户鉴权与租户是否存在由宿主项目负责,插件只要求 tenantId 非空

租户模板开放范围(tenantTemplate)

系统模板可给租户定制的范围(白名单 ∩ 平台开关),mailSetting 的模板方法复用同一份判断:

  • 白名单:插件配置 mailSettingTemplateCodes,只有其中的全局系统模板(level 0)可以给租户定制,不传时为全部
  • 平台开关:白名单内的模板默认开放(tenantOpen 为 true),平台在消息模板管理中通过 POST /templates/:id/tenant-open 逐个关闭;GET /templates 每条返回 tenantCustomizable 标明能否设置
  • services.tenantTemplate.list / get 返回白名单 ∩ 已开放的系统模板,供 fastify-tenant 的租户端使用;平台代租户管理时传 includeTenantClosed: true,只受白名单限制

模板目录中删除的文件,对应的系统模板会在下次启动时被停用(不删除,保留发送记录关联);文件恢复后需在后台手动启用。

使用示例

// 发送邮件
await fastify.message.services.sendMessage({
  type: 0, // 0: 邮件
  name: '[email protected]', // 收件人
  code: 'welcome', // 模板编码
  props: { // 模板变量
    username: 'John',
    appName: 'LeapIn'
  },
  options: {
    title: '系统通知', // 发件人名称
    attachments: [] // 附件列表,内容不入库,发送记录只保存文件名等描述
  },
  userId, // 可选,发送人
  async: false // 为 true 时立即返回待发送记录,后台发送
});

// 重发失败记录
await fastify.message.services.record.retry({ id: recordId });

// 导入模板文件
await fastify.message.services.includeTemplate('./messageTemplate');

从 1.x 升级到 2.0

2.0 将模板引擎从 lodash template(EJS 风格)切换为 Liquid,属于破坏性变更:

  1. 模板文件中的 <%= name %> 改为 {{ name }},需要输出原始 HTML 的变量改为 {{ name | raw }};<% if (a) { %>…<% } %> 改为 {% if a %}…{% endif %}
  2. 模板文件扩展名由 .ejs 改为 .liquid(.ejs 文件不再导入)
  3. 数据库无需手工执行 SQL:启动时(autoUpgrade 默认开启)自动为模板表、记录表补齐新字段,把 code + type + level + tenantId 索引改为唯一索引并创建记录 status 索引,PostgreSQL / MySQL / SQLite 通用。存在重复编码时跳过唯一索引并输出告警,处理重复数据后重启即可;也可以手动调用 services.upgradeSchema()
  4. 数据库中已有模板自动迁移(services.migrateTemplates()):1.x 的合并内容拆成 subject / content / extra,<%= x %>、<%- x %> 改为 {{ x }}(2.0 默认转义输出);含 <% if %> 等逻辑语法的模板会跳过并在日志中列出,需在后台人工改写
  5. 短信发送器收到的 content 为 { text, ...extra },邮件为 { subject, html, text, ...extra }
  6. sendMessage 未传 level 时会优先使用同编码的启用业务模板;只想用系统模板时显式传 level: 0
  7. 发送失败在模块内部处理:写入发送记录(status = 1,error 为原因)、输出错误日志并计入失败统计,sendMessage 不会返回 rejected Promise,业务按返回记录的 status 判断即可
  8. 记录表 content 字段类型由 JSONB 改为 JSON(已有 PostgreSQL 表无需修改)

安全提示

{{ name | raw }} 会原样输出 HTML,变量来自用户输入时有注入风险,只用于可信内容。发件人名称 options.title 会自动去除换行并转义引号。

示例

API

HTTP 接口 (API)

基础路径:/api/v1/message


GET /statistics

获取消息统计数据概览。

权限类型:statistics

查询参数:

| 参数 | 类型 | 必填 | 默认值 | 描述 | |------|--------|----|-----|-----------------------------------| | range | String | 否 | 7d | 时间范围: 7d=近7天, 1m=近1个月, 1y=近1年 |

响应示例:

{
  "range": "7d",
  "rangeLabel": "近7天",
  "totalRecords": 256,
  "byType": {
    "0": 200,
    "1": 56
  },
  "byCode": {
    "welcome": 100,
    "verify": 80,
    "notify": 76
  },
  "templateStats": {
    "total": 5,
    "byStatus": {
      "0": 4,
      "1": 1
    },
    "byType": {
      "0": 3,
      "1": 2
    }
  },
  "recentTrend": [
    { "date": "2026-05-07", "count": 10 },
    { "date": "2026-05-08", "count": 15 },
    { "date": "2026-05-09", "count": 8 }
  ],
  "recentTrendByType": [
    { "date": "2026-05-07", "type": 0, "count": 8 },
    { "date": "2026-05-07", "type": 1, "count": 2 },
    { "date": "2026-05-08", "type": 0, "count": 12 },
    { "date": "2026-05-08", "type": 1, "count": 3 }
  ]
}

响应字段说明:

| 字段 | 类型 | 描述 | |------------------------|--------|---------------------------| | range | String | 当前时间范围参数 | | rangeLabel | String | 时间范围中文描述 | | totalRecords | Number | 时间范围内发送记录数 | | byType | Object | 按消息类型统计(键:0=邮件,1=短信) | | byCode | Object | 按模板编码统计 | | templateStats | Object | 模板统计信息 | | templateStats.total | Number | 模板总数 | | templateStats.byStatus | Object | 按模板状态统计(键:0=启用,1=禁用) | | templateStats.byType | Object | 按模板类型统计(键:0=邮件,1=短信) | | recentTrend | Array | 时间范围内发送趋势 | | recentTrendByType | Array | 时间范围内按类型发送趋势 |


GET /statistics/sse

SSE 实时推送当天消息统计数据。

权限类型:statistics

查询参数:

| 参数 | 类型 | 必填 | 默认值 | 描述 | |----------|--------|----|-----|-----------------------| | interval | Number | 否 | 5 | 推送间隔时间(秒),最小1秒,默认5秒 |

说明:该接口为 Server-Sent Events (SSE) 接口,仅推送当天的实时统计数据,建立连接后按指定间隔自动推送。数据按小时粒度展示发送趋势。

事件格式:

  • data 事件:推送当天统计数据(JSON格式)
  • error 事件:推送错误信息

响应示例:

{
  "date": "2026-05-13",
  "totalRecords": 42,
  "byType": {
    "0": 35,
    "1": 7
  },
  "byCode": {
    "welcome": 20,
    "verify": 12,
    "notify": 10
  },
  "hourlyTrend": [
    { "hour": 8, "count": 5 },
    { "hour": 9, "count": 12 },
    { "hour": 10, "count": 8 }
  ],
  "hourlyTrendByType": [
    { "hour": 8, "type": 0, "count": 4 },
    { "hour": 8, "type": 1, "count": 1 },
    { "hour": 9, "type": 0, "count": 10 },
    { "hour": 9, "type": 1, "count": 2 }
  ]
}

响应字段说明:

| 字段 | 类型 | 描述 | |--------------------------|--------|-----------------------------| | date | String | 当天日期 | | totalRecords | Number | 当天发送记录数 | | byType | Object | 按消息类型统计(键:0=邮件,1=短信) | | byCode | Object | 按模板编码统计 | | hourlyTrend | Array | 按小时发送趋势 | | hourlyTrend[].hour | Number | 小时(0-23) | | hourlyTrend[].count | Number | 发送数量 | | hourlyTrendByType | Array | 按小时按类型发送趋势 | | hourlyTrendByType[].hour | Number | 小时(0-23) | | hourlyTrendByType[].type | Number | 消息类型 | | hourlyTrendByType[].count | Number | 发送数量 |

使用示例:

// 默认5秒推送一次
const eventSource = new EventSource('/api/v1/message/statistics/sse');

// 每10秒推送一次
const eventSource = new EventSource('/api/v1/message/statistics/sse?interval=10');

// 注意:浏览器原生 EventSource 不支持自定义 headers,
// 如需认证可通过 URL 参数传递 token,或使用第三方库如 eventsource-parser

eventSource.onmessage = (event) => {
  const statistics = JSON.parse(event.data);
  console.log('当天实时统计:', statistics);
};

eventSource.addEventListener('error', (event) => {
  const error = JSON.parse(event.data);
  console.error('统计错误:', error);
});

GET /records

获取消息发送记录列表。

权限类型:record

查询参数:

| 参数 | 类型 | 必填 | 默认值 | 描述 | |--------------|--------|----|-----|----------------------| | currentPage | Number | 否 | 1 | 页码 | | perPage | Number | 否 | 20 | 每页数量 | | filter[type] | Number | 否 | - | 发送类型:0=邮件,1=短信 | | filter[code] | String | 否 | - | 模板编码(精确匹配) | | filter[name] | String | 否 | - | 发送对象/邮箱/手机号(精确匹配) | | filter[status] | Number | 否 | - | 发送状态:0=成功,1=失败,2=待发送 | | filter[templateId] | String | 否 | - | 模板ID | | filter[startTime] | String | 否 | - | 发送时间起(ISO 时间,含) | | filter[endTime] | String | 否 | - | 发送时间止(ISO 时间,含) |

响应示例:

{
  "totalCount": 100,
  "currentPage": 1,
  "perPage": 20,
  "pageData": [
    {
      "id": "1",
      "name": "[email protected]",
      "type": 0,
      "code": "welcome",
      "props": { "username": "John" },
      "content": { "subject": "欢迎", "html": "<p>你好</p>" },
      "status": 0,
      "error": null,
      "retryCount": 0,
      "templateId": "1",
      "userId": null,
      "createdAt": "2026-05-12T10:00:00.000Z",
      "updatedAt": "2026-05-12T10:00:01.000Z"
    }
  ]
}

GET /records/:id

获取单条发送记录详情。

权限类型:record

路径参数:

| 参数 | 类型 | 必填 | 描述 | |----|--------|----|------| | id | String | 是 | 记录ID |

错误响应:

| 状态码 | 描述 | |-----|------| | 404 | 记录不存在 |


POST /records/:id/retry

按模版当前内容和记录中的变量重发失败或待发送的记录,返回更新后的记录。

权限类型:record:retry

路径参数:

| 参数 | 类型 | 必填 | 描述 | |----|--------|----|------| | id | String | 是 | 记录ID |

错误响应:

| 状态码 | 描述 | |-----|------| | 400 | 记录已发送成功,或模版已禁用 | | 404 | 记录或模版不存在 |


GET /templates

获取消息模版列表。

权限类型:template

查询参数:

| 参数 | 类型 | 必填 | 默认值 | 描述 | |-----------------|--------|----|-----|------------------| | currentPage | Number | 否 | 1 | 页码 | | perPage | Number | 否 | 20 | 每页数量 | | filter[type] | Number | 否 | - | 模版类型:0=邮件,1=短信 | | filter[code] | String | 否 | - | 模版编码(精确匹配) | | filter[level] | Number | 否 | - | 模版级别:0=系统,1=业务 | | filter[status] | Number | 否 | - | 状态:0=启用,1=禁用 | | filter[tenantId] | String | 否 | - | 所属租户,不传时只返回全局模版 | | filter[keyword] | String | 否 | - | 按模版名称、编码模糊搜索(不区分大小写,%、_ 按字面匹配) |

每条模版额外返回 tenantCustomizable:是否为 mailSettingTemplateCodes 白名单内的全局系统模版,为 true 时可通过 POST /templates/:id/tenant-open 设置是否开放给租户。

响应示例:

{
  "totalCount": 10,
  "currentPage": 1,
  "perPage": 20,
  "pageData": [
    {
      "id": "1",
      "name": "欢迎邮件",
      "code": "welcome",
      "type": 0,
      "level": 0,
      "status": 0,
      "subject": "欢迎 {{ username }}",
      "content": "<p>你好,{{ username }}</p>",
      "description": "注册成功后发送",
      "variables": [{ "name": "username", "label": "用户名", "example": "张三" }],
      "extra": {},
      "tenantOpen": true,
      "tenantCustomizable": true,
      "createdAt": "2026-05-12T10:00:00.000Z",
      "updatedAt": "2026-05-12T10:00:00.000Z"
    }
  ]
}

GET /templates/:id

获取单个消息模版详情。

权限类型:template

路径参数:

| 参数 | 类型 | 必填 | 描述 | |----|--------|----|------| | id | String | 是 | 模版ID |

错误响应:

| 状态码 | 描述 | |-----|------| | 404 | 模版不存在 |


POST /templates

新建业务模版(level=1,默认启用),保存前校验 Liquid 语法白名单。

权限类型:template:manage

请求体:

| 参数 | 类型 | 必填 | 描述 | |-------------|--------|----|--------------------------------------| | name | String | 是 | 模版名称 | | code | String | 是 | 模版编码,同类型业务模版内唯一 | | type | Number | 是 | 模版类型:0=邮件,1=短信 | | subject | String | 否 | 邮件主题模版 | | content | String | 否 | 模版正文:邮件为 HTML,短信为纯文本 | | description | String | 否 | 模版描述 | | variables | Array | 否 | 变量声明 [{ name, label, example }] | | extra | Object | 否 | 其它分段,如短信服务商模版编码 |

错误响应:

| 状态码 | 描述 | |-----|--------------------| | 400 | 模版语法错误 | | 409 | 模版编码已存在 |


PUT /templates/:id

修改业务模版,类型不可修改,请求体字段同新建(均为可选)。

权限类型:template:manage

错误响应:

| 状态码 | 描述 | |-----|-----------------------| | 400 | 模版语法错误 | | 403 | 系统模版不可修改,可复制后修改副本 | | 404 | 模版不存在 | | 409 | 模版编码已存在 |


POST /templates/:id/status

启用 / 禁用模版,禁用后无法通过接口发送。

权限类型:template:manage

| 参数 | 类型 | 必填 | 描述 | |--------|--------|----|--------------| | status | Number | 是 | 0=启用,1=禁用 |


POST /templates/:id/tenant-open

设置系统模版是否开放给租户端定制。只能设置白名单内的全局系统模版(tenantCustomizable 为 true),其它返回 403。关闭后租户端(tenantTemplate.list / get)不再返回、也不能定制该模版,已定制的租户模版仍按原状态参与发送;平台代租户管理(传 includeTenantClosed)不受影响。

权限类型:template:manage

请求体:

| 参数 | 类型 | 必填 | 描述 | |------------|---------|----|---------| | tenantOpen | Boolean | 是 | 是否开放给租户 |


POST /templates/:id/copy

复制为禁用状态的业务模版副本,与已有业务模版编码冲突时追加 _copy 后缀。系统模版可通过复制得到可编辑的副本。

权限类型:template:manage


DELETE /templates/:id

删除业务模版,系统模版返回 403。

权限类型:template:manage


POST /templates/render

渲染模版(预览)。传 template 时渲染未保存的内容,props 不传时使用 variables[].example。

权限类型:template

请求体:

| 参数 | 类型 | 必填 | 描述 | |------------|--------|----|-----------------------------------------------------| | templateId | String | 否 | 模版ID,未传 template 时必填 | | template | Object | 否 | 未保存的模版 { type, subject, content, variables, extra } | | props | Object | 否 | 模版变量 |

响应示例:

{
  "type": 0,
  "subject": "欢迎 张三",
  "html": "<p>你好,张三</p>",
  "text": "你好,张三",
  "props": { "username": "张三" }
}

短信返回 { type: 1, text, props, ...extra }。语法错误返回 400。


POST /templates/send

根据模版发送消息。模版类型由模版自身决定,无需指定。

权限类型:template:send

请求体:

| 参数 | 类型 | 必填 | 描述 | |-----------|--------|----|---------------| | templateId | String | 是 | 模版ID | | name | String | 是 | 发送对象(邮箱/手机号) | | props | Object | 否 | 模版变量 |

请求示例:

{
  "templateId": "1",
  "name": "[email protected]",
  "props": {
    "username": "John",
    "content": "Welcome!"
  }
}

响应示例:

{
  "success": true,
  "recordId": "12"
}

投递失败时同样写入发送记录(status = 1),接口仍返回 200,success 为 false,失败原因见发送记录。

错误响应:

| 状态码 | 描述 | |-----|------------| | 400 | 模版已禁用,无法发送消息 | | 404 | 模版不存在 |


服务方法 (Services)

includeTemplate

从指定目录导入 .liquid / .html / .txt 模板文件到数据库(level=0,同编码同类型覆盖)。语法校验不通过或扩展名不支持的文件会被跳过并记录日志;目录中已不存在的系统模板会被停用。

| 参数 | 类型 | 必填 | 描述 | |-----|--------|----|----------| | dir | String | 是 | 模板文件目录路径 | | options.disableMissing | Boolean | 否 | 是否停用目录中已不存在的系统模板,默认 true |

| 返回值 | 类型 | 描述 | |-----|---------------|------| | - | Promise | 无返回值 |

示例:

await fastify.message.services.includeTemplate('./templates');
messageTemplate

根据模板编码和参数生成消息内容。未传 level 时优先使用启用的业务模板(level 1),没有再使用系统模板;模板不存在抛出 404,模板已停用抛出 400(错误对象带 statusCode)。

| 参数 | 类型 | 必填 | 默认值 | 描述 | |-------|--------|----|-----|------------| | code | String | 是 | - | 模板编码 | | type | Number | 否 | 0 | 模板类型(0:邮件) | | level | Number | 否 | - | 模板级别,不传时业务模板优先 | | tenantId | String | 否 | - | 租户ID,传入时优先使用该租户启用的模板,没有再回退全局模板 | | props | Object | 是 | - | 模板变量 |

| 返回值字段 | 类型 | 描述 | |-----------------|--------|--------| | content.subject | String | 邮件主题 | | content.html | String | HTML内容 | | content.text | String | 纯文本内容 | | props | Object | 模板变量 | | code | String | 模板编码 | | type | Number | 模板类型 | | level | Number | 实际使用的模板级别 | | templateId | Number | 模板ID |

示例:

const message = await fastify.message.services.messageTemplate({
  code: 'welcome',
  props: {
    username: 'John',
    content: 'Welcome!'
  }
});
parseTemplate

解析模板文本中的注释分段,返回 { [分段名]: 内容 },没有分段标记时返回空对象。

| 参数 | 类型 | 必填 | 描述 | |------|--------|----|--------| | text | String | 是 | 模板文本内容 |

parseTemplateFile

把模板文件内容解析为模版字段 { subject, description, variables, content, extra }:邮件正文取 html 分段,短信正文取 text 分段,其余分段进入 extra。

| 参数 | 类型 | 必填 | 默认值 | 描述 | |------|--------|----|-----|---------------| | text | String | 是 | - | 模板文件内容 | | type | Number | 否 | 0 | 模板类型:0=邮件,1=短信 |

validateTemplate

校验 { subject, content, extra } 的 Liquid 语法白名单(基于 @kne/message-template 的 validateTemplateHtml),不通过时抛出 Error('模版语法错误(字段): 原因')。

renderMessage

渲染模版,发送与预览共用。

| 参数 | 类型 | 必填 | 描述 | |----------|--------|----|----------------------------------------------| | template | Object | 是 | { type, subject, content, extra } | | props | Object | 否 | 模板变量 |

  • 邮件:返回 { subject, html, text, ...extra },subject 文本模式渲染,html 变量自动转义({{ name | raw }} 原样输出),text 缺省时由 html 转换
  • 短信:返回 { text, ...extra },文本模式渲染不转义
sendMessage

发送消息(邮件/短信),先写入待发送记录,发送后更新为成功或失败。按插件配置 retry: { times, delay } 重试;SMTP 连接按配置复用连接池,插件关闭时释放。

| 参数 | 类型 | 必填 | 默认值 | 描述 | |---------|--------|----|-----|---------------| | type | Number | 否 | 0 | 消息类型(0:邮件) | | name | String | 是 | - | 接收者(邮件地址/手机号) | | props | Object | 否 | {} | 模板变量 | | code | String | 是 | - | 模板编码 | | level | Number | 否 | - | 模板级别,不传时业务模板优先 | | tenantId | String | 否 | - | 租户ID(宿主项目自定义):优先使用租户模板;邮件通过 services.mailSetting.getEmailConfig 取租户 SMTP(内置/外置由插件参数 getEmailConfig 决定),写入记录供重发使用 | | client | Object | 否 | - | 客户端配置(覆盖默认配置) | | options | Object | 否 | - | 发送选项 | | userId | String | 否 | - | 发送人,写入记录 | | async | Boolean | 否 | false | 为 true 时立即返回待发送记录,后台发送 |

返回发送记录(Record 模型实例),不会 reject:投递失败、模版不存在或已停用时记录 status = 1、error 为原因,并输出错误日志、计入失败统计;业务通过 record.status 判断结果。仅发送记录本身写库失败时返回 null(同样只记日志)。

options 参数详情:

| 字段 | 类型 | 必填 | 描述 | |-------------|--------|----|-------| | title | String | 否 | 发件人名称(自动去除换行、转义引号) | | subject | String | 否 | 邮件主题 | | attachments | Array | 否 | 附件列表,未传时使用插件配置 attachments;发送记录中只保存 filename 等描述,不保存附件内容 |

示例:

await fastify.message.services.sendMessage({
  type: 0,
  name: '[email protected]',
  code: 'welcome',
  props: {
    username: 'John',
    content: 'Welcome!'
  },
  options: {
    title: 'System',
    attachments: [
      {
        filename: 'welcome.pdf',
        path: './welcome.pdf'
      }
    ]
  }
});
verifyEmailConfig

校验 SMTP 配置能否连接并登录,传 to 时再发送一封测试邮件。失败抛出 400(message 为 SMTP 错误原因)。

| 参数 | 类型 | 必填 | 描述 | |---------|--------|----|----| | config | Object | 是 | { host, port, secure, user, pass, fromName, fromAddress },port 默认 465、secure 默认 true | | to | String | 否 | 测试邮件收件人 | | subject | String | 否 | 测试邮件主题 | | html | String | 否 | 测试邮件正文 |

返回 { success: true }。

tenantTemplate

系统模板开放给租户的范围:mailSettingTemplateCodes 白名单 ∩ 平台 tenantOpen 开关。这里只提供范围查询,租户 SMTP 存储与租户模板副本的增删改查见 mailSetting(自带默认实现;接 @kne/fastify-tenant 时也可用它的实现,鉴权始终由宿主负责)。

| 方法 | 参数 | 说明 | |----|----|----| | list | { tenantId, type, includeTenantClosed } | 可给该租户定制的系统模板(全局、启用、level 0),按 code 排序;type 为 0 邮件(默认)/ 1 短信,其它返回 400;includeTenantClosed 为 true 时忽略平台开关(平台代管) | | get | { tenantId, code, type, includeTenantClosed } | 取单个可定制系统模板;不在白名单返回 403,不存在或未开放返回 404 | | isCustomizable | template | 是否为白名单内的全局系统模板 | | setTenantOpen | { id, tenantOpen } | 设置系统模板是否开放给租户,同 POST /templates/:id/tenant-open | | withTenantCustomizable | list | 给模板列表逐条加上 tenantCustomizable,GET /templates 使用 |

可定制的模板为全局、启用、level = 0 的邮件和短信模板;除 templateList 外,type 缺省为 0(邮件),其它值返回 400。配置了插件参数 mailSettingTemplateCodes(数组或 ({ tenantId, type }) => codes,平台端判断 tenantCustomizable 时不传 tenantId)时只允许白名单内的编码,其它编码返回 403。

模板的 tenantOpen(默认 true)为 false 时,template* 方法视为该模板不存在(列表不返回,其它方法返回 404);平台代租户管理时传 includeTenantClosed: true 忽略该限制。租户端由宿主自行鉴权后调用,不要传 includeTenantClosed。

mailSetting

租户消息设置的包内默认实现(2.1.1 起):租户 SMTP 配置存储 + 系统模板的租户定制。services.mailSetting 的方法集合固定,任何插件参数下都完整挂载同一套方法,不会因为传了 getEmailConfig 而少挂某个方法;插件参数只影响发送取值:getEmailConfig 是唯一入口,传了宿主实现时由它内部转调(此时包内 SMTP 表不参与发送),没传时读包内表。

| 方法 | 参数 | 说明 | |----|----|----| | getEmailConfig | { tenantId } | 发送取值入口。传了插件参数 getEmailConfig 时原样返回宿主实现的结果,否则返回该租户启用的包内配置(含 pass);未配置、停用或 tenantId 为空返回 null,发送回退到全局 emailConfig | | smtpDetail | { tenantId } | 配置详情,不回传 pass 只给 hasPassword;未配置返回 { configured: false, enabled: false, port: 465, secure: true, hasPassword: false } | | smtpSave | { tenantId, ...SMTP 字段 } | 保存配置;pass 为空时保留原密码;enabled !== false 时先做连接测试,失败返回 400 且不落库 | | smtpRemove | { tenantId } | 删除该租户配置 | | smtpTest | { tenantId, to, ...SMTP 字段 } | 用已保存密码合并后连接测试,传 to 时再发一封测试邮件 | | templateList | { tenantId, type, codes, includeTenantClosed } | 可定制模板列表并标注租户定制状态;不传 type 时邮件在前、短信在后;codes 在可定制范围内再取交集 | | templateDetail | { tenantId, code, type, includeTenantClosed } | { customized, template },未定制时返回系统模板 | | templateCustomize | { tenantId, code, type, userId, includeTenantClosed } | 以系统模板为底稿创建租户副本,保留编码与名称并直接启用;已定制则直接返回 | | templateSave | { tenantId, code, type, subject, content, description, userId, includeTenantClosed } | 只改主题、正文与说明;未定制时保存即创建副本,校验失败不留副本 | | templateSetStatus | { tenantId, code, type, status, includeTenantClosed } | 启停租户副本;未定制返回 404 | | templateReset | { tenantId, code, type, includeTenantClosed } | 删除租户副本,恢复使用系统模板;未定制返回 404 | | templatePreview | { tenantId, code, type, template, props, includeTenantClosed } | 传 template 时渲染未保存内容,变量定义沿用当前生效模板 | | withTenantCustomizable | list | 2.0.x 兼容入口,等同于 tenantTemplate.withTenantCustomizable | | setTenantOpen | { id, tenantOpen } | 2.0.x 兼容入口,等同于 tenantTemplate.setTenantOpen |

tenantId 必填,为空返回 400(统一转成字符串落库,避免数字与字符串产生两份配置)。

可定制范围(白名单 ∩ tenantOpen ∩ level = 0 ∩ 启用)复用 tenantTemplate,mailSetting 只在其之上维护租户副本(level = 1 + tenantId)。租户副本启用后,messageTemplate 与发送只覆盖该租户,其它租户与全局不受影响。

statistics.getOverview

获取消息统计数据概览。

| 参数 | 类型 | 必填 | 默认值 | 描述 | |------|--------|----|-----|-----------------------------------| | range | String | 否 | 7d | 时间范围: 7d=近7天, 1m=近1个月, 1y=近1年 |

| 返回值字段 | 类型 | 描述 | |---------------------------|--------|---------------------------| | range | String | 当前时间范围参数 | | rangeLabel | String | 时间范围中文描述 | | totalRecords | Number | 时间范围内发送记录数 | | byType | Object | 按消息类型统计(键:0=邮件,1=短信) | | byCode | Object | 按模板编码统计 | | templateStats | Object | 模板统计信息 | | templateStats.total | Number | 模板总数 | | templateStats.byStatus | Object | 按模板状态统计(键:0=启用,1=禁用) | | templateStats.byType | Object | 按模板类型统计(键:0=邮件,1=短信) | | recentTrend | Array | 时间范围内发送趋势 | | recentTrend[].date | String | 日期 | | recentTrend[].count | Number | 发送数量 | | recentTrendByType | Array | 时间范围内按类型发送趋势 | | recentTrendByType[].date | String | 日期 | | recentTrendByType[].type | Number | 消息类型 | | recentTrendByType[].count | Number | 发送数量 |

示例:

// 近7天统计(默认)
const stats = await fastify.message.services.statistics.getOverview();

// 近1个月统计
const stats = await fastify.message.services.statistics.getOverview({ range: '1m' });

// 近1年统计
const stats = await fastify.message.services.statistics.getOverview({ range: '1y' });

console.log('时间范围:', stats.rangeLabel);
console.log('发送数:', stats.totalRecords);
statistics.getRealtime

获取当天实时统计数据,按小时粒度展示发送趋势。

| 返回值字段 | 类型 | 描述 | |--------------------------------|--------|-----------------------------| | date | String | 当天日期 | | totalRecords | Number | 当天发送记录数 | | byType | Object | 按消息类型统计(键:0=邮件,1=短信) | | byCode | Object | 按模板编码统计 | | hourlyTrend | Array | 按小时发送趋势 | | hourlyTrend[].hour | Number | 小时(0-23) | | hourlyTrend[].count | Number | 发送数量 | | hourlyTrendByType | Array | 按小时按类型发送趋势 | | hourlyTrendByType[].hour | Number | 小时(0-23) | | hourlyTrendByType[].type | Number | 消息类型 | | hourlyTrendByType[].count | Number | 发送数量 |

示例:

const realtime = await fastify.message.services.statistics.getRealtime();
console.log('当天日期:', realtime.date);
console.log('当天发送数:', realtime.totalRecords);
console.log('按小时趋势:', realtime.hourlyTrend);
record.list

查询发送记录列表。

| 参数 | 类型 | 必填 | 默认值 | 描述 | |------------|--------|----|-----|------| | filter | Object | 否 | {} | 过滤条件 | | perPage | Number | 否 | 20 | 每页数量 | | currentPage | Number | 否 | 1 | 页码 |

filter 为 Sequelize where 条件,HTTP 接口会转换 type、code、name、status、templateId、tenantId 与时间范围

record.detail

获取单条发送记录详情,不存在时抛出 404。

| 参数 | 类型 | 必填 | 描述 | |----|--------|----|------| | id | String | 是 | 记录ID |

record.retry

重发失败或待发送的记录,参数 { id },返回更新后的记录;再次投递失败不抛错,记录保持 status = 1。记录不存在(404)、已成功(400)、模版缺失或停用时抛错。

processPendingRecords

补发所有待发送(status = 2)的记录,插件启动时自动执行(isTest 时不执行)。返回 { total }。

upgradeSchema

为模板表、记录表补齐缺失字段(含 tenantId),把 code + type + level(或 2.0.0 的同名唯一索引)改为 code + type + level + tenantId 唯一索引(先释放已删除模板的编码;存在重复数据时跳过并告警),并创建记录表 status 索引。这两个索引不在模型中声明(1.x 老库执行 sequelize.sync 时新列尚不存在,建索引会失败并阻断启动),新库同样由本方法创建。PostgreSQL / MySQL / SQLite 通用,autoUpgrade 开启时启动自动执行。返回 { template: [新增列], record: [新增列], uniqueIndex: 'created' | 'exists' | 'duplicated' | 'failed' | 'skipped', statusIndex: 'created' | 'exists' | 'skipped' }。

migrateTemplates

迁移 1.x 模板内容:合并分段拆成 subject / content / extra,<%= x %>、<%- x %> 改为 {{ x }};含 EJS 逻辑语法或迁移后校验不通过的模板跳过。autoUpgrade 开启时启动自动执行。返回 { migrated: [...], skipped: [{ id, code, type, level, reason }] }。

template.list

查询消息模版列表。

| 参数 | 类型 | 必填 | 默认值 | 描述 | |------------|--------|----|-----|------| | filter | Object | 否 | {} | 过滤条件 | | perPage | Number | 否 | 20 | 每页数量 | | currentPage | Number | 否 | 1 | 页码 |

filter 支持的字段:type、code、level、status、tenantId;keyword 按名称、编码模糊搜索。服务方法不会自动限定 tenantId,需要只查全局模板时传 tenantId: ''

template.detail

获取单个消息模版详情。

| 参数 | 类型 | 必填 | 描述 | |----|--------|----|------| | id | String | 是 | 模版ID |

template.create / save / remove / setStatus / copy / render

对应 HTTP 接口的服务方法,参数与接口请求体一致(save / remove / setStatus / copy 额外传 id)。错误对象带 statusCode(400 语法错误、403 系统模版只读、404 模版不存在、409 编码重复)。

  • remove 软删除前会把编码改写为 原编码#deleted#id,删除后可重建同编码模板
  • copy 复制系统模板时保留编码、默认停用,启用后即覆盖同编码系统模板;传 tenantId 时复制为该租户的模板,只影响该租户的发送
  • create 可传 tenantId 创建租户模板,编码在同租户、同类型的业务模板内唯一
  • render 的 props 与 variables[].example 合并,props 优先
template.send

根据模版ID发送消息。

| 参数 | 类型 | 必填 | 默认值 | 描述 | |-----------|--------|----|-----|---------------| | templateId | String | 是 | - | 模版ID | | name | String | 是 | - | 接收者(邮箱/手机号) | | props | Object | 否 | {} | 模版变量 | | userId | String | 否 | - | 发送人 |

| 返回值字段 | 类型 | 描述 | |---------|---------|--------| | success | Boolean | 是否发送成功 | | recordId | String | 发送记录ID |

错误(带 statusCode):

  • 模版不存在 → 404 模版不存在
  • 模版已禁用 → 400 模版已禁用,无法发送消息

示例:

const result = await fastify.message.services.template.send({
  templateId: '1',
  name: '[email protected]',
  props: { username: 'John' }
});

数据模型 (Models)

Template 模型

| 字段 | 类型 | 默认值 | 描述 | |-----------|--------|-----|------------| | id | Number | - | 主键 | | code | String | - | 模板编码 | | type | Number | 0 | 模板类型 | | name | String | - | 模板名称 | | subject | String | - | 邮件主题模版(Liquid) | | content | String | - | 模板正文(Liquid):邮件为 HTML,短信为纯文本 | | description | String | - | 模板描述 | | variables | Array | - | 变量声明 [{ name, label, example }] | | extra | Object | - | 其它分段(Liquid) | | level | Number | 1 | 模板级别(0:系统,1:业务) | | status | Number | 0 | 模板状态(0:启用) | | tenantId | String | '' | 所属租户,空字符串为全局模板 | | tenantOpen | Boolean | true | 系统模板是否开放给租户端定制(仍受 mailSettingTemplateCodes 限制) | | createdAt | Date | - | 创建时间 | | updatedAt | Date | - | 更新时间 |

索引:code + type + level + tenantId 唯一。

TenantEmailConfig 模型

租户 SMTP 配置,表名 t_message_tenant_email_config(paranoid: false)。

| 字段 | 类型 | 默认值 | 描述 | |-------------|---------|------|---------------------------| | id | Number | - | 主键 | | tenantId | String | - | 租户标识,由宿主项目定义(如客户 id) | | enabled | Boolean | true | 是否启用,停用时发送回退到全局配置 | | host | String | - | SMTP 服务器 | | port | Number | 465 | SMTP 端口 | | secure | Boolean | true | 是否使用 SSL | | user | String | - | SMTP 账号 | | pass | String | - | SMTP 密码,接口不回传 | | fromName | String | - | 发件人名称 | | fromAddress | String | - | 发件人地址,为空时使用 SMTP 账号 | | createdAt | Date | - | 创建时间 | | updatedAt | Date | - | 更新时间 |

索引:tenantId 唯一。

Record 模型

| 字段 | 类型 | 默认值 | 描述 | |------------|--------|-----|------| | id | Number | - | 主键 | | type | Number | - | 消息类型 | | code | String | - | 模板编码 | | templateId | Number | - | 模板ID | | props | Object | - | 模板变量 | | name | String | - | 接收者 | | content | Object | - | 实际发送内容(JSON),附件只保存描述 | | options | Object | - | 发送参数,重发时使用 | | status | Number | 0 | 发送状态(0:成功,1:失败,2:待发送) | | error | String | - | 失败原因 | | retryCount | Number | 0 | 重试次数 | | userId | Number | - | 发送人 | | tenantId | String | - | 所属租户,重发时使用租户模板与 SMTP | | createdAt | Date | - | 创建时间 | | updatedAt | Date | - | 更新时间 |