本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:WWW/update/30/plugins/quiz_duel/说明.md。
猜题对决(quiz_duel)插件说明
版本:1.0.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 补别名(否则卸载时数据表不会被删除)
- 当前状态:
quiz_duels1 行;插件未启用(activated: false)