mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-11-08 03:50:50 +08:00
docs(notice): 站内信与通知机制技能文档及变更日志
This commit is contained in:
@@ -65,6 +65,7 @@ POST 到 `system.config/save`,控制器逻辑位于 `extend/base/admin/control
|
||||
extend/base/admin/view/system/config/site.html ← 网站设置
|
||||
extend/base/admin/view/system/config/logo.html ← LOGO 配置
|
||||
extend/base/admin/view/system/config/upload.html ← 上传配置
|
||||
extend/base/admin/view/system/config/notify.html ← 通知设置
|
||||
```
|
||||
|
||||
### 2.2 index.html 结构(Tab 容器)
|
||||
@@ -75,12 +76,14 @@ POST 到 `system.config/save`,控制器逻辑位于 `extend/base/admin/control
|
||||
<li class="layui-this">网站设置</li>
|
||||
<li>LOGO 配置</li>
|
||||
<li>上传配置</li>
|
||||
<li>通知设置</li>
|
||||
</ul>
|
||||
<div class="layui-tab-content">
|
||||
<div class="layui-tab-item layui-show">
|
||||
{include file="system/config/site" /}
|
||||
</div>
|
||||
<!-- 更多 Tab -->
|
||||
<!-- 更多 Tab:每个 Tab 一个 <div class="layui-tab-item">,
|
||||
通知设置 Tab include 的是 system/config/notify -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
@@ -266,7 +269,7 @@ ThinkPHP 模板解析顺序(先找到先使用):
|
||||
- `extend/base/helper.php`(`sysconfig()` 定义,第 133 行)
|
||||
- `extend/base/admin/controller/system/ConfigBase.php`(配置控制器)
|
||||
- `extend/base/admin/service/TriggerServiceBase.php`(`updateSysconfig()` 清缓存,第 45 行)
|
||||
- `extend/base/admin/view/system/config/`(框架默认视图:index/site/logo/upload)
|
||||
- `extend/base/admin/view/system/config/`(框架默认视图:index/site/logo/upload/notify)
|
||||
- `app/admin/model/SystemConfig.php`(配置模型)
|
||||
|
||||
## 相关规则与技能
|
||||
|
||||
167
.agents/skills/ulthon-notice/SKILL.md
Normal file
167
.agents/skills/ulthon-notice/SKILL.md
Normal file
@@ -0,0 +1,167 @@
|
||||
---
|
||||
name: "ulthon-notice"
|
||||
description: "站内信与通知系统规范(NoticeService 统一发送、场景注册表、订阅矩阵、四渠道驱动、timer 异步投递)。需要给管理员发通知/消息提醒、新增通知场景、定制订阅、扩展渠道驱动(邮件/短信/公众号)、排查通知投递失败或未读数异常,或使用通知设置(sysconfig notify 组)配置 SMTP 时调用。"
|
||||
---
|
||||
|
||||
# notice(站内信与通知系统)
|
||||
|
||||
统一入口是 `NoticeService::send()`,业务代码一律 use `app\common\service\NoticeService`(App 层入口,双层铁律)。内核实现见 `extend/base/common/service/NoticeServiceBase.php`。
|
||||
|
||||
## 何时调用
|
||||
|
||||
- 业务流程需要给管理员发站内信/邮件通知(如审批结果、预警、公告)。
|
||||
- 新增通知场景(scene)、调整场景可用渠道、设置强投递(locked)。
|
||||
- 排查通知未投递、未读数不对、邮件发不出去、投递行卡在待发送。
|
||||
- 本地联调邮件链路(Mailpit),或对接短信/公众号渠道。
|
||||
|
||||
## 发通知 API 三种姿态
|
||||
|
||||
```php
|
||||
use app\common\service\NoticeService;
|
||||
|
||||
// 签名(NoticeServiceBase.php:87)
|
||||
// send(array|int|string $adminIds, string $title, string $content, array $options = []): int
|
||||
// $adminIds 支持数组、单个 ID 或 'all'(status=1 全体);返回 notice_id
|
||||
```
|
||||
|
||||
三种姿态与结果:
|
||||
|
||||
| 姿态 | 示例 | 渠道结果 | 订阅过滤 |
|
||||
|------|------|---------|---------|
|
||||
| 不传 scene(缺省 common) | `NoticeService::send(1, '标题', '内容')` | common 注册渠道 [inmail, email] | 生效 |
|
||||
| 传注册 scene | `NoticeService::send('all', '库存预警', '...', ['scene' => 'mall_stock'])` | 场景注册渠道,可再传 `channels` 收窄(∩ 交集) | 生效(locked 豁免) |
|
||||
| 传未注册 scene | `NoticeService::send(1, '标题', '内容', ['scene' => 'my_scene'])` | 缺省 [inmail],记 warning 放行 | 生效(但无法在页面退订) |
|
||||
|
||||
options 还支持 `biz_id`(业务关联串)、`send_admin_id`(发送者,缺省 0)。
|
||||
|
||||
## 渠道解析规则
|
||||
|
||||
`send()` 内两段解析(NoticeServiceBase.php:97-109):
|
||||
|
||||
1. 第一段:`options.channels` ?? 注册场景的 `scene.channels` ?? `[inmail]`
|
||||
2. 第二段:注册场景再与 `scene.channels` 取交集(保序去重);locked 强投递同样仅限场景注册渠道,不豁免交集
|
||||
3. 未注册场景跳过交集,缺省 `[inmail]`,记 `Log::warning` 放行
|
||||
|
||||
额外过滤(buildTargets):
|
||||
|
||||
- email 渠道要求 `system_admin.email` 非空,空则跳过该渠道并记 info(地址来源就是管理员表的 email 字段)
|
||||
- inmail 全局总闸:`sysconfig('notify','notify_inmail_enable',1)`,`(int)` 强转后为 0 则不生成 inmail 行
|
||||
- sysconfig 陷阱:DB 读出的 value 是字符串,`(int)` 强转后再比较,`'0'===0` 永假
|
||||
- sms/wechat 的 target_addr 当前无地址解析逻辑(骨架驱动不外呼,见渠道章节)
|
||||
|
||||
## 场景注册表与 locked
|
||||
|
||||
场景是代码级注册表(`scenes()`,不建数据库表),内置四个:
|
||||
|
||||
| scene | 标题 | channels | locked |
|
||||
|-------|------|----------|--------|
|
||||
| system | 系统公告 | [inmail] | 是 |
|
||||
| common | 通用通知 | [inmail, email] | 否 |
|
||||
| admin_login | 登录提醒 | [inmail, email] | 否 |
|
||||
| mall_stock | 库存预警 | [inmail, email] | 否 |
|
||||
|
||||
扩展新场景:app 层入口类覆盖合并,不要改 extend/base:
|
||||
|
||||
```php
|
||||
public static function scenes(): array
|
||||
{
|
||||
return array_merge(parent::scenes(), [
|
||||
'my_scene' => ['title' => '我的场景', 'channels' => [self::CHANNEL_INMAIL], 'locked' => false],
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
locked 语义:订阅设置页恒选中且置灰(必选),发送侧豁免订阅过滤(强投递);但渠道交集规则不豁免。场景拼写错误没有报错,只有 warning 日志,且不进订阅页与场景总览,排障先查拼写。
|
||||
|
||||
## 订阅矩阵语义与过滤规则
|
||||
|
||||
- 存储:`system_notice_subscribe`(admin_id x scene x channel,UNIQUE,status 0/1)
|
||||
- 三态判定(`isSubscribed()`):locked 场景恒订阅;表无记录视为默认订阅;仅 status=0(显式退订)过滤掉该渠道
|
||||
- 个人设置页 `/admin/notice/subscribe` 渲染矩阵(行=场景,列=站内信/邮箱);保存 POST `/admin/notice/saveSubscribe`,参数 `matrix[scene][channel]=1/0`
|
||||
- locked 行提交与否服务端都忽略(不可关闭语义);layui 只序列化勾选的开关,subscribe.js 在提交前把未勾选的补齐为 0
|
||||
|
||||
## 四渠道说明
|
||||
|
||||
| 渠道 | 驱动 | 投递方式 |
|
||||
|------|------|---------|
|
||||
| inmail | InMailChannel | 投递即落库:send() 内直接置 status=2 成功终态,无外部依赖 |
|
||||
| email | EmailChannel | 异步:行置 status=0,timer 消费者经 PHPMailer SMTP 真实投递 |
|
||||
| sms | SmsChannel | 骨架:读开关、记日志、恒成功,不外呼 |
|
||||
| wechat | WechatChannel | 骨架:同上 |
|
||||
|
||||
email 配置(后台"系统配置 > 通知设置",sysconfig notify 组):`notify_email_enable` 仅 '1' 视为启用(缺省关),`notify_smtp_host/port/secure/user/pass/from/from_name`。secure 映射:ssl 隐式 SMTPS、tls 走 STARTTLS、none 明文(驱动会显式关 SMTPAutoTLS,否则 Mailpit 等纯明文服务被自动升级误判);user 留空则不启用 SMTP 认证;异常消息即 PHPMailer ErrorInfo,落 target.error_message(截 500 字符)。
|
||||
|
||||
骨架对接指引(驱动方法体顶部 TODO 已写):
|
||||
|
||||
- sms:用既有 guzzlehttp/guzzle 调短信服务商 API,读 `notify_sms_sign/appid/secret`,target_addr 需自行解析为手机号;服务商失败码抛 RuntimeException 交消费者退避
|
||||
- wechat:access_token 管理(appid/secret,7200s,需集中缓存防刷)+ 模板消息接口,target_addr 为 openid,errcode 非 0 抛 RuntimeException
|
||||
|
||||
## timer 消费与退避排障
|
||||
|
||||
注册在 `extend/base/common/command/timer/config.php`:
|
||||
|
||||
- `notice_consumer`:`/tools/timer.NoticeConsumer/do`,frequency=10,concurrency=2,消费 status=0 且 `next_retry_time<=now` 的行
|
||||
- `notice_reclaim`:`/tools/timer.NoticeReclaim/do`,frequency=60,concurrency=1,回收 status=1 且 `claim_time < now-600s` 的卡死行(重置回待发送)
|
||||
|
||||
消费模式即 [ulthon-queue](../ulthon-queue/SKILL.md) 模式 A:`(status,id)` 复合索引 `idx_status_id`,每批 20,原子抢占(claim_token),成功回写 status=2(WHERE 含 claim_token + status=1 幂等兜底)。
|
||||
|
||||
失败退避:`next_retry_time = now + 60 * 2^retry_count`(retry_count 为自增后值,即 120/240/480/960 秒),第 5 次尝试失败置 status=3 终态(error_message 保留最后错误)。
|
||||
|
||||
排障路径:
|
||||
|
||||
1. 每轮执行结果查 `system_timer_log` 表(task_name 列定位 notice_consumer/notice_reclaim)
|
||||
2. 手动触发:`php think tools:http:call --url="/tools/timer.NoticeConsumer/do"`
|
||||
3. 日志位置:timer 进程(CLI)走 `runtime/log/`,web 请求走 `runtime/admin/log/YYYYMMDD.log`
|
||||
4. 新增/调整 timer 任务后跑一次 `php think timer --local --temp` 同步 system_timer_config,常驻 timer 进程无需重启
|
||||
|
||||
红线清单:
|
||||
|
||||
- QA 造数改 retry_count 时必须同步 next_retry_time(候选查询按它过滤,漏了行永远不被消费);该列 int unsigned,上限 4294967295
|
||||
- 失败行是终态不自动删除,重发走管理页 `/admin/system.notice/resend`(仅 status=3 可重置)
|
||||
- 后台手动触发重发后靠消费者下一轮拾取,interval 最长 frequency=10 秒
|
||||
|
||||
## target 表清理扩展思路
|
||||
|
||||
`system_notice_target` 每通知 x 每人 x 每渠道一行,持续增长。内置不提供清理,扩展思路(未实现,按需自建):
|
||||
|
||||
- 清理条件建议:已读站内信 + 超期时间,或 notice 主体已软删的连带行(管理页 delete 软删主体,投递行保留不级联)
|
||||
- 分批删除防长事务锁表,循环直到 affected=0:
|
||||
|
||||
```sql
|
||||
DELETE FROM ul_system_notice_target
|
||||
WHERE read_time > 0
|
||||
AND create_time < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 180 DAY))
|
||||
LIMIT 1000;
|
||||
```
|
||||
|
||||
- 落点形态:参照 NoticeReclaimBase 做一个低频 timer 任务(concurrency=1,run_type=auto 单节点跑),或用迁移脚本一次性清理
|
||||
|
||||
## Mailpit 本地联调方法
|
||||
|
||||
Mailpit 是本地 SMTP 捕获容器,收到的邮件不真发出,Web/API 可查:
|
||||
|
||||
```bash
|
||||
# 1. 启动(1025 SMTP / 8025 Web+API),端口可按需换,避免与本机占用冲突
|
||||
docker run -d --name mailpit-notice-qa -p 1025:1025 -p 8025:8025 axllent/mailpit
|
||||
|
||||
# 2. 后台"系统配置 > 通知设置"保存:notify_email_enable=1、
|
||||
# notify_smtp_host=127.0.0.1、notify_smtp_port=1025、notify_smtp_secure=none、
|
||||
# notify_smtp_user 留空(无认证)、notify_smtp_from 填合法地址(如 qa@example.com)
|
||||
|
||||
# 3. 发一条 email 渠道通知(send 是 PHP API 无 HTTP 端点),并手动触发消费
|
||||
php think tools:http:call --url="/tools/timer.NoticeConsumer/do"
|
||||
|
||||
# 4. API 断言收信(Web 界面 http://127.0.0.1:8025 可视化查看)
|
||||
curl -s http://127.0.0.1:8025/api/v1/messages
|
||||
|
||||
# 5. 用完即删,不留常驻
|
||||
docker rm -f mailpit-notice-qa
|
||||
```
|
||||
|
||||
红线:`notify_smtp_from` 缺省值 notice@localhost 在 PHP 8.2 下被 filter_var 判非法(裸域无 TLD),不配置合法地址邮件永远发送失败;`notify_smtp_secure` 必须为 none,否则明文会话被 TLS 升级拒绝。
|
||||
|
||||
## 页面入口与响应形态
|
||||
|
||||
- 个人域(auth=false,登录即可,强制 admin_id=当前会话):`/admin/notice/mine` `/read` `/markRead` `/markAllRead` `/unreadCount` `/subscribe` `/saveSubscribe`。URL 动作段保持 camelCase 原样,不做 snake 转换
|
||||
- 管理页:`/admin/system.notice/index` `/read`(投递明细+失败重发入口)`/delete`(软删)`/modify`(白名单为空,全拒绝)`/resend` `/sceneOverview`(只读场景总览)
|
||||
- 响应形态:success 包装 code=0,业务数据在 data 键,如 unreadCount 返回 `{"code":0,"msg":null,"data":{"count":N}}`;前端判断一律 `res.code === 0`
|
||||
@@ -12,6 +12,8 @@
|
||||
- 自定义规则索引(.agents/rules/project-*.md 清单)
|
||||
- 框架更新策略(默认对比更新,无需写) -->
|
||||
|
||||
本项目当前以 `source/stack/docker-dev` 栈运行:容器 `docker-dev-ulthon_admin-1`(HTTP 端口 8000),MySQL 走容器网络主机名 `mysql`(`docker-dev-mysql-1`),Redis 同栈。php think / phpunit / composer 一律在容器内执行:`docker exec docker-dev-ulthon_admin-1 sh -c "..."`;宿主机 PHP 版本不满足 composer platform 要求,git 等纯文件操作在宿主机仓库根执行即可。
|
||||
|
||||
---
|
||||
|
||||
## 代码分层铁律(Base/App 双层架构)
|
||||
@@ -35,6 +37,7 @@
|
||||
- 调试与验证优先框架内置命令行工具(见下方快速命令),不借助外部数据库 MCP 或临时脚本
|
||||
- 项目运行方式未在上方说明时,执行命令前先按 [ulthon-deploy-environment.md](./.agents/rules/ulthon-deploy-environment.md) 判断环境(如根目录有 Docker 编排文件则勿直接宿主机执行 `php think`)
|
||||
- 在线文档:<https://doc.ulthon.com/read/augushong/ulthon_admin/home/zh-cn/2.x.html>
|
||||
- 通知/站内信:统一走 `NoticeService::send()`,场景注册/订阅矩阵/渠道驱动见 [ulthon-notice](./.agents/skills/ulthon-notice/SKILL.md)
|
||||
|
||||
## 规则与技能
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
### 新增
|
||||
|
||||
- **站内信/通知系统**:通知三表(notice/target/subscribe)与管理员 email 字段,`NoticeService::send()` 统一发送入口(代码级场景注册表、订阅矩阵过滤、locked 强投递)。站内信投递即落库;邮件走 PHPMailer SMTP 由 timer 异步消费(指数退避重试、超时回收);短信/公众号为骨架驱动待对接。后台配套:顶栏未读铃铛与轮询、我的消息、订阅设置、通知管理(投递明细/失败重发/场景总览)、系统配置"通知设置"Tab。规范见 [.agents/skills/ulthon-notice/SKILL.md](./.agents/skills/ulthon-notice/SKILL.md)。
|
||||
- **B-route 多模式运行**:每个运行模式(default/docker-serve/docker-dev/docker-dev-sync/full/author)独立目录,各自直接运行。业务代码始终一套,在仓库根目录。
|
||||
- default 模式:根目录直接 `php think run`
|
||||
- Docker 模式:`cd source/stack/<mode> && docker compose up -d`
|
||||
|
||||
Reference in New Issue
Block a user