# 节点短信提醒方案 ## 一、目标 平台在关键业务节点主动给号主发短信,减少「审核被拒不知道」「有人下单没辅助登录导致订单被强制结束」「账号长期卖不出去」这三类损耗。 阿里云要求短信文案必须提前报备审核,**代码里不拼接文案、不存文案快照**:业务侧只决定「发给谁 + 用哪个模板 CODE」,文案完全由报备的模板决定。 ## 二、已接入节点 | 节点标识 | 场景 | 收件人 | 触发位置 | | --- | --- | --- | --- | | `listing_review_rejected` | 账号审核未通过,提醒登录网站修改后重新上架 | 号主 | `listing/review.go` `Reject()` | | `order_handoff_assist` | 订单支付成功进入待交接,提醒号主及时辅助登录 | 号主 | `order/lifecycle.go` `ConfirmPaidFromChannelTx()` 的非代管分支 | | `listing_stale_price_suggestion` | 账号上架满 N 天仍未被租用,建议调整比例 | 号主 | `jobs/listingstale` 定时扫描 | 模板 CODE(主站可在后台自行更换): - `listing_review_rejected`:`SMS_512520993` - `order_handoff_assist`:`SMS_512540945` - `listing_stale_price_suggestion`:`SMS_512620985` > 代管订单(`handoff_mode=platform`)由客服提交交接说明,不触达号主,因此不会产生 `order_handoff_assist` 短信。 ## 三、整体链路 ```mermaid flowchart LR A["业务事务内
sms.Enqueue()"] --> B["sms_messages
status=pending"] B --> C["jobs/smsdispatch
每分钟轮询"] C --> D{"总开关 / 节点开关
模板 CODE 是否就绪"} D -->|未就绪| E["status=skipped
记录 skip_reason"] D -->|就绪| F{"订单/商品是否属于分站"} F -->|分站已配该节点模板| G["分站通道 + 分站签名"] F -->|未配置| H["主站通道 + 主站签名"] G --> I["Provider.SendTemplate"] H --> I I --> J["sent / 重试 / failed"] ``` 设计要点: - **只入队不发送**:入队与业务数据在同一个事务里提交(与站内信 `notification.Append` 同点),短信通道异常不影响下单、审核等主流程,也不拖慢接口。 - **幂等去重**:`sms_messages` 上的唯一索引 `(node, biz_type, biz_id, user_id)` 保证同一业务对象同一节点只发一次;上架未成交提醒因此天然不会重复轰炸号主。 - **失败重试**:失败按 5 分钟退避重试,最多 3 次,用尽后置为 `failed` 并记录 `last_error`。 - **分站优先、主站回退**:订单/商品带 `distribution_site_id` 时先尝试分站独立短信;分站未启用、密钥解密失败或没配该节点模板,都回退主站通道与主站模板。 ## 四、主站配置 系统配置项 `sms.business_templates`(后台「系统配置 → 节点短信配置」弹窗维护): ```json { "enabled": false, "nodes": { "listing_review_rejected": { "enabled": true, "template_code": "" }, "order_handoff_assist": { "enabled": true, "template_code": "" }, "listing_stale_price_suggestion": { "enabled": true, "template_code": "", "stale_days": 3 } } } ``` - `enabled`:总开关,默认 `false`,避免未配置模板就产生短信费用。 - `nodes[].enabled`:单节点开关。 - `nodes[].template_code`:阿里云报备模板 CODE,**留空即该节点不发短信**。 - `nodes[].stale_days`:仅上架未成交提醒使用,默认 3 天。 - 主站发送使用的签名沿用环境变量 `ALIYUN_SMS_SIGN_NAME`,凭证沿用 `ALIYUN_ACCESS_KEY_ID/SECRET`。 ## 五、分站配置 分销站点短信配置(`distribution_sites.sms_config_json`,「分销站点 → 短信配置」弹窗)新增 `business_templates` 字段: ```json { "enabled": true, "provider": "aliyun", "access_key_id": "LTAI...", "access_key_secret": "(加密存储)", "sign_name": "【分站签名】", "login_template_code": "SMS_xxx", "business_templates": { "order_handoff_assist": "SMS_YYYY" } } ``` - 站点启用独立短信后,`business_templates` 里配置了模板的节点走**分站凭证 + 分站签名 + 分站模板**,短信费用计入分站额度。 - 未配置的节点自动回退主站通道与主站模板。 - 提交时该字段为整体覆盖语义;传空对象即清空,全部回退主站。 - 服务端只保留已登记节点且模板非空的配置项,写错节点名不会静默生效。 ### 凭据轮换 站点 Provider 按「站点 ID + 凭据指纹」缓存,指纹取自 provider / AccessKeyID / 密钥密文 / endpoint / 签名 / 登录模板码。后台改了任一项(尤其是更换 AccessKey)后指纹变化,**运行中的任务会自动重建 Provider**,不需要重启进程;API 侧保存配置时也会主动清一次缓存。 ## 六、数据库 迁移 `000088_sms_messages.sql` 新增 `sms_messages` 队列表: - `status`:`pending` / `sent` / `failed` / `skipped` - `skip_reason`:因总开关关闭、节点停用、模板缺失、通道不可用等原因未发送 - `retry_count` / `last_error` / `sent_at` / `template_code` / `sign_name`:排障用快照 - 唯一索引 `uk_sms_messages_node_biz_user`:节点级幂等 - 索引 `idx_sms_messages_status_id`:派发任务扫描待发送 - 索引 `idx_sms_messages_site_id`:按分站排查 排查方式:直接查 `sms_messages`(`status='skipped'` 看 `skip_reason`,`status='failed'` 看 `last_error`)。 ## 七、上架未成交提醒的扫描口径 `jobs/listingstale` 每小时扫描一次,命中条件: - `rental_listings.status = 'published'` 且 `in_transaction = 0`(在架且未成交) - `published_at` 落在 `[now - stale_days - 24h, now - stale_days]` 窗口内 - `sms_messages` 中不存在同节点的该账号记录(`NOT EXISTS` 排除已提醒) 只处理「刚跨过阈值」的账号,是为了避免功能首次上线时把历史上所有长期滞销账号一次性全部触发,造成短信轰炸与费用失控。配合唯一索引,每个账号最多提醒一次。 单轮最多处理 200 个账号(`defaultBatchSize`)。这里的 `NOT EXISTS` 不只是省一次插入:候选窗口有 24 小时、任务每小时跑一次,如果不排除已提醒账号,窗口内候选超过 200 个时每次都会查回同一批「已提醒」的账号并占满批次名额,排在后面的账号会在 24 小时窗口结束后被永久漏发。 ## 八、测试范围 - 配置解析:默认值、未知节点过滤、模板 CODE 去空格、负数天数归零、`NodeActive` 判定 - 队列:手机号自动补齐、重复入队幂等、状态流转、重试退避 - 派发:主站通道、分站优先、分站未配模板回退主站、分站解析失败回退主站、模板缺失跳过、节点停用跳过、重试用尽置 failed - 业务接入:审核拒绝给号主入队(含分站归属)、支付成功给号主入队、代管订单不入队 - 定时扫描:只命中刚跨阈值的在架未成交账号、重复执行不重复入队、阈值可配置 - 回归用例:候选数超过批次上限时不会漏发后续账号;站点凭据轮换后自动重建 Provider