[开发]积分商城(points_mall)插件说明

👑Lv.11 元老 🌞 骨干会员
2026-10-25 01:15:07

本文由 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 插件发放(插件间零直接类调用)
vipVIP 用户组经 \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/front
  • profile_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_products 0 行(商城为空,需先在后台添加商品);pm_categories 4 行;pm_redemptions 0 行
轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| 浏览 4 次 | 回复 0 次

全部回复 (0)

暂无回复

请 登录