[开发]点赞送分插件(like_points)说明

👑Lv.11 元老 🌞 骨干会员
2026-10-25 01:31:46

本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:WWW/plugins/like_points/说明.md。

点赞送分插件(like_points)说明

点赞时把点赞人的积分转移给被点赞者(零和转移,不凭空造分)。

模式二选一(后台「模式」):

模式行为
纯点赞(不转移)点赞只是点赞,插件完全不动作
积分转移点赞人扣分、被赞人得分;可设前台可选档位集合(≥2 档时点赞在按钮旁弹小浮层让用户选)与每日送分次数上限

积分不足自动降级为纯点赞,点赞本身永不被拦截。


一、安装 / 激活

  1. 将 plugins/like_points/ 放入插件目录(本仓库已就位)。
  2. 手动更新 plugins/plugins_cache.json,加入 like_points 条目(含 4 个 hooks)——否则钩子不生效。

> 注:核心 Plugin::loadPlugins() 在缓存过期(任一 plugin.json 比缓存新)时会自动全量重建并写回,所以通常无需手工介入;此处手工接入是为了让改动可审计。

  1. 后台启用插件(或确认 activated: true)。
  2. 首次请求时 init_after 钩子自动建表(protected/.like_points_table_cache 版本号门控)。

后台入口:插件管理 → 点赞送分,或直接访问 /admin/like-points。


二、设置项

配置键(`like_points_` 前缀)默认说明
enabled1总开关。关闭后点赞不再转移积分
mode''(未设置→推断)模式:off 纯点赞 / transfer 积分转移。唯一决定「要不要转移」的开关
choices空前台可选档位集合(逗号分隔)。≥2 项 → 前台弹浮层;=1 项 → 直接用该档(不弹);含 0 → 用户可主动选「本次不送分」。后台 checkbox 预设 + 自定义输入合并
daily_limit10每人每天最多为多少次点赞送分(0 = 不限)。语义是次数,不是积分值
~~points_per_like~~—⚠️ v1.2.0 起停用。旧「默认档位」,不再参与任何运行时决策;保留旧值只为 mode() 推断老配置(见下)

2.1 模式推断(老站点升级兼容)

mode 键不存在时(v1.1.0 及以前的站点),Plugin::mode() 按旧语义推断,保证升级后行为不变:

points_per_like > 0  或  choices 非空   →  transfer(积分转移)
否则                                    →  off(纯点赞)

在后台保存过一次设置后就会写入明确的 'off' / 'transfer',此后不再推断。

本机现状:points_per_like = 0、choices = 1,2,5,10 → 推断为 transfer,与升级前行为一致。

2.2 为什么去掉「默认档位」

v1.1.0 的「默认档位」与「可选档位集合」关系是隐式的(默认档只在档位 <2 个时生效),容易误解。v1.2.0 改为:

  • 「要不要转移」→ 由模式显式决定
  • 「转多少」→ 只由 choices 决定(≥2 弹浮层由用户选;=1 直接用;其余 → 0 不转移)

三、⚠️ 必须同时处理:核心「获赞奖励积分」

核心在 app/Controllers/ApiController.php:93-99 里已经有一份「被点赞奖励积分」逻辑,设置键 points_vote_received(后台 → 系统设置 → Tab② 积分设置),默认 1。

它的行为是:凭空给作者加分(不扣点赞者),仅在「首次变赞」时发放。

而 vote_after 钩子是在这段逻辑之后才触发的 —— 所以积分转移模式下如果不清零:

作者会同时拿到「核心凭空送的 1 分」+「点赞人转过来的 N 分」= 双份,其中 1 分是凭空造的。

后台在「转移模式 且 points_vote_received > 0」时显示红色警告,并提供「一键设为 0」按钮。

(纯点赞模式下核心这份奖励是它自己的正常行为,与本插件无关 → 不提示。)


四、数据模型(plugins/like_points/data/like_points.sqlite)

lp_log
  id          INTEGER PRIMARY KEY
  user_id     INTEGER   点赞人
  owner_id    INTEGER   被赞人
  target_type TEXT      'thread' | 'post'
  target_id   INTEGER   主题/回复 id
  points      INTEGER   本次实际转移的积分
  created_at  TEXT
  UNIQUE(user_id, target_type, target_id)   ← 幂等的唯一依据
  INDEX (user_id, created_at)               ← 每日上限统计

