悬赏插件

悬赏插件

在指定版块发帖时支持悬赏积分,采纳最佳答案后自动发放积分,过期自动退还

插件 v1.0.4 Twelve 积分 安全 付费 20 金币

xnx_reward 悬赏插件

在指定版块发帖时支持悬赏积分,发帖人采纳最佳答案后自动发放积分给回答人,过期未采纳自动退还发帖人;支持平台手续费、后台强制结束 / 手动退还 / 手动发放等干预能力。通过 20 个 hook 注入 Xiuno BBS 的发帖、列表、详情、个人中心、后台等环节,核心代码零硬编码插件名。

功能特性

  • 发帖悬赏:在发帖页「特殊主题」按钮组注入「悬赏设置」Tab,可填悬赏金额、积分类型、悬赏留言;提交后立即从发帖人账户锁定扣除积分
  • 版块 / 用户组白名单:后台勾选允许悬赏的版块 fid 与用户组 gid(空 = 全部允许),发帖表单与创建逻辑双重校验
  • 采纳最佳答案:楼主或管理员(gid=1,2)可在帖子详情页点击回复旁的「采纳为最佳答案」按钮,htmx 提交后自动发放积分;楼主不能采纳自己的回复
  • 平台手续费:可配置 0–50% 的手续费比例,采纳时按 floor(amount * fee_rate / 100) 扣除手续费,剩余金额发放给被采纳人,手续费记入 uid=0 的平台账户
  • 过期处理:悬赏有效期 1–720 小时,过期后按 expire_action 配置处理 —— refund 退还发帖人 / platform 归入平台不退还
  • 懒加载过期检查:帖子详情页 / 列表页访问时若命中过期悬赏,立即触发 processExpired 完成状态翻转 + 积分退还,无需依赖定时任务
  • 后台干预:管理员可「强制结束」(status 0→3,按 expire_action 处理积分)、「手动退还」(仅 status=3 可用,防重复退还)、「手动发放」(指定 pid 强制采纳,跳过楼主权限与「不能采纳自己」校验)
  • 删帖级联清理:删帖时若存在进行中的悬赏,自动退还积分并删除悬赏主表 + 日志表记录,避免僵尸数据
  • 个人中心:新增「我的悬赏」入口,含「我发布的」与「我获得的」两个 Tab,分页展示
  • 最佳答案卡片:采纳后在帖子详情页顶部展示被采纳回复内容 + 被采纳作者头像 / 昵称 / 实际获得金额;若安装了 xnx_appcenter 插件,额外展示该作者按下载量降序的 6 个应用
  • 列表页防 N+1:帖子列表用 static 变量 + getByTids 一次性 IN 查询批量加载本页所有悬赏数据
  • 多语言:内置简体中文 / 繁体中文 / English 三套语言包

安装说明

  1. 将插件目录放入 plugin/xnx_reward/
  2. 进入后台「插件管理」找到「悬赏插件」点击安装,自动建 2 张表并写入默认配置
  3. 安装后进入后台「插件设置」→「基本设置」勾选允许悬赏的版块与用户组,配置积分类型 / 有效期 / 手续费 / 过期处理方式 / 金额范围
  4. 卸载时会自动删表、删 setting、清插件缓存、注销 XnEvent 监听器

使用指南

发帖人发起悬赏

  1. 在被允许的版块点击「发帖」,发帖表单特殊主题按钮组出现「💰 悬赏设置」按钮(用户组不在白名单或版块不在白名单时不显示)
  2. 点击按钮展开悬赏配置面板,填写悬赏金额(受 min_amount / max_amount 限制)、选择积分类型、可选填悬赏留言(最多 200 字)
  3. 面板底部实时显示「发布后将扣除 X 积分」提示
  4. 提交帖子后,thread_create_thread_end.php hook 拦截请求,调用 RewardService::createReward() 完成金额校验 + 积分锁定扣除 + 主表 / 日志表写入;积分不足、金额超范围、版块 / 用户组不允许时阻断发帖并提示错误

回答人参与悬赏

回答人无需特殊操作,按普通回帖方式在悬赏帖下回复即可。被采纳后积分自动入账,可在个人中心「我的悬赏 → 我获得的」查看记录。

发帖人采纳最佳答案

  1. 进入自己的悬赏帖详情页(第一页),每条非主帖、非自己发的回复旁会出现「采纳为最佳答案」按钮(仅楼主或管理员可见)
  2. 点击按钮弹出确认框,确认后 htmx POST 到 /reward-accept-{reward_id}-{pid}
  3. 后端 RewardService::acceptAnswer() 校验权限 / 状态 / 帖子归属,CAS 将 status 0→1,计算手续费后调用 CreditsService::add() 发放积分给被采纳人
  4. 前端通过 HX-Trigger: xnxRewardAccepted 响应头 + htmx:after:swap 双重触发 toast 提示并延迟 1.5 秒刷新页面,刷新后顶部显示最佳答案卡片

