本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:WWW/plugins/pledge_checkin/说明.md。
誓约打卡(pledge_checkin)
押积分立誓,按期打卡。达标者赢走未达标者的押金,未达标者的押金被按占比瓜分。
积分全程由插件托管(不在任何人账户里「冻结」,而是实扣 + 结算时发放),到期自动结算,账目守恒。
玩法(两种)
发帖时在「誓约打卡」折叠面板里二选一:
玩法一:誓约担保(guarantee)
| 角色 | 做什么 |
|---|---|
| 誓约人(楼主) | 押一笔誓约金(pc_stake_points),定累计打卡目标天数(pc_target_days) |
| 担保人(其他人) | 在报名期内押担保金(各自金额随意,受 pc_max_points_per_user 上限约束)——本质是「赌他做不到」 |
报名期结束(signup_end_at)→ 任务转 active,进入打卡期。
玩法二:一起打卡(group)
发起人设一个全局目标天数(pc_target_days),多人各自押分加入。
打卡期结束时,达标的人瓜分未达标的人的押分,并收回自己的本金。
结算规则
誓约担保
| 结果 | `result` | 资金去向 |
|---|---|---|
| 誓约人累计打卡 ≥ 目标天数 | owner_win | 誓约金原路退回誓约人 + 誓约人赢走全部担保金 |
| 誓约人未达标,且有人担保 | guarantors_win | ① 每个担保人先退回自己的本金 ② 誓约金按各人押金占比瓜分(floor 取整,余额给押得最多的人) |
| 誓约人未达标,但无人担保 | no_guarantor | 誓约金全额退回誓约人(没有对手方,不存在输赢) |
一起打卡
| 结果 | `result` | 资金去向 |
|---|---|---|
| 无人达标 | all_failed | 全员全额原路退回(防积分凭空消失) |
| 全员达标 | all_win | 各自取回本金(池子里没钱可分) |
| 部分达标 | partial | 达标者 = 本金 + floor(池子 × 各自投入 / 达标者总投入);floor 余额给投入最多的达标者 |
非正常结束
| 结果 | `result` | 资金去向 |
|---|---|---|
| 发起人报名期内取消 | owner_cancel | 全额退款 |
| 管理员强制取消 | admin_cancel | 全额退款 |
| 主题被删除(含回收站) | orphan | 全额退款 |
| 报名期结束但无人参与 | no_participant | 全额退款 |
守恒约束:
settleGuarantee/settleGroup的每一个分支都满足「所有
payouts之和 == 托管总额」。取整产生的余额一律显式指派给某人,绝不丢弃。这一点由
pc_probe.php的 17 组资金场景逐一断言(每组都校验「净变化合计 = 0」)。
数据表(独立库 data/pledge_checkin.sqlite)
| 表 | 用途 |
|---|---|
pc_tasks | 任务主表:玩法 / 誓约金 / 目标天数 / 总天数 / 各类上限 / 时间窗 / 报名截止 / 开始与截止时间 / 担保总额 / 状态 / 结算标记 / 结果。UNIQUE(topic_id) |
pc_guarantors | 担保人:task_id + user_id + points。UNIQUE(task_id, user_id) |
pc_members | 一起打卡的参与者:task_id + user_id + points + status + done_days。UNIQUE(task_id, user_id) |
pc_checkins | 打卡流水:task_id + user_id + kind(owner/member) + day_key + created_at。UNIQUE(task_id, kind, day_key) |
索引:idx_pc_tasks_status(status, settled)、idx_pc_tasks_user(user_id, status)、
idx_pc_guarantors_task(task_id)、idx_pc_members_task(task_id)、idx_pc_checkins_task(task_id, kind)。
day_key 是去重键,不是日期:誓约人用 YYYY-MM-DD,成员用 {uid}|YYYY-MM-DD
(同一任务里多人各自每天一次,互不冲突)。
目录结构
plugins/pledge_checkin/
├── plugin.json 清单(9 个钩子 + route:admin 权限 + admin_url)
├── Plugin.php 主类:托管 / 报名 / 打卡 / 结算 / 心跳(约 1200 行)
├── FrontController.php 前台:列表 / 详情 / 报名 / 打卡 / 放弃 / 取消
├── AdminController.php 后台:列表 / 强制结算 / 强制取消 / 手动心跳
├── cron.php ★ 自带定时入口(宝塔计划任务打这里)
├── hook/
│ ├── init_after.php 建表兜底(插件「已启用但 activate 从未跑」是常态)
│ ├── route_register.php 6 条前台路由
│ ├── admin_route_register.php 4 条后台路由(不写 /admin 前缀)
│ ├── nav_plugin_links.php 导航「应用」入口
│ ├── post_create_extra.php 发帖页「誓约打卡」折叠表单
│ ├── thread_create_after.php 发帖成功后建任务 + 托管誓约金
│ ├── thread_edit_after.php 编辑帖子时同步标题快照(不允许改誓约参数)
│ ├── thread_redpacket_area.php ★ 帖子详情页的任务卡片(主交互面)
│ └── thread_show_top.php 一次性提示(读后即焚)
├── views/{index,detail,admin}.php
├── assets/{style.css,script.js}
├── lang/{zh,zh_tw,en}.php 各 122 键,键集合零差异、键序一致
└── data/ ★ 运行期生成,不进发布包路由
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /pledge-checkin | 我发起的 / 我参与的 |
| GET | /pledge-checkin/{id} | 任务详情 |
| POST | /pledge-checkin/join | 押担保金 / 押分加入 |
| POST | /pledge-checkin/checkin | 打卡 |
| POST | /pledge-checkin/giveup | 放弃(一起打卡成员) |
| POST | /pledge-checkin/cancel | 发起人取消(仅报名期) |
| GET | /admin/pledge-checkin | 后台任务列表 + 统计 |
| POST | /admin/pledge-checkin/settle | 强制结算 |
| POST | /admin/pledge-checkin/cancel | 强制取消 + 全额退款 |
| POST | /admin/pledge-checkin/tick | 手动跑一轮心跳 |
所有 POST 都过 Csrf::verifyOrDie();后台 4 条另有 AdminController extends app\Controllers\Admin\BaseController 提供的
管理员判定 + 二次验证(都在基类构造函数里,误继承 app\Core\Controller 会完全不鉴权)。
到期结算怎么跑
FlintHub 核心没有通用插件 cron 钩子 —— cron_trigger.php 的消费者只认 rebuild_search / stats,
对插件仅硬编码了 AiAssistant::flushBatch();app/Helpers/Plugin.php 里也搜不到任何 cron。
规范 §11.26 明确「自定义任务类型勿塞核心队列,插件自带消费端」。所以本插件用三条互补的路径:
| # | 触发点 | 特点 |
|---|---|---|
| ① | 懒结算 Plugin::tick() | 挂在任务卡片渲染(thread_redpacket_area)、前台列表/详情、后台列表、各写操作入口。$_SESSION['_pc_tick'] 60 秒节流,所以正常浏览量的站点根本不需要 cron |
| ② | plugins/pledge_checkin/cron.php | 站点长期无人访问时的兜底。见下节配置 |
| ③ | 后台「立即结算」按钮 | 运营手动兜底 |
tickRun($limit) 做两件事,单次最多处理 $limit 个任务(不常驻):
- 报名截止 → 开始:
status = signup且signup_end_at <= now→ 转active(无人参与则退款关闭) - 到期 → 结算:
deadline_at <= now且settled = 0→ 走settle()
宝塔计划任务配置(推荐)
宝塔面板 → 计划任务 → Shell 脚本,周期 每 5 分钟:
wget -q -O /dev/null "https://你的域名/plugins/pledge_checkin/cron.php?key="<CRON_KEY>见站点根config.php的define('CRON_KEY', '...')- 也支持请求头
X-Cron-Key传密钥 - 密钥为空 → 503 拒绝;密钥错误 → 403 拒绝(
hash_equals防时序攻击) - 文件锁
data/lock/pledge_checkin_cron.lock(LOCK_EX | LOCK_NB)防并发重入 - 生产 Nginx 只拦
plugins/[^/]+/data/,插件根目录的.php正常执行 →cron.php可被 HTTP 直访 ✓ - ⚠️ 不配也不会导致积分异常:懒结算会兜住,只是站点长期无人访问时结算延后
关键实现说明
1. 积分托管与守恒
采用实扣 + 结算发放,不是「冻结字段」:
报名 → Points::deduct(用户, N, '...托管', taskId, 'pledge_checkin') ← 真扣,进托管池
结算 → Points::award(用户, N, '...结算', taskId, 'pledge_checkin') ← 真发托管池余额是算出来的(pc_tasks.guarantee_total + 各 pc_guarantors.points / pc_members.points),
不是单独维护的字段,因此不存在「池子对不上账」的可能。
Points::deduct() 返回 bool,必须判(余额不足返回 false 而非抛异常)。
本插件在 createForThread() / join() 里都做了判定,失败即走补偿。
2. 跨库一致性:先落插件库 → 再扣核心库 → 失败补偿
插件库(SQLite)与核心库是两个 PDO 连接,无法跨库事务。顺序固定为:
① INSERT pc_tasks / pc_guarantors / pc_members (插件库,无事务)
② Points::deduct(...) (核心库,在外层事务内)
③ 失败 → DELETE 刚插入的行(按 lastInsertId 精确删除)createForThread() 用 $pdo->inTransaction() 探测外层事务(核心发帖流程自带事务):
已在外层事务里就不自建、不提交,让核心的 commit/rollback 决定最终结果;
只有独立调用时才自己 begin()/commit()。
⚠️ 框架 Database 与原生 PDO 的事务方法名不同:框架是 begin() / commit() / rollback()(小写 b,
没有 inTransaction());原生 PDO 才是 beginTransaction() / rollBack() / inTransaction()。
本插件统一写 $pdo = $db->getConnection(); $nested = $pdo->inTransaction(); 再 $db->begin()。
3. 结算的幂等与乐观锁
applyPayouts() 把「发积分」和「标记已结算」放进同一个核心库事务,标记用乐观锁:
UPDATE pc_tasks SET settled = 1, status = ?, result = ?
WHERE id = ? AND settled = 0rowCount() === 1 才算成功。这样并发(懒结算 + cron + 后台按钮同时打过来)也只会结算一次。
4. 一主题一任务
pc_tasks 有 UNIQUE(topic_id)。createForThread() 先查一次,撞唯一约束也能兜住 → 同一帖子不会出现两个任务。
5. 打卡时间窗(支持跨零点)
pc_window_enabled = 1 时,只有 [window_start, window_end) 之间的分钟数能打卡。
起止相同视为全天。跨零点(如 22:00 ~ 02:00)判定为:
$cur >= $start || $cur < $end⚠️ 表单里是 <input type="time">,提交的是 "08:00" 这种字符串 —— 直接 (int) 会误得 8(= 8 分钟)。
因此统一走 Plugin::parseHm(),它同时接受 "HH:MM" 与纯分钟数。
6. 孤儿帖保护:软删必须双查
Thread::find() 对软删(回收站)的帖子返回 null,getDeletedById() 才读得到。
所以 topicAlive() 必须两查:
$model = new \app\Models\Thread();
if ($model->find($topicId) !== null) return true; // 正常存活
if ($model->getDeletedById($topicId) !== null) return true; // 软删(可恢复)→ 不算孤儿
return false; // 真没了⚠️ 只查 find() 会把回收站里可恢复的帖子误判为孤儿并退款 —— 用户把帖子丢进回收站再恢复,
誓约就凭空取消了。查询异常时保守返回 true,宁可晚退款也不误退款。
7. 一次性提示(thread_show_top)
报名 / 打卡 / 放弃 / 取消 都是传统表单 POST + 302 跳转,跳转前无法输出 HTML。
故用「一次性 session 提示」:
FrontController::backWithTip() → $_SESSION['pc_tip'] = ['k'=>ok|error, 'msg'=>..., 'thread_id'=>...]
↓ 302 跳 /thread/{id}
thread_show_top → 读 session → 页顶渲染 mn-alert → **立即 unset**- 跨帖保护:session 带
thread_id,只在同帖渲染;不匹配时零输出且不 unset
(否则用户从 A 帖跳到 B 帖,提示会被 B 帖吃掉)
- 读后即焚:刷新页面不重放
- ⚠️ 只用
mn-alert-success/mn-alert-error两个变体 —— 核心没有.mn-alert-warning类 - ⚠️ 渲染钩子在闭包作用域,
$this不可用 → 钩子里一律htmlspecialchars($s, ENT_QUOTES, 'UTF-8')+I18n::get(),
禁 $this->e() / $this->t()(会吞成「HTML 半截 + 200 + 无告警」)
8. 目录名含下划线的坑(删掉会卸载不干净)
本插件目录名 pledge_checkin 含下划线,而核心 app/Helpers/Plugin.php 用目录名硬拼类名
\Plugin\pledge_checkin\Plugin → class_exists 恒为 false → activate/deactivate/uninstall
静默跳过(不报错、后台还提示成功),典型症状是卸载后表残留。
因此 Plugin.php 末尾必须保留:
\class_alias(Plugin::class, 'Plugin\pledge_checkin\Plugin', false);9. 不写核心回复
按设计,本插件不在帖子里插入任何楼层。主交互面是帖子详情页的任务卡片(thread_redpacket_area 锚点),
外加导航「应用」入口和 /pledge-checkin 列表页。好处:不污染楼层、不触发回复通知、帖主删不掉。
10. 前端
- CSS 只用既有
--mn-变量(核心原子类已迁mn-,旧类名静默失效;且不存在--mn-danger-/--mn-spectrum-) .mn-input/.mn-btn与插件类同权重(0,1,0)→ 需要提权(input.pc-input/button.pc-btn-primary/progress.pc-progress)- 进度条用原生
<progress>(避免内联style宽度,零内联约束) script.js无框架,只做「玩法切换显示对应字段」与「时间窗开关」,全部走data-*属性- 静态资源带
?v=filemtime(改资源必让指纹变)
11. 前台页容器:统一 .mn-section(规范 §6.4-2)
所有插件前台页(列表 / 详情)最外层写作:
.mn-section(assets/css/modern.css:672)负责卡片外观;插件自己的 .pc-page 只设 padding: 20px- ⚠️ 不要再设
max-width / margin: 0 auto —— 那会与核心中栏宽度打架,出现「比其他插件页宽/窄」 - 参照实现:
points_mall/views/shop.php(<div class="mn-section pm-shop"> + .pm-shop{padding:20px})、
user_level/views/mine.php、medal/views/index.php、red_packet/views/index.php
- 帖子详情页内嵌的卡片(
thread_redpacket_area 输出)不需要 mn-section —— 它在主帖正文内部,
与 red_packet 的 rp-thread-card 同一定位
12. 导航入口必须带图标(规范 §9.3)
nav_plugin_links 的输出必须含图标,字号 16px(不是 14px —— 14px 只适用于 nav_user_menu_items):
$view = $GLOBALS['__view'] ?? null; // init.php:97 注入
if ($view instanceof \app\Core\TemplateCompiler) {
echo ''
. $view->icon('clipboard-list', 16) . $label . '';
} else {
echo '' . $label . ''; // 兜底仍保证 16px
}
- 图标名必须在核心
TemplateCompiler::icon() 的 $map 内(未知名回退  问号) - 自检:页面响应体里不应出现
 - ⚠️ 该钩子一次渲染触发两次(PC 左栏 + 顶栏下拉),文件顶部禁声明函数/类,高亮必须用
