You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

11 KiB

设计:评论邮件配置与通知偏好

日期:2026-04-20
状态:已定稿(待实现计划)

1. 背景与目标

当前评论系统仅支持游客昵称与正文输入,不采集游客邮箱;登录用户评论流程也不与邮箱和通知偏好联动。本次需求是在不阻断评论核心体验的前提下,补齐评论邮件通知所需的最小闭环:

  • 增加全局邮件发件配置(管理员可配置,默认未配置)。
  • 游客评论默认要求填写邮箱,勾选匿名后邮箱可不填。
  • 登录用户可自由评论;若没有邮箱,不阻断但不接收通知。
  • 每个登录用户可自行配置是否接收评论邮件通知,默认开启。
  • 管理员配置好发件参数后,可执行“测试发件”验证可用性。

2. 范围与非目标

2.1 本期范围

  • 评论通知总开关与 SMTP 发件参数的全局配置。
  • 游客评论表单新增邮箱与匿名逻辑。
  • 用户通知偏好开关(用户级配置,默认开启)。
  • 评论触发通知的基础发送策略(仅通知被回复评论作者)。
  • 管理员测试发件接口与后台按钮。

2.2 非目标

  • 不实现复杂通知订阅中心(如多事件类型细粒度订阅)。
  • 不实现邮件模板系统化编辑器。
  • 不实现消息队列重试平台;本期采用轻量失败记录与跳过策略。
  • 不实现游客通知收件(游客仅作为评论发表者,不作为通知接收主体)。

3. 核心业务规则

3.1 游客评论规则

  • 默认 guestIsAnonymous = false
    • guestEmail 必填,且需通过邮箱格式校验。
  • guestIsAnonymous = true
    • guestEmail 允许为空。
  • 无论匿名与否,guestDisplayNamebody 仍按现有规则校验。

3.2 登录用户评论规则

  • 评论始终允许提交(不因邮箱缺失阻断)。
  • 用户无邮箱时展示提示:“未填写邮箱将无法接收评论通知”。
  • 用户关闭通知偏好时展示提示:“你已关闭评论邮件通知”。

3.3 通知触发与跳过规则

仅当以下条件全部满足时发送邮件:

  1. 全局 commentEmailNotifyEnabled = true
  2. SMTP 全局配置完整且通过基础合法性检查。
  3. 目标接收用户 commentNotifyEnabled = true
  4. 目标接收用户资料中存在有效邮箱。
  5. 事件存在有效通知对象(本期:被回复评论作者,且不是自己)。

任一条件不满足时直接跳过发送,不影响评论创建成功。

4. 数据模型与配置模型

4.1 全局配置新增键(global)

  • commentEmailNotifyEnabled: boolean,默认 false
  • commentMailFromEmail: string,默认 ""
  • commentSmtpHost: string,默认 ""
  • commentSmtpPort: number,默认 465
  • commentSmtpSecure: boolean,默认 true
  • commentSmtpUser: string,默认 ""
  • commentSmtpPass: string,默认 ""

校验约束:

  • commentSmtpPort:整数,范围 1-65535
  • commentMailFromEmail:为空允许;非空时必须邮箱格式合法。
  • 其余字符串允许空(空表示未配置完成)。

4.2 用户级配置新增键(both)

  • commentNotifyEnabled: boolean,默认 trueuserOverridable = true

说明:该项采用现有 global/user 覆盖机制,用户未设置时继承默认值 true

4.3 评论表扩展

post_comments 新增字段:

  • guest_emailtext,可空。
  • guest_is_anonymousboolean,非空,默认 false

服务层强制约束:

  • kind = "guest"guest_is_anonymous = false 时,guest_email 必填且格式合法。
  • kind = "guest"guest_is_anonymous = true 时,guest_email 可空。
  • kind = "user" 时忽略游客邮箱与匿名字段(统一写入空值/默认值)。

5. API 与服务设计

5.1 评论创建接口扩展

