--- todos: - id: "ddl-mail-tables" content: "在 slot-notification 新增 mail_config(SMTP 直字段)/ mail_template / mail_send_log 表 DDL" status: pending - id: "mail-module" content: "实现 app/mail 模块:Model、Entity、Cache、MailService、SmtpMail SDK" status: pending - id: "notification-api" content: "新增 EmailController + MailConfigAdminController 与 route 注册" status: pending - id: "sdk-client" content: "扩展 slot-lib / slot-sdk 的 MailService 客户端方法" status: pending - id: "console-migrate" content: "slot-console CommonController 改调 notification;废弃本地 MailService" status: pending - id: "admin-ui" content: "slot-admin 后端 + slot-admin-vue 邮件配置管理页" status: pending - id: "data-migration" content: "编写 mail_config 从 slot-console 配置组到表的幂等迁移脚本" status: pending - id: "verify" content: "跑 verify-slot-backend.sh,验证发信/验码/后台 CRUD 链路" status: pending isProject: false --- # slot-notification 邮件配置表化(对齐短信) ## 现状与结论 当前两套通知渠道配置方式不一致: | 能力 | 短信(已收口) | SMTP 邮件(未收口) | |---|---|---| | 配置存储 | [`slot-notification/app/sms/model/SmsConfigModel.php`](slot-notification/app/sms/model/SmsConfigModel.php) → `sms_config` 表 | [`slot-console/app/service/lib/MailService.php`](slot-console/app/service/lib/MailService.php) → `ConfigService::getGroupData('mail_config')` | | 发送服务 | [`slot-notification/app/sms/services/SmsService.php`](slot-notification/app/sms/services/SmsService.php) | `slot-console` 本地 `MailService` + PHPMailer | | 验证码 API | `/innerapi/sms/sendVerifyCode` | `slot-console` `CommonController::sendEmailCode` | | 后台管理 | slot-admin → slot-lib → notification `/innerapi/admin/*` | slot-admin 通用配置页 `email_config`(另一套) | **结论:应该建表。** 你已选择「完整对齐短信」,建议把 SMTP 邮件作为 slot-notification 的第二条外发渠道,与短信保持同一套「配置表 + 发送日志 + innerapi + 后台管理 + SDK 客户端」模式。 注意:slot-notification 里现有 [`MailController`](slot-notification/app/innerapi/controller/MailController.php) / [`EmailService`](slot-notification/app/message/service/EmailService.php) 实际是**站内消息**兼容别名,不是 SMTP。新 SMTP 能力应使用独立命名空间(建议 `app/mail/*`)和独立路由前缀(建议 `/innerapi/email/*`),避免与 `/innerapi/mail/*`(站内消息)冲突。 ```mermaid flowchart LR subgraph before [当前] lobby1[Lobby] --> console1[slot-console CommonController] console1 --> config1["ConfigService mail_config"] console1 --> mail1[MailService PHPMailer] end subgraph after [目标] lobby2[Lobby] --> console2[slot-console 薄代理] console2 --> sdk[slot-lib MailService] sdk --> notif[slot-notification EmailController] notif --> table["mail_config 表"] notif --> smtp[SmtpMail PHPMailer] notif --> log["mail_send_log 表"] end ``` --- ## 表结构设计(邮件用直字段,短信保留 JSON) 在 `s_message`(与 `sms_config` 同库)新增两张表。 ### 为什么邮件不用 JSON、短信用 JSON? | 对比 | 短信 `sms_config.config` | 邮件 `mail_config` | |---|---|---| | 字段形态 | 各平台差异大(Buka 要 appId/senderId,Ant/Chuanglan 字段不同) | SMTP 标准字段固定,截图与代码一致 | | 后台表单 | 按 `platform` 动态展示不同表单项 | 固定 6~8 个输入框,与现有配置页一一对应 | | 查询/校验 | 结构不统一,适合 JSON | 可直接列级校验、索引、迁移映射 | **结论:邮件配置用直接字段更好;不必为了「和短信表长得像」而强行 JSON。** ### 当前邮件配置项(来自截图,仅 SMTP 连接) 截图中的 6 项即为 `mail_config` 配置组现有字段,与 [`MailService`](slot-console/app/service/lib/MailService.php) 读取项一致。**这就是邮件配置表应存的全部内容**: | 配置项 | 列名建议 | 说明 | |---|---|---| | smtp host | `smtp_host` | 如 `email-smtp.us-east-1.amazonaws.com` | | 用户名 | `username` | SMTP 登录账号 | | 密码 | `password` | SMTP 密码/应用专用密码 | | 端口 | `port` | 如 `587` | | 发件人地址 | `from_address` | 原 `from` | | 发件人名称 | `from_name` | 如 `TOGOO` | 加密方式(`tls`/`ssl`)**不单独落库**:发送时按 `port` 推断即可(`587` → STARTTLS,`465` → SSL),与现网行为一致。 ### 邮件模板(独立概念,不属于 mail_config) 验证码邮件的标题/正文来自 `email_template` 配置组(`title`、`binding_email`),属于**发送模板**,不是 SMTP 连接配置。 发送邮件时需要两张表配合: ```mermaid flowchart LR sendMail[MailService 发信] --> mailConfig["mail_config\nSMTP 连接"] sendMail --> mailTemplate["mail_template\n标题/正文模板"] ``` ### `mail_config`(仅 SMTP 连接,直字段) | 字段 | 类型 | 说明 | |---|---|---| | `id` | bigint PK | 主键 | | `platform` | varchar(32) | 渠道标识,如 `AwsSes`、`Zoho` | | `status` | tinyint | 1 启用 / 2 禁用 | | `num` | int | 排序,降序优先 | | `smtp_host` | varchar(255) | SMTP 主机 | | `username` | varchar(128) | SMTP 用户名 | | `password` | varchar(512) | SMTP 密码 | | `port` | int | SMTP 端口 | | `from_address` | varchar(255) | 发件人邮箱 | | `from_name` | varchar(128) | 发件人显示名 | | `create_time` / `update_time` | datetime | 审计字段 | ### `mail_template`(邮件发送模板,独立表) | 字段 | 类型 | 说明 | |---|---|---| | `id` | bigint PK | 主键 | | `template_code` | varchar(64) | 模板编码,如 `bindingEmail`、`authEmail` | | `template_name` | varchar(128) | 模板名称 | | `title` | varchar(255) | 邮件标题模板 | | `body` | text | 邮件正文模板,支持 `{code}` 等变量 | | `status` | tinyint | 1 启用 / 2 禁用 | | `create_time` / `update_time` | datetime | 审计字段 | 首期迁移:把 `email_template` 配置组中的 `title` + `binding_email` 迁入 `mail_template`(`template_code=authEmail` 或按业务拆分)。 **注意**:`message_template` 是站内消息模板,与 SMTP 外发邮件模板是不同业务,不混用。 ### `mail_send_log`(发送日志,放 `s_common`) 镜像 [`sms_send_log`](slot-notification/app/sms/model/SmsSendLogModel.php): - `unique_sn`, `email`, `platform`, `subject`, `content`, `status`, `type`, `retry_num`, `create_time` --- ## 后端实现(slot-notification) ### 1. 数据层 新建 `app/mail/` 模块,参照 `app/sms/`: - `MailConfigModel` / `MailSendLogModel` - `MailConfigEntity`(后台 CRUD,复用短信 Entity 分页模式) - 在 [`initDB.php`](slot-notification/app/command/initDB.php) 补充 DDL;另提供 `db/mail_config.sql` 便于手工执行 ### 2. 发送层 - `composer.json` 增加 `phpmailer/phpmailer` - `MailService`:从 `mail_config` 取 SMTP 连接,从 `mail_template` 取标题/正文 → PHPMailer 发信 → 写 `mail_send_log` - `app/mail/services/sdk/BaseMail.php` + `SmtpMail.php`(首期只实现 SMTP,结构与 `BaseSms` 一致,便于后续扩展 SendGrid 等) - `MailCache`:验证码 Redis 缓存(镜像 `SmsCache`),key 规则与 slot-console [`EmailCodeLibService`](slot-console/app/service/lib/EmailCodeLibService.php) 保持一致,避免迁移期验证码失效 ### 3. API 层 新增路由([`config/route.php`](slot-notification/config/route.php)): - `POST /innerapi/email/send-verify-code` - `POST /innerapi/email/verify-code` - `POST /innerapi/admin/mail-config/index|save|update|destroy` - `POST /innerapi/admin/mail-template/index|save|update|destroy`(模板 CRUD,与配置分开) 新建 `EmailController`(发送/校验)与 `MailConfigAdminController`(配置 CRUD),**不要**复用现有站内消息 `MailController`。 ### 4. 渠道选择策略 首期与短信一致:按 `status=启用` + `num desc` 取第一条;预留后续 failover(注释掉的短信轮询逻辑可一并参考)。 --- ## 跨服务改造 ### slot-lib / slot-sdk 在 [`slot-lib/src/services/notification/`](slot-lib/src/services/notification/) 新增 `MailService`(或 `EmailService`): - `sendVerifyCode(email, type)` - `verifyCode(email, code, type)` - `getMailConfigPageList` / `saveMailConfig` / `updateMailConfig` / `destroyMailConfig` 同步扩展 [`slot-sdk/src/service/notification/NotificationService.php`](slot-sdk/src/service/notification/NotificationService.php)。 ### slot-console(调用方改造) [`CommonController::sendEmailCode`](slot-console/app/api/controller/CommonController.php) / `verifyEmailCode`: - 改为调用 slot-lib `MailService`,不再直接依赖本地 `MailService` + `ConfigService` - 本地 [`MailService`](slot-console/app/service/lib/MailService.php) 标记废弃或删除(确认无其他引用后) 限流、参数校验仍留在 slot-console Controller 层(与现有短信 napi 模式一致)。 ### slot-admin + slot-admin-vue 镜像短信后台: - 后端:[`slot-admin/app/game/controller/SmsController.php`](slot-admin/app/game/controller/SmsController.php) → 新建 `MailController` - 前端:新建 `mail/config/` 页,仅 6 个 SMTP 字段 + platform/status/num;另建 `mail/template/` 页管理标题/正文模板 - 菜单/权限:新增 `mailConfig` 路由与按钮权限 --- ## 数据迁移 新增一次性迁移脚本(建议 `slot-notification/app/command/MigrateMailConfig.php`): 1. 从 `s_config` 读取 `mail_config` 配置组 → 写入 `mail_config` 表(6 个 SMTP 字段) 2. 从 `s_config` 读取 `email_template` 配置组 → 写入 `mail_template` 表(`title` + `binding_email`) 3. 幂等插入(已存在则跳过) 迁移完成后,`slot-console` 侧 `mail_config` / `email_template` 配置组可保留只读一段时间,确认无回退需求后再清理。 --- ## 风险与边界 - **命名冲突**:`/innerapi/mail/*` 继续保留给站内消息;SMTP 统一走 `/innerapi/email/*` - **验证码兼容**:`MailCache` 的 Redis key 必须与 slot-console 现有 `ShareRedisKeyManagerService::emailCode()` 一致 - **模板范围**:本期只迁移验证码邮件模板(`binding_email`/`title`);充值/提现等 MQ 站内邮件模板(`message_template` 配置组)不在本次范围 - **多 SMTP failover**:表结构支持,首期实现「取第一条启用配置」即可 --- ## 验收标准 1. slot-admin 可 CRUD `mail_config`,与短信配置页体验一致 2. Lobby 发/验邮箱验证码链路走 slot-notification,功能与迁移前一致 3. 发送成功/失败均写入 `mail_send_log` 4. slot-console 不再读取 `ConfigService::getGroupData('mail_config')` 5. 执行 `verify-slot-backend.sh` 通过