| 项目 | 内容 |
|---|---|
| 版本 | v1 |
| 日期 | 2026-06-06 |
| 适用范围 | SAST People 邮件模板、发送任务、发送记录、测试发送、面试通知和招新结果通知 |
| 目标 | 将所有系统邮件统一收口到邮件中心,业务模块不再直接发送邮件 |
信息架构更新(2026-07-21):管理员可见导航与主链路见
docs/email-center-flow-redesign.md(业务工作台:默认「发结果通知」,四页签,面试邮件仅记录/模板)。本文 §1–4、§6+ 的平台边界与不变量仍有效;§5 五页签布局以 flow-redesign 为准。
当前 /dashboard/emails 实际承担的是“招新结果邮件发送”页面,但同时塞入了“面试通知模板”配置。页面名称、功能边界和实际能力不一致,导致用户会误以为它能管理所有邮件。
现有实现还有几个结构性问题:
- 结果通知邮件通过
email_batch/email_delivery留存批次和投递记录。 - 面试预约、改约、取消邮件在
action/user-flow/interviewSchedule.ts中直接渲染并调用sendRawEmail,没有进入统一邮件记录。 - 测试邮件、结果通知、面试通知的调用方式不统一。
- 模板配置分散在页面弹窗中,缺少统一的模板列表、预览、测试发送入口。
- 发送记录只能覆盖结果通知批次,不能回答“某个同学是否收到过面试通知”。
- 业务模块知道过多邮件细节,例如 subject、HTML 渲染、SMTP 发送方式。
邮件中心必须成为平台级能力:所有涉及邮件的业务都只提交邮件请求,由邮件中心完成模板、渲染、快照、入队、发送、记录和重试。
-
单一出口 所有邮件必须经由邮件中心服务层。业务模块禁止直接调用 SMTP、
sendRawEmail、React Emailrender或具体模板组件。 -
可追踪 每一封邮件必须有投递记录,包含收件人、主题、模板、状态、失败原因、正文快照、关联业务对象。
-
快照不可变 邮件发出前保存
subject和htmlSnapshot。后续模板变更不能影响历史邮件内容。 -
模板和发送解耦 模板负责内容结构,发送任务负责收件人和业务上下文。业务模块只传变量,不拼 HTML。
-
失败可恢复 邮件发送失败必须落库,并能从邮件中心重试。不能只依赖 toast 或 server log。
-
UI 面向操作 邮件中心不是数据库表查看器。页面必须围绕管理员的常见任务组织:看状态、发通知、改模板、查失败、重试、核对正文。
- 邮件模板注册和展示
- 模板变量定义、默认值、校验
- 邮件预览和测试发送
- 创建单封投递记录
- 创建批量发送任务
- 渲染 subject 和 HTML
- 保存正文快照
- 入队发送
- SMTP 实际发送
- 更新发送状态
- 失败原因记录
- 重试和恢复中断任务
- 邮件发送审计
- 决定业务事件是否需要邮件通知
- 提供业务变量,例如姓名、流程名、面试时间、地点
- 提供关联对象 ID,例如 flowId、userFlowId、scheduleId
- 展示业务侧操作结果,例如“已创建面试预约”
- 选择 SMTP transporter
- 直接调用
sendRawEmail - 手动
renderReact Email 模板 - 自行保存邮件快照
- 自行维护发送状态
- 自行实现邮件重试
第一阶段必须覆盖以下邮件:
| category | templateKey | 触发场景 | 发送方式 |
|---|---|---|---|
result |
recruitment.result.accepted |
招新通过结果通知 | 批量 |
result |
recruitment.result.rejected |
招新不通过结果通知 | 批量 |
interview |
interview.schedule.created |
面试预约创建 | 单封 |
interview |
interview.schedule.rescheduled |
面试改约 | 单封 |
interview |
interview.schedule.cancelled |
面试取消 | 单封 |
test |
任意模板 + .test 标记 |
管理员测试发送 | 单封 |
当前已有的 interview.schedule 可以作为兼容 key,但新设计建议拆成三个明确模板 key。三种状态可以共享 React Email 组件,但模板注册层需要暴露独立条目,方便预览、测试和文案管理。
导航名称使用 邮件中心。页面顶部说明:
统一管理系统邮件模板、发送任务和发送记录。招新结果通知、面试通知和测试邮件都从这里追踪。
页面使用 tab 结构:
邮件中心
├─ 概览
├─ 发送任务
├─ 发送记录
├─ 模板管理
└─ 配置
目标:让管理员快速判断邮件系统是否健康。
内容:
- 今日发送总数
- 今日失败数
- 待发送数
- 发送中数
- 最近失败原因 Top 5
- 最近 5 个发送任务
- 邮件服务配置状态,例如 SMTP 是否配置、队列是否可用
UI:
- 顶部 4 个指标卡,紧凑排列。
- 下方左右两栏:最近任务、最近失败。
- 移动端改为单列堆叠。
空状态:
- 没有发送记录:显示“暂无邮件发送记录”,并引导到模板测试或结果通知发送。
目标:管理批量邮件任务。
当前第一阶段只支持结果通知批量任务,但 UI 名称不写死“结果邮件”。
结构:
- 左侧:流程列表和搜索。
- 右侧:当前流程下的通知任务。
- 每个任务分为“通过通知”和“不通过通知”。
主要操作:
- 查看待通知名单
- 预览邮件样张
- 创建发送任务
- 发送任务
- 重试失败
- 恢复中断
状态:
draft:已创建任务但未发送queued:已入队sending:投递中completed:全部成功failed:存在失败partial:部分成功、部分失败,当前可以由统计计算,不一定要做数据库枚举
目标:查所有邮件,而不是只查结果通知。
筛选:
- 邮件类型:结果通知、面试通知、测试邮件
- 模板
- 状态:待发送、发送中、成功、失败
- 流程
- 收件人
- 创建人
- 时间范围
表格列:
- 状态
- 类型
- 主题
- 收件人
- 关联对象
- 创建时间
- 发送时间
- 创建人
- 操作
行操作:
- 查看正文快照
- 查看失败原因
- 重试
- 复制收件地址
- 跳转关联业务对象,例如流程、报名记录、面试预约
移动端:
- 使用列表卡片,不展示宽表。
- 卡片第一行显示状态、类型、时间。
- 第二行显示主题和收件人。
- 操作折叠到菜单或底部按钮组。
目标:统一编辑、预览、测试所有模板。
列表列:
- 模板名称
- 类型
- 状态
- 可用变量摘要
- 最近修改时间
- 操作
模板操作:
- 编辑
- 预览
- 测试发送
- 恢复默认
模板编辑 UI 原则:
- 不用双栏挤压输入框。
- 主编辑区域单栏,保证输入宽度。
- 变量提示只作为辅助,不占据主要空间。
- 复杂变量说明用折叠区域或 hover/tooltip。
- 预览在独立弹窗或右侧抽屉中显示,不和编辑字段挤在同一个窄弹窗里。
面试模板编辑建议:
- 标题字段:邮件标题、邮件主标题
- 正文字段:开头说明
- 落款字段:组织落款
- 说明文案:时间、地点、讲师、备注和参会入口会自动生成到信息卡片里。
第一阶段只展示只读配置状态:
- SMTP host
- 发件人
- 测试收件人
- 队列状态
- 生产环境是否启用真实收件人
敏感信息不展示明文。
保留现有表,渐进扩展:
email_template_content:模板内容表,继续使用。email_template_setting:结果通知链接配置,短期保留;长期并入模板配置模型。email_batch:从“结果邮件批次”扩展为通用邮件任务。email_delivery:从“结果邮件投递”扩展为所有邮件投递记录。
建议新增字段:
| 字段 | 类型 | 说明 |
|---|---|---|
category |
varchar(32) | result / interview / test |
template_key |
varchar(80) | 已有,继续使用 |
name |
varchar(255) | 任务名称 |
status |
enum | 继续使用现有状态 |
total_count |
integer | 已有 |
fk_flow_id |
integer nullable | 关联流程,可为空 |
fk_created_by |
integer nullable | 已有 |
metadata |
jsonb | 额外上下文 |
现有 accept 字段是结果通知特有字段。长期建议迁移到 metadata.accept 或新增 variant 字段。短期可以保留,避免一次性重构过大。
建议新增字段:
| 字段 | 类型 | 说明 |
|---|---|---|
category |
varchar(32) | 邮件类型 |
template_key |
varchar(80) | 模板 key |
fk_email_batch_id |
integer nullable | 单封业务邮件可以为空或挂到自动批次 |
fk_flow_id |
integer nullable | 关联流程 |
fk_user_flow_id |
integer nullable | 已有,继续使用 |
fk_user_id |
integer nullable | 当前非空,后续测试邮件可能为空 |
related_schedule_id |
integer nullable | 面试预约 ID |
created_by |
integer nullable | 触发人 |
metadata |
jsonb | 变量、业务上下文、测试标记 |
需要注意:当前 fkUserId 是 not null。测试邮件或外部联系人不一定有 Link 用户 ID。第一阶段可以继续给测试邮件使用当前登录用户 ID;长期应改为 nullable。
新增模板注册表或静态 registry:
export type EmailTemplateDefinition = {
key: string;
category: "result" | "interview" | "test";
name: string;
description: string;
variables: EmailVariableDefinition[];
defaultSubject: string;
defaultTitle?: string;
defaultBody?: string;
render: (input: EmailRenderInput) => Promise<RenderedEmail>;
};变量定义:
export type EmailVariableDefinition = {
key: string;
label: string;
required: boolean;
example: string;
description?: string;
};注册示例:
export const emailTemplates = [
{
key: "interview.schedule.created",
category: "interview",
name: "面试预约通知",
variables: [
{ key: "candidateName", label: "候选人姓名", required: true, example: "张三" },
{ key: "flowName", label: "流程名称", required: true, example: "2026 免试招新" },
{ key: "startsAt", label: "开始时间", required: true, example: "2026-06-06 16:00" },
{ key: "location", label: "地点", required: false, example: "大学生活动中心 101" },
],
render: renderInterviewScheduleEmailByTemplate,
},
];新增目录:
lib/email-center/
├─ types.ts
├─ registry.ts
├─ render.ts
├─ create-delivery.ts
├─ create-batch.ts
├─ enqueue.ts
├─ send.ts
├─ retry.ts
└─ query.ts
await createEmailDelivery({
templateKey: "interview.schedule.created",
to: attendeeEmail,
recipientUserId: candidateId,
variables: {
candidateName,
flowName,
organizerName,
startsAt,
endsAt,
location,
meetingLink,
scheduleLink,
},
related: {
flowId,
userFlowId,
scheduleId,
},
createdBy: session.uid,
sendImmediately: true,
});行为:
- 校验模板存在。
- 校验必填变量。
- 渲染 subject 和 HTML。
- 创建
email_delivery。 - 如果
sendImmediately为 true,则入队或直接 fallback 发送。 - 返回 deliveryId。
await createEmailBatch({
templateKey: "recruitment.result.accepted",
name: "2026 免试招新 通过通知",
recipients,
variablesFor: (recipient) => ({
candidateName: recipient.name,
flowName,
}),
related: {
flowId,
},
createdBy: session.uid,
});行为:
- 创建
email_batch。 - 为每个收件人创建
email_delivery。 - 每封邮件保存独立 HTML 快照。
- 不自动发送,除非调用方指定
sendImmediately。
await enqueueEmailDelivery(deliveryId);
await sendEmailDelivery(deliveryId);
await retryEmailDelivery(deliveryId);
await retryEmailBatch(batchId);sendEmailDelivery 是唯一允许调用 SMTP 的地方。
| 当前位置 | 当前行为 | 新行为 |
|---|---|---|
action/user/sendEmail.ts |
创建结果邮件批次和投递 | 改为 emailCenter.createBatch |
action/email/send.ts |
发送结果邮件批次 | 改为 emailCenter.retryBatch/sendBatch |
queue/sendEmail.tsx |
SMTP 实际发送 | 保留为底层 delivery sender,但移动到邮件中心 |
action/user-flow/interviewSchedule.ts |
直接 render + sendRawEmail |
改为 emailCenter.createDelivery |
action/email/test-send.ts |
发送测试邮件 | 改为 emailCenter.createDelivery(category: "test") |
后续业务代码不允许新增:
sendRawEmail(...)
render(<SomeEmail />)
createTransport(...)可以通过 lint 规则或代码 review 约束。短期可以通过 rg "sendRawEmail|createTransport|@react-email/render" 检查。
| 功能 | 最低角色 |
|---|---|
| 查看邮件中心 | 管理员 role >= 3 |
| 查看发送记录 | 管理员 role >= 3 |
| 查看正文快照 | 管理员 role >= 3 |
| 编辑模板 | 管理员 role >= 3 |
| 测试发送 | 管理员 role >= 3 |
| 创建结果通知任务 | 管理员 role >= 3 |
| 重试失败邮件 | 管理员 role >= 3 |
讲师 role 2 不直接进入邮件中心。讲师触发面试预约时,邮件由业务 action 代为创建,但记录归邮件中心保存。
- 页面标题:
邮件中心 - 顶部说明:简短说明覆盖范围,不写过宽泛口号。
- Tabs 使用持久 URL query,例如
/dashboard/emails?tab=records。 - 表格默认高密度但不过度拥挤。
- 主要动作按钮放在当前 tab 右上角。
- 危险操作和重试操作必须有明确状态反馈。
避免当前问题:
- 不使用左右栏挤压输入框。
- 不把所有变量以大段技术文本堆在顶部。
- 不让模板正文承担自动信息卡片的内容。
推荐结构:
编辑模板
说明:时间、地点、讲师和参会入口会自动生成。
[邮件标题]
[邮件主标题]
[开头说明 textarea]
[落款]
提示:正文建议保留 {candidateName} 和 {flowName}
[恢复默认] [预览] [保存]
详情弹窗分三段:
- 投递信息:状态、收件人、模板、创建时间、发送时间。
- 关联对象:流程、候选人、面试预约。
- 正文快照:iframe 预览。
失败时顶部显示失败原因和“重试”按钮。
- 当前
/dashboard/emails只做必要 bug 修复。 - 页面文案可先改为
招新通知,避免误导。 - 完成本文档并作为后续实现依据。
- 新增
lib/email-center/*。 - 把 SMTP 发送集中到
sendEmailDelivery。 - 保留现有
email_batch/email_delivery行为。 - 结果通知发送改为使用新服务层,但 UI 不大改。
- 面试预约、改约、取消邮件改为
createEmailDelivery。 - 所有面试邮件进入
email_delivery。 - 发送失败后业务 action 能返回明确错误,同时邮件中心有失败记录。
/dashboard/emails改为邮件中心。- 新增 tabs:概览、发送任务、发送记录、模板管理、配置。
- 当前结果通知 UI 移入发送任务。
- 当前模板弹窗移入模板管理。
- 发送记录支持 category/template/status 筛选。
- 为
email_batch/email_delivery增加 category、templateKey、related IDs、metadata 等字段。 - 迁移历史结果邮件记录。
- 调整
fkUserIdnullable 策略,支持测试邮件和非用户收件人。
- 增加邮件中心单元测试。
- 增加关键 action 的集成测试。
- 增加 lint/code review 检查,禁止业务层直接发送邮件。
- 完善错误恢复和队列健康检查。
| 风险 | 处理 |
|---|---|
| 一次性重构过大 | 分 phase,先抽服务层,再改 UI |
| 历史结果邮件记录兼容 | 保留现有字段,新增字段 nullable |
| 面试邮件发送失败影响预约流程 | 先创建预约,再创建邮件记录;邮件失败要记录并给出可重试入口 |
| 模板变量不一致 | 模板 registry 统一定义变量和校验 |
| 测试邮件污染正式记录 | 使用 category test 和 metadata 标记 |
| 敏感信息泄露 | 邮件中心不展示 SMTP 密码,不在日志输出 HTML 全文 |
建议最先实现:
- 新增
lib/email-center/types.ts和registry.ts。 - 新增
createEmailDelivery,支持单封邮件记录。 - 将
sendRawEmail封装为sendEmailDelivery。 - 改造面试预约邮件走
createEmailDelivery。 - 扩展发送记录查询,能看到面试邮件。
- 将页面改名为
邮件中心,保留旧 UI 但增加发送记录覆盖所有邮件。
这批完成后,系统才真正满足“所有涉及邮件的都走邮件中心”的底线。