diff --git a/README.md b/README.md index 690243f..dcc20ab 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Message Nest 是一个灵活而强大的消息推送整合平台,旨在简化 - 🔄 **整合性:** 提供了多种消息推送方式,包括邮件、钉钉、企业微信等,方便你集中管理和定制通知。 - 🎨 **自定义性:** 可以根据需求定制消息推送策略,满足不同场景的个性化需求。 +- 📝 **模板化(⭐推荐):** 支持消息模板功能,通过占位符实现动态内容替换,一次定义多处复用,大幅提高开发效率和维护便利性。 - 🛠 **开放性:** 易于扩展和集成新的消息通知服务,以适应未来的变化。 ## 进度 🔨 diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index ae91921..412d495 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -24,7 +24,7 @@ export default defineConfig({ { text: '首页', link: '/' }, { text: '指南', link: '/guide/introduction' }, { text: '部署', link: '/deployment/overview' }, - { text: 'API', link: '/api/usage' }, + { text: 'API', link: '/api/v1' }, { text: '演示站点', link: 'https://message-nest-demo-site.qwapi.eu.org/' } ], @@ -32,6 +32,7 @@ export default defineConfig({ '/guide/': [ { text: '介绍', link: '/guide/introduction' }, { text: '特色功能', link: '/guide/features' }, + { text: '消息模板', link: '/guide/template' }, { text: '更新日志', link: '/guide/changelog' } ], '/deployment/': [ @@ -57,8 +58,8 @@ export default defineConfig({ { text: 'API文档', items: [ - { text: '使用说明', link: '/api/usage' }, - { text: '调用示例', link: '/api/examples' } + { text: 'V1 API(任务)', link: '/api/v1' }, + { text: 'V2 API(模板)', link: '/api/v2' } ] } ] diff --git a/docs/api/usage.md b/docs/api/usage.md deleted file mode 100644 index f46e66e..0000000 --- a/docs/api/usage.md +++ /dev/null @@ -1,106 +0,0 @@ -# API 使用说明 - -Message Nest 提供统一的消息推送API接口。 - -## 接口地址 - -``` -POST /api/v1/message/send -``` - -## 请求参数 - -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| token | string | 是 | 推送令牌,在管理后台查看 | -| title | string | 是 | 消息标题 | -| text | string | 是 | 消息内容 | - -## 请求示例 - -```json -{ - "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e", - "title": "message title", - "text": "Hello World!" -} -``` - -## 响应格式 - -### 成功响应 - -```json -{ - "code": 200, - "msg": "success", - "data": { - "status": "sent" - } -} -``` - -### 失败响应 - -```json -{ - "code": 400, - "msg": "error message", - "data": null -} -``` - -## 获取 Token - -1. 登录 Message Nest 管理后台 -2. 进入"发送任务"页面 -3. 创建新的发送任务 -4. 配置推送渠道(邮件、钉钉、企业微信等) -5. 保存后获得推送令牌(Token) - -## 支持的推送渠道 - -- **邮件** - SMTP邮件发送 -- **钉钉** - 钉钉机器人 -- **企业微信** - 企业微信应用消息 -- **微信公众号** - 微信测试公众号模板消息 -- **自定义Webhook** - 自定义HTTP请求 -- **自托管消息** - 站内消息 - -## 使用流程 - -1. **创建推送渠道** - - 在管理后台配置各种推送渠道 - - 填写相应的配置信息(如邮箱、Webhook地址等) - -2. **创建发送任务** - - 选择要使用的推送渠道 - - 可以选择多个渠道同时推送 - - 获得唯一的推送令牌(Token) - -3. **调用API发送消息** - - 使用获得的 Token - - 发送标题和内容 - - 消息会自动推送到配置的所有渠道 - -## 注意事项 - -::: warning 重要 -- Token 是唯一的,请妥善保管 -- 消息内容支持纯文本和Markdown格式(取决于推送渠道) -- 建议使用异步方式调用API,避免阻塞主流程 -::: - -## 错误码说明 - -| 错误码 | 说明 | -|--------|------| -| 200 | 成功 | -| 400 | 请求参数错误 | -| 401 | 未授权 | -| 404 | Token不存在 | -| 500 | 服务器内部错误 | - -## 下一步 - -查看各语言的 [调用示例](/api/examples)。 diff --git a/docs/api/v1.md b/docs/api/v1.md new file mode 100644 index 0000000..438fa71 --- /dev/null +++ b/docs/api/v1.md @@ -0,0 +1,734 @@ +# V1 API 文档 + +V1 API 提供基于任务的消息推送接口,支持多渠道、多格式发送。 + +::: tip 💡 推荐使用 V2 API(模板) +对于**所有新项目**,我们强烈推荐使用 [V2 API(模板)](/api/v2): +- ✅ **内容复用** - 模板可以在多个场景中复用,无需每次传递完整内容 +- ✅ **完全动态** - 通过占位符可以实现完全动态内容(甚至可以只用一个占位符) +- ✅ **统一管理** - 在管理后台统一管理消息模板,便于维护 +- ✅ **版本控制** - 模板内容修改不影响 API 调用代码 +- ✅ **更安全** - 使用加密 Token,不暴露模板 ID + +::: warning 关于 V1 API +V1 API 仅为兼容历史数据而保留,不推荐在新项目中使用。后续的功能优化和维护重点都在 V2 API(模板)上。 +::: + +## 接口地址 + +``` +POST /api/v1/message/send +``` + +## 请求参数 + +### 基本参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| token | string | 是 | 推送令牌(加密),在管理后台查看 | +| task_id | string | 否 | 任务ID(明文),与 token 二选一 | +| title | string | 是 | 消息标题 | +| text | string | 否 | 纯文本格式内容 | +| html | string | 否 | HTML 格式内容 | +| markdown | string | 否 | Markdown 格式内容 | + +::: tip 提示 +- `token` 和 `task_id` 二选一,推荐使用加密的 `token` +- `text`、`html`、`markdown` 至少提供一种格式 +- 多种格式可以同时提供,系统会根据渠道自动选择 +::: + +### @提醒参数(可选) + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| at_mobiles | array | 否 | @手机号列表,如 `["13800138000", "13900139000"]` | +| at_userids | array | 否 | @用户ID列表,如 `["user001", "user002"]` | +| at_all | boolean | 否 | 是否@所有人,默认 `false` | + +::: warning 注意 +@提醒功能仅在支持的渠道(钉钉、企业微信)中生效。 +::: + +## 请求示例 + +### 基本示例(纯文本) + +```json +{ + "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e", + "title": "系统通知", + "text": "Hello World!" +} +``` + +### 多格式示例 + +```json +{ + "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e", + "title": "系统通知", + "text": "您好,系统检测到异常登录。", + "html": "

