Xiuno BBS 消息通知系统
1. 概述
Xiuno BBS 的站内通知由两套独立系统合并构成,在前端统一展示:
| 维度 | notice(消息通知) | notify(互动通知) |
|---|---|---|
| 数据表 | bbs_notice |
bbs_notify |
| 用途 | 管理员/系统主动发送的消息 | 用户互动自动触发(点赞、收藏、关注、回复等) |
| 类型字段 | 整数:1=公告、2=评论、3=系统、99=其他 | 字符串:like、favorite、follow、reply、thread、forum_post |
| 管理后台 | 有(发送、列表、删除) | 无 |
| 计数器 | user.notices + user.unread_notices(反范式) |
动态统计(无冗余字段) |
| 模型文件 | model/notice.func.php |
model/notify.func.php |
核心特性:
- 两套系统在顶部导航栏合并显示未读徽章和下拉菜单
- 通知列表页按类型分标签页,
interact标签页合并展示互动通知 - 单条/全部标记已读(AJAX,无页面刷新)
- 查看详情时自动标记已读再跳转
- 管理后台发送和管理通知
- 插件可通过模型函数集成
2. 数据库设计
2.1 bbs_notice 表
sql
CREATE TABLE IF NOT EXISTS `bbs_notice` (
`nid` int(11) unsigned NOT NULL auto_increment,
`fromuid` int(11) unsigned NOT NULL default '0',
`recvuid` int(11) unsigned NOT NULL default '0',
`create_date` int(11) unsigned NOT NULL default '0',
`isread` tinyint(3) unsigned NOT NULL default '0',
`is_read` tinyint(1) unsigned NOT NULL default '0',
`type` tinyint(3) unsigned NOT NULL default '0',
`message` longtext NOT NULL,
PRIMARY KEY (`nid`),
KEY (`fromuid`, `type`),
KEY (`recvuid`, `type`),
KEY (`recvuid`, `is_read`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
| 字段 | 类型 | 说明 |
|---|---|---|
| nid | int(11) unsigned AI | 主键 |
| fromuid | int(11) unsigned | 发送者用户ID |
| recvuid | int(11) unsigned | 接收者用户ID |
| create_date | int(11) unsigned | 创建时间(Unix 时间戳) |
| isread | tinyint(3) unsigned | 是否已读(旧字段,兼容保留) |
| is_read | tinyint(1) unsigned | 是否已读(新字段,推荐使用) |
| type | tinyint(3) unsigned | 类型:0=全部、1=公告、2=评论、3=系统、99=其他 |
| message | longtext | 消息内容(支持 HTML) |
2.2 bbs_notify 表
| 字段 | 类型 | 说明 |
|---|---|---|
| nid | int AI | 主键 |
| uid | int | 接收者用户ID |
| from_uid | int | 触发者用户ID |
| type | varchar | 类型:like、favorite、follow、reply、thread、forum_post |
| tid | int | 关联帖子ID |
| pid | int | 关联回帖ID |
| content | text | 内容摘要 |
| create_date | int | 创建时间(Unix 时间戳) |
| is_read | tinyint | 是否已读 |
2.3 bbs_user 表扩展字段
| 字段 | 类型 | 说明 |
|---|---|---|
| notices | mediumint(8) unsigned | 总通知数(反范式计数器,notice 系统专用) |
| unread_notices | mediumint(8) unsigned | 未读通知数(反范式计数器,notice 系统专用) |
2.4 is_read 与 isread 兼容
bbs_notice 表同时维护 isread 和 is_read 两个字段。所有写入操作同时更新两个字段,读取时优先使用 is_read。notice_format() 函数统一输出 is_read 键。
2.5 自动初始化
表和字段的首次创建由 route/my.php 自动完成(无需手动建表):
- 检测
bbs_notice表是否存在,不存在则创建 - 检测
bbs_user表的notices和unread_notices字段是否存在,不存在则添加
3. 数据模型 API
3.1 notice 模型(model/notice.func.php)
模型在 model.inc.php 中无条件加载,所有页面均可调用。
基础 CRUD(双下划线前缀)
| 函数 | 签名 | 说明 |
|---|---|---|
notice__create |
`(array $arr): int | FALSE` |
notice__update |
(int $nid, array $arr): bool |
按 nid 更新 |
notice__read |
`(int $nid): array | NULL` |
notice__delete |
(int $nid): bool |
按 nid 删除 |
notice__find |
(array $cond, array $orderby, int $page, int $pagesize): array |
底层分页查询 |
业务逻辑函数
| 函数 | 签名 | 说明 |
|---|---|---|
notice_send |
`(int fromuid, int recvuid, string message, int type=99): int | FALSE` |
notice_find_by_recvuid |
(int $recvuid, int $page, int $pagesize, int $type): array |
按接收者查询(已格式化) |
notice_update |
(int $nid, array $arr=NULL): bool |
标记单条已读,递减 unread_notices |
notice_update_by_recvuid |
(int $recvuid, array $arr=NULL): bool |
全部标记已读,置 unread_notices=0 |
notice_delete |
(int $nid): bool |
删除单条,更新计数器 |
notice_delete_by_recvuid |
(int $recvuid): bool |
清空用户所有通知,重置计数器 |
notice_find |
(array $cond, int $page, int $pagesize): array |
分页查询(已格式化) |
notice_find_latest_by_recvuid |
(int $recvuid, int $pagesize=5): array |
获取最新 N 条(下拉菜单用) |
notice_count |
(array $cond): int |
按条件统计总数 |
notice_count_unread |
(int $recvuid): int |
统计未读数(优先用 is_read) |
notice_send() 详解
php
$nid = notice_send($fromuid, $recvuid, $message, $type);
$fromuid和$recvuid必须大于 0 且不相等,否则返回FALSE$type传 0 会自动转为 99- 成功后自动递增
user.notices和user.unread_notices
notice_format() 格式化
为通知记录补充展示信息(原地修改引用参数):
create_date_fmt--- 人类可读时间from_username/from_user_avatar_url--- 发送者信息recv_username/recv_user_avatar_url--- 接收者信息name/class/icon--- 来自global $notice_menu的类型信息- 统一
is_read和isread键
注意 :该函数依赖 global $notice_menu,在路由层需提前声明。若未定义,函数内有默认值兜底。
3.2 notify 模型(model/notify.func.php)
| 函数 | 签名 | 说明 |
|---|---|---|
notify_create |
(int $uid, int $from_uid, string $type, int $tid=0, int $pid=0, string $content=''): mixed |
创建互动通知(自己互动自己不创建) |
notify_read |
(int $nid): array |
读取单条(已格式化) |
notify_find_by_uid |
(int $uid, int $page, int $pagesize): array |
按接收者查询(已格式化) |
notify_count_unread |
(int $uid): int |
统计未读数 |
notify_mark_read |
(int $nid): mixed |
标记单条已读 |
notify_mark_all_read |
(int $uid): bool |
标记全部已读(直接 SQL) |
notify_delete_by_uid |
(int $uid): mixed |
删除用户所有通知 |
notify_delete_by_tid |
(int $tid): mixed |
删除帖子关联的所有通知 |
notify_format |
(array &$notify): void |
格式化:补充用户名、头像、URL、类型标签、消息摘要 |
notify_format() 类型映射
| type 值 | type_label 语言键 | summary 语言键 | 消息模板 |
|---|---|---|---|
like |
notify_type_label_like |
notify_summary_like |
{用户} 赞了你的回帖 |
reply |
notify_type_label_reply |
notify_summary_reply |
{用户} 回复了你的评论 |
follow |
notify_type_label_follow |
notify_summary_follow |
{用户} 关注了你 |
favorite |
notify_type_label_favorite |
notify_summary_favorite |
{用户} 收藏了你的帖子 |
thread |
notify_type_label_thread |
notify_summary_thread |
{用户} 发了新帖: {内容} |
forum_post |
notify_type_label_forum_post |
notify_summary_forum_post |
{用户} 版块有新帖: {内容} |
4. HTTP 路由
4.1 路由注册
php
// index.inc.php(前端)
case 'notice': include _include(APP_PATH.'route/notice.php'); break;
case 'my': include _include(APP_PATH.'route/my.php'); break;
4.2 notice 路由(route/notice.php)
| 端点 | 方法 | 权限 | 说明 |
|---|---|---|---|
notice-mark_read |
POST | 登录用户 | 标记已读(单条/全部) |
notice-unread_count |
GET | 登录用户 | 获取未读数(纯文本) |
notice-dropdown |
GET | 登录用户 | 下拉菜单通知列表(HTML 片段) |
notice-create |
GET/POST | 管理员 | 发送通知 |
notice-list |
GET | 管理员 | 通知列表 |
notice-delete |
POST | 管理员 | 删除通知 |
POST notice-mark_read
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| csrf_token | string | 是 | CSRF 令牌 |
| notice_id | int | 否 | 通知ID(标记单条时必填) |
| all | int | 否 | 传 1 标记全部已读 |
响应:
json
{"code": 0, "message": "操作成功", "unread_count": 3}
4.3 my 路由中的通知相关端点(route/my.php)
| 端点 | 方法 | 说明 |
|---|---|---|
my-notice |
GET | 通知列表页(合并 notice + notify) |
my-notice-interact |
GET | 互动通知标签页 |
my-notice-{type} |
GET | 按类型筛选(1/3/99) |
my-notice |
POST | 通知操作(readall/readone/delete) |
my-notify_unread |
GET | 合并未读徽章 HTML |
my-notify_dropdown |
GET | 合并下拉菜单 HTML |
my-notify_mark_read |
POST | 同时标记两个系统全部已读 |
my-notify_read-{nid} |
POST | 标记单条 notify 已读 |
GET my-notice 通知列表页
URL 模式:
?my-notice.htm--- 全部通知?my-notice-interact.htm--- 互动通知(notify 来源)?my-notice-{type}.htm--- 按类型筛选(1=公告、3=系统、99=其他)?my-notice-{type}-{page}.htm--- 分页
类型菜单 ($notice_menu):
| key | 名称 | 说明 |
|---|---|---|
| 0 | 全部 | 默认标签页 |
| interact | 互动 | 合并展示 notify 系统的互动通知 |
| 1 | 公告 | notice type=1 |
| 3 | 系统 | notice type=3 |
| 99 | 其他 | notice type=99 |
数据合并逻辑:
type=0(全部):同时查询 notify 和 notice,按时间倒序合并type=interact(互动):只查询 notify 系统type=数字(公告/系统/其他):只查询 notice 系统对应类型
每条合并后的通知包含 source 字段(notify 或 notice),用于区分来源和路由标记已读请求。
POST my-notice 通知操作
| act 值 | 参数 | 说明 |
|---|---|---|
| readall | uid | 标记两个系统全部已读 |
| readone | nid | 标记单条 notice 已读 |
| delete | nid | 删除单条 notice |
POST my-notify_mark_read
同时标记两个系统全部已读,返回合并后的未读数:
json
{"code": 0, "message": "操作成功", "unread_count": 0}
POST my-notify_read-{nid}
标记单条 notify 已读。URL 格式:?my-notify_read-{nid}.htm
json
{"code": 0, "message": "操作成功", "unread_count": 2}
4.4 管理后台端点
所有后台端点需要管理员权限($gid == 1)。
GET/POST notice-create --- 发送通知
POST 参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| csrf_token | string | 是 | CSRF 令牌 |
| message | string | 是 | 通知内容 |
| recvuid | int | 是 | 接收者 UID |
通知类型固定为 1(公告)。不能给自己发通知。
5. 前端集成
5.1 模板文件
| 文件 | 用途 |
|---|---|
view/htm/my_notice.htm |
用户通知列表页(含标签页、标记已读、查看详情) |
view/htm/header_nav.inc.htm |
顶部导航栏铃铛、下拉菜单、未读徽章 |
5.2 通知列表页(my_notice.htm)
页面结构
css
┌─────────────────────────────────────────┐
│ 消息 [全部标为已读] │
├─────────────────────────────────────────┤
│ 全部 | 互动 | 公告 | 系统 | 其他 │ ← 标签页
├─────────────────────────────────────────┤
│ ┌───────────────────────────────────┐ │
│ │ 🔔 公告 3分钟前 │ │ ← 类型 + 时间
│ │ 系统通知摘要 │ │ ← 摘要(未读蓝色)
│ │ 通知详情内容... [标为已读] [详情] │ │ ← 详情 + 操作按钮
│ └───────────────────────────────────┘ │
│ ┌───────────────────────────────────┐ │
│ │ ❤ 赞了你 5分钟前 │ │
│ │ 有人赞了你的回帖 │ │
│ │ 用户A 赞了你的回帖 [详情] │ │ ← 已读无"标为已读"按钮
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
CSS 样式类
| 选择器 | 说明 |
|---|---|
.notice-card |
通知卡片(圆角边框) |
.notice-card.notice-unread |
未读通知:浅主色背景 + 3px 左侧蓝色竖条 |
.notice-card .notice-summary |
摘要标题(未读时蓝色) |
.notice-mark-read |
"标为已读"按钮 |
.notice-view-detail |
"查看详情"按钮 |
图标映射
模板中 _notice_icon($source, $type) 函数根据来源和类型返回 Tabler Icons 图标名:
| source | type | 图标 |
|---|---|---|
| notify | like | heart-filled |
| notify | reply | message |
| notify | follow | user-plus |
| notify | favorite | star-filled |
| notify | thread | file-text |
| notify | forum_post | news |
| notice | 1 | speakerphone |
| notice | 2 | message |
| notice | 3 | file-text |
| 其他 | - | bell |
5.3 JavaScript 交互
通知页面使用原生 fetch() API,不依赖 jQuery 或 XN 对象。所有交互逻辑集中在 my_notice.htm 的 <script> 中。
单条标为已读(.notice-mark-read)
点击后根据 data-source 属性路由到不同端点:
source=notify→ POSTmy-notify_read-{nid}.htmsource=notice→ POSTnotice-mark_read(带notice_id参数)
成功后:
- 移除卡片的
notice-unread样式 - 移除"标为已读"按钮
- 更新顶部未读徽章
- 显示 Toast 提示
javascript
// 请求示例
fetch('?my-notify_read-123.htm', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest'},
body: 'csrf_token=xxx'
});
全部标为已读(#notice-mark-all-read)
POST 到 my-notice,参数 act=readall,同时标记两个系统全部已读。
成功后:
- 移除所有卡片的
notice-unread样式 - 移除所有"标为已读"按钮
- 移除"全部标为已读"按钮本身
- 清空顶部未读徽章
查看详情(.notice-view-detail)
- 已读状态:直接跳转到
data-url - 未读状态:先 AJAX 标记已读,完成后跳转
5.4 顶部导航栏集成
header_nav.inc.htm 中的通知铃铛合并了两个系统的未读数:
php
$notify_unread = notify_count_unread($uid);
$notice_unread_count = notice_count_unread($uid);
$total_unread = $notify_unread + $notice_unread_count;
下拉菜单
展开时 AJAX 加载 my-notify_dropdown,返回两个系统合并后的最新 5 条通知 HTML。每条通知项包含 data-nid 和 data-source 属性,点击时自动标记已读。
下拉菜单中的"标为全部已读"
POST 到 my-notify_mark_read,同时标记两个系统全部已读。
未读徽章轮询
通过定时请求 my-notify_unread 更新铃铛上的红点徽章。
6. 管理后台
6.1 菜单配置
在 admin/menu.conf.php 中注册:
php
'notice' => array(
'url' => url('notice-list'),
'text' => lang('notice'),
'icon' => 'icon-bell',
'tab' => array(
'list' => array('url' => url('notice-list'), 'text' => lang('notice_admin_notice_list')),
'post' => array('url' => url('notice-create'), 'text' => lang('notice_admin_send_notice')),
)
),
6.2 管理模板
| 文件 | 用途 |
|---|---|
admin/view/htm/admin_notice_create.htm |
发送通知表单 |
admin/view/htm/admin_notice_list.htm |
通知列表页 |
admin/view/htm/admin_notice_list.inc.htm |
通知列表项模板 |
6.3 发送通知流程
- 管理员访问
?notice-create.htm - 填写消息内容和接收者 UID
- 提交表单,后端调用
notice_send($uid, $recvuid, $message, 1) - 通知类型固定为
1(公告) - 成功后返回操作成功提示
7. 插件集成指南
7.1 发送通知
model/notice.func.php 已作为核心模型无条件加载,插件可直接调用:
php
// 发送通知
$nid = notice_send($from_uid, $to_uid, '您有一条新的消息', 99);
if ($nid !== FALSE) {
message(0, '通知发送成功');
} else {
message(-1, '通知发送失败');
}
7.2 发送互动通知
php
// 创建互动通知(如点赞)
notify_create($post_uid, $current_uid, 'like', $tid, $pid);
7.3 读取未读数
php
// notice 系统未读数
$notice_unread = notice_count_unread($uid);
// notify 系统未读数
$notify_unread = notify_count_unread($uid);
// 合并未读数
$total = $notice_unread + $notify_unread;
7.4 标记已读
php
// notice 单条已读
notice_update($nid);
// notice 全部已读
notice_update_by_recvuid($uid);
// notify 单条已读
notify_mark_read($nid);
// notify 全部已读
notify_mark_all_read($uid);
7.5 插件 Hook 点
| Hook 位置 | 文件 | 说明 |
|---|---|---|
my_nav_notice_before.htm |
my_notice.htm |
通知标签页列表前 |
my_nav_notice_after.htm |
my_notice.htm |
通知标签页列表后 |
my_common_start.htm |
my_notice.htm |
通知页面顶部 |
my_common_end.htm |
my_notice.htm |
通知页面底部 |
my_notice_js.htm |
my_notice.htm |
通知 JS 代码区后 |
7.6 防御性调用
如需兼容旧版,可用 function_exists 检查:
php
if (function_exists('notice_send')) {
notice_send($from, $to, $msg, 99);
}
新版中通知模型已作为核心功能始终加载,此检查非必须。
8. 数据流图
scss
┌──────────────┐ ┌──────────────┐
│ 用户互动 │ │ 管理员发送 │
│ (点赞/回复等) │ │ (admin POST) │
└──────┬───────┘ └──────┬───────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ notify_create│ │ notice_send │
│ │ │ user__update │
└──────┬───────┘ │ (计数器++) │
│ └──────┬───────┘
▼ ▼
┌──────────────┐ ┌──────────────┐
│ bbs_notify │ │ bbs_notice │
└──────┬───────┘ └──────┬───────┘
│ │
└────────┬────────┘
│
┌───────────┼───────────┐
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────────┐
│通知列表 │ │顶部铃铛 │ │下拉菜单 │
│my-notice│ │徽章红点 │ │最新5条合并 │
│ │ │ │ │按时间排序 │
│ 合并展示 │ │合并未读数│ │ │
└────────┘ └────────┘ └────────────┘
9. 语言键
通知相关语言键定义在 lang/zh-cn/bbs_common.php。
notice 系统语言键
| 键名 | 中文 | 使用场景 |
|---|---|---|
notice |
消息 | 页面标题、菜单 |
notice_my_error |
操作错误 | 通用错误 |
notice_my_update_failed |
操作失败 | 标记已读失败 |
notice_my_update_sucessfully |
操作成功 | 操作成功 |
notice_my_update_readed |
已标为已读 | 单条标记已读 |
notice_my_update_allread |
已全部标为已读 | 全部标记已读 |
notice_my_nomessage |
没有相关消息 | 空状态提示 |
notice_admin_send_notice |
发送通知 | 后台发送标题 |
notice_admin_send_notice_message_empty |
内容不能为空 | 验证错误 |
notice_admin_send_notice_recvuid_empty |
接收人不能为空 | 验证错误 |
notice_admin_send_notice_user_empty |
用户不存在 | 验证错误 |
notice_admin_send_notice_self |
不能给自己发通知 | 验证错误 |
notify 系统语言键
| 键名 | 中文 | 使用场景 |
|---|---|---|
notify_type_label_like |
赞 | 点赞类型标签 |
notify_type_label_reply |
回复 | 回复类型标签 |
notify_type_label_follow |
关注 | 关注类型标签 |
notify_type_label_favorite |
收藏 | 收藏类型标签 |
notify_type_label_thread |
新帖 | 新帖类型标签 |
notify_type_label_forum_post |
版块帖子 | 版块帖子类型标签 |
notify_summary_like |
有人赞了你 | 点赞摘要 |
notify_summary_reply |
有人回复了你 | 回复摘要 |
notify_summary_follow |
有人关注了你 | 关注摘要 |
notify_summary_favorite |
有人收藏了帖子 | 收藏摘要 |
notify_summary_thread |
有人发了新帖 | 新帖摘要 |
notify_summary_forum_post |
版块有新帖 | 版块帖子摘要 |
notify_btn_mark_read |
标为已读 | 标记已读按钮 |
notify_btn_view_detail |
查看详情 | 查看详情按钮 |
mark_all_read |
全部标为已读 | 全部标记按钮 |
通用语言键
| 键名 | 中文 | 说明 |
|---|---|---|
operate_successfully |
操作成功 | 通用成功提示 |
parameters_error |
参数错误 | 参数校验失败 |
insufficient_privilege |
权限不足 | 非管理员访问管理接口 |
please_login |
请先登录 | 未登录用户访问 |
not_exists |
不存在 | 记录未找到 |
10. 文件清单
| 文件路径 | 角色 |
|---|---|
model/notice.func.php |
notice 数据模型:CRUD + 业务逻辑 + 格式化 |
model/notify.func.php |
notify 数据模型:CRUD + 格式化 |
model.inc.php |
模型加载器(无条件加载两个模型) |
index.inc.php |
前端路由注册(notice case) |
admin/index.inc.php |
后台路由注册(notice case) |
route/notice.php |
notice 路由:mark_read、unread_count、dropdown、create、list、delete |
route/my.php |
my 路由:通知列表页、合并接口、数据库自动初始化 |
view/htm/my_notice.htm |
前端通知列表模板 + JS |
view/htm/header_nav.inc.htm |
顶部导航栏铃铛、下拉菜单、徽章 |
admin/view/htm/admin_notice_create.htm |
后台发送通知表单 |
admin/view/htm/admin_notice_list.htm |
后台通知列表 |
admin/view/htm/admin_notice_list.inc.htm |
后台通知列表项模板 |
admin/menu.conf.php |
后台菜单配置 |
lang/zh-cn/bbs_common.php |
中文语言包 |
lang/en-us/bbs_common.php |
英文语言包 |
11. 常见问题
Q: 通知列表页点击"标为已读"报错?
确保 my_notice.htm 中有 .notice-mark-read 按钮的点击事件处理。该按钮的 AJAX 请求根据 data-source 属性路由到不同端点:
source=notify→my-notify_read-{nid}.htmsource=notice→notice-mark_read
Q: 顶部铃铛未读数不准确?
未读数由两个系统合并计算。如果 user.unread_notices 反范式计数器与实际不一致,可通过后台执行 SQL 修复:
sql
UPDATE bbs_user u SET unread_notices = (
SELECT COUNT(*) FROM bbs_notice WHERE recvuid = u.uid AND is_read = 0
);
Q: 如何新增通知类型?
- 在
route/my.php的$notice_menu数组中添加新类型 - 在
notice_format()的默认$notice_menu中同步添加 - 在
my_notice.htm的_notice_icon()函数中添加图标映射 - 添加对应的语言键
Q: 通知系统的 CSRF 保护?
所有 POST 端点都需要 csrf_token 参数,通过 CsrfService::check() 验证。前端从 <meta name="csrf-token"> 获取令牌。