一行 = 一次「已处理的点赞」。档位为 0(或模式为 off)时不落任何数据。


五、关键机制

5.1 幂等(为什么必须自建表)

vote_after 在「首次点赞 / 重复点赞 / 取消赞」时都会触发。核心的 award_like 标志只对「凭空发分」那一份有效,插件拿不到它 → 只能自建唯一索引回答「这一次点赞是否已处理过」。

做法:INSERT OR IGNORE + 读 rowCount()(1 = 抢到,0 = 已处理)。

⚠️ 不能在 INSERT OR IGNORE 后无条件读 lastInsertId() —— 被忽略时它会返回上一次插入的 rowid,不可信。

5.2 原子性(跨库的现实取舍)

积分在核心库(data/meta/business.sqlite),幂等锁在插件库 —— 跨库无法原子。

顺序刻意选为「先占位,再转移」:

① 插件库:INSERT OR IGNORE lp_log   (抢到幂等锁)
② 核心库:BEGIN → deduct(点赞人) → award(被赞人) → COMMIT
③ 任一步失败 → 回滚 + 删除占位行(允许以后重试)

反过来(先转移后占位)在崩溃时会「积分转了但没留痕」→ 用户取消赞再点就能重复送分(可刷分)。当前顺序的最坏情况只是「占位在、积分没转」→ 该次点赞以后不再送分(少送,不多送)。

5.3 短路顺序(v1.2.0)

总开关关闭 → 模式 off → 档位解析 → 档位 0 → 命中每日上限 → 幂等已处理 → 余额不足 → 转移

每一档不满足都只跳过送分,绝不抛异常影响点赞本身。

「模式」与「档位 0」是两道独立短路:前者是全局开关,后者是本次点赞的选择。

5.4 档位解析(v1.2.0)

服务端 Plugin::resolveChoice($raw) 是唯一决策点:

输入结果
前端参数在 choices 白名单里用该值
前端参数不在白名单(篡改 / 过期 / 非法)回落
choices 恰好 1 项用该唯一档
其余(无参数 / choices 为空)0 → 本次不转移

⚠️ 前端传值一律不可信 —— 浮层只是 UI,真正决定档位的是服务端白名单。

⚠️ 参数解析用 ctype_digit() 严格判定,而不是 (int) 强转 —— 否则 'abc' 会被转成 0,在「0 也是合法档位」时被误当成「用户选了 0 档」。

5.5 前台浮层(v1.2.0)

注入条件(hook/layout_head_end.php):$template === 'thread/show' 且 插件已启用 且 mode === 'transfer' 且 choices ≥ 2 项。任一不满足 → 完全不注入 CSS/JS。

形态:贴着点赞按钮的小浮层(position:fixed + 按钮的 getBoundingClientRect() 算坐标),没有全屏遮罩。

位置规则做法
水平与按钮居中对齐
垂直优先按钮上方(间距 8px);上方放不下 → 翻到下方
越界夹进视口(距边缘 ≥8px),窄屏也不会被推出屏幕

交互流程(核心零改动,全靠 htmx 的两个事件):

点击「赞」 → htmx:confirm(拦下,preventDefault) → 弹浮层(挂 document.body)
          → 用户点档位 → htmx:configRequest(注入 like_points_choice) → 请求发出
          → ApiController::vote() → Vote::toggle() → vote_after → 插件读 $_POST

细节做法原因
拦下请求htmx:confirm + preventDefault(),稍后自己调 detail.issueRequest(true)htmx v2 对每个请求无条件触发该事件
注入参数htmx:configRequest 改 detail.parameters该事件在 confirm 放行之后、请求组装之前触发
事件委托挂 document,判据 closest('.vote-btn')点赞按钮会被 htmx 以 outerHTML 换掉
浮层挂载document.body挂按钮附近会被 htmx 的 swap 一起清掉
关闭点浮层外 / ESC / 滚动 / 改窗口大小 → 放弃本次点赞(不发请求)浮层贴按钮,按钮一移就错位
外部点击监听延后一帧(setTimeout 0)才挂触发 htmx:confirm 的那次点击还在冒泡,同步挂上会被它立刻关掉
取消赞不弹按钮带 active 类时直接放行hx-vals 里 vote 恒为 "1",靠 Vote::toggle() 翻转
文案window.__t('lang.like_points.*')I18n::jsSubset() 只注入 js./lang. 前缀的键
档位集合传递挂在自身 <script> 的 data-lp-choices 上避免内联脚本(规范零内联红线),也省一次 AJAX
样式变量只用 9 套主题共有的 --mn-* 变量新造变量不被主题覆盖 → 夜间会「浅底浅字」

