[插件] 公告弹窗中心(notice_center)插件发布

👑Lv.11 元老 🌏 正式会员
2026-09-30 14:56:43

因为有全局显示弹窗,所以核心的:app\Views\layouts\main.php需要覆盖一下,下个版本会更新在核心文件里。

版本:1.0.0 | 作者:FlintHub 

功能:全站公告弹窗 —— 后台发一条公告,满足条件的访客进站时自动弹出。 支持受众定向(所有人 / 游客 / 登录用户 / 指定用户组)、生效时间窗、弹出频率控制、 页面范围限定、优先级排序、已读持久化(登录用户落库 / 游客落本地)。


一、功能简介

1.1 一条公告由什么决定「弹不弹」

四道筛选依次通过才弹(全部在服务端完成):

① 总开关开启(后台「启用」勾选)
② 公告本身 is_active = 1
③ 时间窗命中:start_at ≤ 现在 ≤ end_at(留空 = 不限制该端)
④ 受众命中:all / guest / member / groups(指定用户组)
⑤ 新用户条件:new_user_days > 0 时,仅注册未满 N 天的用户可见
⑥ 页面范围命中:all / home / forum / thread / custom(自定义路径前缀)

再叠加频率控制(freq,客户端判定):

freq 语义 已读存哪
once 只弹一次,之后永久不再弹 登录用户 → ntc_reads 表;游客 → localStorage
daily 每天弹一次 localStorage 记日期
session 每个会话弹一次 sessionStorage
always 每次访问都弹 不记录

1.2 只弹一条

Plugin::MAX_POPUP = 5 —— 服务端最多渲染 5 条候选到 DOM, 但前端过滤后只显示优先级最高的一条(决策 5:避免弹窗轰炸)。 多渲染几条是给「已读过滤后仍有备选」留余地。

1.3 弹窗外观

三种类型,只影响标题颜色:

type 语义 视觉
notice 普通公告 默认色
urgent 紧急通知 标题 --mn-error(红)
guide 新用户引导 标题 --mn-primary(主色)

弹窗结构:遮罩 + 对话框(标题 / 富文本正文 / 一个可选按钮)。 按钮文字留空则默认显示「知道了」,按钮链接留空则按钮不跳转、只关闭。


二、弹窗判定与渲染链路

访客请求页面
   │
   ├─ layout_head_end  ─→ Plugin::shouldInject($template)
   │                       ├─ 无候选(无 cache.json)→ return,连 CSS/JS 都不加载
   │                       └─ 有候选 → 输出 <link> + <script defer>
   │
   └─ layout_body_end  ─→ Plugin::renderPopup()
                           ├─ 复用同一次规则解析(请求内静态缓存,不重复计算)
                           └─ 输出 <div id="ntcRoot" hidden> + 每条公告 <article hidden>
                                   │
                             script.js(defer)接管
                                   ├─ 按 freq 过滤已读
                                   ├─ 取首条可弹的 → 移除 hidden,加 body.ntc-lock
                                   └─ 「弹出即已读」:立即写本地 + 登录用户 POST /notice/read

为什么 DOM 走服务端直出:弃用了「<template> + JS 克隆」方案 —— DOM 直接进 HTML 更简单、无闪烁,且不需要额外接口取数(用户 2026-09-30 拍板)。


三、路由与接口

3.1 前台(hook/route_register.php,路径写全)

方法 路径 说明 登录 CSRF
POST /notice/read 登录用户标记已读(幂等) ✅ 必需 ✅ 必需
  • 游客调用 → 401;CSRF 失败 → 403;id ≤ 0 → 400。
  • user_id 只从 session 取,不接受客户端传参 → 越权面归零。
  • 必须 POST:Service Worker 对同源 GET 非导航请求一律 Cache-First, GET 会被缓存吞掉,已读永远写不进库(规范 §11.5 第 35 条)。

3.2 后台(hook/admin_route_register.php,不写 /admin 前缀,实际路径自动带)