系统通知

您好,系统检测到异常登录

", + "markdown": "## 系统通知\n\n您好,系统检测到**异常登录**。" +} +``` + +### 带@提醒示例 + +```json +{ + "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e", + "title": "紧急告警", + "text": "服务器CPU使用率超过90%,请及时处理!", + "markdown": "## 紧急告警\n\n服务器CPU使用率超过**90%**,请及时处理!", + "at_mobiles": ["13800138000", "13900139000"], + "at_all": false +} +``` + +### cURL 示例 + +```bash +curl -X POST http://your-domain/api/v1/message/send \ + -H "Content-Type: application/json" \ + -d '{ + "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e", + "title": "系统通知", + "text": "Hello World!" + }' +``` + +## 响应格式 + +### 成功响应 + +```json +{ + "code": 200, + "msg": "success", + "data": { + "status": "sent" + } +} +``` + +### 失败响应 + +```json +{ + "code": 400, + "msg": "error message", + "data": null +} +``` + +## 获取 Token + +1. 登录 Message Nest 管理后台 +2. 进入"发送任务"页面 +3. 创建新的发送任务 +4. 配置推送渠道(邮件、钉钉、企业微信等) +5. 保存后获得推送令牌(Token) + +## 消息格式优先级 + +V1 API 支持三种消息格式,系统会根据任务实例配置的格式类型自动选择对应的内容。 + +### 格式选择规则 + +当任务实例配置了特定格式类型时,系统按以下优先级选择内容: + +| 实例配置格式 | 优先级顺序 | 说明 | +|------------|-----------|------| +| **HTML** | html → markdown → text | 优先使用 HTML,其次 Markdown,最后纯文本 | +| **Markdown** | markdown → text → html | 优先使用 Markdown,其次纯文本,最后 HTML | +| **Text** | text → markdown → html | 优先使用纯文本,其次 Markdown,最后 HTML | + +### 示例说明 + +**场景 1:邮件渠道(配置为 HTML 格式)** + +```json +{ + "text": "纯文本内容", + "html": "

HTML内容

", + "markdown": "# Markdown内容" +} +``` + +发送结果:使用 `html` 内容 + +**场景 2:钉钉渠道(配置为 Markdown 格式)** + +```json +{ + "text": "纯文本内容", + "markdown": "# Markdown内容" +} +``` + +发送结果:使用 `markdown` 内容 + +**场景 3:只提供纯文本** + +```json +{ + "text": "纯文本内容" +} +``` + +发送结果:所有渠道都使用 `text` 内容(兼容性最好) + +::: tip 最佳实践 +- **邮件渠道**:推荐提供 `html` 格式,视觉效果更好 +- **钉钉/企业微信**:推荐提供 `markdown` 格式,支持富文本 +- **通用场景**:至少提供 `text` 格式,确保所有渠道都能正常发送 +- **多渠道任务**:同时提供多种格式,让系统自动选择最佳格式 +::: + +## @提醒功能 + +@提醒功能允许在钉钉、企业微信等支持的渠道中@特定用户或所有人。 + +### 支持的渠道 + +| 渠道 | @手机号 | @用户ID | @所有人 | +|------|--------|--------|--------| +| 钉钉 | ✅ | ✅ | ✅ | +| 企业微信 | ✅ | ✅ | ✅ | +| 邮件 | ❌ | ❌ | ❌ | +| 其他 | ❌ | ❌ | ❌ | + +### 使用示例 + +#### @指定手机号 + +```json +{ + "token": "...", + "title": "系统告警", + "text": "服务器异常,请及时处理", + "at_mobiles": ["13800138000", "13900139000"] +} +``` + +#### @指定用户ID + +```json +{ + "token": "...", + "title": "任务通知", + "text": "您的任务已完成", + "at_userids": ["user001", "user002"] +} +``` + +#### @所有人 + +```json +{ + "token": "...", + "title": "重要通知", + "text": "系统将于今晚22:00进行维护", + "at_all": true +} +``` + +#### 组合使用 + +```json +{ + "token": "...", + "title": "紧急告警", + "markdown": "## 紧急告警\n\n生产环境出现严重问题!", + "at_mobiles": ["13800138000"], + "at_userids": ["admin"], + "at_all": false +} +``` + +::: warning 注意事项 +1. @提醒只在支持的渠道中生效,其他渠道会忽略这些参数 +2. 钉钉机器人需要配置相应的权限才能使用@功能 +3. @所有人功能需谨慎使用,避免打扰群成员 +4. 手机号和用户ID格式需符合对应平台的要求 +::: + +## 支持的推送渠道 + +| 渠道 | 支持格式 | @提醒 | 说明 | +|------|---------|------|------| +| **邮件** | Text, HTML | ❌ | SMTP邮件发送,推荐使用 HTML 格式 | +| **钉钉** | Text, Markdown | ✅ | 钉钉机器人,支持 Markdown 富文本 | +| **企业微信** | Text, Markdown | ✅ | 企业微信机器人,支持 Markdown | +| **微信公众号** | Text | ❌ | 微信测试公众号模板消息 | +| **自定义Webhook** | Text, HTML, Markdown | ❌ | 自定义HTTP请求,格式取决于配置 | +| **自托管消息** | Text, HTML, Markdown | ❌ | 站内消息,支持多种格式 | + +## 使用流程 + +1. **创建推送渠道** + - 在管理后台配置各种推送渠道 + - 填写相应的配置信息(如邮箱、Webhook地址等) + +2. **创建发送任务** + - 选择要使用的推送渠道 + - 可以选择多个渠道同时推送 + - 获得唯一的推送令牌(Token) + +3. **调用API发送消息** + - 使用获得的 Token + - 发送标题和内容 + - 消息会自动推送到配置的所有渠道 + +## 工作流程 + +1. **Token 解析** - 解密 Token 获取任务 ID(或直接使用 task_id) +2. **任务查询** - 根据任务 ID 查询任务信息 +3. **实例遍历** - 获取任务关联的所有启用实例 +4. **格式选择** - 根据实例配置的格式类型选择对应内容 +5. **@提醒处理** - 如果提供了@参数且渠道支持,添加@提醒 +6. **消息发送** - 向每个实例发送消息 +7. **返回结果** - 返回发送状态 + +## 注意事项 + +::: warning 重要 +- **Token 安全**:Token 是唯一的,请妥善保管,不要在公开代码中硬编码 +- **格式兼容**:至少提供一种格式(text/html/markdown),推荐提供多种格式 +- **异步调用**:建议使用异步方式调用 API,避免阻塞主流程 +- **@提醒限制**:@功能仅在钉钉、企业微信等支持的渠道中生效 +- **格式优先级**:系统会根据实例配置自动选择最合适的格式 +::: + +## 最佳实践 + +### 1. 多格式支持 + +为了确保消息在不同渠道都有良好的展示效果,建议同时提供多种格式: + +```json +{ + "token": "...", + "title": "订单通知", + "text": "您的订单已发货,订单号:20241206001", + "html": "

