官方不提供现金交易,线下交易风险自担,请仔细鉴别!
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 三套语言包
安装说明
- 将插件目录放入
plugin/xnx_reward/ - 进入后台「插件管理」找到「悬赏插件」点击安装,自动建 2 张表并写入默认配置
- 安装后进入后台「插件设置」→「基本设置」勾选允许悬赏的版块与用户组,配置积分类型 / 有效期 / 手续费 / 过期处理方式 / 金额范围
- 卸载时会自动删表、删 setting、清插件缓存、注销 XnEvent 监听器
使用指南
发帖人发起悬赏
- 在被允许的版块点击「发帖」,发帖表单特殊主题按钮组出现「💰 悬赏设置」按钮(用户组不在白名单或版块不在白名单时不显示)
- 点击按钮展开悬赏配置面板,填写悬赏金额(受 min_amount / max_amount 限制)、选择积分类型、可选填悬赏留言(最多 200 字)
- 面板底部实时显示「发布后将扣除 X 积分」提示
- 提交帖子后,
thread_create_thread_end.phphook 拦截请求,调用RewardService::createReward()完成金额校验 + 积分锁定扣除 + 主表 / 日志表写入;积分不足、金额超范围、版块 / 用户组不允许时阻断发帖并提示错误
回答人参与悬赏
回答人无需特殊操作,按普通回帖方式在悬赏帖下回复即可。被采纳后积分自动入账,可在个人中心「我的悬赏 → 我获得的」查看记录。
发帖人采纳最佳答案
- 进入自己的悬赏帖详情页(第一页),每条非主帖、非自己发的回复旁会出现「采纳为最佳答案」按钮(仅楼主或管理员可见)
- 点击按钮弹出确认框,确认后 htmx POST 到
/reward-accept-{reward_id}-{pid} - 后端
RewardService::acceptAnswer()校验权限 / 状态 / 帖子归属,CAS 将 status 0→1,计算手续费后调用CreditsService::add()发放积分给被采纳人 - 前端通过
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=refund调CreditsService::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.htmPHP 端解析后注入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.php、lib/CacheHelper.php、CsrfService - 与
xnx_appcenter弱集成(可选,未安装不影响核心功能)
版本历史
- 1.0.3:当前版本,包含发帖悬赏、采纳最佳答案、过期处理、后台强制结束 / 手动退还 / 手动发放、删帖级联清理、个人中心列表、最佳答案卡片 + 作者应用展示、三语言包
版本 1.0.4 | 兼容 Xiuno 1.1+