本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:WWW/plugins/quiz_duel/说明.md。
猜题对决(quiz_duel)插件说明
版本:1.2.0 | 作者:SoSite | 状态:✅ 已启用
功能:一对一猜题对赌——出题人设题并押注,应战人猜出题人的答案,猜中者赢家通吃(拿走双倍注额)。
一、功能简介
- 出题(挑战):填写问题、选项 A / B、自己的答案,并押注一笔积分 → 生成一条
open状态的挑战 - 应战:他人选择 A 或 B 并押上同等注额 → 立即结算
- 应战人选的答案 == 出题人设的答案 → 应战人赢
- 否则 → 出题人赢
- 赢家通吃:赢家获得
stake × 2(双方的注额之和) - 三个页面:等待应战列表(
/duel)、创建挑战(/duel/create)、我的对决(/duel/my)、对决详情(/duel/detail/{id}) - 侧栏:
/duel页面右栏输出「导航 + 赢家榜 + 最近对决」三块面板(桌面走钩子,移动端走视图内兜底) - 前端交互:htmx 局部刷新(成功响应携带
data-redirect时延迟跳转),零内联 JS
二、目录结构
plugins/quiz_duel/
├── plugin.json # 插件清单(4 个钩子;无 admin_url / permissions)
├── Plugin.php # 主类:建表、赢家榜、最近历史、今日应战数、总盈亏、生命周期
├── QuizDuelController.php # 前台:列表 / 创建 / 我的 / 详情 / 应战结算
├── hook/
│ ├── init_after.php # 幂等建表兜底(缓存标记 protected/.quiz_duel_table_cache)
│ ├── route_register.php # 前台路由注册
│ ├── nav_plugin_links.php # 应用下拉菜单入口
│ └── right_sidebar_after.php # 右栏「导航 / 赢家榜 / 最近对决」(仅 /duel 前缀页面输出)
├── views/
│ ├── index.php # 等待应战列表
│ ├── create.php # 创建挑战
│ ├── detail.php # 详情 / 应战
│ └── my.php # 我的对决
├── assets/ # style.css / script.js(零内联)
├── data/ # quiz_duel.sqlite(独立库)+ .htaccess / web.config
└── lang/ # zh.php / en.php / zh_tw.php(各 64 键,三语零差异)三、数据表
插件独立库:plugins/quiz_duel/data/quiz_duel.sqlite
| 表 | 用途 |
|---|---|
quiz_duels | 对决记录(creator_id / opponent_id / question / option_a / option_b / creator_choice / opponent_choice / stake / status / winner_id / created_at / accepted_at / completed_at) |
- 索引:
idx_quiz_duels_status、idx_quiz_duels_creator、idx_quiz_duels_opponent status状态机:open(等待应战)→completed(已结算)- 建表:
activate()+init_after双保险;卸载:uninstall()删表
四、路由
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /duel | 等待应战的挑战列表(需登录) |
| GET | /duel/create | 创建挑战页(需登录) |
| POST | /duel/create | 提交挑战(需登录 + CSRF) |
| GET | /duel/my | 我的对决(需登录) |
| GET | /duel/detail/{id} | 对决详情(需登录) |
| POST | /duel/accept | 应战并结算(需登录 + CSRF) |
本插件无后台页,故
plugin.json不声明admin_url与permissions(二者均为可选字段)。
五、核心规则
创建挑战的字段校验(store())
| 字段 | 规则 |
|---|---|
question | 长度 4 ~ 200 字 |
option_a / option_b | 长度 1 ~ 50 字 |
creator_choice | 必须是 A 或 B |
stake | duel_bet_min ~ duel_bet_max |
校验通过 → Points::deduct() 扣押注 → 写 quiz_duels(status = 'open')。
写库失败自动退款(Points::award,理由「猜题对决退款」)。
应战结算(accept())
① 校验 choice ∈ {A, B}
② 取对决记录,必须存在且 status = 'open'(否则「已关闭」)
③ 不能应战自己的挑战
④ 扣应战人积分(Points::deduct,理由「猜题对决押注」)
⑤ 判定:opponent_choice == creator_choice → 应战人赢,否则出题人赢
⑥ 更新记录(opponent_id / opponent_choice / status='completed' / winner_id / accepted_at / completed_at)
⑦ 发奖:Points::award(winnerId, stake * 2)- 结算与发奖在同一个 try 块内:任一步抛异常 → 退还应战人积分(理由「猜题对决退款」)并返回错误
Points::award()外层再套一层 try/catch:封装内异常降级为日志,不阻断「已结算、即将返回 JSON」的响应
配置项(核心 settings 表,惰性默认)
| 键 | 默认 | 说明 |
|---|---|---|
duel_bet_min | 10 | 最低押注额 |
duel_bet_max | 500 | 最高押注额 |
统计接口(Plugin.php)
| 方法 | 说明 |
|---|---|
getLeaderboard($limit) | 赢家榜(按 SUM(stake) 降序,仅 completed) |
getRecentHistory($limit) | 最近对决(按 completed_at 降序) |
getTodayJoinCount($userId) | 今日应战次数(status IN ('accepted','completed')) |
getUserProfit($userId) | 用户总盈亏 = 赢得的 SUM(stake*2) − 输掉的 SUM(stake) |
跨库无法 JOIN:用户名 / 头像经
User::batchGetUsers()/User::batchGetNames()从核心库批量补全。
六、注意事项
- ⚠️ 需要登录才能查看与参与,游客访问会被
requireLogin()重定向 - ⚠️ 依赖核心积分体系:
Points::deduct()/Points::award() - ⚠️ 只有
status = 'open'的挑战可被应战,已结算的挑战再次应战返回「已关闭」;不能应战自己的挑战 - ⚠️ 一次应战即终局:没有「出题人反悔」「超时作废」机制——
open状态的挑战会一直挂着,直到有人应战 - ⚠️ 出题人与应战人各扣一次注额,赢家拿
stake × 2,平台不抽成(零和) - ⚠️ 本插件目录名含下划线,核心
Plugin::activate/deactivate/uninstall()的类名推导
("\\Plugin\\{$name}\\Plugin")会拼出 \Plugin\quiz_duel\Plugin 而无法命中 \Plugin\QuizDuel\Plugin
→ 已在 Plugin.php 末尾用 class_alias 补别名(否则卸载时数据表不会被删除)
样式依赖(2026-10-08 修复)
- ⚠️⚠️ **视图只能用
duel-或核心mn-类**,禁用旧原子类(.flex-1/.gap-/.mt-/.mb-*/
.fs- / .fw- / .c-999 / .text-center,以及 Bootstrap 式的 .row / .col-6 / .g-10)——
核心已把旧原子类**统一迁移到 mn-* 并删除旧定义**(见 assets/css/modern.css 的 [迁移] 注释),
继续用旧类名会静默失效(不报错、不写日志,只是样式不生效)。
- **本插件样式统一走
duel-*命名空间**(与核心解耦),工具类集中在assets/style.css第 28 节。 - 典型症状:主列
.duel-flex-1若漏掉flex: 1,在.duel-flex-layout(flex + wrap)里
宽度会退化成「内容宽度」而非撑满容器 → 整个内容区看起来变窄(本次即为此问题)。
- 历史坑:视图曾
<link>核心的/assets/css/components.css,该文件已不存在(404),本次已移除。 - ⚠️ 两轮扫描的教训:第一轮只比对了
duel-*类,漏掉了 4 个非duel-前缀的 Bootstrap 式旧类
(align-items-center / gap-2 / ms-2 / mb-0)—— 它们在前台实际加载的 CSS 里同样没有定义
(.mb-0 只在后台 admin.css 且作用域是 .admin-container)。第二轮已补齐为
duel-items-center / duel-gap-8 / duel-ms-8 / duel-mb-0(-2 按 Bootstrap = 0.5rem = 8px)。
→ 改完必须全量比对「视图里所有静态类 ↔ 前台实际加载的 CSS」,不能只看自己改过的那批前缀。
(判定「某类有没有定义」时,admin.css / editor.css 前台不加载,算进去会漏判。)
- 当前状态:
quiz_duels1 行;插件已启用(activated: true) - 版本历史:
- 1.2.0(2026-10-08)修复「内容页变窄」——核心原子类迁移后插件仍在用旧类名导致静默失效,
视图 21 个失效类全部改 duel-*、移除 404 的 components.css 引用、style.css 第 28 节补齐定义
- 1.1.0(2026-10-03)侧栏/卡片布局重构
- 1.0.0 首个版本