⚠️ 隐式依赖:vote_after 读 $_POST['like_points_choice'],依赖「核心在 ApiController::vote() 方法体内触发钩子、$_POST 尚未销毁」。若核心将来改钩子时机,此处会静默拿到 null(后果仅为回落「不转移」,不报错,故不易发现)。

5.6 明确的口径

场景行为
模式 = 纯点赞插件完全不动作(不解析档位、不落数据),点赞照常
取消赞不退分(核心口径;再次点赞也不会重复送,幂等锁在)
自赞不转移(自赞转移 = 刷分漏洞)
点赞人余额不足降级为纯点赞(不扣不送),点赞不被拦;占位释放,以后有分仍可补送
浮层选 0 档本次不转移,不落数据
浮层未选(点外部 / ESC / 滚动)本次点赞不发生(请求根本没发出)
前端脚本未加载 / 未生效点赞照常发出 → 服务端按 choices 决定(≥2 档无参数 → 0,不转移;仅 1 档 → 用该档)
档位填超上限夹取到 1000
目标不存在不校验(上游 ApiController 已校验);探针可用假 id

5.7 通知

走核心内置机制:Points::deduct() → notifyPointsDeduct(),Points::award() → notifyPointsGain()。

与核心其他积分场景一致,无独立开关。


六、与核心的集成点

钩子用途
init_after建表守卫(版本号门控,比对内容非判存在)
vote_after主逻辑(参数以局部变量注入,闭包作用域,$this 不可用)
layout_head_end前台资源注入(仅 thread/show + transfer + choices ≥ 2 时输出 CSS/JS)
admin_route_register后台路由(不写 /admin 前缀,核心自动拼接)

Plugin.php 末尾注册了 class_alias(Plugin::class, 'Plugin\like_points\Plugin', false) ——

目录名含下划线,核心按目录名硬拼生命周期类名(Plugin.php:699/750/846),不注册别名会导致

class_exists() 恒为 false → 卸载时数据表不会被删除(留下孤儿表)。


七、验证记录

见 .workbuddy-ai/memory/ 当日日志。要点:双版本 php -l、plugin_hygiene.py、

i18n 三语键集合零差异、CLI 逻辑探针(模式推断 / 档位白名单 / 降级)、真实 Plugin::hook('vote_after')

分发、jsdom 前台浮层回归(含定位算法与 3 组 A/B 反证)、HTTP 端到端(含资源注入条件与带档位参数的 POST)。


八、已知取舍

  1. 跨库非原子(见 5.2)—— 极端崩溃窗口下可能「少送一次分」,不会「多送」。
  2. 取消赞不退 —— 站长口径。若将来要支持退分,需在 vote_after 的 vote === 0 分支补「查占位 → 反向转移 → 删占位」,且要处理「作者已把分花掉」的欠账问题。
  3. 每日上限按次数(不是积分值)。
  4. 积分不足的点赞不补送(占位已释放,用户取消赞再点赞会重新尝试)。
  5. 前台浮层依赖 JS:脚本未加载时退化为「服务端按 choices 决定」—— 这是刻意的降级路径,不是缺陷。
  6. 纯点赞模式下 choices 仍原样保存(后台 UI 只是隐藏档位区,checkbox 仍随表单提交)—— 这样从「纯点赞」切回「转移」时已勾的档位不会丢;mode 才是唯一判定口径,故不影响行为。
  7. 转移模式下 choices 至少 1 项(后台保存时校验,否则报错)—— 否则前台无档可选,配置无效。
轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| 瀏覽 5 次 | 回覆 0 次

全部回覆 (0)

暫無回覆

請 登入