[survey_poll]问卷与投票热
- 问卷与投票ID survey_poll版本 11.1.0插件制作者 cxk123售价 30 积分7210 行 / 432.9 KBHook / 路由 / 后台页三分钟做出一份能收上来、能看懂、能导出的问卷或投票:单选、多选、评分、填空与矩阵评分(逐行统计)、图片链接,支持匿名、游客填写、多条件 AND/OR 显示逻辑与跳转到指定题、分页作答(长问卷每页 N 题,翻页自动存草稿、回来接着填)、截止与回收目标(距截止不足设定小时会自动提醒发起人),题目可上移/下移/复制、可单题存进题库并插入任意问卷,选项可随机(同一答卷内顺序稳定)并支持逐选项名额与分组共享名额,结果页支持交叉分析与两份问卷并排对比、单选/多选/评分三题型带纯内联 SVG 图表(环形图 / 百分比堆叠条 / 评分仪表,零外部依赖、零新增查询)、可逐份回看任意一份答卷的逐题作答、可生成只读结果快照分享链接(随时撤销),导出支持宽表 / 长表 / 交叉表 / 按题汇总四种 CSV,发布后改题会自动留版本快照(可看历史与逐题对比、不回溯已收数据),发起人还能看到答卷质量审计(秒填 / 同设备多份 / 直线作答的可疑计数与清单)并按关键词检索文字作答、查看跨问卷的「我的作答档案」,问卷可设「截止前允许填答者撤回或修改自己的作答」(默认一人一次不可改),后台另有数据自检与一键修复,并提供只读聚合 JSON 接口(token 可撤销、按分钟限频、只出聚合不出身份)。收费插件安装说明购买后点击“授权码”获取一次性授权码;在后台 > 插件广场点击在线安装或更新,并填写该授权码。已购买用户更新免费:每次在线安装或更新前,重新点击“授权码”获取新的一次性授权码即可,无需再次购买;也可直接下载新版后用后台“插件上传”更新。尊重并感谢他人劳动,请勿传播代码。app/plugins/survey_poll/plugin.php插件预览5 张开发日志最新 10 条
此项目接受所有登录用户协助维护和提交更新。
主楼 版本 1.0.0 更新:
1.0.0 首次发布
· 五种题型:单选、多选(可设上限)、评分(1-5 星)、填空、矩阵评分,一份问卷最多 10 题
· 作答校验:必答、选项白名单、多选上限、矩阵逐行评分,错误提示精确到「第 N 题」
· 生命周期:草稿 → 发布 → 结束;到点或收满目标份数自动结束;发布后题目冻结,防止样本被污染
· 一人一次:登录用户按账号去重,游客按设备去重(只保存哈希,不记录原始 IP)
· 匿名作答:可选,开启后不记录填写者;与「一人一次」互斥,界面已明示
· 结果页:条形图、评分分布与平均分、答卷明细分页;三种可见性(提交后立即 / 结束后 / 仅发起人)
· 导出:CSV 一行一份答卷,带题目标题,矩阵题合并为「行名:分数」
· 主题内嵌:发帖时挂上问卷,帖子内直接投票,版块与首页列表显示「投票中 · N 人参与」
· 后台:概览、立即结束到期问卷、允许游客/默认匿名/结果公开等 10 项设置插件质量报告
插件:
survey_pollHook 功能范围 循环 使用频率 文件读写 修改系统表 读写系统表 读写自己的表 page.footer 全站页面页脚 否 5 - - - - topic.after_render 首页/版块列表/主题查看逐条渲染 是(列表跳过) 5 - - - 写 plugin_survey_poll_forms, plugin_survey_poll_answers, plugin_survey_poll_items, plugin_survey_poll_questions, plugin_survey_poll_drafts, plugin_survey_poll_templates, plugin_survey_poll_shares, plugin_survey_poll_versions, plugin_survey_poll_api_tokens 修正❗️ topic.index_data.loaded 首页/版块/用户主题列表查询后 否 5 - - - 读 plugin_survey_poll_forms, plugin_survey_poll_answers topic.title_suffix 首页/版块主题列表逐条标题 是 5 - - - - topic.after_save 主题保存后 否 3 - - - 写 plugin_survey_poll_forms 颜色说明:黄色表示有数据读写,红色表示 Hook 在系统循环中执行,浅蓝色表示有文件写入。“列表跳过”表示回调在列表路径会提前返回,“回调内”表示操作位于回调自身或其帮助函数的循环体内。“文件批处理”表示逐个处理用户一次提交的文件,属于必要操作,不标记修正。只有操作实际处于需要优化的循环路径时,才会标记“修正❗️”。
数据字典
plugin_survey_poll_forms字段 类型 可空 默认值 约束 - 动态定义 - - 无法静态解析 plugin_survey_poll_questions字段 类型 可空 默认值 约束 - 动态定义 - - 无法静态解析 plugin_survey_poll_answers字段 类型 可空 默认值 约束 - 动态定义 - - 无法静态解析 plugin_survey_poll_items字段 类型 可空 默认值 约束 - 动态定义 - - 无法静态解析 plugin_survey_poll_templates字段 类型 可空 默认值 约束 - 动态定义 - - 无法静态解析 plugin_survey_poll_shares字段 类型 可空 默认值 约束 - 动态定义 - - 无法静态解析 plugin_survey_poll_versions字段 类型 可空 默认值 约束 - 动态定义 - - 无法静态解析 plugin_survey_poll_api_tokens字段 类型 可空 默认值 约束 - 动态定义 - - 无法静态解析 plugin_survey_poll_drafts字段 类型 可空 默认值 约束 - 动态定义 - - 无法静态解析 版本 2.0.0 更新:
2.0.0 更新
· 新增「图片链接」题型:可以让填写者上传现场照片或粘贴图片地址,支持站内附件上传,危险链接会被直接丢弃
· 新增「复制为新问卷」:一键把已有问卷(含全部题目与选项)复制成新草稿,改一改就能复用,不用重新录入
· 修复:列表页在设置了「回收目标」的问卷上会逐行查询已收份数(已改为整页一次批查,行渲染零查询)
· 修复:主题投票角标给每个列表页增加 2 条查询(改为写入时维护开关,站点没有主题问卷时零开销;首页查询量回到 6 条)
· 修复:发布问卷现在会正确触发对外的发布事件,供「关注与动态」等插件消费版本 2.1.0 更新:
2.1.0 更新
· 修复:站点存在「挂在主题上的问卷」时,版块与首页列表会无条件多做两次预载查询(与行数无关但白花)——现在与角标开关共用同一个标记,没有主题问卷时首页查询量回到 6 条
· 完善:验收套件拆分为三个专项套件(题型逻辑 38 项 / 跨插件联动 20 项 / 性能与审计 15 项),加上原有总套件共 127 项自动化检查,覆盖六种题型逐项校验、发布冻结、截止与回收目标、级联删除、主题内嵌与角标、数据量放大 25 倍的查询量不变、以及两个静态审计脚本的结论版本 2.1.1 更新:
2.1.1 更新
· 按 Hook 审计表修正:主题列表行标题钩子(每行调用)不再读任何数据表,改为只读整页预载缓存;没有预载数据时不显示角标,而不是退化成逐行查询
· 站点「是否存在主题问卷」的开关改为写路径维护 + 读路径自愈,避免标记过期导致角标静默消失
· 验收补充:三个专项套件新增「列表页查询量不随行数增长」「渲染钩子零查询」相关断言版本 3.0.0 更新:
3.0.0 更新
· 新增「跳题逻辑」:题目可设「仅当第 N 题选择了某选项时才出现」(只支持一跳),作答页自动显隐,服务端按同一条件跳过校验与入库,必答只在题目真正出现时生效
· 新增「我的作答」:填写过的人可以在结果页看到自己提交的每一题答案;即使发起人关闭了聚合结果的公开可见,参与者依然能看到自己的作答
· 修复:列表行投票角标改由列表行渲染钩子输出(读整页预载缓存、零数据库访问),解决部分列表场景下角标不显示的问题
· 修复:题目逻辑字段此前只建了列、没有任何入口与效果,现在编辑页有「显示条件」输入框并全程生效版本 4.0.0 更新:
4.0.0 更新(矩阵逐行统计、结束通知、题目重排与导出升级)新增
① 矩阵评分题按行给结果,每行单独显示分布与平均分
② 问卷结束通知:到点/收满后通知发起人,可选择同时通知所有填答者
③ 题目支持上移/下移/复制,跳题条件自动跟随
④ 导出升级:矩阵题每行单列、匿名问卷不再导出填写者列修复
⑤ 修复 5 处:矩阵只给混合平均分;跳题条件失效会静默隐藏题目(现在校验并提示);列表角标可能重复或整个消失(整页预载取错主题 id);只有「我的作答」时会渲染他人明细(越权);匿名问卷认不出本人作答版本 5.0.0 更新:
【5.0.0】从「收数据」到「看懂数据」:交叉分析 / 模板库 / 结果快照分享 / 选项名额与防刷一、新增
- 交叉分析(结果页)
- 结果页多了一块「交叉分析」:选一题做「分组维度」、再选一题做「统计题目」,一键生成交叉表;
每行右侧给出合计,多选参与时表头会写成「组合合计」。
- 只发一条聚合查询;没选参数时一条都不发,所以默认结果页的查询量与 4.0.0 完全一样。
- 矩阵题、评分题、单选题、多选题都能参与;权限沿用结果页原有的可见性规则。
- 问卷模板库(新页面)
- 编辑页新增「另存为模板」;模板库页
?a=survey_template列出自己的模板(管理团队看全站),
每行可「以模板建问卷」(一键生成草稿)或「用模板预填表单」。
- 模板里保存题干、题型、选项、必答、多选上限、跳题条件与选项名额,与问卷结构同构。
- 每人模板数量上限由后台配置
template_max控制(1–200)。
- 结果快照分享(只读链接)
- 结果页可生成一条只读分享链接,随时可撤销;有效期由后台配置
share_days控制(0 = 长期)。 - 分享页只展示聚合统计(条形 / 评分 / 交叉),不含逐份答卷、填写者身份,也没有导出入口。
- 链接 token 为 32 位随机 hex;撤销后旧链接立即失效。面板只在
?panel=share时查询。
- 选项名额与防刷
- 题目可逐选项设置「名额上限」(编辑页第 4 个输入框,与选项一一对应,逗号分隔,0/空 = 不限)。
- 答题页显示「剩余 N 个名额」;某个选项满额后直接置灰不可选并标「名额已满」,提交时也会再拦一次。
- 新增后台配置
daily_limit(0–100,0 = 关闭):开启后按设备指纹限制同一设备的每日提交次数。
二、修复
- 模板库里每个模板都显示「0 题」、题目摘要空白(点进去建出来的问卷题目却是齐的)——
列表查询漏取了模板的题目 JSON 列,现已补上,模板列表恢复正常。
- 旧库升级后打开模板库 / 分享页报错——读取函数补上 schema 兜底。
- 以模板建问卷时题目顺序与来源不一致——补回排序字段。
- 复制问卷后选项名额丢失——复制分支现在会一并搬运名额。
- 已填过的页面仍多发一条名额查询——按页面状态短路,不再多发。
三、性能
- 交叉分析、名额校验、分享页都是整批一条查询,页面请求路径上零循环内查库。
- 无配额的问卷在答题页不会因为新功能多查一次;交叉块不带参数时不查询;分享面板非分享模式不查询。
- 实测查询数:列表页 6、答题页 5(有配额 6)、结果页 9、交叉页 10、模板库 3、分享页 5、首页 8,
数据量翻 25 倍后不变。
四、已知行为(请先读这三条)
- 多选交叉的合计是「选项组合数」,不是人数。 分组题或统计题是多选时,一份答卷会同时落进多个格子,
所以行/列合计会大于答卷份数;表头会写成「组合合计」并给出说明。单选题 × 单选题时合计恰好等于答卷数。
- 名额是软上限。 判定是「先查后插」,没有加事务/行锁;极端并发(同一秒多人抢同一个满额选项)
可能超出 1–2 人。需要硬约束请用既有的「收满即结束」机制:把问卷的目标份数
target设上,收满自动结束。- 设备指纹的时序。
daily_limit = 0时系统不写任何设备标识(隐私取向);
因此事后才开启每日上限时,历史答卷没有指纹,当天的计数从「开启那一刻」重新起算。
五、升级说明
- 原地升级,无需卸载重装;老库第一次读写时会自动建表/补列/补索引,重复安装幂等。
- 新增两张表:
plugin_survey_poll_templates(模板)、plugin_survey_poll_shares(分享链接)。 - 新增两列:
questions.quota_json(逐选项名额)、answers.device_key(设备指纹)。 - 新增三项后台配置:
daily_limit(0–100)、share_days(0–365)、template_max(1–200);
这次起后台配置表单是「没提交的键沿用当前值」,旧配置不会被新字段覆盖成默认值。
- 未设置名额、未开启日限的问卷行为与 4.0.0 完全一致。
版本 6.0.0 更新:
【6.0.0】收到的数据要能信、长问卷要能填、问卷之间要能比:答卷质量审计 / 分页作答 / 结果对比 / 题库单题复用一、新增
- 答卷质量审计(结果页,只给发起人)
- 提交时记录「填写时长」与三类可疑标记:秒填、同设备多份、直线作答(三点以上选择类作答全落同一项)。
- 结果页给发起人一块「答卷质量审计」:三类各多少份 + 命中合计,并可按「查看可疑答卷」看清单
(最多最近 200 份,只给发起人与管理团队)。可疑答卷不会被自动剔除,统计里仍然计入。
- 三个计数与「答卷总数」共用同一条
GROUP BY quality_flags查询,所以审计功能给结果页增加 0 条查询。 - 清单刻意不显示填写者身份;秒填阈值由后台配置
min_seconds控制(0 = 关闭该维度)。
- 分页作答(长问卷多屏填写)
- 后台配置
page_size(0–10,0 = 不分页):每页 N 题,长问卷自动切成多屏,带进度条与上/下一页。 - 跨页的跳题条件仍由服务端判定:条件题在上一页时,条件成立就照常渲染、不成立这一页干脆不渲染,
不再依赖浏览器对隐藏框的
:checked判断。- 上一页的作答以隐藏字段原样携带,最终提交仍是一次整卷校验;页码越界(0 / 负数 / 非数字 / 999)
一律回落到合法页,不会出现空白页或缺答提交。
- 结果对比(两份问卷并排)
- 结果页新增入口
?a=survey_compare&left=&right=:两份问卷的聚合结果并排显示,题目按文本对齐,
对不上的题显式标注「另一份问卷没有这道题」;评分题给并排平均分。
- 一条聚合查询取回两边的题 × 取值计数后纯内存对齐;权限沿用结果页可见性,
看不到结果的人连选择器都看不到。已发布的私有结果问卷,他人访问直接 403。
- 题库单题复用(模板库升级为题库)
- 编辑页可以把某一行「存入题库」(
kind=question单题,与整卷模板同一张表、同一套解析器)。 - 也可以把题库里的一题「插入到问卷」的指定位置:插入前做结构同构校验(题型、选项、名额、
必答/多选上限与来源一致),受
max_questions与template_max双上限约束;已发布问卷题目冻结,不能插入。二、修复
- 统计静默截断(本轮最严重):结果页/分享页过去是「先取回前 2 万行明细再在内存里聚合」,
超过两万行的问卷统计会静默失真(数字变小但不报错),大问卷还会把全部明细读进内存。
现改为一条GROUP BY聚合查询(题 × 行 × 取值 → 计数),口径与总数一致,且小问卷查询量不变。- 秒填答卷反而逃过标记:提交时长过去用
0同时表示「同一秒内提交」和「没带起始时间戳(未知)」,
而判据写的是「时长 > 0 才算秒填」,于是最快的答卷(0 秒)不被标记。现在
-1 = 未知、0 = 真的 0 秒,
0 秒会被正常记为秒填,未知时长不会被误判。- 删除问卷后留下悬空引用:分享链接与模板的来源引用未清理,删除后分享页/模板库仍指向已删问卷。现已级联清理。
- CSV 公式注入(CWE-1236):导出的答卷里,以
=、+、-、@开头的文本会被 Excel 当公式执行。
现在导出时统一中和(前缀单引号),文本内容不变、打开不再有执行风险。
- 已发布问卷的受限保存会丢字段:已发布问卷的保存分支只写提交上来的字段,未提交的字段被写成空值。
现在受限保存只更新提交过的字段,其余保持原值。
三、性能
- 结果页仍是「一次表单 + 一次聚合 + 一次分页查答卷」:实测 9 条,与 5.0.0 持平(审计没有多花一条查询)。
- 实测查询数:作答页 5(分页全在内存里算,不新增查询)、编辑页 5(题库只多一条列表查询)、
对比页 8、分享页 6;数据量翻 25 倍后不变。
- 页面请求路径上没有任何循环体内的数据库调用(静态审计:0 处)。
四、已知行为(请先读这三条)
- 可疑标记是启发式,不是判决。 秒填看的是「页面下发的时间戳到提交的间隔」,客户端可伪造;
同设备多份只在开启「每日提交上限」时才可能命中(关闭时不写任何设备标识);直线作答只对
3 个以上选择类作答点生效。标记只做提示,不会删数据、不会改统计,请结合业务判断。- 分页的页数只由题数与
page_size决定,与作答进度无关;改小page_size会让进行中的填写人换页边界,
建议在问卷未发布时决定。
- 题库只做结构同构校验,不做语义查重:题干文本相同但结构不同的题不会被判为重复,可各自入库;
插入时按位置插入,原有题目的相对顺序不变。
五、升级说明
- 原地升级,无需卸载重装;老库第一次读写时自动补列/补索引,重复安装幂等。
- 新增列:
answers.duration(填写秒数,-1 = 未知)、answers.quality_flags(可疑标记位)、
templates.kind(form整卷 /question单题,老数据默认为form)。- 新增索引:
answers(form_id,quality_flags)、answers(device_key,created_at)。 - 新增两项后台配置:
page_size(0–10)、min_seconds(0–3600);后台表单仍是「没提交的键沿用当前值」。 - 默认配置下(
page_size = 0)填写体验与 5.0.0 完全一致;审计块只对发起人可见,普通填答者页面不变。
版本 7.0.0 更新:
【7.0.0】问卷更像专业工具了:显示条件 2.0(多条件 + 跳题)+ 选项随机与分组配额 + 宽表/交叉表导出 + 版本留档与逐题对比 + 只读聚合接口一、新增
- 显示条件 2.0:多条件与跳题
- 支持多条件 AND(
0=甲&&1=乙)与 OR(0=甲||1=乙),也支持「只跳转不带条件」(-> 4)。 - 新增跳转到指定题(
-> N,1 起、只能向后,因此不可能成环):条件不成立的那道题连同它后面的题一起不显示,
分页作答时整页被跳过会在页面上给出可见提示,不会出现空白页。
- 渲染、必答校验、入库三处走同一份服务端推进结果:被跳过的题既不渲染、也不校验、更不会入库。
- 结构性非法的条件(语法错、超条数、
&&与||混用、自引用、跳转目标不成形或非前向)在保存期就被拒绝,一行数据都不写。
- 选项随机化 + 分组配额
- 题级 / 问卷级选项随机(单选、多选):同一份答卷内顺序稳定(跨页、报错重渲染都不变),不同填写者顺序不同。
- 顺序由身份键与题目 id 确定性推导,不依赖随机源,也不落库;统计口径不变(明细里存的始终是选项文本)。
- 评分题与矩阵行不打乱(它们的顺序自带语义)。
- 分组配额:多个选项共享一个名额,写法
上限:选项1|选项2#组名,多组用;分隔;拼错的成员被忽略,不会让整题报错。
- 导出增强
- 宽表 CSV:一行一答卷、题目为列,矩阵逐行展开,没有行数上限。
- 交叉表 CSV:行 = 分组维度、列 = 统计维度、末行末列给合计;
?a=survey_export&id=N&format=cross&group=题号&target=题号。 - 两种导出的数字与结果页同源同口径,CSV 一律做公式注入中和。
- 版本留档与逐题对比
- 发布、草稿保存、题目调整、题库插入、以及发布后改题,都会自动留一份快照(题数 / 题目定义 / 时间 / 操作者 / 原因);
与上一份内容完全相同则不重复留档。
- 快照页可看历史,并与当前题目做字段级逐题对比(题干 / 题型 / 选项 / 必答 / 多选上限 / 条件 / 名额 / 随机)。
- 快照只记录「当时的题目长什么样」,不回溯已收答卷。
- 只读聚合 JSON 接口
?a=survey_api&token=…返回「题 × 选项计数 + 答卷数」(评分给均分、矩阵逐行给均分),方便接入自己的看板。- token 在结果页生成 / 撤销,可随时作废;按分钟限频(默认每分钟 60 次,后台可调),超限返回 429。
- 只出聚合:没有逐份答卷、没有答卷 id、没有填写者身份,文字题与图片题连作答内容都不返回。
二、修复(本轮实测复现的真实缺陷)
- 导出静默丢数据:收了 5005 份答卷的问卷,宽表 CSV 只导出前 5000 行,页面上没有任何提示;
而「填写者」列的批查会把全部答卷 id 拼进
IN (…),答卷数超过数据库变量上限时导出直接报错(截断恰好把这个问题挡住)。
修法:宽表改为「答卷头 1 条查询 + 明细 1 条 JOIN 查询 + 游标流式输出」,没有行数上限;填写者昵称改走子查询批查。- 填答页把「不能填」说成「已填过」:未登录访客打开「仅限登录」的问卷、发起人预览草稿、站点关闭游客填写这三种情况,
页面都只显示「你已经填写过了,感谢参与。」,让人既不知道自己没答过,也不知道该登录还是等发布。
修法:新增五状态判定(可填 / 已答过 / 未登录 / 草稿 / 已结束),每种状态给准确文案,未登录时给登录入口。- 投票角标整片消失:站点标记缺失时(刚安装 / 站点导入 / 标记被清掉),第一次浏览恰好落到「本页没有挂问卷的主题」
就会把标记写成 0,此后首页与版块列表的角标被永久短路,且没有任何提示。
修法:两条「本页没命中」分支共用一次存在性检查 —— 确认真没有才落 0,确认真有就把缺失标记自愈成 1。- 提交校验失败整屏丢内容:少填一道必答题被拒后,已填好的内容全丢(单页模式连本页输入都不回显,
分页模式连前几页藏在隐藏字段里的作答也一起丢),只能从头再填。
修法:校验失败不再重定向,留在原屏重渲染(回显 + 跨页隐藏字段 + 计时戳都在),成功仍走 PRG 跳转。三、性能
- 既有页面查询量不变或更省:列表 6 / 首页 6~9 / 答题页 ≤6 / 结果页 ≤9 / 编辑页 GET ≤6(已发布问卷 2 条)/ 分享页 ≤7。
- 新增页面自带上限:版本快照页 3、接口面板 ≤10、只读接口 4、宽表导出 2(答卷头 + 一条 JOIN 流式明细)。
- 跳转判定与选项随机都是纯内存:0 条数据库往返、0 行落库,页面请求路径上没有循环体内查询(静态门禁 0 处)。
四、已知行为(请先读这五条)
- 跳转只能向后:
-> N的目标必须是本题之后的题号,保存期即拒绝其它取值,因此不可能成环。 - 随机的粒度:只对单选 / 多选打乱;游客的身份键含「每日盐」,所以同一个游客跨天再来会拿到另一套顺序
(与既有的「按天去重」口径一致)。
- 整页被跳过时页码不缩水:页数只由题数与每页题数决定,被跳过的页会给可见提示,不会空白死循环。
- 版本快照不回溯数据:它记录的是题目定义的历史,不改变已经收到的答卷。
- 导出规模:宽表按答卷顺序游标流式输出,不再有行数上限;答卷头仍一次性读回,超大问卷的导出耗时随数据量线性增长。
五、升级说明
- 原地升级,无需卸载重装;老库第一次读写时自动建表 / 补列 / 建索引,重复安装幂等;结构标记由
6升到7。 - 新增两张表:
plugin_survey_poll_versions(版本快照)、plugin_survey_poll_api_tokens(只读接口 token),各带一枚索引;
加上
questions.random/questions.quota_group_json与forms.random_options三处新列(共 8 表 / 16 索引)。- 删除问卷时级联再补两处:接口 token 立即作废(保留调用痕迹)、版本快照整份删除。
- 不用条件 2.0、不开随机与配额、不发接口 token 的老问卷,行为与 6.0.0 完全一致(老的单条件结构继续可读)。
版本 8.0.0 更新:
8.0.0 大版本更新:三处数据完整性 BUG + 投放渠道 / 作答时段 / 长表导出
本次仍是「先修 BUG,再加功能」:修掉 4 处真实缺陷(其中「发布后改题」会直接吃掉已收答卷的统计),再补三项新能力。
一、修掉的 4 处缺陷(都可复现、都留了回归断言)
- 发布后改题会让已收答卷的统计全部归零
现象:一份已收 2 份答卷的问卷,用「改题」把第 1 题换个说法保存后,结果页每一题都显示「0 份作答」,导出与交叉表也一起归零——答卷本体还在库里,但明细全部变成了孤儿行。
根因:保存题目走的是「先把该表单的题目行整表删掉、再整批 INSERT」,而题目主键是 AUTOINCREMENT:重新插入的题目拿到全新 id,而plugin_survey_poll_items.question_id指向的还是旧 id。
修法:改成按内容指纹复用——题型 + 题面 + 选项完全相同的题目保留原行(明细继续有效),只有真正被改写的题目才是新题(统计从 0 开始,语义正确);顺序与必答 / 显示条件 / 多选上限 / 名额 / 随机 / 分组配额的变化用一条 CASE 多列 UPDATE 写回。被换掉的老题只删题目行,答卷明细原样留档(兑现 7.0.0 的「不回溯已收数据」),且因为主键不复用,留档明细永远不会错挂到新题上。- 匿名问卷的作答去重键直接写明文
u:<uid>,与「匿名」承诺不符
现象:勾了「匿名作答(不记录填写者,提交后无法追溯)」,可库里
bucket_key里明明白白写着u:42——任何能读到这张表的人都能把每份答卷对回具体账号。
修法:拆成两个键——去重键bucket_key用每份问卷独有的私盐做 HMAC(同一问卷内仍能一人一次,但键值不含 uid、也无法跨问卷比对);「我的作答」另用站点盐化的owner_key认领,功能不受影响。勾选项与后台开关的文案也改成如实描述(结果与导出都不显示填写者、库内只存不可逆去重键)。- 结束广播超过 200 位填答者时:前 200 人每轮重复收通知,第 201 人永远收不到
现象:收满 201 份答卷的问卷结束后,计划任务每 5 分钟给同一批前 200 人再发一遍「问卷已结束」,第 201 位填答者一次都收不到,问卷永远停在「还没发完」。
根因:批量广播用GROUP BY user_id LIMIT 201取「第一页」,发完 200 条后既不推进游标也不置位。
修法:新增notify_cursor记录「已经发到哪个 user_id」,每轮从游标之后续发 200 人;取不满一页即归档。实测 200 → 201 → 201,每人恰好一条。- 发起人结束小结是 check-then-act,两轮任务重叠会重复通知
修法:改成 CAS 抢占(
UPDATE … SET notified_at=? WHERE id=? AND notified_at=0,按 rowCount 判定),notify_state的归档同样带守卫。诚实说明:CAS 的原子性需要真并发才能复现,串行套件造不出交错,因此证据是「结构断言 + 重复运行不重复发」。二、三项新能力
- 投放渠道归因:结果页给出一键复制的渠道链接(
?a=survey_view&id=N&ch=微信,内置站内 / 微信 / 邮件三个现成按钮),填答页把渠道随表单带回并落库到answers.channel;结果页新增「投放渠道」面板(按渠道回收量排序 + 未标记份数 + 每条链接可复制),宽表与长表导出都带「渠道」列。渠道只是归因标签(像 UTM),不参与任何权限判定。 - 作答时段分布:结果页新增「作答时段」面板——24 小时柱状分布 + 最近 14 天趋势 + 回收高峰小时 + 跨天跨度;数据来自一条 GROUP BY,小时与日期在内存里归并,零 N+1。
- 长表导出:
?a=survey_poll_export&id=N&mode=long一行一个答案(答卷号 / 填写者 / 提交时间 / 渠道 / 题号 / 题目 / 题型 / 矩阵行 / 答案文本 / 数值),直接喂给 pandas / R / SQL;仍然是「一条 JOIN + 游标流式输出」,没有行数上限,匿名问卷同样不输出填写者列。
三、验证
- 八套
tools/lab_verify_survey*.sh串行全绿(v8 新增 99 项断言;既有套件的期望值随版本推进就地更新并注明理由:版本号 / 结构标记schema=8/ 索引 15→17 / 宽表与矩阵导出列数 +1 渠道列 / 结果页预算 9→11)。 - 六道静态门禁全部通过;计划任务仍为 2 个(close + cleanup)。
- 四张新预览图(投放渠道 / 作答时段 / 长表导出 / 匿名结果)与 7.0.0、6.0.0 的图聚合为 12 张。
版本 9.0.0 更新:
9.0.0 大版本更新:匿名认领键稳定性修复 + 「找回我的作答」
一、修掉的缺陷(同一处问题的两个侧面,都会让匿名问卷的「我的作答」凭空消失)
- 站点改名就让匿名问卷的「我的作答」全体打不开
现象:填答者看不到自己填过的内容、列表里「已作答」标记消失;他以为没答过想再答一次,又会被去重拦住——用户看到的是「系统说我答过,但我什么都看不到」,而答卷其实好好地在库里。
根因:匿名认领键的站点私盐每次都从站点名派生、从不落库,改名即全体失配。
修法:首次使用时生成随机私盐并落库;认领查询改成「当前口径 + 历史派生口径」的键集合,老答卷仍然认领得到。- 「落库」之后私盐每个请求都换一把(同一现象、不同根因)
根因:配置读取器没把该键读回来,
site_salt()每次调用都以为「还没有盐」而重新生成并写库。
修法:读取器补上该键。实测:修复前 3 次请求 3 把不同的盐,修复后恒定。二、新增能力:找回我的作答(自助)
匿名问卷里,当「你确实答过(去重键命中)但当前认领键查不到你那份」时,填答页出现「找回我的作答」:
一键把认领键修正为当前口径,结果页的「我的作答」立刻恢复。它同时是历史数据修复的用户侧入口,
不需要站务改库;校验严格(本人身份键 + 去重键双重命中,只能改自己那一行),不能凭 ID 猜别人的答卷。三、验证
- 专项套件扩到 111 项 / 0 失败(新增 11 项:私盐落库与稳定、认领键形态、本人的「我的作答」、他人不可见、历史口径兼容、失配时自助找回与恢复)。
- 六道静态门禁全绿(含循环内查库审计)。
- 市场登记原地更新(topic 1617),
verify_market.sh输出「市场登记与本地源码完全一致」。
版本 10.0.0 更新:
10.0.0 大版本更新:四处真缺陷修复 + 四项新能力
本轮四条缺陷都先在实验室真机复现(取证脚本
tools/probe_survey_v10_bugs.sh:同一套操作分别对 9.0.0 与 10.0.0
各跑一遍,把观察值原样打出来),再修,再在套件里留回归断言。一、修掉的 4 处真缺陷(现象 → 根因 → 修法)
- 后台点一次「保存设置」,匿名问卷的「我的作答」全体再次失配(9.0.0 修复的回归,本轮性质最重)
现象:管理员保存一次插件设置,匿名问卷的填答者就看到「你已经填写过这份问卷了」,却什么也看不到。
实测:保存前站点私盐anon_site_salt有值,保存后变成空串;用保存后的盐重算认领键 → 命中 0 份。
根因:配置写入是config_json的整体覆盖(不是 merge),而后台写入器只回传它自己校验的 18 个键,
读取器里的anon_site_salt被整行抹掉,下一次用得着时又随机生成一枚。
修法:写入器以当前配置为基线,只覆盖它真正拥有的键 —— 读写字段集合天然对称,以后新增键也不会再被抹掉。- 「复制为新问卷」静默丢掉「选项随机」与「分组名额」
现象:副本里题级「随机」变回「跟随问卷」、分组共享名额整组消失(选项还在,但超额再也拦不住),页面没有任何提示。
根因:复制分支手写了一份字段清单,7.0.0 新增的两个字段没有同步进来。
修法:改用唯一的那份序列化器(与「另存为模板 / 存题库 / 版本快照」共用同一份字段清单)。- 从题库插入一道题,会把整份问卷的显示条件与跳转规则清空
现象:插入后所有题的条件输入框变空、作答页不再跳题,而提示只说「已插入题目」,一个字都没提条件丢了。
根因:插入路径用了一个从未赋值的变量($old_total)来构造「旧题号映射」,映射恒为空 → 每个条件都取不到
引用目标而被丢弃;丢弃发生在重映射阶段而不是清理阶段,所以连「有 N 处条件被清空」的提示都不会出现。
修法:在插入之前先取一次题目数,映射按「被引用题目的身份」把条件与跳转目标平移到插入后的新位置。- 多选题 / 矩阵题的「N 份作答」把答项数当成了人数
现象:多选题 3 人各选 2 项显示「6 份作答」;矩阵题 2 行 × 3 人显示「6 份作答」,而同一块里每行又写着「3 人评分」。
只读 JSON 接口与结果对比页照抄了同一口径。
根因:统计里的total数的是明细行数,单选/评分/填空恰好一题一行所以等于人数,多选/矩阵则不是。
修法:同一趟数据库往返里追加一段「每题作答人数」(COUNT(DISTINCT answer_id)),
页头 / 分享页 / 对比页 / JSON 接口 / 导出全部改读它;答项数另开字段保留,两个口径不再混淆。二、新增的 4 项能力
- 按题汇总 CSV(
?a=survey_export&format=summary):一行一个「题目 × 项目」,含作答人数、计数与占比;
单选多选逐选项、评分 1–5 星 + 平均分、矩阵展开到「行 · 分数」+ 每行均分。最适合直接交给别人看结论。
- 文字作答关键词检索(结果页
?kw=):在填空 / 图片题里按关键词找到具体作答,最多 100 条;
%_一律按字面量匹配(a_b不会再命中axb),只给发起人与管理团队,不扩大逐条内容的可见范围。- 我的作答档案(
?a=survey_mine):跨问卷查看「我答过哪些、每道题我选了什么」,并可一键导出「我的作答 CSV」。
归属只用本人认领键,别人看不到。
- 后台数据自检与一键修复:一条语句取回 7 项数据完整性不变量(孤儿明细 / 无明细答卷 / 匿名问卷缺私盐 /
悬空分享 / 悬空接口 token / 孤儿版本快照 / 悬空主题回链),悬空引用可一键修复;
刻意保留「历史留档的作答明细」与「当时被条件全部跳过的答卷」并写明理由——那两类删掉才是数据丢失。三、兼容与性能
- 既有行为不推翻:默认导出仍是宽表、填答与结果口径不变;单选 / 评分 / 填空 / 图片题的页面输出逐字节不变。
- 三处新页面都不随行数增长:检索面板一条查询(不传
kw时 0 条)、档案页固定 3 条、后台自检固定 1 条;
结果页与分享页的查询条数没有变化。
四、验证
- 六道静态门禁全绿;新套件
tools/lab_verify_survey_v10.sh覆盖 4 处修复的回归断言 + 4 项能力 + 结构幂等; - 既有 9 套 survey_poll 套件全部复跑通过,版本相关断言已随 10.0.0 抬正并在行内注明理由。
版本 10.0.1 更新:
10.0.1 修复:后台设置保存失败时看不到真正的错误原因- 后台「保存设置」在校验失败时直接 fall through 成整页 HTML,而这张表单被核心前端接管(fetch + JSON 解析)→ 用户只看到笼统的「操作失败」,真正的校验提示被吞掉(例如「单份问卷题目上限需要填写 1 到 30 之间的整数」)。
- 修法:错误是「一条消息」而不是一整页 → AJAX 请求回
{"ok":0,"message":…}信封;无 JS 浏览器(ajax_request()为假)仍走原来的整页渲染,字节不变。 - 新增 3 张预览图(10.0.0 的三项能力):按题汇总 CSV(
format=summary)· 文字作答关键词检索(?kw=)· 我的作答档案(?a=survey_mine)。
版本 11.0.0 更新:
survey_poll 11.0.0
四项新能力
- 逐份答卷回看:结果页的「答卷明细」每一行都多了一个「查看这份作答」入口,点开就是那一份答卷的
逐题作答(含选项、评分、矩阵逐行、文字与图片链接),还能按上一份 / 下一份连续翻看图省事。
它是只读页:权限沿用结果页的可见性(看不到聚合结果的人同样打不开),匿名问卷只写「匿名」,
页面不输出账号、认领键或设备指纹;图片题只给链接不内联渲染(内联会把查看者的 IP 送给填答者的图片服务器)。
出图/页面查询量与题数、答卷数无关;结果页新增的入口是纯内存拼链接,查询数 +0。- 作答草稿(长问卷跨会话续填):长问卷按「每页 N 题」分屏作答时,翻到下一页会自动把整卷已填内容存成草稿;
关掉标签页、换设备、断网之后回来重新打开,会自动恢复到你上次填的那一页并给出提示,
另有显式的「保存草稿」与「放弃草稿,从头填」。草稿只属于填写者本人(与答卷同一把身份键),
提交成功后立刻删除。载荷有明确上界(只收作答字段、单值 200 字、最多 300 个位置、20KB),
题目或选项在两次填写之间被改过时,草稿里那些已经不存在的位置会被丢弃,不会把野值塞进页面。- 截止前撤回 / 修改我的作答:问卷可勾选「允许填答者在截止前撤回 / 修改自己的作答」(默认关闭,
保持「一份问卷一份样本」的既有语义)。打开后,填答页与结果页都会给填答者两个入口:
「修改我的作答」把已提交的作答整卷回填一遍供修改,「撤回我的作答」直接撤回(撤回后可以重新填)。
改答覆盖自己那一行(答卷份数不增加),并且会先把自己占用的选项名额退还,
否则在满额问卷里「保留原来的选择」会被自己的旧记录判成名额已满。- 到期前提醒发起人:距截止时间不足设定小时数(默认 24 小时,可在后台调,填 0 关闭)时,
计划任务会提醒一次发起人「问卷将在 X 时间截止」,可以去看回收进度或延长截止时间。
提醒是抢占式的,计划任务重叠执行也只会发一次;关闭时这一步一次查询都不发。修掉的七处缺陷
- 匿名问卷保存后收到的答卷会「失联」(最严重的一处):过去只要保存一次问卷,匿名问卷的去重私盐就会被换成
一枚新随机盐 —— 该问卷已经收到的答卷因此既认不出重复填写(同一个人可以再投一次),
也认不出「我答过」(「已作答」标记与「我的作答」一起消失,用户看到「系统说我答过,但我什么也看不到」)。
现在保存时会固化原有的派生盐(所有历史去重键逐字节继续成立),只有新建或刚切到匿名才生成新盐。- 后台「一键修复」会把上面这个问题主动执行一遍:它的「补齐匿名私盐」写回的也是一枚新随机盐,
修完反而让老答卷失配。现在它固化派生盐,修完历史答卷仍然全部认领得到。
- 双击提交会把同一份答卷的明细写两遍:快速双击(或网络重试、脚本并发)时,后到的那次请求虽然被
一人一次的约束挡住插入,却仍然把自己的作答追加进了先到者的答卷 —— 选项计数与平均分被算重,
导出里同一题出现两行,而用户看到的是「提交成功」。现在每次提交带一枚声明串,只有真正建出这份答卷的
那次请求才会写明细,其余一律回「你已经填写过这份问卷了」。- 列表页「全部」标签名不副实:过去它被一条无条件条件压成了「进行中 + 我自己发起的」,
游客点「全部」看到的与「进行中」一模一样,登录用户也永远看不到别人已结束的问卷。
现在四个标签各按自己的语义取数,别人的草稿在任何标签下都不出现。- 已发布问卷的编辑页有「看起来能改、实际被吞」的控件:改了「回收目标」页面回一句「问卷已更新」,
库里还是旧值。现在回收目标与「可撤回 / 可修改」在已发布后也会真正保存;
而会改变作答口径的三个开关(匿名 / 允许游客填写 / 选项随机)改为真的置灰并写明冻结原因。- 「同设备多份」是个恒为 0 的假指标:设备指纹只在开启「每日提交上限」时才落库(这是既有的隐私口径),
但结果页一直把它当成一个可看的计数展示。现在日限关闭时该维度直接标注「未启用」并说明原因 ——
隐私口径一个字没放松,界面不再撒谎。- 后台「数据自检」的说明文字把 Markdown 星号原样显示给管理员看(
**不修复**这种),已改成正常的文案。
结构与兼容
- 新增一张草稿表
plugin_survey_poll_drafts与三个列(答卷的claim_token、问卷的editable/warned_at),
全部走幂等补列;结构标记
8 → 9,老版本升级上来会自动补齐。- 删除问卷时会一并清掉它的作答草稿;后台数据自检新增「孤儿作答草稿」这一条不变量,可一键修复。
- 打开「可撤回 / 可修改」之前的问卷,行为与上一版完全一致(默认关闭 = 一人一次不可改)。
- 列表页 / 结果页 / 导出路径查询数不变;填答页在「尚未作答」时固定多一条查询(查找草稿),没有草稿也只花这一条。
版本 11.1.0 更新:
survey_poll 11.1.0
结果可视化:单选环形图 / 多选百分比堆叠条 / 评分仪表(纯内联 SVG)
- 结果页与只读分享页都多了一层图形化结果:单选题是一枚环形图(占比结构一眼可见,圆心写作答人数,
右侧逐项图例给出计数与百分比);多选题是一条百分比堆叠条(各选项占作答人数的比例,
并写明「平均每人选 N 项」);评分题是一段仪表弧(平均分落在 1–5 的哪一档,配一句档位结论)。- 全部是纯内联 SVG + 现有 CSS:不引任何外部库、不发任何额外请求、页面里没有脚本 —— 分享页会把这段
标记原样发给站外的人,所以它必须自洽。图形化这一步不新增任何查询(数据全部取自结果页原有的那条聚合结果)。
- 原有的条形与数字一个都没动:图形只是加在它们前面,按数字核对、导出核对的老用法完全不受影响。
- 选项很多时自动收敛:只画有票的选项,超过 8 项时尾部合并成「其他」(精确计数仍在图例里)。
升级与并发:两条最容易留暗坑的路径,各补一次实测与一处修复
- 升级路径自愈:从老版本升上来时(尤其是 8.0.0 之前建的匿名问卷,它们的去重私盐从来没落过库),
第一次打开问卷相关页面就会自动把结构补齐,并把「匿名 + 私盐为空」的历史问卷一次性固化派生盐 ——
固化接的正是原来那个现算的值,所以历史答卷的去重键与认领键逐字节不变(换版不换盐、不丢认领),
数据自检里那条「匿名问卷缺私盐」的告警也随之归零。- 改答的并发正确性:两个标签页同时提交「修改我的作答」时,过去会交错成「删 A → 删 B → 插 A → 插 B」,
同一份答卷的明细被写两遍(统计跟着翻倍)。现在改答先抢一次乐观锁,后到者一条明细都不动并提示刷新重试。
修掉的缺陷
- 复选框的「取消勾选」过去永远无效:匿名作答 / 允许游客填写 / 选项随机 / 截止前可撤回可修改
这四个开关只能打开、关不掉(保存后仍是勾选状态)。原因是上次给所有字段统一加了「没提交就沿用库里的值」——
这对文本框是对的,但浏览器取消勾选时根本不提交这个键。现在编辑页带一个隐藏标记,
带了它「缺席 = 取消勾选」,不带(发布 / 复制 / 题库这些程序化提交)仍然沿用库里的值。- 匿名问卷「取消匿名 → 再开匿名」会换盐:这条路径过去会生成一枚新随机盐,让该问卷历史答卷的去重键与
认领键再次全体失配(同一批用户可以再投一次、「我的作答」消失)。现在判据改成「库里有没有按匿名键落库的答卷」,
有就一律固化派生盐。- 问卷列表页多出来的一条查询已优化回基线:结构升级标记改在插件配置里放一份内存副本,
每次请求的「结构是否最新」判断变成零查询(A/B 实测:列表页与 11.0.0 同为 6 条)。
- 挂问卷的版块列表页也少一次白付的查询:站点标记已经是「有」时不再重复做存在性检查
(A/B 实测:11.0.0 与 11.1.0 同为 12 条,无增长)。
- 静态门禁从「3 条待人工确认」收敛到 0 条:配置解析函数内补上请求方法闸、
编辑页里直接读
$_POST的匿名闭包换成具名助手(表单数组当参数传)、
主题钩子里显式复核 CSRF 令牌(与核心同一套比对口径);另修掉一处 LIKE 片段缺ESCAPE的写法。兼容性
- 老版本升上来无需任何手工操作:结构标记自动推进,列与表自动补齐,历史答卷、分享链接、接口 token、
版本快照、草稿全部原样保留。
- 不打开新开关时,作答行为与上一版完全一致;结果页/分享页新增的图形只是渲染层的变化,导出格式一个字节没改。