# 通知渠道集成文档 本模块(`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=智慧教务 ``` ## 使用方式 ### 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 { // 实现发送逻辑 } async sendBatch( items: Array<{ payload: NotificationPayload; recipient: ChannelRecipient }> ): Promise { // 实现批量发送逻辑 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。