xnx_poll 帖子投票插件
为帖子添加内嵌投票功能。支持单选/多选、倒计时、实时结果展示、用户组权限控制,htmx 无刷新投票体验。
功能
- 发帖集成:在发布页右侧栏添加投票配置卡片(与「引用帖子」卡片同款样式)
- 投票类型:单选 / 多选(最多可选数) - 持续时间(小时,0=永久) - 结果可见性:投票后立即可见 / 结束后可见 - 允许修改投票、允许游客、允许参与的用户组(多选)
- 帖子展示:在帖子正文下方显示投票卡片
- 倒计时实时刷新(到期自动切换为结束态) - 已投票用户高亮已选项 - 总票数 + 参与人数统计 - 作者可提前结束、管理员可强制结束
- htmx 无刷新投票:fetch + FormData 提交,实时返回新卡片 + toast 提示
- 管理功能:
- 后台投票列表(按状态 tab 筛选:全部/进行中/已结束/已过期) - 查看参与记录(display_name + 所选项 + 投票时间) - CSV 导出 - 强制结束、删除(含级联清理)
- 多语言:zh-cn / zh-tw / en-us 三语言同步
- 级联清理:删帖自动级联清理投票主表/选项/记录
安装
- 复制
xnx_poll目录到plugin/ - 后台 → 插件管理 → 安装并启用
- 后台 → 投票设置 → 配置允许发起的用户组
- 发帖时勾选「开启投票」配置参数即可
路由
| 路由名 | URL | 说明 | |---|---|---| | poll_vote | ?poll-vote.htm | 投票提交(POST) | | poll_end | ?poll-end-{poll_id}.htm | 提前结束(POST) | | poll_refresh | ?poll-refresh-{poll_id}.htm | 刷新卡片(GET) | | poll_admin | ?plugin-poll_admin.htm | 后台投票管理(仅管理员) |
文件结构
plugin/xnx_poll/
├── conf.json
├── install.php / uninstall.php / upgrade.php
├── setting.php + view/htm/setting.htm
├── model/PollService.php
├── route/
│ ├── poll.php # 前台投票
│ └── admin_poll.php # 后台管理
├── hook/
│ ├── model_inc_file.php
│ ├── model_route_table_end.php
│ ├── index_route_case_end.php
│ ├── admin_index_route_case_end.php
│ ├── thread_create_thread_end.php # 发帖成功后创建投票
│ ├── model_thread_delete_end.php # 删帖级联清理
│ ├── post_ref_thread_after.htm # 桌面端发布页配置
│ ├── post_ref_thread_after_mobile.htm # 手机端发布页配置
│ ├── post_js.htm # 加载 poll_post.js
│ ├── thread_message_after.htm # 帖子正文下方投票卡
│ ├── footer_js_after.htm # 加载 poll.js
│ ├── lang_zh_cn_bbs.php
│ ├── lang_zh_tw_bbs.php
│ └── lang_en_us_bbs.php
├── view/htm/
│ ├── poll_card.htm # 投票卡模板(前台 + 路由共用)
│ ├── poll_admin.htm # 后台管理页
│ └── setting.htm # 后台设置页
├── static/js/
│ ├── poll_post.js # 发布页交互
│ └── poll.js # 投票卡交互(投票/结束/倒计时)
└── lang/
├── zh-cn.php
├── zh-tw.php
└── en-us.php 数据库表
xnx_poll(主表)
| 字段 | 类型 | 说明 | |---|---|---| | id | int UNSIGNED PK | 投票 ID | | tid | int UNSIGNED | 帖子 ID | | uid | int UNSIGNED | 发起人 | | title | varchar(255) | 投票问题 | | type | tinyint | 0=单选 1=多选 | | max_choices | tinyint | 多选时最多可选数 | | duration_hours | int | 时长(0=永久) | | expire_at | int | 到期时间戳(0=永久) | | result_visibility | tinyint | 0=投票后 1=结束后 | | allow_change | tinyint | 是否允许修改 | | allow_guests | tinyint | 是否允许游客 | | allow_groups | varchar(255) | 允许的用户组(逗号分隔) | | status | tinyint | 0=进行中 1=手动结束 2=过期 3=已删 | | participants_count | int | 参与人数(原子计数) | | create_time | int | 创建时间 | | end_time | int | 结束时间 |
xnx_poll_option(选项表)
| 字段 | 类型 | 说明 | |---|---|---| | id | int UNSIGNED PK | 选项 ID | | poll_id | int | 投票 ID | | label | varchar(255) | 选项内容 | | sort | int | 排序 | | votes_count | int | 票数(原子计数) |
xnx_poll_vote(投票记录表)
| 字段 | 类型 | 说明 | |---|---|---| | id | int UNSIGNED PK | 记录 ID | | poll_id | int | 投票 ID | | option_id | int | 所选项 ID | | uid | int UNSIGNED | 投票人 | | vote_time | int | 投票时间 | | UNIQUE KEY | (poll_id, option_id, uid) | 同一用户对同一选项只能投一次 |
关键技术点
- 版本号缓存:
xnx_poll_poll_{id}_v{v}+xnx_poll_poll_v_{id}双键机制避免长键哈希 - 原子计数:
UPDATE ... SET votes_count = votes_count + 1无并发问题 - CAS 防并发结束:
UPDATE xnx_poll SET status=1 WHERE id=? AND status=0防止重复结束 - HTMX 兼容:所有 hook 用
if 包裹(不用return;,hook 内 return 会从宿主函数退出) - htmx 4 事件名:冒号分隔格式(
htmx:config:request等) - 显示名:用
user_find_by_uids/user_read_cache取display_name,不用db_find('user') - CSRF:所有 POST 表单走
CsrfService::input()+CsrfService::check() - 多语言:双写
lang/.php+hook/lang__bbs.php(前台实际读 hook) - 卡片样式:统一
x-card(无 border + 阴影 + dark mode border)
已知限制
- 倒计时依赖 JS 在前台运行(无 cron 后台扫描过期)
- 删除投票仅清理数据库与缓存,不通过消息中心通知参与者
- 游客投票需开启 allow_guests 且未登录态可触发(按 IP 记录,故一行仅一票)
测试要点
- 后台启用插件、配置用户组权限
- 发帖创建单选/多选投票
- 已登录用户参与投票(已投提示拦截)
- 游客在开启游客权限时参与(按 IP 记录)
- 倒计时到期自动刷新卡片
- 作者提前结束、管理员后台强制结束
- 后台列表筛选(tab 切换)、查看记录、CSV 导出、删除
- 删主帖后级联清理(数据库 + 缓存)
版本 1.0.5 | 兼容 Xiuno 1.1+
版本:v1.0.5
适用版本:1.1
开发者:贰先生