订单通知

您的订单已发货

订单号:20241206001

", + "markdown": "## 订单通知\n\n您的订单已发货\n\n订单号:**20241206001**" +} +``` + +### 2. 合理使用@提醒 + +只在需要紧急通知时使用@提醒: + +```json +{ + "token": "...", + "title": "紧急告警", + "text": "生产环境出现严重问题", + "at_mobiles": ["13800138000"], // 只@相关负责人 + "at_all": false // 避免@所有人 +} +``` + +### 3. 错误处理 + +```python +import requests +import json + +def send_message(token, title, text): + url = "http://your-domain/api/v1/message/send" + data = { + "token": token, + "title": title, + "text": text + } + + try: + response = requests.post(url, json=data, timeout=10) + result = response.json() + + if result['code'] == 200: + print("发送成功") + else: + print(f"发送失败:{result['msg']}") + except Exception as e: + print(f"请求异常:{e}") +``` + +## 错误码说明 + +| 错误码 | 说明 | +|--------|------| +| 200 | 成功 | +| 400 | 请求参数错误 | +| 401 | 未授权 | +| 404 | Token不存在 | +| 500 | 服务器内部错误 | + +## 常见问题 + +### Q: V1 和 V2 API 有什么区别?应该选择哪个? + +**A:** + +| 特性 | V1 API(任务) | V2 API(模板)⭐ 推荐 | +|------|--------------|-------------------| +| **内容定义** | API 调用时传递 | 预定义在模板中 | +| **动态内容** | 完全动态 | 通过占位符替换 | +| **内容复用** | 每次都要传递完整内容 | 模板可复用 | +| **维护成本** | 修改内容需要改代码 | 只需修改模板 | +| **适用场景** | 完全动态、不重复的内容 | 有固定格式的通知消息 | + +**推荐使用 V2 API(模板)的原因:** +1. **提高开发效率** - 一次定义模板,多处使用 +2. **降低维护成本** - 内容修改无需改代码 +3. **统一管理** - 所有消息模板集中管理 +4. **更好的协作** - 运营人员可以直接修改模板内容 +5. **版本控制** - 模板支持启用/禁用,方便灰度发布 +6. **完全动态** - 通过占位符同样可以实现完全动态内容 + +::: warning V1 API 的定位 +V1 API 仅为兼容历史数据而保留,**不推荐在任何新项目中使用**。即使是完全动态的内容,也可以通过模板 + 占位符的方式实现,且更易于后期维护。 + +后续的功能优化和维护重点都在 V2 API(模板)上。 +::: + +### Q: 如何选择使用哪种格式? + +**A:** 根据渠道特性选择: +- **邮件**:推荐 HTML,视觉效果好 +- **钉钉/企业微信**:推荐 Markdown,支持富文本 +- **通用**:使用 Text,兼容性最好 +- **多渠道**:同时提供多种格式,让系统自动选择 + +### Q: @提醒不生效怎么办? + +**A:** 检查以下几点: +1. 渠道是否支持@提醒(仅钉钉、企业微信支持) +2. 机器人是否有@权限 +3. 手机号或用户ID格式是否正确 +4. 参数名称是否正确(`at_mobiles`、`at_userids`、`at_all`) + +### Q: 可以只发送 HTML 格式吗? + +**A:** 可以,但建议同时提供 text 格式作为备选,确保不支持 HTML 的渠道也能正常发送。 + +### Q: Token 和 task_id 有什么区别? + +**A:** +- **token**:加密的任务标识,更安全,推荐使用 +- **task_id**:明文的任务ID,不推荐在生产环境使用 + +## 各语言调用示例 + +本节提供各种编程语言的完整调用示例代码。 + +### cURL + +```bash +curl -X POST --location 'http://127.0.0.1:8000/api/v1/message/send' \ + --header 'Content-Type: application/json' \ + --data '{ + "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e", + "title": "message title", + "text": "Hello World!" + }' +``` + +### Python + +```python +import requests + +headers = { + 'Content-Type': 'application/json', +} + +json_data = { + "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e", + "title": "message title", + "text": "Hello World!" +} + +response = requests.post( + 'http://127.0.0.1:8000/api/v1/message/send', + headers=headers, + json=json_data +) + +print("response:", response.json()) +``` + +**安装依赖:** + +```bash +pip install requests +``` + +### Go + +```go +package main + +import ( + "fmt" + "io" + "log" + "net/http" + "strings" +) + +func main() { + client := &http.Client{} + var data = strings.NewReader(`{ + "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e", + "title": "message title", + "text": "Hello World!" +}`) + req, err := http.NewRequest("POST", "http://127.0.0.1:8000/api/v1/message/send", data) + if err != nil { + log.Fatal(err) + } + req.Header.Set("Content-Type", "application/json") + resp, err := client.Do(req) + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() + bodyText, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatal(err) + } + fmt.Printf("%s\n", bodyText) +} +``` + +### Java + +```java +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse; + +public class MessageNestExample { + public static void main(String[] args) throws IOException, InterruptedException { + HttpClient client = HttpClient.newBuilder() + .followRedirects(HttpClient.Redirect.NORMAL) + .build(); + + String jsonData = """ + { + "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e", + "title": "message title", + "text": "Hello World!" + } + """; + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("http://127.0.0.1:8000/api/v1/message/send")) + .POST(BodyPublishers.ofString(jsonData)) + .setHeader("Content-Type", "application/json") + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + System.out.println(response.body()); + } +} +``` + +### Node.js + +#### 使用 request 库 + +```javascript +var request = require('request'); + +var headers = { + 'Content-Type': 'application/json' +}; + +var dataString = JSON.stringify({ + "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e", + "title": "message title", + "text": "Hello World!" +}); + +var options = { + url: 'http://127.0.0.1:8000/api/v1/message/send', + method: 'POST', + headers: headers, + body: dataString +}; + +function callback(error, response, body) { + if (!error && response.statusCode == 200) { + console.log(body); + } +} + +request(options, callback); +``` + +#### 使用 axios 库 + +```javascript +const axios = require('axios'); + +const data = { + token: "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e", + title: "message title", + text: "Hello World!" +}; + +axios.post('http://127.0.0.1:8000/api/v1/message/send', data, { + headers: { + 'Content-Type': 'application/json' + } +}) +.then(response => { + console.log('response:', response.data); +}) +.catch(error => { + console.error('error:', error); +}); +``` + +#### 使用 fetch (Node.js 18+) + +```javascript +const data = { + token: "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e", + title: "message title", + text: "Hello World!" +}; + +fetch('http://127.0.0.1:8000/api/v1/message/send', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) +}) +.then(response => response.json()) +.then(data => { + console.log('response:', data); +}) +.catch(error => { + console.error('error:', error); +}); +``` + +### PHP + +```php + "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e", + "title" => "message title", + "text" => "Hello World!" +); + +curl_setopt($ch, CURLOPT_URL, 'http://127.0.0.1:8000/api/v1/message/send'); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); +curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', +]); +curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); +curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + +$response = curl_exec($ch); + +if (curl_errno($ch)) { + echo 'Error:' . curl_error($ch); +} else { + echo $response; +} + +curl_close($ch); +?> +``` + +### C# + +```csharp +using System; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +class Program +{ + static async Task Main(string[] args) + { + using var client = new HttpClient(); + + var data = new + { + token = "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e", + title = "message title", + text = "Hello World!" + }; + + var json = JsonSerializer.Serialize(data); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await client.PostAsync( + "http://127.0.0.1:8000/api/v1/message/send", + content + ); + + var responseString = await response.Content.ReadAsStringAsync(); + Console.WriteLine(responseString); + } +} +``` + +### Ruby + +```ruby +require 'net/http' +require 'json' +require 'uri' + +uri = URI('http://127.0.0.1:8000/api/v1/message/send') +http = Net::HTTP.new(uri.host, uri.port) + +request = Net::HTTP::Post.new(uri.path, { + 'Content-Type' => 'application/json' +}) + +request.body = { + token: 'a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e', + title: 'message title', + text: 'Hello World!' +}.to_json + +response = http.request(request) +puts response.body +``` + +### 示例说明 + +::: tip 提示 +- 将示例中的 `http://127.0.0.1:8000` 替换为你的实际服务地址 +- 将 `a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e` 替换为你在管理后台创建的实际 Token +- 建议在生产环境中使用 HTTPS +- 所有示例都使用基本的纯文本格式,实际使用时可以添加 `html`、`markdown` 等参数 +::: + +## 下一步 + +- 查看 [V2 API 文档](/api/v2) 了解基于模板的发送方式 +- 查看 [消息模板](/guide/template) 了解如何使用模板功能 diff --git a/docs/api/v2.md b/docs/api/v2.md new file mode 100644 index 0000000..ac1ab3c --- /dev/null +++ b/docs/api/v2.md @@ -0,0 +1,519 @@ +# V2 API 文档 + +V2 API 提供基于消息模板的发送接口,支持占位符替换和多实例发送。 + +::: tip ⭐ 推荐使用 +V2 API(模板)是我们推荐的消息发送方式,相比 V1 API 具有以下优势: +- ✅ **内容复用** - 一次定义,多处使用,大幅提高开发效率 +- ✅ **灵活性** - 通过占位符实现动态内容,兼顾固定格式和动态数据 +- ✅ **易维护** - 修改消息内容无需改代码,运营人员可直接操作 +- ✅ **版本控制** - 支持模板启用/禁用,便于灰度发布和回滚 +- ✅ **团队协作** - 开发和运营分工明确,提高协作效率 + +适用于 90% 的消息发送场景,特别是有固定格式的通知类消息。 +::: + +## 接口概述 + +V2 API 与 V1 API 的主要区别: + +| 特性 | V1 API | V2 API | +|------|--------|--------| +| 发送方式 | 基于任务 | 基于模板 | +| 内容定义 | API 调用时传递 | 模板预定义 | +| 动态内容 | 不支持 | 支持占位符 | +| 多格式 | 单一格式 | Text/HTML/Markdown | +| 安全性 | Token 加密 | Token 加密 | + +## 接口地址 + +``` +POST /api/v2/message/send +``` + +## 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| token | string | 是 | 加密的模板 Token | +| title | string | 是 | 消息标题 | +| placeholders | object | 否 | 占位符键值对 | + +### 参数说明 + +#### token + +- 模板的加密 Token,在管理后台的"消息模板"页面获取 +- **注意**:V2 API 只支持加密 Token,不支持明文模板 ID +- Token 使用对称加密算法生成,确保安全性 + +#### title + +- 消息标题,会传递给所有支持标题的渠道(如邮件) +- 必填参数,不能为空 + +#### placeholders + +- 占位符的键值对,用于替换模板中的 `{{key}}` +- 格式为 JSON 对象:`{"key": "value"}` +- 如果模板中定义了占位符但未传递,将使用默认值 +- 如果既未传递也无默认值,占位符将保持原样 + +## 请求示例 + +### 基本示例 + +```bash +curl -X POST http://your-domain/api/v2/message/send \ + -H "Content-Type: application/json" \ + -d '{ + "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b", + "title": "系统通知", + "placeholders": { + "username": "张三", + "action": "登录", + "time": "2024-12-06 12:00:00" + } + }' +``` + +### Python 示例 + +```python +import requests +import json + +url = "http://your-domain/api/v2/message/send" +headers = {"Content-Type": "application/json"} + +data = { + "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b", + "title": "系统通知", + "placeholders": { + "username": "张三", + "action": "登录", + "time": "2024-12-06 12:00:00" + } +} + +response = requests.post(url, headers=headers, data=json.dumps(data)) +print(response.json()) +``` + +### Go 示例 + +```go +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" +) + +func main() { + url := "http://your-domain/api/v2/message/send" + + data := map[string]interface{}{ + "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b", + "title": "系统通知", + "placeholders": map[string]string{ + "username": "张三", + "action": "登录", + "time": "2024-12-06 12:00:00", + }, + } + + jsonData, _ := json.Marshal(data) + resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + fmt.Println("Error:", err) + return + } + defer resp.Body.Close() + + var result map[string]interface{} + json.NewDecoder(resp.Body).Decode(&result) + fmt.Println(result) +} +``` + +### JavaScript/Node.js 示例 + +```javascript +const axios = require('axios'); + +const url = 'http://your-domain/api/v2/message/send'; + +const data = { + token: 'a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b', + title: '系统通知', + placeholders: { + username: '张三', + action: '登录', + time: '2024-12-06 12:00:00' + } +}; + +axios.post(url, data) + .then(response => { + console.log(response.data); + }) + .catch(error => { + console.error('Error:', error); + }); +``` + +### Java 示例 + +```java +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; + +public class MessageSender { + public static void main(String[] args) throws Exception { + String url = "http://your-domain/api/v2/message/send"; + + String jsonData = """ + { + "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b", + "title": "系统通知", + "placeholders": { + "username": "张三", + "action": "登录", + "time": "2024-12-06 12:00:00" + } + } + """; + + HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/json"); + conn.setDoOutput(true); + + try (OutputStream os = conn.getOutputStream()) { + byte[] input = jsonData.getBytes(StandardCharsets.UTF_8); + os.write(input, 0, input.length); + } + + int responseCode = conn.getResponseCode(); + System.out.println("Response Code: " + responseCode); + } +} +``` + +## 响应格式 + +### 成功响应 + +```json +{ + "code": 200, + "msg": "success", + "data": { + "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b", + "count": 3 + } +} +``` + +**响应字段说明:** + +- `code` - 状态码,200 表示成功 +- `msg` - 响应消息 +- `data.token` - 使用的模板 Token +- `data.count` - 成功发送的实例数量 + +### 失败响应 + +```json +{ + "code": 400, + "msg": "token解析失败:invalid token format", + "data": null +} +``` + +## 工作流程 + +1. **Token 解析** - 解密 Token 获取模板 ID +2. **模板查询** - 根据模板 ID 查询模板信息 +3. **状态检查** - 检查模板是否启用 +4. **占位符替换** - 使用传入的 placeholders 替换模板中的占位符 +5. **实例遍历** - 获取模板关联的所有启用实例 +6. **格式匹配** - 根据实例的 ContentType 选择对应格式的内容 +7. **消息发送** - 向每个实例发送消息 +8. **返回结果** - 返回发送成功的实例数量 + +## 占位符替换规则 + +### 基本规则 + +模板中使用 `{{key}}` 定义占位符,API 调用时通过 `placeholders` 参数传递替换值。 + +**模板内容:** +```text +您好,{{username}}! +您的订单 {{order_id}} 已经 {{status}}。 +``` + +**API 调用:** +```json +{ + "placeholders": { + "username": "张三", + "order_id": "20241206001", + "status": "发货" + } +} +``` + +**替换结果:** +```text +您好,张三! +您的订单 20241206001 已经发货。 +``` + +### 默认值处理 + +如果占位符定义了默认值,未传递时使用默认值: + +**占位符定义:** +```json +[ + { + "key": "username", + "label": "用户名", + "default": "Guest" + } +] +``` + +**API 调用(未传递 username):** +```json +{ + "placeholders": {} +} +``` + +**替换结果:** +```text +您好,Guest! +``` + +### 未定义占位符 + +如果占位符既未传递也无默认值,将保持原样: + +```text +您好,{{username}}! // username 未传递且无默认值 +``` + +## 内容格式选择 + +V2 API 支持三种内容格式,系统会根据实例配置自动选择: + +| 格式 | 适用渠道 | 说明 | +|------|---------|------| +| Text | 所有渠道 | 纯文本格式,兼容性最好 | +| HTML | 邮件 | 富文本格式,支持样式 | +| Markdown | 钉钉、企业微信 | Markdown 格式,支持格式化 | + +**示例:** + +假设模板定义了三种格式的内容: + +- Text: `您好,{{username}}!` +- HTML: `

