
本文介绍一种通过 google apps script 实现表单提交防重的方案:在不依赖邮箱字段的前提下,利用提交的姓名自动检测并高亮重复项,从而有效阻止同一用户多次投票或提交。
本文介绍一种通过 google apps script 实现表单提交防重的方案:在不依赖邮箱字段的前提下,利用提交的姓名自动检测并高亮重复项,从而有效阻止同一用户多次投票或提交。
Google 表单本身不支持实时校验已提交数据(例如提交时动态查询 Sheets 中是否已存在相同姓名),也无法在前端拦截重复提交。因此,直接在 onFormSubmit 触发器中修改表单行为(如关闭表单、弹出提示)是不可行的——e.response 和 FormApp.getActiveForm() 在该上下文中无法调用 setCustomClosedFormMessage(),且表单状态已锁定。
但我们可以采用「后置标记 + 人工干预」的务实策略:当用户提交后,脚本立即扫描历史记录,若发现同名提交,则高亮该行,便于管理员快速识别与处理。这种方式稳定、低侵入、无需修改表单设置,且完全规避了原代码中 getSheetByName("Form Responses 1") 失败的问题(因您已将工作表重命名为 "elections",而原脚本仍硬编码旧名称)。
以下是优化后的完整脚本(已适配您的环境):
function onFormSubmit(e) {
// 获取本次提交的原始值数组:[时间戳, 姓名, ...其他字段]
const values = e.values;
if (!values || values.length < 2) return; // 安全防护:确保至少有2列(时间+姓名)
const submittedRow = e.range.rowStart;
const submittedName = values[1].trim(); // 假设姓名在第2列(索引1),去除首尾空格
// 获取绑定的电子表格及目标工作表
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName("elections");
if (!sheet) {
console.error("❌ 错误:未找到名为 'elections' 的工作表,请检查拼写和大小写。");
return;
}
// 读取全部数据(含标题行),提取姓名列(假设为B列,即索引1)
const data = sheet.getDataRange().getValues();
if (data.length <= 1) return; // 无有效数据,跳过
// 提取所有姓名(跳过标题行:从第2行开始)
const names = data.slice(1).map(row => row[1] ? row[1].trim() : "");
// 遍历历史记录,查找重复姓名(区分大小写、忽略空格)
for (let i = 0; i < names.length; i++) {
if (names[i] === submittedName) {
const duplicateRow = i + 2; // +2 因为 data.slice(1) 跳过了标题,且 sheet 行号从1开始
sheet.getRange(duplicateRow, 2).setBackground("#ffcccc"); // B列标红(浅红背景)
sheet.getRange(submittedRow, 2).setBackground("#ffcccc"); // 同时标红本次提交行,强化提示
console.log(`✅ 发现重复姓名 "${submittedName}",位于第 ${duplicateRow} 行和第 ${submittedRow} 行`);
break;
}
}
}? 关键配置说明:
- ✅ 确保表单响应保存到名为
"elections"的工作表(大小写必须完全一致); - ✅ 姓名字段需固定在电子表格的 B列(第2列);如实际在C列,请将所有
row[1]改为row[2],getRange(..., 2)改为getRange(..., 3); - ✅ 脚本需绑定到响应目标电子表格(而非表单),并通过 “编辑 → 当前项目触发器” 添加
onFormSubmit触发器(事件源选 From spreadsheet,事件类型选 On form submit)。
⚠️ 注意事项:
- 此方案不自动删除或拒绝提交,而是通过视觉标记辅助人工审核,符合 Google Forms 的安全模型;
- 若需更强约束,可结合「限制每人仅1次响应」+「要求登录」设置(在表单设置中启用),再辅以本脚本做双重校验;
- 避免在循环中调用
deleteRow()—— 这会动态改变行号,导致漏检或报错,且破坏原始提交审计链。
通过该方案,您既能守住业务规则(一人一票),又能保持系统健壮性与可维护性。


















