其实就是模仿微博和说说的那种方式,拿去玩吧。
短文字 + 图片动态流。全部自建,零插件依赖(不读不写任何其他插件的数据表,也不调用其他插件的类方法)。
一、它能干什么
| 功能 | 说明 |
|---|---|
| 发布 | 纯文字 / 文字 + 图片(最多 9 张,可在后台调);表情 shortcode( 等)自动还原 |
| 流 | 三个 tab:最新 / 热门(按点赞数,不加权)/ 关注 |
| 我的 | /suiyu/mine |
| 个人页 | /suiyu/u/{id}(含关注按钮、关注数/粉丝数) |
| 互动 | 点赞(可取消)、评论(含楼中楼)、转发(可附一句,repost_of 指向最初原创,禁止套娃) |
| 关注 | 自建 sy_follows,不碰 user_profile 的 user_follows |
| 搜索 | 自建 LIKE 搜索(%/_ 已转义 + ESCAPE '\'),支持「只搜我的」;不接入核心全站搜索 |
| 审核 | 敏感词库(拦截 / 替换 两种动作) |
| 举报 | 同一人同一帖只报一次(UNIQUE(post_id,user_id) + INSERT OR IGNORE);后台可「删碎语」或「忽略」 |
| 通知 | 点赞 / 评论 / 关注 → 走核心 Notification(type = suiyu_like / suiyu_comment / suiyu_follow) |
| 后台 | 碎语列表 / 回收站 / 敏感词库 / 举报处理 / 设置 |
v1.0.0 明确不做:@提及(@用户名)、碎语进全站搜索、热门加权排序、核心个人主页上的关注按钮。
二、入口
| 端 | 位置 | 实现 |
|---|---|---|
| PC | 左侧栏「应用」面板 | hook/nav_plugin_links.php |
| 移动端 | 顶栏「应用」下拉 | 同一个钩子、同一份输出(双触发) |
| 全站右栏 | 「碎语广场」面板(最近 5 条) | hook/right_sidebar_after.php |
移动端不是第三个触发点:
.mn-mobile-icon-apps与.mn-apps-dropdown是同一份 DOM, 靠@media (min-width:769px){ .mn-nav-right .mn-mobile-icon-apps{display:none} }切换显隐。⚠️ 右栏在
@media (max-width:1024px)起整块display:none→ 面板只是辅助入口, 真正的导航入口是「应用」。
三、目录结构
plugins/suiyu/
├── plugin.json # name=suiyu · admin_url=/admin/suiyu
│ # permissions=["route:admin","system:settings"] ← ★ 缺一不可
├── Plugin.php # 建表 + 全部业务静态方法(前台/后台/探针共用)
├── FrontController.php # 前台 13 条路由
├── AdminController.php # 后台 13 条路由(extends app\Controllers\Admin\BaseController)
├── hook/
│ ├── init_after.php # 惰性建表兜底(门控 = md5(ddl()) **内容哈希**,不是「文件是否存在」)
│ ├── route_register.php # 前台路由(**写全路径**)
│ ├── admin_route_register.php # 后台路由(**不写 /admin**)
│ ├── layout_head_end.php # 前台注入 style.css(全站)+ script.js(仅本插件页)
│ ├── nav_plugin_links.php # ★ 唯一导航入口(双触发 · 禁顶层符号)
│ └── right_sidebar_after.php # 右栏「碎语广场」面板
├── views/
│ ├── stream.php # 流页(全站 / 我的 / 个人页 共用)
│ ├── _list.php # htmx 翻页片段(⚠️ 不带 <link>/<script>)
│ ├── _card.php # 单条卡片(流 / 片段 / 搜索 共用)
│ ├── _comment.php # 单条评论(详情页 + 评论 XHR 回包 共用)
│ ├── show.php # 详情 + 评论
│ ├── search.php # 搜索页
│ ├── _panel.php # 右栏面板
│ └── admin/{_nav,index,trash,words,reports,settings}.php
├── assets/{style.css,script.js} # 零内联,全部外链
├── lang/{zh,zh_tw,en}.php # 143 键 × 3 语,键集合与键序零差异
└── data/suiyu.sqlite # 独立库(.htaccess / web.config 由 Plugin::db 自动补)
四、数据表(6 张,全部在 plugins/suiyu/data/suiyu.sqlite)
| 表 | 用途 | 关键约束 |
|---|---|---|
sy_posts |
碎语 | status:0 正常 / 1 回收站;repost_of 指向最初原创 |
sy_comments |
评论 | reply_to 跨帖会被归零(防挂载到别的帖子下) |
sy_likes |
点赞明细 | UNIQUE(post_id, user_id) |
sy_follows |
关注 | UNIQUE(follower_id, followee_id) |
sy_blocked_words |
敏感词 | word UNIQUE;action:block / replace |
sy_reports |
举报 | UNIQUE(post_id, user_id) → 同一人同一帖只能报一次 |
计数一致性(红线)
like_count / comment_count / repost_count 与明细行永远在同一个事务里改;
取消点赞时先看 rowCount() > 0 才 -1,并统一用 MAX(x-1, 0) 兜底防负。
图片删除策略
| 操作 | 图片文件 |
|---|---|
| 软删(前台删除 / 后台「删除」) | 保留(可恢复) |
| 彻底删除(后台「彻底删除」) | 一并删除(Upload::delete()) |
五、路由
前台(route_register,写全路径)
| 方法 | 路径 | 响应 |
|---|---|---|
| GET | /suiyu |
页面(?tab=new|hot|follow&page=N) |
| GET | /suiyu/mine |
页面 |
| GET | /suiyu/u/{id} |
页面 |
| GET | /suiyu/post/{id} |
页面 |
| GET | /suiyu/search |
页面(?q=&mine=1) |
| GET | /suiyu/list |
htmx 片段(viewRaw) |
| POST | /suiyu/post |
PRG |
| POST | /suiyu/post/{id}/delete |
PRG |
| POST | /suiyu/repost |
PRG |
| POST | /suiyu/like |
XHR → JSON |
| POST | /suiyu/comment |
XHR → JSON(回包含渲染好的评论 HTML) |
| POST | /suiyu/follow |
XHR → JSON |
| POST | /suiyu/report |
XHR → JSON |
后台(admin_route_register,不写 /admin)
/admin/suiyu · /admin/suiyu/trash · /admin/suiyu/words · /admin/suiyu/reports · /admin/suiyu/settings
(另有 delete/{id}、restore/{id}、purge/{id}、words/{add,edit,delete}、reports/{id}/handle)
六、后台设置项
| 设置键 | 默认 | 范围 | 说明 |
|---|---|---|---|
suiyu_per_page |
15 | 1–50 | 前台列表每页条数 |
suiyu_max_len |
300 | 10–2000 | 单条碎语 / 评论最大字数 |
suiyu_max_img |
9 | 0–9 | 单条最多图片数(0 = 关闭图片) |
suiyu_like_points |
0 | 0–100 | 每被点赞一次作者获得的积分(0 = 不奖励) |
suiyu_report_enabled |
1 | 0/1 | 是否显示举报入口 |
⚠️ 保存走
Settings::update(),需要plugin.json声明system:settings权限。 缺声明时表现为「提示保存成功、回读 DB 值没变」(只写error.log后静默 return)。⚠️ 首次启用后请到后台「维护 → 清理缓存」清一次设置缓存 ——
settings_cache.php不会自动补新键,旧站点读不到这 5 个新键(后台不显示、config.php读不到)。
七、几个刻意的设计取舍
nav_plugin_links用字面量图标<i class="fa mn-fs-16"></i>,不用$view->icon()—— 核心TemplateCompiler::icon()的返回值恒带style="font-size:Npx"内联样式, 违反插件零内联红线(plugin_hygiene.py的口径是「任意style属性都算」)。右栏面板「无数据不渲染」:一条碎语都没有时直接
return,不输出空占位卡。 想改成「空态也显示引导」,把hook/right_sidebar_after.php里的if (empty($syItems)) { return; }去掉即可(一行)。后台表格列序刻意避开核心的按列序硬编码 (
assets/css/admin.css:180给td:nth-child(4)限宽省略号、:182-189给1/3/5/6/7列加nowrap)。 本插件列表统一用ID | 内容 | 作者 | 互动 | 状态 | 时间 | 操作—— 长文本落在第 2 列(不受限), 短文本落在受限列 → 零特异性战争、零!important。缩略图两档:
thumb_(400×300,列表九宫格)+md_(800×600,详情页)。 ⚠️ 一律走Upload::thumbUrl($rel, $prefix),禁止手拼'thumb_' . $rel——makeThumbnail()把前缀加在 basename 上,而makeRelPath()返回ns/YYYY-MM/<hex>.ext(namespace 是前导目录),手拼必 404。图片灯箱靠
data-fullurl:assets/js/lightbox.js:45只收集[data-fullurl], .post-text-content img。 碎语图不在.post-text-content里 → 不写data-fullurl就点图毫无反应(完全静默)。评论走 XHR 而非整页刷新:
/suiyu/comment返回 JSON,其中html字段是服务端渲染好的评论片段 (复用views/_comment.php)→ 前端就地插入,楼层/回复关系与刷新后一致。加载更多用 htmx(
hx-get="/suiyu/list"+hx-swap="outerHTML"), 因此views/_list.php绝不能出现<link>/<script>(否则每点一次 DOM 就多一个节点,视觉无感但会累积)。
八、卸载
后台「插件」页点卸载 → 调 Plugin::uninstall(),DROP 掉本插件独立库里的 6 张表,
不触碰核心库、不删除其他插件数据。上传的图片文件不会被自动删除(如需清理请手动处理 assets/uploads/)。
