245 lines
8.0 KiB
Markdown
245 lines
8.0 KiB
Markdown
# 通知渠道集成文档
|
||
|
||
本模块(`src/modules/notifications`)为系统提供多渠道通知发送能力,支持站内消息、短信、微信公众号模板消息和邮件四种渠道。
|
||
|
||
## 架构概览
|
||
|
||
```
|
||
调用方 (Server Action / 其他模块)
|
||
│
|
||
▼
|
||
dispatcher.ts ── 读取用户通知偏好 (notification_preferences)
|
||
│ ── 读取用户联系方式 (users.phone / users.email)
|
||
│
|
||
├── in_app (站内消息,总是启用)
|
||
├── sms (短信,smsEnabled && phone)
|
||
├── wechat (微信模板消息,pushEnabled && openId)
|
||
└── email (邮件,emailEnabled && email)
|
||
│
|
||
▼
|
||
data-access.ts ── logNotificationSend (console 日志)
|
||
```
|
||
|
||
## 模块结构
|
||
|
||
| 文件 | 职责 |
|
||
|------|------|
|
||
| `types.ts` | 通知渠道类型定义(NotificationPayload, ChannelSendResult 等) |
|
||
| `channels/types.ts` | 渠道发送者接口(NotificationChannelSender, ChannelRecipient) |
|
||
| `channels/sms-channel.ts` | 短信渠道(阿里云/腾讯云/Mock) |
|
||
| `channels/wechat-channel.ts` | 微信公众号模板消息渠道 |
|
||
| `channels/email-channel.ts` | 邮件渠道(Nodemailer SMTP) |
|
||
| `channels/in-app-channel.ts` | 站内消息渠道(复用 messaging data-access) |
|
||
| `dispatcher.ts` | 通知分发器(按偏好选择渠道、并行发送) |
|
||
| `data-access.ts` | 通知数据访问(偏好查询、联系方式查询、日志记录) |
|
||
| `actions.ts` | Server Actions(sendNotificationAction, sendClassNotificationAction) |
|
||
| `index.ts` | 模块统一导出 |
|
||
|
||
## 渠道配置
|
||
|
||
### 1. 站内消息(in_app)
|
||
|
||
- **默认启用**,无需配置。
|
||
- 复用现有 `messaging` 模块的 `createNotification`,写入 `message_notifications` 表。
|
||
- 用户可在站内通知中心查看。
|
||
|
||
### 2. 短信(SMS)
|
||
|
||
支持三种 Provider,通过 `SMS_PROVIDER` 环境变量选择:
|
||
|
||
| Provider | 说明 | 环境变量 |
|
||
|----------|------|----------|
|
||
| `mock`(默认) | 开发环境模拟,仅记录日志 | 无需其他配置 |
|
||
| `aliyun` | 阿里云短信 | `SMS_ACCESS_KEY_ID`, `SMS_ACCESS_KEY_SECRET`, `SMS_SIGN_NAME`, `SMS_TEMPLATE_CODE` |
|
||
| `tencent` | 腾讯云短信 | 同上(复用相同变量名) |
|
||
|
||
**模板变量替换**:将 `payload.title` / `payload.content` 填入模板变量 `title` / `content`。
|
||
|
||
### 3. 微信公众号模板消息(wechat)
|
||
|
||
通过微信公众号 API 发送模板消息:
|
||
|
||
1. **获取 access_token**:`GET https://api.weixin.qq.com/cgi-bin/token`(带缓存,提前 5 分钟刷新)
|
||
2. **发送模板消息**:`POST https://api.weixin.qq.com/cgi-bin/message/template/send`
|
||
|
||
**环境变量**:
|
||
|
||
| 变量 | 说明 |
|
||
|------|------|
|
||
| `WECHAT_APP_ID` | 公众号 AppID |
|
||
| `WECHAT_APP_SECRET` | 公众号 AppSecret |
|
||
| `WECHAT_TEMPLATE_ID` | 模板消息 ID |
|
||
|
||
**模板数据映射**:
|
||
|
||
- `keyword1` ← `payload.title`
|
||
- `keyword2` ← `payload.content`
|
||
- `keyword3` ← `payload.type`
|
||
|
||
可通过 `payload.metadata.wechatKeywords` 自定义覆盖。
|
||
|
||
> **注意**:当前 `users` 表无 `wechat_open_id` 字段,微信渠道暂不会实际触发。扩展 schema 后在 `data-access.ts` 的 `getUserContactInfo` 中补充查询即可。
|
||
|
||
### 4. 邮件(email)
|
||
|
||
使用 Nodemailer 通过 SMTP 发送,支持 HTML 邮件模板(根据通知类型显示不同颜色)。
|
||
|
||
**环境变量**:
|
||
|
||
| 变量 | 默认值 | 说明 |
|
||
|------|--------|------|
|
||
| `EMAIL_HOST` | - | SMTP 主机(配置后启用真实发送) |
|
||
| `EMAIL_PORT` | `587` | SMTP 端口(465 使用 SSL) |
|
||
| `EMAIL_USER` | - | SMTP 用户名 |
|
||
| `EMAIL_PASS` | - | SMTP 密码 |
|
||
| `EMAIL_FROM` | `noreply@example.com` | 发件人地址 |
|
||
|
||
## Mock 模式(开发环境)
|
||
|
||
所有渠道均提供 Mock 实现,**无需任何外部服务即可运行**:
|
||
|
||
- SMS: `SMS_PROVIDER=mock`(默认)→ 仅 `console.info` 记录
|
||
- WeChat: 未配置 `WECHAT_APP_ID` 等 → 自动使用 Mock
|
||
- Email: 未配置 `EMAIL_HOST` → 自动使用 Mock
|
||
- 站内消息: 始终真实写入数据库(无 Mock)
|
||
|
||
## 生产环境配置
|
||
|
||
### 阿里云短信示例
|
||
|
||
```env
|
||
SMS_PROVIDER=aliyun
|
||
SMS_ACCESS_KEY_ID=LTAI5tXXXXXXXXXXXX
|
||
SMS_ACCESS_KEY_SECRET=XXXXXXXXXXXXXXXXXXXXXXXX
|
||
SMS_SIGN_NAME=智慧教务
|
||
SMS_TEMPLATE_CODE=SMS_123456789
|
||
```
|
||
|
||
### 微信公众号示例
|
||
|
||
```env
|
||
WECHAT_APP_ID=wx1234567890abcdef
|
||
WECHAT_APP_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||
WECHAT_TEMPLATE_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||
```
|
||
|
||
### 邮件 SMTP 示例
|
||
|
||
```env
|
||
EMAIL_HOST=smtp.example.com
|
||
EMAIL_PORT=587
|
||
EMAIL_USER=notification@example.com
|
||
EMAIL_PASS=xxxxxxxx
|
||
EMAIL_FROM=智慧教务 <notification@example.com>
|
||
```
|
||
|
||
## 使用方式
|
||
|
||
### 1. 通过 Server Action 调用
|
||
|
||
```ts
|
||
import { sendNotificationAction, sendClassNotificationAction } from "@/modules/notifications/actions"
|
||
|
||
// 发送给单个用户
|
||
await sendNotificationAction({
|
||
userId: "user-xxx",
|
||
title: "作业提醒",
|
||
content: "您有一份新作业待提交",
|
||
type: "info",
|
||
actionUrl: "/homework/123",
|
||
})
|
||
|
||
// 发送给班级所有学生(教师权限)
|
||
await sendClassNotificationAction("class-xxx", {
|
||
title: "考试通知",
|
||
content: "明天下午 2 点期中考试",
|
||
type: "warning",
|
||
actionUrl: "/exams/456",
|
||
})
|
||
```
|
||
|
||
### 2. 直接调用分发器(服务端)
|
||
|
||
```ts
|
||
import { sendNotification } from "@/modules/notifications"
|
||
|
||
await sendNotification({
|
||
userId: "user-xxx",
|
||
title: "成绩发布",
|
||
content: "您的数学成绩为 95 分",
|
||
type: "success",
|
||
})
|
||
```
|
||
|
||
## 渠道选择逻辑
|
||
|
||
分发器根据用户通知偏好(`notification_preferences` 表)和联系方式决定启用渠道:
|
||
|
||
| 渠道 | 启用条件 |
|
||
|------|----------|
|
||
| in_app | `pushEnabled`(默认 true),总是兜底 |
|
||
| sms | `smsEnabled` && 用户有手机号 |
|
||
| email | `emailEnabled` && 用户有邮箱 |
|
||
| wechat | `pushEnabled` && 用户有 wechatOpenId |
|
||
|
||
> 通知偏好中的 `homeworkNotifications` / `gradeNotifications` 等按通知类别控制,由调用方在构造 payload 前决定是否调用发送。
|
||
|
||
## 扩展新渠道
|
||
|
||
1. 在 `channels/` 下创建新文件,实现 `NotificationChannelSender` 接口:
|
||
|
||
```ts
|
||
import "server-only"
|
||
import type { NotificationChannelSender, ChannelRecipient } from "./types"
|
||
import type { NotificationPayload, ChannelSendResult, NotificationChannel } from "../types"
|
||
|
||
// 注意:先在 types.ts 的 NotificationChannel 联合类型中添加 "your_channel"
|
||
const channel: NotificationChannel = "your_channel"
|
||
|
||
class YourChannelSender implements NotificationChannelSender {
|
||
readonly channel = channel
|
||
async send(payload: NotificationPayload, recipient: ChannelRecipient): Promise<ChannelSendResult> {
|
||
// 实现发送逻辑
|
||
}
|
||
async sendBatch(
|
||
items: Array<{ payload: NotificationPayload; recipient: ChannelRecipient }>
|
||
): Promise<ChannelSendResult[]> {
|
||
// 实现批量发送逻辑
|
||
return []
|
||
}
|
||
}
|
||
|
||
export function createYourSender(): NotificationChannelSender {
|
||
return new YourChannelSender()
|
||
}
|
||
```
|
||
|
||
2. 在 `types.ts` 的 `NotificationChannel` 类型中添加新渠道:
|
||
|
||
```ts
|
||
export type NotificationChannel = "in_app" | "email" | "sms" | "wechat" | "your_channel"
|
||
```
|
||
|
||
3. 在 `dispatcher.ts` 的 `SenderRegistry` 和 `selectChannels` 中注册新渠道。
|
||
|
||
4. 在 `index.ts` 中导出新的发送器工厂。
|
||
|
||
## 权限说明
|
||
|
||
- `sendNotificationAction`: 需要 `MESSAGE_SEND` 权限
|
||
- `sendClassNotificationAction`: 需要 `MESSAGE_SEND` 权限,且教师只能给自己所教班级发送
|
||
|
||
> 项目无独立 `NOTIFICATION_SEND` 权限点,复用 `MESSAGE_SEND`(教师/管理员/年级主任均拥有)。
|
||
|
||
## 外部 SDK 依赖
|
||
|
||
所有外部 SDK 均使用**动态 import**,避免增加构建体积:
|
||
|
||
| 渠道 | SDK | 安装命令 |
|
||
|------|-----|----------|
|
||
| 阿里云短信 | `@alicloud/dysmsapi20170525`, `@alicloud/openapi-client`, `@alicloud/credentials` | `npm i @alicloud/dysmsapi20170525 @alicloud/openapi-client @alicloud/credentials` |
|
||
| 腾讯云短信 | `tencentcloud-sdk-nodejs` | `npm i tencentcloud-sdk-nodejs` |
|
||
| 邮件 | `nodemailer` | `npm i nodemailer @types/nodemailer` |
|
||
|
||
> **Mock 模式无需安装任何 SDK**,开发环境开箱即用。生产环境按需安装对应 SDK。
|