本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:WWW/plugins/qa/说明.md。
问答社区(qa)插件说明
版本:1.0.0 | 作者:FlintHub | 状态:⛔ 未启用
功能:完整问答闭环——提问 / 回答 / 投票 / 采纳最佳答案 / 标签管理,含前台与后台两套页面。
一、功能简介
- 提问:标题 + 富文本正文(编辑器 base64 原文,显示时统一解码 + 净化)+ 标签多选
- 回答:对问题进行回答,支持 htmx 局部提交
- 投票:对问题与回答均可投「赞成 / 反对」,重复点击取消,不能给自己投票;
新增赞成票给内容作者 +1 积分(与核心帖子获赞口径一致)
- 采纳最佳答案:仅问题作者可采纳;采纳后给回答者发放积分(后台可配,默认 5),自问自答不奖励
- 标签体系:标签增删改 + 每标签问题计数自动刷新(改标签时新旧标签计数一并刷新,无残留)
- 列表四视图:最新(
newest)/ 最高票(votes)/ 待回答(unanswered)/ 热门(hot,按view_count + answer_count*3加权) - 后台管理:问题列表与编辑、删除、标签管理
- 前端交互:htmx 局部刷新(投票列整列交换,高亮由服务端渲染),零内联 JS
二、目录结构
plugins/qa/
├── plugin.json # 插件清单(8 个钩子 + admin_url + permissions: route:admin, system:settings)
├── Plugin.php # 主类:建表、提问/回答/投票/采纳/标签的业务逻辑(控制器与 CLI 共用)
├── QaController.php # 前台控制器(列表/提问/详情/回答/投票/采纳/标签)
├── AdminController.php # 后台控制器(问题管理 / 标签管理)
├── hook/
│ ├── init_after.php # 幂等建表兜底(缓存标记 protected/.qa_table_cache)
│ ├── route_register.php # 前台路由注册
│ ├── admin_route_register.php # 后台路由注册(需 route:admin)
│ ├── nav_main_links.php # 顶部主导航入口
│ ├── nav_plugin_links.php # 左栏「应用」/ 顶部「应用」下拉入口
│ ├── nav_user_menu_items.php # 用户下拉菜单入口(仅登录用户)
│ ├── layout_head_end.php # 【空实现,直接 return】样式由各视图 section('css') 自带
│ └── right_sidebar_after.php # 右栏「最新问题 / 热门问题 / 热门标签」(仅 /qa 前缀页面输出)
├── views/
│ ├── question/list.php # 问题列表(四 Tab + 分页)
│ ├── question/ask.php # 提问 / 编辑(共用,question 为 null 时是新增)
│ ├── question/show.php # 问题详情(问题卡 + 回答列表 + 回答表单)
│ ├── tag/list.php # 标签列表
│ └── admin/{index,edit,tags}.php # 后台问题列表 / 编辑 / 标签管理
├── assets/ # style.css / script.js(零内联)
├── data/ # qa.sqlite(独立库)+ .htaccess / web.config
└── lang/ # zh.php / en.php / zh_tw.php(各 83 键,三语零差异)三、数据表
插件独立库:plugins/qa/data/qa.sqlite
| 表 | 用途 |
|---|---|
qa_questions | 问题(user_id / title / content / view_count / vote_count / answer_count / accepted_answer_id / status) |
qa_answers | 回答(question_id / user_id / content / vote_count / is_accepted) |
qa_votes | 投票(user_id / target_type(q/a)/ target_id / vote;UNIQUE(user_id, target_type, target_id)) |
qa_tags | 标签(name UNIQUE / description / question_count) |
qa_question_tags | 问题 ↔ 标签关联(PRIMARY KEY(question_id, tag_id)) |
- 索引 8 个:
idx_qa_q_user/idx_qa_q_status/idx_qa_q_created/idx_qa_a_question/idx_qa_a_user/idx_qa_a_accepted/idx_qa_v_target/idx_qa_qt_tag status:1正常(前台仅展示status = 1的问题;后台可查看全部)- 级联删除:删问题会一并清理标签关联 / 该问题的投票 / 其下回答的投票 / 回答 / 问题本身;删标签会清理标签关联
- 建表:
activate()+init_after双保险;卸载:uninstall()按依赖顺序删五张表
四、路由
前台
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /qa | 问题列表(`?tab=newest\ |
| GET | /qa/ask | 提问页(需登录) |
| POST | /qa/ask | 提交问题(需登录 + CSRF + 限流) |
| GET | /qa/question/{id} | 问题详情(游客可看) |
| GET / POST | /qa/edit/{id} | 编辑问题(需登录,仅作者本人) |
| POST | /qa/question/{id}/answer | 提交回答(需登录 + CSRF + 限流) |
| POST | /qa/vote | 投票(需登录 + CSRF + 限流) |
| POST | /qa/accept/{id} | 采纳回答(需登录 + CSRF,仅问题作者) |
| GET | /qa/tags | 标签列表 |
| GET | /qa/tag/{name} | 某标签下的问题列表 |
后台
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /admin/qa | 问题管理列表 |
| GET | /admin/qa/edit/{id} | 问题编辑页 |
| POST | /admin/qa/edit/{id} | 保存修改(CSRF) |
| POST | /admin/qa/delete/{id} | 删除问题(CSRF,级联) |
| GET | /admin/qa/tags | 标签管理页 |
| POST | /admin/qa/tags/add /edit/{id} /delete/{id} | 标签增改删(CSRF) |
⚠️ 后台路由需声明
route:admin;写全局设置需system:settings。二者均已声明。
五、核心规则
限流(RateLimiter::hit)
| 动作 | 阈值 |
|---|---|
提问 qa_save | 每 小时 最多 5 次 |
回答 qa_answer | 每 小时 最多 20 次 |
投票 qa_vote | 每 分钟 最多 30 次(带 IP 维度) |
投票口径(Plugin::toggleVote())
- 入参
vote∈{1, -1, 0};0= 取消 - 重复点击同方向 = 取消(等价
vote = 0) - 不能给自己投票(
item.user_id === userId→ 拒绝) vote_count由SUM(qa_votes.vote)重算后回写,不做增量累加(避免漂移)- 仅当「最终
vote = 1且原状态 ≠ 1」时,给内容作者Points::award(…, 1, …, 'qa_q'|'qa_a')
采纳口径(Plugin::acceptAnswer())
- 仅问题作者可采纳(否则
err_permission) - 采纳前先把该问题下所有回答的
is_accepted清零,再置新的为 1,同步qa_questions.accepted_answer_id - 发放积分:
qa_accept_points(默认 5);自问自答不发放 - 回答列表排序:
is_accepted DESC, vote_count DESC, created_at ASC(采纳置顶)
配置项(核心 settings 表)
| 键 | 默认 | 说明 |
|---|---|---|
qa_accept_points | 5 | 采纳回答给回答者的积分 |
qa_per_page | 15 | 前台每页问题数 |
两键在
activate()时写入默认值(已存在则不覆盖)。
计数联动
- 提问 →
Settings::runtimeIncr('_qa_questions') - 回答 →
Settings::runtimeIncr('_qa_answers'),并重算qa_questions.answer_count - 标签计数 → 每次改标签后,对「旧标签 ∪ 新标签」逐个重算
question_count
六、注意事项
- ⚠️ 正文是编辑器 base64 原文,落库前不转义;显示时统一
Content::decode()解码 + 净化。
改动任何输出路径时务必确认净化层未被绕过
- ⚠️ 游客可浏览问题列表与详情,但提问 / 回答 / 投票 / 采纳均需登录
- ⚠️
layout_head_end.php是空实现(直接return):本插件所有页面都在各自视图里
通过 section('css') 加载样式,不需要全局注入,不要误删这个文件(plugin.json 里仍注册着)
- ⚠️ **
nav_main_links.php/nav_plugin_links.php/right_sidebar_after.php用
$_SERVER['REQUEST_URI'] 判前缀做高亮**,而核心只有 left_sidebar.php:100 调用
nav_plugin_links 时注入 __nav_uri,main.php:160/218 调用 nav_main_links /
nav_plugin_links 时不传任何参数。因此本插件采用 REQUEST_URI 是当前唯一可行写法,
在站点根目录部署下行为正确;若日后改为二级目录部署(BASE_PATH 非空),
前缀判断会因路径带前缀而失效——届时需核心侧统一为这几个钩子注入 __nav_uri,而非只改插件
- 当前状态:
qa_questions1 行 /qa_answers2 行 /qa_votes2 行 /qa_tags2 行 /
qa_question_tags 2 行;插件未启用(activated: false)