GAS开发Google Sheet侧边栏:邮件Reject链接自动触发问题排查求助
背景
我用GAS(Google Apps Script)给Google Sheet做了个侧边栏,表格里存着人员数据库和PO记录。应用能通过Gmail给指定人员发送PO邮件,邮件里带ACCEPT和REJECT链接,链接代码如下:
<span id="acceptLink"> <a href="<?= url ?>?poyear=<?= poyear ?>&state=Accept&name=<?= name ?>&did=<?= draftid ?>" aria-invalid="true"> <span class="accept"><?= stringAccept ?></span> </a> </span> <span id="rejectLink"> <a href="<?= url ?>?poyear=<?= poyear ?>&state=Reject&name=<?= name ?>&did=<?= draftid ?>" aria-invalid="true"> <span class="reject"><?= stringReject ?></span> </a> </span>
这些脚本块会根据对应PO选中的用户编辑插入。询问人员可调度性时,会保存一份不带ACCEPT和REJECT按钮的邮件草稿,用户点击ACCEPT后,就把这份草稿发给对方。
问题
数据库里大概70人,但给其中部分用户发送「可调度性核查」邮件后,会立刻收到REJECT响应(用户根本没操作)。这个问题只出现在少数用户身上,99%的用户都正常。
需要定位并修复这个问题,同时想知道怎么在应用的托管页面(doGet函数)里查看是否收到了REJECT请求,当前doGet代码如下:
function doGet(e){ if(gIfSim) { //Debug code. //e={parameter:{state:"Accept", name:"Preet Mehta", did:"r-8262072107973556655"}}; } //***THESE ARE THE VARS PASSED BACK USING THE SCRIPTLET*** var passedInIPState = e.parameter.state; var draftID = e.parameter.did; var poYear = e.parameter.poyear; //poYear will be like "2223" var sheetName = "PO " + poYear; var wsPO = getSheet(sheetName); var colAlPO = getColAlpha("PO"); var messageFromDraft; try { //CORRESPONDING DRAFT IS AVAILABLE IN THE GMAIL - CHECKED messageFromDraft = GmailApp.getDraft(draftID); } catch(e) { return HtmlService.createTemplateFromFile('rejectResponse').evaluate(); } //IF THE FLOW REACHES HERE THEN THE VALID ACCEPT/REJECT RESPONSE var getMessagePlain = messageFromDraft.getMessage().getPlainBody(); Logger.log(getMessagePlain); var j = JSON.parse(getMessagePlain); Logger.log(j); var rowIndex = j.rIndex; var airedCount = wsPO.getRange(colAlPO.airedcounter + rowIndex).getValue(); airedCount = airedCount - 1; wsPO.getRange(colAlPO.airedcounter + rowIndex).setValue(airedCount); var valueOfstatusINPO = wsPO.getRange(colAlPO.status + rowIndex).getValue(); if( passedInIPState == "Accept" && valueOfstatusINPO == "WAITING FOR RESPONSE") { //BUSINESS LOGIC FOR ACCEPT RESPONSE AND THE PAGE IS SERVED return HtmlService.createTemplateFromFile('acceptResponse').evaluate(); } else if( passedInIPState == "Accept" && valueOfstatusINPO != "WAITING FOR RESPONSE") { //BUSINESS LOGIC FOR ACCEPT RESPONSE STATE 2 AND THE PAGE IS SERVED return HtmlService.createTemplateFromFile('rejectResponse').evaluate(); } else if(passedInIPState == "Reject") { //BUSINESS LOGIC FOR REJECT RESPONSE AND THE PAGE IS SERVED //THIS CODE CHANGES THE PO SHEET ROW TO REJECT WHICH ALSO REFLECTS THAT BEHAVIOR return HtmlService.createTemplateFromFile('firstReject').evaluate(); } }
自动触发Reject的排查方向与修复方案
1. 邮件客户端预加载链接
部分企业邮箱、邮件过滤工具或邮件客户端会自动预加载邮件中的所有链接,用来检测恶意内容,这会导致Reject链接被无感知触发。
修复方案:
- 将GET请求改为POST请求:修改链接为表单提交(或用JavaScript发起POST),因为预加载机制一般只会触发GET请求;
- 添加随机验证Token:生成唯一的token并随邮件发送,链接中携带该token,doGet函数验证token有效才执行操作;
- 增加确认步骤:点击Reject链接后跳转至确认页面,用户手动点击确认按钮才执行Reject逻辑。
2. 链接生成异常
少数用户的邮件中Reject链接可能存在参数错误、转义问题,导致链接被错误解析触发。
排查方法:
发送邮件前打印生成的完整链接,对比异常用户和正常用户的链接参数,检查是否存在state=Reject被提前解析、参数值被截断等问题。
3. 草稿邮件JSON解析错误
如果异常用户对应的草稿邮件纯文本内容不符合JSON格式,会导致JSON.parse报错,但当前代码未捕获该错误,可能导致逻辑异常进入Reject分支。
修复方案:
给JSON.parse添加try-catch块,捕获解析错误并记录日志,避免错误传播导致误触发。
在doGet函数中查看Reject请求的方法
通过添加详细日志记录,可追踪所有进入doGet的请求,包括Reject请求的来源、参数等信息。修改后的doGet函数如下:
function doGet(e){ // 记录完整请求详情:时间、state、草稿ID、PO年份、用户名、用户代理 const requestLog = { timestamp: new Date().toISOString(), state: e.parameter.state, draftID: e.parameter.did, poYear: e.parameter.poyear, name: e.parameter.name, userAgent: e.headers['User-Agent'] || 'Unknown' }; Logger.log(JSON.stringify(requestLog)); if(gIfSim) { //Debug code. //e={parameter:{state:"Accept", name:"Preet Mehta", did:"r-8262072107973556655"}}; } const passedInIPState = e.parameter.state; const draftID = e.parameter.did; const poYear = e.parameter.poyear; const sheetName = "PO " + poYear; const wsPO = getSheet(sheetName); const colAlPO = getColAlpha("PO"); let messageFromDraft; try { messageFromDraft = GmailApp.getDraft(draftID); } catch(e) { Logger.log(`获取草稿失败,ID: ${draftID},错误:${e.message}`); return HtmlService.createTemplateFromFile('rejectResponse').evaluate(); } const getMessagePlain = messageFromDraft.getMessage().getPlainBody(); Logger.log(`草稿纯文本内容:${getMessagePlain}`); let j; try { j = JSON.parse(getMessagePlain); Logger.log(`解析后的JSON数据:${JSON.stringify(j)}`); } catch(parseErr) { Logger.log(`JSON解析错误:${parseErr.message},内容:${getMessagePlain}`); return HtmlService.createTemplateFromFile('rejectResponse').evaluate(); } const rowIndex = j.rIndex; let airedCount = wsPO.getRange(colAlPO.airedcounter + rowIndex).getValue(); airedCount = airedCount - 1; wsPO.getRange(colAlPO.airedcounter + rowIndex).setValue(airedCount); const valueOfstatusINPO = wsPO.getRange(colAlPO.status + rowIndex).getValue(); Logger.log(`当前PO状态:${valueOfstatusINPO},请求state:${passedInIPState}`); if( passedInIPState === "Accept" && valueOfstatusINPO === "WAITING FOR RESPONSE") { Logger.log("执行Accept逻辑"); return HtmlService.createTemplateFromFile('acceptResponse').evaluate(); } else if( passedInIPState === "Accept" && valueOfstatusINPO !== "WAITING FOR RESPONSE") { Logger.log("Accept请求但PO状态不符,返回rejectResponse"); return HtmlService.createTemplateFromFile('rejectResponse').evaluate(); } else if(passedInIPState === "Reject") { Logger.log("执行Reject逻辑"); return HtmlService.createTemplateFromFile('firstReject').evaluate(); } else { Logger.log(`收到未知state请求:${passedInIPState}`); return HtmlService.createTemplateFromFile('rejectResponse').evaluate(); } }
查看日志的方式
- 在Google Apps Script编辑器中,点击顶部菜单栏「查看」→「日志」,查看基础日志记录;
- 如需更详细的日志(包括历史记录、请求上下文),可使用Google Cloud Logging:在脚本编辑器中点击「资源」→「云平台项目」,跳转至对应项目的Cloud Logging页面,搜索相关日志关键词即可。
内容的提问来源于stack exchange,提问作者Preet Mehta

