You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

GAS开发Google Sheet侧边栏:邮件Reject链接自动触发问题排查求助

GAS邮件自动触发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>
      &nbsp;&nbsp;
    <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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 05:54:24