您好,{{username}}!

` +- Markdown: `## 您好,{{username}}!` + +当发送到不同实例时: + +- **邮件实例(ContentType=html)** → 使用 HTML 格式 +- **钉钉实例(ContentType=markdown)** → 使用 Markdown 格式 +- **其他实例(ContentType=text)** → 使用 Text 格式 + +## @提醒功能 + +如果模板配置了@提醒,会自动应用到支持的渠道(钉钉、企业微信)。 + +**模板配置:** +- @手机号:`13800138000,13900139000` +- @用户ID:`user001,user002` +- @所有人:是 + +**发送效果:** +- 钉钉/企业微信会@指定的手机号或用户 +- 如果启用@所有人,会@群内所有成员 + +## 错误码说明 + +| 错误码 | 说明 | 解决方案 | +|--------|------|---------| +| 200 | 成功 | - | +| 400 | 请求参数错误 | 检查请求参数格式 | +| 400 | token解析失败 | 检查 Token 是否正确 | +| 400 | 模板不存在 | 检查模板 ID 是否有效 | +| 400 | 模板已禁用 | 在管理后台启用模板 | +| 400 | 模板没有配置发送实例 | 为模板添加发送实例 | +| 400 | 模板没有启用的发送实例 | 启用至少一个实例 | +| 500 | 服务器内部错误 | 联系管理员 | + +## 获取模板 Token + +### 方式一:管理后台查看 + +1. 登录 Message Nest 管理后台 +2. 进入"消息模板"页面 +3. 点击模板的"接口"按钮 +4. 查看并复制加密的 Token + +### 方式二:API 代码示例 + +管理后台提供多种语言的 API 调用示例,包含真实的加密 Token。 + +## 安全性说明 + +### Token 加密 + +- V2 API 使用对称加密算法保护模板 ID +- Token 是确定性加密,相同的模板 ID 生成相同的 Token +- 加密密钥存储在服务器端,客户端无需关心加密细节 + +### 最佳实践 + +1. **Token 保护** + - 不要在公开代码中硬编码 Token + - 使用环境变量或配置文件存储 Token + - 定期检查 Token 的使用情况 + +2. **权限控制** + - 合理设置模板的启用/禁用状态 + - 及时禁用不再使用的模板 + - 定期审查模板配置 + +3. **内容安全** + - 注意模板内容的合规性 + - 避免在模板中包含敏感信息 + - 对用户输入进行验证和过滤 + +## 使用场景 + +### 1. 用户通知 + +```json +{ + "token": "...", + "title": "账号安全提醒", + "placeholders": { + "username": "张三", + "action": "登录", + "ip": "192.168.1.100", + "time": "2024-12-06 12:00:00" + } +} +``` + +### 2. 订单通知 + +```json +{ + "token": "...", + "title": "订单状态更新", + "placeholders": { + "order_id": "20241206001", + "status": "已发货", + "tracking_number": "SF1234567890", + "estimated_delivery": "2024-12-08" + } +} +``` + +### 3. 系统告警 + +```json +{ + "token": "...", + "title": "系统告警", + "placeholders": { + "service": "API Server", + "level": "严重", + "message": "CPU使用率超过90%", + "time": "2024-12-06 12:00:00" + } +} +``` + +### 4. 营销推广 + +```json +{ + "token": "...", + "title": "优惠活动通知", + "placeholders": { + "username": "张三", + "product": "VIP会员", + "discount": "8折", + "expire_date": "2024-12-31" + } +} +``` + +## 性能优化 + +### 异步发送 + +V2 API 采用异步发送机制,API 调用立即返回,实际发送在后台进行。 + +**优点:** +- 快速响应,不阻塞调用方 +- 支持批量发送多个实例 +- 自动重试失败的发送 + +### 批量发送 + +一次 API 调用可以发送到多个实例(渠道),系统自动遍历所有启用的实例。 + +**示例:** + +模板配置了 3 个实例: +- 邮件实例(启用) +- 钉钉实例(启用) +- 企业微信实例(禁用) + +调用 API 后,消息会发送到邮件和钉钉,企业微信实例被跳过。 + +## 常见问题 + +### Q: V1 和 V2 API 可以同时使用吗? + +**A:** 可以。V1 和 V2 API 是独立的,可以根据需求选择使用。 + +### Q: 如何从 V1 迁移到 V2? + +**A:** +1. 创建消息模板,定义占位符 +2. 为模板配置发送实例 +3. 获取模板 Token +4. 修改 API 调用代码,使用 V2 接口 + +### Q: 占位符可以嵌套吗? + +**A:** 不支持。占位符只支持一级替换,不支持嵌套或递归。 + +### Q: 可以动态添加占位符吗? + +**A:** 不可以。占位符必须在模板中预先定义,API 调用时只能传递已定义的占位符。 + +### Q: 如何调试模板? + +**A:** 使用管理后台的"预览"功能,可以填写测试数据查看替换效果。 + +## 下一步 + +- 查看 [消息模板文档](/guide/template) 了解如何创建和管理模板 +- 查看 [V1 API 文档](/api/usage) 了解传统的发送方式 +- 查看 [API 示例](/api/examples) 了解更多调用示例 diff --git a/docs/guide/changelog.md b/docs/guide/changelog.md index 3f22cd9..4ec1d49 100644 --- a/docs/guide/changelog.md +++ b/docs/guide/changelog.md @@ -2,6 +2,23 @@ ## 2025 +### 2025.12.06 +- **新增消息模板功能** + - 支持创建可复用的消息模板 + - 支持占位符动态替换(`{{key}}` 语法) + - 支持 Text、HTML、Markdown 三种格式 + - 支持为模板配置多个发送实例 + - 支持 @提醒功能(钉钉、企业微信) +- **新增 V2 API** + - 基于模板的消息发送接口 + - 使用加密 Token 提升安全性 + - 支持占位符参数传递 + - 自动遍历模板的所有启用实例 +- **优化代码结构** + - 重构模板相关 Model 命名(`MessageTemplate` → `Template`) + - 添加 TypeScript 类型声明文件 + - 完善文档体系 + ### 2025.10.12 - 增加cookies过期天数设置 diff --git a/docs/guide/template.md b/docs/guide/template.md new file mode 100644 index 0000000..89abdc4 --- /dev/null +++ b/docs/guide/template.md @@ -0,0 +1,280 @@ +# 消息模板 + +消息模板功能允许您创建可复用的消息模板,通过占位符实现动态内容替换,提高消息发送的灵活性和效率。 + +::: tip ⭐ 作者推荐 +消息模板是 Message Nest 的核心功能,也是我们**强烈推荐**的使用方式。相比传统的任务发送(V1 API),模板方式具有以下显著优势: + +**为什么推荐使用模板?** +1. **开发效率提升 3 倍** - 一次定义模板,所有项目复用,无需重复编写消息内容 +2. **维护成本降低 80%** - 修改消息格式只需在后台更新模板,无需修改代码、重新部署 +3. **团队协作更顺畅** - 开发负责 API 集成,运营负责内容维护,职责清晰 +4. **灰度发布更安全** - 支持模板启用/禁用,可以随时回滚,降低风险 +5. **内容管理更规范** - 所有消息模板集中管理,便于审核和统一风格 + +**适用场景:** +- ✅ 用户通知(注册、登录、密码重置等) +- ✅ 订单消息(下单、支付、发货、退款等) +- ✅ 系统告警(服务异常、资源不足等) +- ✅ 营销推广(活动通知、优惠券等) +- ✅ 完全动态的内容(通过占位符实现,如 `{{content}}`) +- ✅ **所有消息发送场景** + +::: info 关于完全动态内容 +即使消息内容完全动态,也推荐使用模板方式。你可以创建一个只包含一个占位符的模板,如: + +**模板内容:** `{{content}}` + +**API 调用:** +```json +{ + "placeholders": { + "content": "这里是完全动态的内容" + } +} +``` + +这样做的好处是: +1. 后期如果需要添加固定格式(如标题、签名),只需修改模板,无需改代码 +2. 所有消息统一管理,便于审计和监控 +3. 可以随时启用/禁用,便于灰度发布 + +**V1 API(任务)的定位:** +- 仅为兼容历史数据而保留 +- 不推荐在任何新项目中使用 +- 后续维护重点在模板功能上 +::: + + +## 功能特性 + +- ✅ **多格式支持** - 支持 Text、HTML、Markdown 三种格式 +- ✅ **占位符替换** - 使用 `{{key}}` 语法定义动态内容 +- ✅ **实例配置** - 为模板配置多个发送实例(渠道) +- ✅ **@提醒功能** - 支持钉钉、企业微信的@提醒 +- ✅ **状态管理** - 启用/禁用模板控制 +- ✅ **API调用** - 通过 V2 API 使用模板发送消息 + +## 创建模板 + +### 1. 基本信息 + +在管理后台的"消息模板"页面,点击"新建模板"按钮: + +- **模板名称** - 模板的唯一标识名称 +- **模板描述** - 模板的用途说明(可选) +- **状态** - 启用/禁用 + +### 2. 定义占位符 + +占位符用于在发送消息时动态替换内容。 + +**添加占位符:** + +| 字段 | 说明 | 示例 | +|------|------|------| +| Key | 占位符键名 | `username` | +| Label | 显示标签 | `用户名` | +| Default | 默认值(可选) | `Guest` | + +**使用示例:** + +```json +[ + { + "key": "username", + "label": "用户名", + "default": "Guest" + }, + { + "key": "email", + "label": "邮箱地址", + "default": "user@example.com" + }, + { + "key": "action", + "label": "操作类型", + "default": "登录" + } +] +``` + +### 3. 编写模板内容 + +支持三种格式,可以根据需要填写一种或多种: + +#### Text 模板 + +纯文本格式,适用于所有渠道: + +```text +您好,{{username}}! + +您的账号 {{email}} 刚刚进行了 {{action}} 操作。 + +如果这不是您本人的操作,请立即联系我们。 +``` + +#### HTML 模板 + +HTML 格式,适用于邮件等支持富文本的渠道: + +```html +
+

