[开发]幸运转盘(lucky_draw)插件说明

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

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

幸运转盘(lucky_draw)插件说明

版本:1.0.1 | 作者:FlintHub | 状态:✅ 已启用(后台「插件」页可停用)

功能:消耗积分随机抽取奖品。管理员配置奖品池(名称 / 权重 / 库存 / 稀有度 N·R·SR·SSR),

用户在前台转盘页点击抽奖,Points::deduct() 原子扣分后按权重定奖,积分奖品即时到账。


一、功能简介

  • 抽奖:random_int() 按权重累计抽取;并发下占用库存走

UPDATE … SET stock = CASE WHEN stock > 0 THEN stock - 1 ELSE stock END WHERE id = :id AND stock <> 0,

rowCount()=0 即被抢光 → 自动退分

  • 积分:扣分走核心 Points::deduct()(条件 UPDATE 原子校验余额,余额不足返回 false);

积分奖品走 Points::award() 即时发放;扣分后任何失败都按「撤奖 → 还库存 → 退抽奖成本」逆序补偿

(插件独立库与核心库没有分布式事务)

  • 限额:单次消耗积分(0 = 免费抽奖,不调用 Points::deduct)、每日次数上限(0 = 不限)均在后台可配
  • 稀有度:N / R / SR / SSR 四档,仅作展示徽章与转盘配色,不影响概率
  • 库存:-1 = 不限库存(占用时原样保留,仍算占用成功);0 = 暂时抽空(自动退出可抽集合)
  • 记录:每次抽奖写一条流水(冗余奖品名/稀有度/成本,历史记录不随奖品编辑而漂移)
  • 前端:抽奖为 AJAX JSON 接口;转盘为 CSS conic-gradient 按权重生成扇区,

指针停靠位置由服务端定奖结果驱动(动画是表现,结果是权威);按钮禁用时给出具体原因,

抽完之后若余额不足或次数用尽,前端即时禁用按钮,不再让用户一路点进服务端错误

  • 布局:对齐全站「积分类应用页」范式(points_mall / task_center / community_bank)——

头部 h1.mn-fs-20.mn-fw-600 + 右侧积分 chip + 信息型说明句,正文拆成

转盘 / 奖品池 / 记录三张独立 .mn-section 卡片,分区块标题用核心 .mn-section-header

二、目录结构

plugins/lucky_draw/
├── plugin.json                 # 清单:4 钩子 + route:admin / system:settings 权限
├── Plugin.php                  # 主类:建表 DDL / 配置读写 / 奖品池 / 定奖 / 库存 / 记录 / 生命周期
│                               #   ⚠️ 文件末尾有 class_alias:目录名含下划线,不写会被核心静默跳过生命周期
├── AdminController.php         # 后台:配置保存 + 奖品池增删改(PRG + session flash)
├── FrontController.php         # 前台:转盘页 / 抽奖接口 / 记录页
├── hook/
│   ├── init_after.php          # marker 版本化门控的惰性建表(data/.lucky_draw_schema)
│   ├── route_register.php      # 前台路由
│   ├── admin_route_register.php# 后台路由(/admin 前缀由核心 group 自动补)
│   └── nav_plugin_links.php    # 左栏「应用」+ 移动端「应用」下拉入口
├── views/
│   ├── admin.php               # 后台:基本配置 Tab + 奖品池 Tab
│   ├── index.php               # 前台:转盘 + 奖品池 + 最近记录
│   └── records.php             # 前台:全部抽奖记录
├── assets/
│   ├── style.css               # 转盘 / 徽章 / 表单样式(零内联)
│   └── script.js               # 转盘生成与定奖动画、抽奖请求、后台 Tab、删除确认
├── data/                       # 插件独立库(.htaccess / web.config 由核心自动补写)
└── lang/{zh,zh_tw,en}.php      # 三语语言包(68 键,集合/键序一致)

三、数据表(插件独立库 data/lucky_draw.sqlite)

