feat(P2): 实现质量保障类5项功能(无障碍/视觉回归/通知渠道/漏洞扫描/灾备)

## 新增功能

### 1. 屏幕阅读器兼容性增强(a11y)
- 无障碍工具库:src/shared/lib/a11y.ts
- aria-live Hook:src/shared/hooks/use-aria-live.ts
- a11y 组件:skip-link/visually-hidden/focus-trap/aria-status
- 增强 UI:table.tsx 系统性 ARIA role,dialog.tsx aria-modal
- 审计文档:docs/accessibility/a11y-audit.md(WCAG 2.1 AA 清单)

### 2. 视觉回归测试
- 测试套件:tests/visual/(homepage + 3 个 dashboard)
- 3 视口(desktop/tablet/mobile)× 2 主题(light/dark)
- 动态元素遮罩,避免误报
- playwright.config.ts 新增 visual-chromium 项目
- 文档:docs/testing/visual-regression.md

### 3. 短信/微信推送渠道集成
- 新模块:src/modules/notifications/
- 4 个渠道:SMS(阿里云/腾讯云)、WeChat(公众号)、Email(SMTP)、In-App
- 分发器按用户偏好并行多渠道发送
- 外部 SDK 动态 import,Mock 模式开发可用
- 文档:docs/notifications/channels.md

### 4. 漏洞扫描 CI 集成
- CI security-scan job:npm audit + Snyk + Trivy FS + OWASP ZAP
- 独立工作流 security.yml:每周一深度扫描 + 容器镜像扫描
- 配置:suppressions.json + .trivyignore
- 本地脚本:security-scan.sh/ps1
- 文档:docs/security/scanning.md(SLA 分级)

### 5. 灾备方案
- 脚本:backup-verify/backup-offsite-sync/dr-drill/failover/health-check
- CI 增强:备份后校验+异地同步,每周灾备演练
- 独立工作流 dr-drill.yml:每周一凌晨 4 点自动演练
- 文档:docs/dr/dr-plan.md(RTO 4h/RPO 24h)+ dr-runbook.md(6 故障场景)

## 验证
- npx tsc --noEmit:0 错误
- npm run lint:0 错误 0 警告
This commit is contained in:
SpecialX
2026-06-17 20:18:29 +08:00
parent b86255f0ea
commit 6585e10c6f
53 changed files with 7491 additions and 37 deletions

View File

@@ -0,0 +1,238 @@
# 通知渠道集成文档
本模块(`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 ActionssendNotificationAction, 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"
const channel: NotificationChannel = "your_channel" as NotificationChannel
class YourChannelSender implements NotificationChannelSender {
readonly channel = channel
async send(payload: NotificationPayload, recipient: ChannelRecipient): Promise<ChannelSendResult> {
// 实现发送逻辑
}
async sendBatch(items) { /* ... */ }
}
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。