!empty($__nav_uri) && ... 守卫
13. 性能红线:每请求 hook 查询 ≤ 5 次(规范 §11.24)
帖子详情页的查询预算(thread_redpacket_area 是唯一带查询的钩子):
步骤 次数 getTaskByTopic()1 tick() → 仅当它真的处理了任务才 getTask() 重读0~2 decorate() 内部(countCheckins 或 checkinDayMap)1 getGuarantors() / getMembers()1 User::batchGetNames()1 合计(常态) 4(原为 6)
两处优化:
decorate(array $task, int $userId = 0, ?array $myRow = null) —— 调用方已查过参与人列表时,
把「自己那一行」传进来,省掉内部再查一次
Plugin::tick() 有返回值(本轮处理的任务数),只有 > 0 才重读任务,避免每次渲染白读一次
14. 按钮配色:主按钮走核心类,危险按钮走主题成对变量
2026-10-13 用户实测报出两个视觉缺陷,根因都在这两条规则上。
① 主按钮(押注担保 / 打卡)—— markup 直接写核心 mn-btn-primary,本插件不再自造配色
押注担保
曾经的写法是自造 button.pc-btn-primary((0,1,1)),踩了两个坑:
状态 曾经的胜出规则 结果 常态 button.pc-btn-primary (0,1,1) background: var(--mn-primary)正常 hover .mn-btn:hover (0,2,0) color: var(--mn-primary) 压过自造 (0,1,1) 的 color:#fff主色字压 primary-hover 底,实测对比度 1.35:1 → 「鼠标放上去看不清字」
改用核心 .mn-btn-primary 后:核心 .mn-btn-primary:hover((0,2,0),且在 modern.css 中位于 .mn-btn:hover 之后)自带
color: var(--mn-text-inverse),天然正确。实测 hover 对比度由 1.35:1 → 2.69:1(= 常态水平)。
⚠️ 常态对比度由主题决定:沙金主题 modern-sand.css 会把 .mn-btn-primary 覆盖成
linear-gradient(135deg,#cfa963,#bd8f45) 金色渐变(白字 ≈2.2:1),全站所有主按钮一致,不是本插件的问题。
⚠️ 量这类按钮的对比度必须解析 background-image —— 渐变会把 background-color 重置成 transparent,
只看 backgroundColor 会误判成「按钮隐形」。
② 危险按钮(取消誓约 / 放弃)—— 用主题自带的 --mn-alert-error-{bg,text} 成对变量
button.pc-btn-danger {
color: var(--mn-alert-error-text);
background: var(--mn-alert-error-bg);
border-color: var(--mn-alert-error-text);
}
绝不能用 --mn-danger —— 全站从未定义该变量(核心只在 modern.css:639/643 用带兜底的
var(--mn-danger, #dc2626))。background: var(--mn-danger) 属 invalid at computed-value time
→ 整条声明作废、背景退回 transparent,而 color:#fff 与 border-color(= currentColor 白)照旧生效
→ 浅色主题下「白字 + 白边框 + 透明底」= 按钮隐形,hover 才显形(用户报的「取消誓约按钮不显示」)。
--mn-alert-error-{bg,text} 在 9 个主题全部有定义且互为可读配色(浅色主题深红字/浅红底,
夜间主题浅红字/深红底),实测常态对比度 4.41:1。
15. 押注额上限与余额提示(避免「点了没反应」)
用户报「押注担保按钮点击没响应」的真相:后端链路完全正常(302 + 一次性提示都正常),
但表单硬编码 value="100",而测试账号只有 34 积分 → 点下去必然失败,
失败提示又渲染在页面顶部(按钮在正文中部)→ 用户观感就是「点了没反应」。
现在卡片/详情页在渲染押注表单前就把余额摆出来:
变量 含义 $pcStakeCap可押上限 = min(本人余额, 单人押分上限, MAX_STAKE) $pcStakeVal默认值 = 额度未超支时用任务额度,超支时压到 $pcStakeCap $pcCanJoin$pcStakeCap >= 1,否则不渲染按钮,改显示「积分不足,无法参与」
配套 UI:
input[name=points] 的 max = $pcStakeCap(浏览器原生校验兜住超支)- 表单旁
<span class="pc-balance">我的积分:{points}</span>;余额 < 任务额度时加 .pc-balance-low 警示色 - 余额取
Auth::getCurrentUser()['points'](请求级静态缓存,布局已预热 → 零额外查询,不占 §11.24 的 5 次预算)
后端失败文案也改为带具体数字(Plugin::join() 抛异常前查一次余额):
积分不足:本次需押 {need} 积分,你当前只有 {have} 积分
后台
入口:后台 → 插件 → 誓约打卡(/admin/pledge-checkin)。
- 统计卡:总任务数 / 进行中 / 已结算 / 托管积分
- 立即结算:手动跑一轮心跳(报名截止转进行中 + 到期结算)
- 任务表:玩法 / 状态 / 发起人(链到帖子)/ 押金(+ 担保总额)/ 结算结果 / 操作(强制结算、强制取消)
- 后台弹层与脚本都留在
<main class="admin-content"> 内(放到 </main> 后面 = 无 UI、零日志)
参数上限一览
项 上限 常量 单笔押金 / 誓约金 100000 MAX_STAKE目标天数 / 总天数 365 MAX_DAYS报名期 720 小时(30 天) MAX_SIGNUP_HOURS担保人数量 100(0 = 不限) MAX_GUARANTORS参与人数 200(0 = 不限) MAX_MEMBERS每人押金 100000(0 = 不限) MAX_STAKE
硬校验(不满足直接拒绝并回提示):
目标天数 ∈ [1, 365]、总天数 ∈ [1, 365]目标天数 <= 总天数(否则永远不可能达标)- 誓约担保模式下
誓约金 ∈ [1, 100000] - 时间窗起止 ∈
[0, 1439],起止相同视为全天
语言包
lang/zh.php / lang/zh_tw.php / lang/en.php,各 122 键,键集合零差异、键序一致
(校验脚本 .workbuddy-ai/tools/fh_i18n_check.php → PASS ③ 插件 pledge_checkin 三语一致 122 键)。
键为完整键(plugin.pledge_checkin.xxx),I18n 自动合并,无需额外钩子。
⚠️ 文件名必须是 zh.php / zh_tw.php / en.php(不是 zh-cn.php)。
注意事项
data/ 不进发布包 —— cli/release_build.php 与 Updater::PLUGIN_EXCLUDES = ['data/'] 都会硬排除它。
静态资源因此必须放在 data/ 之外(本插件在 assets/)。
plugin.json 必须写 min_core —— 缺失会兜底成 1.1.2,低于该版本的核心会静默失效(不报错)。- 后台路由不写
/admin 前缀 —— 核心挂载点会自动拼接,写了就是 404(规范 §11.22)。 - 渲染钩子禁
die() / exit() / header(),且 $this 不可用。 $_SESSION['user_points'] 不存在 —— 核心从不写这个键,插件不要拿它当余额用。- 卸载走
uninstall() 删 4 张表,但依赖文件末尾的 class_alias。 - 改
style.css / script.js 后,?v=filemtime 会自动变;若走了 CDN 记得刷缓存。 - 本机
config.php 的 CRON_KEY 为空时,cron.php 返回 503;要真启用计划任务必须先配置它。
当前状态
v1.0.0,已完成开发与验证:
验证项 结果 php -l(8.2.9nts + 8.0.2nts 双版本)19 文件 × 2 版本 全过 php_var_cjk_check.php(全角标点紧跟 $var)OK(19 个文件) plugin_case_lint.py(命名空间 Linux 大小写精确匹配)✅ 57 个插件全可加载 fh_i18n_check.php61 PASS / 0 FAIL(含本插件三语 122 键一致) 积分守恒探针 pc_probe.php(17 组资金场景) 94 PASS / 0 FAIL 零残留对比 pc_residue_check.php(快照差值) 10 PASS / 0 FAIL HTTP 端到端冒烟 pledge_http_smoke.php 44 PASS / 0 FAIL 恢复后只读验证 pc_verify_restore.php 18 PASS / 0 FAIL 按钮配色真实浏览器复验 pc_ui_pw2.js(Playwright + 渐变解析) 21 PASS / 0 FAIL 渲染分支直验 pc_render_probe.php(正常 / 超额 / 零余额三分支) 全通过 失败文案 + 零写入 pc_errmsg_check.php 3 PASS(自建夹具、可重复运行;实测 积分不足:本次需押 3301 积分,你当前只有 3300 积分,净写入 0)
规范符合性(已逐条对照 docs/插件开发规范.md)
条款 要求 本插件 §6.1 / §6.4-2 前台插件页统一 .mn-section 容器 ✅ <div class="mn-section pc-page"> §6.2 前台链接走 $this->url(),禁硬编码 ✅ 全部链接/表单 action 均用 §6.4-1 / §9.3 nav_plugin_links 图标 16px✅ icon('clipboard-list', 16) + mn-fs-16 兜底 §6.4-4 图标名必须在核心 $map 内 ✅ 仅用 clipboard-list(),响应体无  §7.3 零内联(禁 on* / 禁内联 style) ✅ 事件走 data-* + 外部 script.js §9.0 双触发钩子顶部禁声明函数/类 ✅ §11.22 后台路由不写 /admin 前缀 ✅ §11.24 每请求 hook 查询 ≤ 5 次 ✅ 常态 4 次(详见「关键实现说明 13」) §11.26 插件自带消费端(不塞核心队列) ✅ cron.php §11.34b 路由路径不与站点真实文件/目录同名 ✅ /pledge-checkin §11.35 投稿包单根 plugins/<name>/、排除 data/ ✅(出包时执行) — 目录名含下划线须 class_alias ✅ Plugin.php 末尾 — 按钮配色:主按钮走核心类、危险按钮走主题成对变量 ✅ 详见「关键实现说明 14」 — 押注额不得超过余额,且常驻余额提示 ✅ 详见「关键实现说明 15」
冒烟覆盖:前后台路由可达 / 后台三重门禁(游客 302 login、未二次验证 302 verify、管理员 200)/
静态资源 200 + Content-Type / data/ 目录 403 / cron.php 无 key 与错 key 均拒绝 /
CSRF 403 + 「页面已过期」/ 后台表格非空态与空态两条渲染路径 /
导航项带图标(2/2 双触发) / 列表页与详情页均为 .mn-section 容器 / 无图标问号回退 /
零写入(4 表 + error.log 无新增)。
⚠️ 验证脚本的安全约束(2026-10-13 事故后加固)
pc_probe.php 早期版本用 DELETE FROM pc_tasks(全表删除)清理测试数据,
曾把用户真实创建的誓约任务一起删掉(帖子 219《我们来一次誓约打卡吧》,100 积分被 Points::deduct
真实扣除后悬空)。现已加固:
- 安全闸:探针启动时若插件库
pc_tasks 非空 → 直接 exit(2) 拒绝运行 - 精确清理:只删探针自己登记的
task_id($GLOBALS['PROBE_TASK_IDS']),
任何情况下都不再对插件库做无条件 DELETE
- 精确自检:探针 ⑥ 段按自己登记的
task_id 核对核心库残留,不把真实业务数据算作残留 - 零残留脚本改为快照对比:先
--baseline 记基线,跑完探针再对比差值 ——
旧版把「related_type='pledge_checkin' 计数必须为 0」当判据,会把真实数据误报成残留
- 跑探针前建议先备份
plugins/pledge_checkin/data/pledge_checkin.sqlite