过期退还

  • 悬赏创建时按 expire_hours 计算过期时间(0=不过期)
  • 帖子详情页 / 列表页访问时若检测到悬赏已过期且仍为进行中状态,立即触发 processExpired():CAS 将 status 0→2,若 expire_action=refund 则退还积分给发帖人,写 type=2 退还日志
  • expire_action=platform,过期后积分不退还,归入平台

后台管理

访问 /admin/plugin-setting-xnx_reward(基本设置)与 /admin/?reward-list.htm(悬赏列表):

| Tab | 路径 | 用途 | |---|---|---| | 基本设置 | ?plugin-setting-xnx_reward | 配置版块 / 用户组白名单、积分类型、有效期、手续费、过期处理、金额范围 | | 悬赏列表 | ?reward-list.htm | 查看所有悬赏记录,按状态 / 版块 / UID 筛选,含帖子链接、发帖人、金额、状态、过期时间、最佳答案 pid |

列表中对进行中(status=0)的悬赏可执行「强制结束」和「手动发放」;对已关闭(status=3)的悬赏可执行「手动退还」。所有 POST 操作经 CsrfService::check() CSRF 校验。

技术细节

Hook 注入点(共 20 个)

| Hook 文件 | 注入位置 | 作用 | |---|---|---| | model_inc_file.php | 全局 model 加载 | 注册 RewardService.php 到类加载列表 | | model_route_table_end.php | 路由表生成末尾 | 注册前台 / 后台 reward 路由 | | index_route_case_end.php | 前台路由 switch 末尾 | case 'reward' 分发到 route/reward.php | | admin_index_route_case_end.php | 后台路由 switch 末尾 | case 'reward' 分发到 route/admin_reward.php | | thread_create_thread_end.php | 帖子创建后 | 拦截悬赏字段,校验后创建悬赏 + 锁定扣除积分 | | thread_info_start.php | 帖子详情页加载开始 | 加载 $reward_data(含懒加载过期检查) | | model_thread_delete_end.php | thread_delete() 函数末尾 | 删帖级联清理:进行中悬赏退还积分 + 删表 | | post_special_topic_tab_nav.htm | 发帖页特殊主题按钮组 | 输出「悬赏设置」按钮 | | post_special_topic_tab_pane.htm | 发帖页特殊主题 Tab 容器 | 输出悬赏金额 / 积分类型 / 留言表单 | | thread_subject_after.htm | 帖子详情页标题后 | 输出 💰 悬赏金额 badge | | thread_list_inc_subject_after.htm | 帖子列表标题后 | 用 static + getByTids 批量输出 💰 badge | | post_list_inc_subject_after.htm | 回复列表标题后 | 输出「采纳为最佳答案」按钮(htmx) | | post_list_inc_username_after.htm | 回复列表用户名后 | 被采纳回复输出「已采纳」角标 | | thread_postlist_before.htm | 帖子详情页回复列表前 | 占位 hook(保留扩展用) | | thread_message_actions_before.htm | 帖子详情页正文底部 | 互斥渲染悬赏卡片 / 最佳答案卡片 | | user_nav_end.htm | 个人中心导航末尾 | 注入「我的悬赏」入口 | | footer_js_after.htm | 页面 footer JS 后 | 发帖页 / 详情页加载 reward.js + 注入 i18n | | lang_zh_cn_bbs.php | 简体中文语言包 | 注入所有 xnx_reward_* 翻译 | | lang_zh_tw_bbs.php | 繁体中文语言包 | 注入所有 xnx_reward_* 翻译 | | lang_en_us_bbs.php | 英文语言包 | 注入所有 xnx_reward_* 翻译 |

状态机

[0 进行中] ──采纳──► [1 已采纳]   (发放积分给被采纳人,扣手续费)
     │
     ├──过期──► [2 已过期]         (按 expire_action 退还/归平台)
     │
     └──后台强制结束──► [3 已关闭]  (按 expire_action 处理积分)
                              │
                              └──后台手动退还──► (仅 status=3 可退还,写 type=2 日志,status 不变)

所有状态翻转均用 CAS(db_update WHERE id=? AND status=0)防止并发重复处理。

积分发扣机制

