本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:WWW/plugins/points_mall/说明.md。
积分商城(points_mall)插件说明
版本:1.0.0 | 作者:FlintHub | 状态:✅ 已启用
功能:把用户积分兑换为头衔 / 勋章 / VIP 用户组 / 程序源码四类虚拟商品;
支持商品分类、库存、冷却时间、防重复购买,并可在帖子正文内嵌商品卡片(原地兑换)。
一、功能简介
- 四类商品(
pm_products.type)
| 类型 | 说明 | 发放动作 |
|---|---|---|
title | 头衔 | 写入 pm_redemptions.granted_title,在帖子楼层 / 博客详情 / 个人主页用户名旁展示 |
medal | 勋章 | 经钩子 points_mall_medal_redeem 通知 medal 插件发放(插件间零直接类调用) |
vip | VIP 用户组 | 经 \app\Models\User::updateGroupId() 改核心 users.group_id |
source | 程序源码 | 生成 16 字节随机下载令牌,走 /shop/download/{token} 下载 |
- 商品分类:
pm_categories支持排序、上下移动、停用;删除分类前校验该分类下是否仍有商品 - 兑换四道闸门:防重复(唯一索引)→ 冷却时间 → 库存 → 积分余额,任一不过即拒绝
- 帖子内嵌卡片:正文里写
/shop/item/{id}链接(或裸 URL),渲染时自动替换为商品卡片,
卡片内可原地 AJAX 兑换,无需跳转
- 头衔展示:兑换
title类商品后,头衔自动出现在帖子楼层、博客详情、个人主页 - 后台管理:商品增删改查 + 分类管理 + 兑换记录查看
二、目录结构
plugins/points_mall/
├── plugin.json # 插件清单(7 个钩子 + admin_url + permissions: route:admin)
├── Plugin.php # 主类:建表/迁移、商品与分类查询、兑换核心事务、卡片渲染、生命周期
├── FrontController.php # 前台:商品列表 / 详情 / 兑换 / 源码下载
├── AdminController.php # 后台:商品与分类的增删改查、图片与源码上传
├── hook/
│ ├── init_after.php # 幂等建表兜底(缓存标记 protected/.points_mall_table_cache)
│ ├── route_register.php # 前台路由注册
│ ├── admin_route_register.php # 后台路由注册(需 route:admin)
│ ├── nav_plugin_links.php # 左栏「应用」/ 顶部「应用」下拉入口
│ ├── layout_head_end.php # 仅在展示头衔的页面注入插件 CSS
│ ├── controller_view_before.php # ① 批量注入头衔 ② 正文 /shop/item/{id} → 商品卡片
│ └── profile_user_card_after.php # 个人主页用户卡片头衔展示
├── views/
│ ├── shop.php # 前台商品列表(含分类筛选)
│ ├── item.php # 前台商品详情(分享落地页)
│ └── admin.php # 后台管理页(商品列表·兑换记录 / 分类管理 / 新增商品 三 Tab)
├── assets/ # style.css / script.js(零内联)
└── lang/ # zh.php / en.php / zh_tw.php(各 106 键,三语零差异)三、数据表
插件独立库:plugins/points_mall/data/points_mall.sqlite
| 表 | 用途 |
|---|---|
pm_products | 商品(name / description / image / points / stock / type / target_value / cooldown_hours / is_active / category_id) |
pm_categories | 商品分类(name / sort_order / is_active) |
pm_redemptions | 兑换记录(product_id / user_id / points_cost / granted_title / download_token) |
- 索引:
idx_pm_red_unique(pm_redemptions(product_id, user_id)唯一,防重复购买的最后防线)、idx_pm_cat - 建表:
activate()+init_after双保险(幂等 DDL) - 旧库升级:
ensureCategoryColumn()用PRAGMA table_info探测后ALTER TABLE ADD COLUMN,不丢数据 - 卸载:
uninstall()删三张表
四、路由
前台
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /shop | 商品列表(支持分类筛选) |
| GET | /shop/item/{id} | 商品详情页(卡片分享落地页) |
| POST | /shop/redeem | 提交兑换(CSRF + 登录) |
| GET | /shop/download/{token} | source 类商品的源码下载(登录 + 令牌校验) |
后台
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /admin/points-mall | 管理页(商品列表 · 兑换记录 / 分类管理 / 新增商品) |
| POST | /admin/points-mall/add /edit /delete | 商品增改删(CSRF) |
| POST | /admin/points-mall/category/add /edit /delete /move | 分类增改删与上下移动(CSRF) |
⚠️ 后台路由需在
plugin.json声明route:admin(已声明)。本插件不写全局 settings,故无需system:settings。
五、核心规则
兑换流程(Plugin::redeem())
参数校验 → 商品存在且上架 → ① 防重复(hasRedeemed)
→ ② 冷却(cooldown_hours,比对用户最近一次兑换时间)
→ ③ 库存(stock:-1 不限,0 售罄)
→ ④ 类型前置校验(medal 需 medal 插件已激活)
→ ⑤ 扣积分(Points::deduct,原子防透支)
→ ⑥ 按类型发放
→ ⑦ 写兑换记录 + 扣库存(同库事务 + rowCount 断言)- 防重复:同一用户对同一商品一生只能兑换一次(
pm_redemptions唯一索引兜底并发双击) - 冷却:
cooldown_hours > 0时,按「用户最近一次兑换(任意商品)」计算间隔,不足则拒绝。
与防重复不冲突——防重复管「一生一次」,冷却管「兑换间隔」
- 库存:
stock = -1不限量;stock = 0售罄;扣减用原子UPDATE ... WHERE stock > 0+rowCount()检查防超卖 - 失败退款:发放失败或事务冲突时,自动退回积分(
Points::award),不写兑换记录 - 源码下载令牌:
bin2hex(random_bytes(16)),32 位十六进制,存pm_redemptions.download_token
帖子内嵌卡片(hook/controller_view_before.php)
- 仅
thread/show视图生效 - 两路匹配:
<a href="...shop/item/{id}...">整段(自动链接生成)+ 裸文本 URL(富文本编辑器内容) - 替换范围:主帖渲染内容
_renderedContent、摘要_threadExcerpt、各楼posts[*]._rendered - 商品不存在或已下架 → 保留原链接,不替换
- 卡片兑换表单带
data-pm-redeem标识,由assets/script.js接管原地渲染
头衔注入
controller_view_before:批量(一次IN查询)注入$data['pmTitles'],覆盖thread/show/blog/detail/plugins/user_profile/frontprofile_user_card_after:个人主页用户卡片单独走getUserTitles()
六、注意事项
- ⚠️ 依赖核心积分体系:
Points::deduct()/Points::award(),以及settings._runtime_total_points运行时计数 - ⚠️
vip类型依赖核心user_groups表,且必须经User::updateGroupId()写入(禁止裸 SQL 改users) - ⚠️
medal类型依赖 medal 插件;未激活时兑换直接拒绝(错误键err_medal_off),发放经钩子points_mall_medal_redeem解耦 - ⚠️ 商品图 / 源码文件走核心
Upload上传,落盘在站点assets/uploads/(非插件目录) - ⚠️ 建表缓存标记在
protected/.points_mall_table_cache:手工删表后需一并删该标记,否则不会重建 - ⚠️ 本插件目录名含下划线,核心
Plugin::activate/deactivate/uninstall()的类名推导("\\Plugin\\{$name}\\Plugin")
会拼出 \Plugin\points_mall\Plugin 而无法命中 \Plugin\PointsMall\Plugin → 已在 Plugin.php 末尾用
class_alias 补别名,使核心生命周期回调恢复正常(否则卸载时数据表不会被删除)
- 当前状态:
pm_products0 行(商城为空,需先在后台添加商品);pm_categories4 行;pm_redemptions0 行