方法 实际路径 说明
GET /admin/notice-center 公告列表 + 总开关
GET /admin/notice-center/edit 新增表单
GET /admin/notice-center/edit/{id} 编辑表单
POST /admin/notice-center/save 保存(新增 / 编辑共用)
POST /admin/notice-center/delete 删除
POST /admin/notice-center/toggle 启用 / 停用单条
POST /admin/notice-center/sort 上移 / 下移(`dir=up
POST /admin/notice-center/settings 保存全局总开关

全部写操作均为 POST + 表单内 CSRF,走 PRG(302 → GET),提示文案存 $_SESSION 读后即焚。


四、后台可配项

设置项 键名 默认 范围 说明
总开关 notice_center_enabled '1'(开) '0' / '1' 关掉后前台连 CSS/JS 都不加载

公告级字段(每条公告单独设置):

字段 默认 范围 / 取值 说明
title — ≤ 200 字符,必填 空标题不落库
content — 富文本 存原文,渲染时走 Content::formatPostContent() 净化
type notice notice / urgent / guide 非法值归一为 notice
priority 0 -100 ~ 100 越大越靠前
btn_text 空 ≤ 40 字符 空则显示「知道了」
btn_url 空 白名单链接 javascript: 等被拒;站内路径自动补前导 /
audience all all / guest / member / groups 非法值归一为 all
group_ids 空 逗号分隔,如 1,3,5 仅 audience=groups 时生效
new_user_days 0 0 ~ 3650 0 = 不启用该条件
freq once once / daily / session / always 非法值归一为 once
path_scope all all / home / forum / thread / custom 非法值归一为 all
path_custom 空 路径前缀 仅 path_scope=custom 时生效
start_at / end_at 空 Y-m-d H:i:s 留空 = 不限制该端
is_active 1 0 / 1 单条启停

五、数据表

独立库:plugins/notice_center/data/notice_center.sqlite

表名 用途 关键结构
ntc_notices 公告主表(18 列) id 主键;is_active / priority / type / audience / freq / path_scope / 时间窗 / 按钮字段
ntc_reads 已读记录 复合主键 (notice_id, user_id) → INSERT OR IGNORE 天然幂等;read_at 记时间

索引(一律排在 CREATE TABLE 之后,规范 §10.3):

索引 列 用途
idx_ntc_reads_user ntc_reads(user_id) 按用户取已读集合
idx_ntc_notices_active ntc_notices(is_active, priority) 列表 / 候选集排序

候选集文件缓存:data/cache.json

  • 内容 = 「启用 + 时间窗内」的公告(与用户无关的公共数据),按 priority DESC, id DESC。
  • 结果是空时删掉该文件 → 前台 hasCandidates() 靠 is_file() 短路,整条链路零 SQLite 查询。
  • 写入用 .tmp + LOCK_EX + rename 原子替换,避免读到半截 JSON。
  • 格式版本 CACHE_VERSION = 1:改缓存结构必须 +1,旧缓存自动视为失效。

六、目录结构

plugins/notice_center/
├── plugin.json                   插件元信息(5 个 hooks / 2 个权限)
├── Plugin.php                    主类(894 行):建表 / 生命周期 / 规则引擎 / 缓存 / 渲染
├── AdminController.php           后台控制器(413 行):列表 / 表单 / 保存 / 删除 / 启停 / 排序 / 设置
├── FrontController.php           前台控制器(55 行):只做 POST /notice/read
├── hook/
│   ├── init_after.php            惰性建表兜底(命中戳文件后零查询)
│   ├── route_register.php        前台路由(POST /notice/read)
│   ├── admin_route_register.php  后台路由(8 条)
│   ├── layout_head_end.php       只输出 <link> + <script defer>,不输出 DOM
│   └── layout_body_end.php       输出弹窗 DOM
├── views/
│   ├── admin.php                 列表页(含总开关 + 排序按钮 + 已读列)
│   ├── edit.php                  新增 / 编辑表单(含右侧实时预览)
│   └── popup.php                 前台弹窗 markup 片段
├── assets/
│   ├── style.css                 前台弹窗样式(z-index 100010)
│   ├── script.js                 前台行为(过滤 / 弹出 / 已读上报 / 焦点管理)
│   ├── admin.css                 后台列表与预览样式
│   └── admin.js                  后台交互(删除确认 / 条件显隐 / 实时预览)
├── lang/{zh,en,zh_tw}.php        三语各 88 键(集合与键序完全一致)
└── data/
    ├── notice_center.sqlite      独立库
    ├── schema.version            建表戳(存内容,非判存在)
    └── cache.json                候选集缓存(可不存在 = 无候选)
最后由 flinthub 于 2026-09-30 14:56 编辑
轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| 浏览 12 次 | 回复 2 次

全部回复 (1)

💡Lv.10 顾问 🌏 正式会员
2026-09-30 15:02:00
你这是开挂了啊zhichi
知识,奉行,知行合一
#1 楼

请 登录