# 通知渠道集成文档

本模块（`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。