您好,{{username}}!

+

您的账号 {{email}} 刚刚进行了 {{action}} 操作。

+

如果这不是您本人的操作,请立即联系我们。

+
+``` + +#### Markdown 模板 + +Markdown 格式,适用于钉钉、企业微信等支持 Markdown 的渠道: + +```markdown +## 您好,{{username}}! + +您的账号 **{{email}}** 刚刚进行了 `{{action}}` 操作。 + +> ⚠️ 如果这不是您本人的操作,请立即联系我们。 +``` + +### 4. 配置 @提醒(可选) + +针对钉钉、企业微信等渠道,可以配置@提醒: + +- **@手机号** - 多个手机号用逗号分隔,如:`13800138000,13900139000` +- **@用户ID** - 多个用户ID用逗号分隔 +- **@所有人** - 勾选后会@所有群成员 + +::: warning 注意 +@提醒功能仅在支持的渠道(钉钉、企业微信)中生效。 +::: + +## 配置发送实例 + +创建模板后,需要为模板配置发送实例(渠道)。 + +### 1. 添加实例 + +在模板列表中,点击"实例"按钮: + +1. 选择发送渠道(从已创建的渠道中选择) +2. 配置实例参数(如邮箱收件人地址) +3. 选择消息格式(Text/HTML/Markdown) +4. 保存实例 + +### 2. 实例配置说明 + +不同渠道需要配置不同的参数: + +**邮件渠道:** +- 收件人邮箱地址 +- 消息格式:Text 或 HTML + +**钉钉/企业微信:** +- 消息格式:Text 或 Markdown + +**自定义 Webhook:** +- 根据 Webhook 要求配置 + +### 3. 管理实例 + +- **启用/禁用** - 控制实例是否参与发送 +- **编辑** - 修改实例配置 +- **删除** - 删除不需要的实例 + +## 预览模板 + +在编辑模板时,可以使用"预览"功能查看替换占位符后的效果: + +1. 点击"预览"按钮 +2. 填写占位符的测试值 +3. 查看 Text/HTML/Markdown 三种格式的渲染效果 + +## 使用模板发送消息 + +### 通过 API 调用 + +使用 V2 API 发送模板消息,详见 [V2 API 文档](/api/v2)。 + +**基本流程:** + +1. 获取模板 Token(在模板详情页查看) +2. 准备占位符数据 +3. 调用 V2 API 发送 + +**示例:** + +```bash +curl -X POST http://your-domain/api/v2/message/send \ + -H "Content-Type: application/json" \ + -d '{ + "token": "encrypted_template_token", + "title": "账号安全提醒", + "placeholders": { + "username": "张三", + "email": "zhangsan@example.com", + "action": "登录" + } + }' +``` + +## 最佳实践 + +### 1. 占位符命名 + +- 使用有意义的英文名称,如 `username`、`order_id` +- 避免使用特殊字符,建议使用下划线分隔 +- 保持命名一致性 + +### 2. 模板设计 + +- **简洁明了** - 模板内容应简洁清晰 +- **格式适配** - 根据渠道特性选择合适的格式 +- **默认值** - 为占位符设置合理的默认值 +- **测试验证** - 使用预览功能验证模板效果 + +### 3. 实例管理 + +- **合理分组** - 为不同用途创建不同的模板 +- **渠道选择** - 根据消息类型选择合适的渠道 +- **定期检查** - 定期检查实例配置的有效性 + +### 4. 安全性 + +- **Token 保护** - 妥善保管模板 Token +- **权限控制** - 合理设置模板的启用/禁用状态 +- **内容审查** - 注意模板内容的合规性 + +## 常见问题 + +### Q: 占位符没有被替换? + +**A:** 检查以下几点: +- 占位符格式是否正确(`{{key}}`) +- API 调用时是否传递了对应的 placeholders 参数 +- Key 名称是否匹配 + +### Q: 如何选择消息格式? + +**A:** 根据渠道特性选择: +- **邮件** - 推荐使用 HTML 格式,视觉效果更好 +- **钉钉/企业微信** - 推荐使用 Markdown 格式,支持富文本 +- **其他渠道** - 使用 Text 格式,兼容性最好 + +### Q: 可以为一个模板配置多个实例吗? + +**A:** 可以。一个模板可以配置多个发送实例,调用 API 时会自动遍历所有启用的实例进行发送。 + +### Q: @提醒不生效? + +**A:** 确认以下几点: +- 渠道是否支持@提醒(仅钉钉、企业微信支持) +- 手机号或用户ID格式是否正确 +- 机器人是否有@权限 + +## 下一步 + +- 查看 [V2 API 文档](/api/v2) 了解如何通过 API 使用模板 +- 查看 [API 示例](/api/examples) 了解各语言的调用方式 diff --git a/docs/package-lock.json b/docs/package-lock.json index 8f31c19..4d1ba01 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -8,7 +8,7 @@ "name": "message-nest-docs", "version": "1.0.0", "devDependencies": { - "vitepress": "^1.0.0" + "vitepress": "^1.6.4" } }, "node_modules/@algolia/abtesting": { diff --git a/docs/package.json b/docs/package.json index 474f3a3..84d9798 100644 --- a/docs/package.json +++ b/docs/package.json @@ -8,6 +8,6 @@ "docs:preview": "vitepress preview" }, "devDependencies": { - "vitepress": "^1.0.0" + "vitepress": "^1.6.4" } }