依赖 lib/CreditsService.php(内部自带事务 + 行锁):

  • 创建悬赏CreditsService::sub() 锁定扣除 → 外层 START TRANSACTION 包写主表 + 日志 → 失败则 ROLLBACK 并补偿 CreditsService::add() 退还(因 CreditsService 内部已 COMMIT 无法回滚)
  • 采纳发放:CAS 翻转 status 0→1 → CreditsService::add() 发放 amount - fee_amount 给被采纳人 → 发放失败则回滚 CAS 复位 status=0 → 写 type=1 发放日志 + type=3 手续费日志(uid=0)
  • 过期退还:CAS 翻转 status 0→2 → 若 expire_action=refundCreditsService::add() 退还发帖人 → 写 type=2 退还日志
  • 删帖退还:进行中悬赏直接 CreditsService::add() 退还(reason=reward_delete_refund 区分场景)→ 写 type=2 日志 → 删主表 + 日志表
  • 后台强制结束:CAS 翻转 status 0→3 → 按 expire_action 处理积分
  • 后台手动退还:仅 status=3 可用,检查是否已有 type=2 日志防重复 → CreditsService::add() 退还 → 写 type=2 日志,status 保持 3
  • 后台手动发放:跳过楼主权限与「不能采纳自己」校验,复用 doAccept() 内部逻辑

缓存策略

  • CacheHelper::registerKeys() 注册 4 类缓存键,单帖悬赏数据缓存 300 秒,用户列表版本号缓存 86400 秒
  • 任何数据变更(创建 / 采纳 / 过期 / 后台干预 / 删帖)均调 clearCache() 清整个插件缓存前缀
  • 用户列表(我发布的 / 我获得的)用版本号机制:数据变更时 bumpUserVersion($uid) 递增版本号,旧版本缓存自动失效
  • 版本号用哨兵格式 array('__v'=>$v) 写入,避免被 remember() 判 MISS 导致 bump 永久失效

前端交互

  • 发帖页 reward.js:监听 fid 下拉框 change 动态显隐悬赏卡片(白名单版块校验,fid=0 占位符时不隐藏)、监听金额 / 积分类型变化实时更新扣除提示
  • 详情页 reward.js:监听 htmx:after:swap(#xnx_reward_card 替换)+ xnxRewardAccepted 自定义事件(HX-Trigger 响应头),2 秒内去重,toast 提示后延迟 1.5 秒刷新页面
  • 翻译字符串由 footer_js_after.htm PHP 端解析后注入 window.XNX_REWARD_I18N(static/js 是纯静态文件不会被 PHP 解析)
  • 后台手动发放用 Bootstrap 5 Modal 收集 pid,单按钮操作(close / refund)用统一隐藏表单 + XN.confirm 确认

与 xnx_appcenter 的弱集成

reward_best_answer.htm 检测 xnx_appcenter 插件是否存在,存在则查询被采纳作者的 6 个应用(按 download_count 降序,status=1)展示在最佳答案卡片下方,含应用图标 / 名称 / 简介 / 下载量,点击跳转应用详情。未安装 xnx_appcenter 时该区域不渲染。

数据表

| 表名 | 用途 | 关键字段 | |---|---|---| | xnx_reward | 悬赏主表(一条帖子一条悬赏) | id, tid(UNIQUE), fid, uid, credits_type, amount, message, expire_at(0=不过期), status(0进行中/1已采纳/2已过期/3已关闭), best_pid, best_uid, best_time, fee_amount, created;KEY uid/fid/status/best_uid | | xnx_reward_log | 积分流水表 | id, reward_id, uid, type(0锁定扣除/1发放/2退还/3手续费), credits_type, amount, created;KEY reward_id/uid |

所有表使用 utf8mb4 字符集 + InnoDB 引擎。

兼容性

  • Xiuno BBS 1.1+
  • PHP 8.x(全局数组访问已加 isset 兜底)
  • Bootstrap 5(后台 Modal / Tab / 表单)
  • htmx(采纳交互,需核心已加载 htmx.js)
  • XN.toast / XN.confirm(xiuno-modern.js 提供)
  • 原生 JS,无 jQuery 依赖
  • 依赖 lib/CreditsService.phplib/CacheHelper.phpCsrfService
  • xnx_appcenter 弱集成(可选,未安装不影响核心功能)

版本历史

  • 1.0.3:当前版本,包含发帖悬赏、采纳最佳答案、过期处理、后台强制结束 / 手动退还 / 手动发放、删帖级联清理、个人中心列表、最佳答案卡片 + 作者应用展示、三语言包

版本 1.0.4 | 兼容 Xiuno 1.1+

v1.0.4 2026-08-03
作者信息
XiunoX 开发者,独立开发者 & 全栈工程师 DiscuzX 插件/模板、WordPress 主题/插件、XiunoX 框架二次开发、APP 与小程序自研项目。
27
应用
0
安装
2026-08
加入
应用详情
应用分类 插件
版本 v1.0.4
适用版本 1.1
开发者 Twelve
下载量 0
浏览 3
更新时间 2026-08-03
发布时间 2026-08-03
最新回复
查看全部
  • 暂无回复

查看全部评论 / 发表评论
完整评论功能在讨论帖中,点击前往