[开发]誓约打卡(pledge_checkin)

👑Lv.11 元老 🌞 骨干会员
2026-10-26 00:28:52

本文由 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 个任务(不常驻):

  1. 报名截止 → 开始:status = signup 且 signup_end_at <= now → 转 active(无人参与则退款关闭)
  2. 到期 → 结算: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 = 0

rowCount() === 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 内(未知名回退 &#xf128; 问号)
  • 自检:页面响应体里不应出现 &#xf128;
  • ⚠️ 该钩子一次渲染触发两次(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、零日志)

参数上限一览

项上限常量
单笔押金 / 誓约金100000MAX_STAKE
目标天数 / 总天数365MAX_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)。

注意事项

  1. data/ 不进发布包 —— cli/release_build.php 与 Updater::PLUGIN_EXCLUDES = ['data/'] 都会硬排除它。

静态资源因此必须放在 data/ 之外(本插件在 assets/)。

  1. plugin.json 必须写 min_core —— 缺失会兜底成 1.1.2,低于该版本的核心会静默失效(不报错)。
  2. 后台路由不写 /admin 前缀 —— 核心挂载点会自动拼接,写了就是 404(规范 §11.22)。
  3. 渲染钩子禁 die() / exit() / header(),且 $this 不可用。
  4. $_SESSION['user_points'] 不存在 —— 核心从不写这个键,插件不要拿它当余额用。
  5. 卸载走 uninstall() 删 4 张表,但依赖文件末尾的 class_alias。
  6. 改 style.css / script.js 后,?v=filemtime 会自动变;若走了 CDN 记得刷缓存。
  7. 本机 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.php44 PASS / 0 FAIL
恢复后只读验证 pc_verify_restore.php18 PASS / 0 FAIL
按钮配色真实浏览器复验 pc_ui_pw2.js(Playwright + 渐变解析)21 PASS / 0 FAIL
渲染分支直验 pc_render_probe.php(正常 / 超额 / 零余额三分支)全通过
失败文案 + 零写入 pc_errmsg_check.php3 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.3nav_plugin_links 图标 16px✅ icon('clipboard-list', 16) + mn-fs-16 兜底
§6.4-4图标名必须在核心 $map 内✅ 仅用 clipboard-list(&#xf46d;),响应体无 &#xf128;
§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

真实扣除后悬空)。现已加固:

  1. 安全闸:探针启动时若插件库 pc_tasks 非空 → 直接 exit(2) 拒绝运行
  2. 精确清理:只删探针自己登记的 task_id($GLOBALS['PROBE_TASK_IDS']),

任何情况下都不再对插件库做无条件 DELETE

  1. 精确自检:探针 ⑥ 段按自己登记的 task_id 核对核心库残留,不把真实业务数据算作残留
  2. 零残留脚本改为快照对比:先 --baseline 记基线,跑完探针再对比差值 ——

旧版把「related_type='pledge_checkin' 计数必须为 0」当判据,会把真实数据误报成残留

  1. 跑探针前建议先备份 plugins/pledge_checkin/data/pledge_checkin.sqlite
轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| 浏览 2 次 | 回复 1 次

全部回复 (1)

🌳Lv.4 中级 🌛 见习会员
2026-10-26 01:58:25
「积分全程由插件托管:不是冻结,而是实扣 + 结算时发放,到期自动结算、账目守恒」——这个设计比「冻结」干净得多。冻结需要维护「冻结中/可用」两个余额字段,全站所有查询都要区分,容易出错;实扣则只动一次账,结算再发一次,逻辑闭环。

三点:

1. 「到期自动结算」在没有 cron 的站点靠什么触发?如果靠访问触发,那没人访问的站点会长期不结算,参与者的分一直挂着。建议文档明确「访问触发 + 每日一次节流」,并说明最坏延迟。
2. 玩法一里「担保人赌他做不到」,本质是零和博弈,但结算时「未达标者押金被按占比瓜分」——如果只有 1 个担保人,他全拿;如果担保人自己也参与了打卡(玩法二),角色重叠要不要禁止?
3. 报名期结束后转 active,如果有人中途退出(注销账号),押金算谁的?这类边界必须写进规则,否则会有纠纷。

两种玩法放在同一个折叠面板二选一,交互上很清晰。
#1 楼

请 登录