表用途关键列
ld_prizes奖品池weight 权重 / stock 库存(-1 不限)/ rarity / prize_type(points\
ld_records抽奖流水user_id / prize_id / prize_name(冗余快照) / cost_points / created_at,索引 (user_id, created_at)

全局设置(核心 settings 表,键前缀 lucky_draw_):lucky_draw_enabled / lucky_draw_cost / lucky_draw_daily_limit,缺省值收敛在 Plugin::DEFAULTS 一处。

四、安装 / 卸载

  • 启用:后台 → 插件 → 幸运转盘 → 启用。首次任一请求会经 init_after(marker 门控)建表并写入默认配置
  • 卸载:uninstall() 顺序 = 删两张表 → 删 data/.lucky_draw_schema marker → 清核心 settings 残留 → 双清缓存。

marker 必须与表同生共死:表删了 marker 还在的话,重装后 init_after 会判定「schema 已是最新」直接跳过建表,

之后整站 /lucky-draw 全是 no such table 的 500

  • 若手工删过表,也要同步删 data/.lucky_draw_schema

五、管理入口

  • 后台地址:/admin/lucky-draw(基本配置 + 奖品池两个页内 Tab)
  • 前台入口:左栏「应用」面板 / 移动端「应用」下拉 → /lucky-draw(未登录点击会先跳登录)

六、更新记录

版本内容
1.0.1修复「不限库存(stock=-1)的奖品永远抽不到」:扣库存语句 WHERE stock > 0 对 -1 的 rowCount() 恒为 0,被调用方判成「已被抢空」并退分,而后台新增奖品时 stock 默认值恰好就是 -1(开箱即坏)。改为 CASE 分支 + WHERE stock <> 0<br>修复「单次消耗配成 0 时每次抽奖都报积分不足」:Points::deduct() 首行 if ($points <= 0) return false;,免费抽奖必须跳过扣分<br>失败补偿改为「撤奖 → 还库存 → 退抽奖成本」逆序,堵住「记录写失败但奖品积分已到账」的白嫖路径<br>清页缓存改走核心 PageCache::invalidate():原先 glob(__DIR__.'/../data/runtime/pages/*.html') 指向插件自己的 data 目录,从未生效过<br>删除奖品的确认框从内联 onsubmit 改为 data-ld-confirm + script.js 绑定(审计 FH-INL-002)<br>抽奖按钮禁用时显示具体原因;抽完后余额不足/次数用尽即时禁用<br>卸载时同步删除 schema marker;后台 Tab 补齐 aria-selected / aria-controls;修复 ?edit= 找不到奖品时的未定义变量告警<br>前台布局对齐全站积分类应用页范式:头部改为 h1.mn-fs-20.mn-fw-600 + 图标 + 右侧积分 chip(原为居中自写 22px 标题),副标题换成信息型文案(「每次消耗 N 积分,今日还可抽 M 次」),stats 精简为两块,正文拆成转盘/奖品池/记录三张独立 .mn-section;抽奖按钮改用核心 mn-btn mn-btn-primary(前台没有 .btn 基类),警示条改用自定义 .ld-alert-warn(核心没有 .mn-alert-warning),「查看全部」改用 .mn-section-more
1.0.0首个版本:奖品池 / 权重定奖 / 库存 / 每日限额 / 中奖记录 / 三语

轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| 瀏覽 8 次 | 回覆 1 次

全部回覆 (1)

🌳Lv.4 中级 🌛 见习会员
2026-10-26 02:05:57
并发占库存那段 SQL 写得很好:`UPDATE … SET stock = CASE WHEN stock > 0 THEN stock - 1 ELSE stock END WHERE id = :id AND stock <> 0`,用 rowCount()=0 判断被抢光并自动退分——这是无锁扣库存的标准写法,比「先查库存再更新」可靠得多。 三点: 1. 积分奖品「即时到账」要放在同一个事务里,如果扣分成功但发奖失败(比如对方账号异常),必须能回滚。建议「扣分 + 占库存 + 发奖 + 写抽奖记录」四步一个事务。 2. 权重抽取用 random_int() 按累计权重,安全(不是 mt_rand)。但要防「权重全为 0」或「库存为 0 的奖品仍参与抽取」,抽取前应过滤掉不可用奖品再归一化。 3. 稀有度(N/R/SR/SSR)如果只是展示,没问题;如果影响概率,必须和权重保持一致,否则运营改了权重忘了改稀有度,用户会觉得概率不透明。建议把「综合概率」展示在抽奖页。 1.0.1 已是修正版,说明并发问题已经踩过一轮。
#1 樓

請 登入