沿用现有两个评论 POST 入口:

  • POST /api/public/profile/:publicSlug/posts/:postSlug/comments
  • POST /api/public/unlisted/:publicSlug/:shareToken/comments

请求体扩展(仅对游客生效):

  • guestDisplayName: string
  • guestIsAnonymous?: boolean(默认 false
  • guestEmail?: string
  • body: string

服务端行为:

  • 根据会话判定 kind,不信任客户端自报。
  • 登录用户分支忽略 guestIsAnonymousguestEmail
  • 游客分支按规则校验并入库。

5.2 通知发送服务

新增评论通知服务入口(命名可在实现计划定稿),在评论创建成功后触发。

本期通知对象策略:

  • 仅在“回复评论”场景下,通知父评论对应的登录用户作者。
  • 顶层评论不触发广播通知。
  • 评论者与接收者相同则跳过(防止自通知)。

失败处理:

  • 发送失败只记录日志,不回滚评论创建事务结果。
  • 日志中包含 postId/commentId/receiverUserId/reason 便于追踪。

5.3 管理员测试发件接口

新增管理员接口(建议):

  • POST /api/config/global/comment-email-test

行为:

  • 要求管理员身份。
  • 读取当前全局邮件配置,若配置不完整返回 400
  • 向“当前管理员账号邮箱”发送测试邮件;管理员无邮箱则返回 400
  • 成功返回 200 与确认消息。

6. 前端交互设计

6.1 管理后台配置页

文件:app/pages/me/admin/config/index.vue(扩展现有页面)

新增“评论通知邮件”分组:

  • 通知总开关。
  • 发件人邮箱。
  • SMTP 主机、端口、安全连接、用户名、密码。
  • “发送测试邮件”按钮(仅在保存后可点,或点击时读取当前值直接测试)。

交互要求:

  • smtpPass 输入框掩码显示。
  • 空配置允许保存。
  • 当配置不完整时展示“未配置完成,无法发件”提示。

6.2 评论组件

文件:app/components/PostComments.vue(扩展游客表单)

游客态新增:

  • guestEmail 输入框(默认必填)。
  • 匿名评论 复选框(勾选后邮箱改为非必填)。

登录态新增:

  • 若用户邮箱为空,显示提示文案。
  • 若用户通知偏好关闭,显示提示文案。

6.3 用户设置页

在用户个人设置页增加:

  • 接收评论邮件通知 开关(默认开启)。

7. 错误处理与安全

  • 评论接口输入校验失败:返回 400(保持现有错误风格)。
  • 未登录访问管理员发件测试接口:401/403
  • 邮件配置不完整触发测试:400
  • 测试发件失败:返回 502 或统一错误码,并记录服务端日志。
  • 管理端展示配置时,smtpPass 不做明文回显(可回显占位符或保持空,提交时按“有改才覆盖”策略实现)。

8. 测试与验收

8.1 服务层/接口测试

  • 游客评论:
    • 非匿名 + 无邮箱 -> 400
    • 非匿名 + 合法邮箱 -> 200
    • 匿名 + 空邮箱 -> 200
  • 登录用户无邮箱评论 -> 200(不阻断)
  • 通知开关关闭 -> 不触发发送
  • 用户通知偏好关闭 -> 不触发发送
  • 管理员测试发件:
    • 配置不完整 -> 400
    • 管理员无邮箱 -> 400
    • 配置完整且邮箱存在 -> 200

8.2 前端手动验收

  • 管理员可保存评论邮件配置并看到状态提示。
  • 配置完成后测试发件按钮可成功触发。
  • 游客默认邮箱必填,勾选匿名后可不填邮箱发表评论。
  • 登录用户无邮箱时出现提示但仍可评论。
  • 用户可在设置页关闭通知并立即生效。

9. 实施顺序建议

  1. 扩展配置注册表与全局配置管理页。
  2. 扩展评论表结构与服务校验。
  3. 增加用户通知偏好配置与设置页开关。
  4. 接入通知发送服务与日志。
  5. 增加管理员测试发件接口与按钮。
  6. 完成测试与联调验收。

10. 数据库迁移回滚预案(Task4)

10.1 限制说明(SQLite)

  • SQLite 不支持通用 DROP COLUMN 语法,无法直接删除 post_comments.guest_emailpost_comments.guest_is_anonymous
  • 因此 Task4 回滚不能依赖“反向删列”语句,必须采用“建新表 + 搬迁 + 替换”的结构化回滚方案。

10.2 回滚执行前置条件

  • 仅在维护窗口执行,暂停写入评论相关流量,避免迁移过程中出现并发写导致数据不一致。
  • 执行前完成数据库备份(至少一份可恢复快照),并验证备份可用性后再开始回滚。

10.3 回滚步骤(可执行,针对 migration 0009_curly_jack_murdock.sql

说明:以下步骤用于撤销 Task4 新增列 guest_emailguest_is_anonymous,并恢复到 0002_post-comments.sql 对应的 post_comments 结构。

PRAGMA foreign_keys = OFF;
BEGIN TRANSACTION;

CREATE TABLE `post_comments_rollback` (
  `id` integer PRIMARY KEY NOT NULL,
  `post_id` integer NOT NULL,
  `parent_id` integer,
  `author_user_id` integer,
  `guest_display_name` text,
  `body` text NOT NULL,
  `kind` text NOT NULL,
  `deleted_at` integer,
  `deleted_by_user_id` integer,
  `created_at` integer DEFAULT (cast((julianday('now') - 2440587.5) * 86400000 as integer)) NOT NULL,
  `updated_at` integer DEFAULT (cast((julianday('now') - 2440587.5) * 86400000 as integer)) NOT NULL,
  FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON UPDATE no action ON DELETE cascade,
  FOREIGN KEY (`parent_id`) REFERENCES `post_comments_rollback` (`id`) ON UPDATE no action ON DELETE no action,
  FOREIGN KEY (`author_user_id`) REFERENCES `users` (`id`) ON UPDATE no action ON DELETE set null,
  FOREIGN KEY (`deleted_by_user_id`) REFERENCES `users` (`id`) ON UPDATE no action ON DELETE set null
);

INSERT INTO `post_comments_rollback` (
  `id`, `post_id`, `parent_id`, `author_user_id`, `guest_display_name`,
  `body`, `kind`, `deleted_at`, `deleted_by_user_id`, `created_at`, `updated_at`
)
SELECT
  `id`, `post_id`, `parent_id`, `author_user_id`, `guest_display_name`,
  `body`, `kind`, `deleted_at`, `deleted_by_user_id`, `created_at`, `updated_at`
FROM `post_comments`;

DROP TABLE `post_comments`;
ALTER TABLE `post_comments_rollback` RENAME TO `post_comments`;

CREATE INDEX `post_comments_post_id_idx` ON `post_comments` (`post_id`);
CREATE INDEX `post_comments_parent_id_idx` ON `post_comments` (`parent_id`);

COMMIT;
PRAGMA foreign_keys = ON;

执行后校验(至少包含以下检查):

  1. 行数一致:SELECT COUNT(*) FROM post_comments; 与回滚前快照对比。
  2. 结构一致:PRAGMA table_info('post_comments'); 中不应包含 guest_emailguest_is_anonymous
  3. 外键一致:PRAGMA foreign_key_check; 返回空集。
  4. 关键查询回归:按 post_idparent_id 的评论查询可正常命中索引并返回结果。

若任一步骤失败,停止后续操作并使用回滚前备份恢复,不做“半回滚”上线。

10.4 风险与注意事项

  • 回滚方案仅调整表结构,不应修改业务逻辑代码与已发布 migration 文件。
  • 若任一步骤异常,立即停止后续操作,使用回滚前备份恢复并重新评估。