2025年12月6日 星期六

「Google Apps Script」 填寫表單寄送PDF 加強優化版 (全域變數、時間開關)


💡 簡單的比喻:全自動飲料店
1. Setting 分頁:就像是店長貼在牆上的SOP 公告(幾點開門、飲料配方代號是什麼)。
2. onFormSubmit (文書員):就像是一個超快手搖飲店員
    ◦ 客人一下單(填表單)。
    ◦ 他馬上拿出貼紙(文件範本)。
    ◦ 寫上客人的名字(取代 {{姓名}})。
    ◦ 封膜(轉成 PDF)。
    ◦ 直接送到客人手上(寄 Email)。
3. checkFormStatus (警衛):就像是一個定時巡邏的保全
    ◦ 每 10 分鐘來店門口看一下。
    ◦ 如果時間到了晚上 10 點(END_TIME),他就自動把鐵門拉下來,不讓客人再點餐了。
這段程式碼就是幫你省去「手動複製貼上名字做證書」以及「半夜爬起來關閉報名表」的辛苦工作!
💡 這段程式碼就像是聘請了一位**「全能型的活動總召機器人」**。
如果你要在學校辦一場活動,通常需要做兩件很麻煩的事:
第一是有人報名就要馬上發「報名確認函」或「電子證書」給他; 第二是要一直盯著時間,時間到了要把報名表關掉。
這段程式碼就是把這兩件事完全自動化。我們可以把它拆成三個主要部分來理解:
1. 閱讀老闆的指令(讀取設定)
機器人開工的第一件事,是先搞清楚狀況。
找筆記本:程式會先去你的 Google 試算表中,尋找一個叫做 "Setting" 的分頁
背下來:這個分頁就像是老闆(你)給機器人的備忘錄,上面寫著重要的資訊,比如:「範本檔案的編號 (ID) 是多少?」、「報名表什麼時候要關?」、「寄信的主旨要寫什麼?」機器人會把這些資料全部讀進腦袋裡記住
2. 超速文書處理員(自動發證書/通知)
這是程式的核心功能,只要有人送出表單(onFormSubmit),這位文書處理員就會立刻醒來工作
找聯絡人:它會先看報名表,找出填表人的 Email。如果不小心沒填或找不到 Email,它就會停止工作並回報錯誤
準備白紙:它會去拿你指定的「文件範本」(例如:活動證書母片),**影印(複製)**一份新的出來,並幫新檔案取好名字(例如:研習名稱 - 姓名 - 日期),這樣才不會改到原始的母片
填空題:接著,它會打開那份新影印的文件,玩「填空遊戲」。
    ◦ 它會看報名表裡的資料,比如 {{姓名}}
    ◦ 然後把文件裡對應的 {{姓名}} 全部替換成真的名字(例如:王小明)
    ◦ 不只是文件,連寄出的 Email 主旨和內容,它也會幫你把名字換好,讓信看起來很有誠意
快遞寄送
    ◦ 文件填好後,它會自動把它轉成 PDF 檔(就像把證書護貝,比較正式)
    ◦ 它還會很貼心地把 Email 內容整理一下(把換行符號變成網頁讀得懂的格式 <br />),確保信件排版漂亮
    ◦ 最後,它會把這份 PDF 當作附件,寫一封信寄給報名的人
3. 嚴格的警衛(自動開關門)
除了文書處理,這段程式還有另一個功能:checkFormStatus,這位就像是門口的警衛
看時間:這位警衛需要你設定一個鬧鐘(觸發器),例如每 10 分鐘叫他檢查一次。他醒來後會看 "Setting" 裡的設定:現在幾點?活動開始時間 (START_TIME) 和結束時間 (END_TIME) 是幾點
執行門禁
    ◦ 如果現在時間在「活動期間內」,而且門是關著的,他就會把表單打開,讓大家報名
    ◦ 如果時間已經過了(或是還沒到),而且門開著,他就會把表單關閉,並且掛上一個告示牌(例如:「本表單目前不接受回應」)
--------------------------------------------------------------------------------
🎯範本下載:
   

/**
 * ------------------------------------------------------------------
 * 輔助函式:讀取 "Setting" 工作表中的設定值
 * ------------------------------------------------------------------
 */
function getSettings() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName('Setting');

  if (!sheet) {
    throw new Error('錯誤:找不到名為 "Setting" 的工作表,請在試算表中建立此分頁。');
  }

  var lastRow = sheet.getLastRow();
  if (lastRow < 2) return {};

  var data = sheet.getRange(2, 1, lastRow - 1, 2).getValues();
  var settings = {};

  data.forEach(function (row) {
    var key = row[0];
    var value = row[1];
    if (key) {
      settings[key] = value;
    }
  });

  return settings;
}

/**
 * ------------------------------------------------------------------
 * 主函式:表單提交時觸發
 * ------------------------------------------------------------------
 */
function onFormSubmit(e) {
  try {
    // 1. 讀取 Setting
    var settings = getSettings();

    if (!settings.TEMPLATE_ID || !settings.FOLDER_ID) {
      Logger.log('錯誤:Setting 工作表中缺少 TEMPLATE_ID 或 FOLDER_ID。');
      return;
    }

    var docTemplateId = settings.TEMPLATE_ID;
    var folderId = settings.FOLDER_ID;
    var emailSubjectTemplate = settings.EMAIL_SUBJECT || '您的文件已完成';
    var emailBodyTemplate = settings.EMAIL_BODY || '您好,您的文件已備妥,請查收附件。';

    // 2. 取得表單資料
    var formData = e.namedValues;
    var recipientEmail = '';
    if (formData['Email Address']) recipientEmail = formData['Email Address'][0];
    else if (formData['電子郵件地址']) recipientEmail = formData['電子郵件地址'][0];
    else if (formData['Email']) recipientEmail = formData['Email'][0];
    else if (formData['電子信箱']) recipientEmail = formData['電子信箱'][0];

    if (!recipientEmail) {
      Logger.log('錯誤:無法在表單回應中找到電子郵件地址,腳本終止。');
      return;
    }

    // 3. 準備檔案與資料夾
    var templateFile = DriveApp.getFileById(docTemplateId);
    var destinationFolder = DriveApp.getFolderById(folderId);
    var schoolName = formData['研習名稱'] ? formData['研習名稱'][0] : '研習活動';
    var chineseName = formData['姓名'] ? formData['姓名'][0] : '學員';
    var newDocName = schoolName + ' - ' + chineseName + ' - 證明 ' + new Date().toLocaleDateString();

    var newDocFile = templateFile.makeCopy(newDocName, destinationFolder);

    // 4. 文件內容替換
    var newDoc = DocumentApp.openById(newDocFile.getId());
    var body = newDoc.getBody();

    var finalSubject = emailSubjectTemplate;
    var finalBody = emailBodyTemplate;

    for (var key in formData) {
      if (formData.hasOwnProperty(key)) {
        var value = formData[key][0];

        // 替換 Word 內容
        body.replaceText('{{' + key + '}}', value);

        // 替換 Email 變數
        var regex = new RegExp('{{' + key + '}}', 'g');
        finalSubject = finalSubject.replace(regex, value);
        finalBody = finalBody.replace(regex, value);
      }
    }

    newDoc.saveAndClose();

    // 5. 轉換 PDF 並寄送
    var pdfFile = newDocFile.getAs('application/pdf');

    // --- HTML 格式修正區段 ---
    // 將試算表的換行符號轉為 HTML <br>
    finalBody = finalBody.replace(/\r\n/g, '<br>').replace(/\n/g, '<br>');

    GmailApp.sendEmail(recipientEmail, finalSubject, '您的信箱不支援 HTML 顯示', {
      htmlBody: finalBody,  // 設定 HTML 內容
      attachments: [pdfFile.setName(newDocName + '.pdf')],
      name: '自動通知系統'
    });
    // -----------------------

    Logger.log('成功:已寄送文件給 ' + recipientEmail);

  } catch (error) {
    Logger.log('發生嚴重錯誤: ' + error.toString());
  }
}


/**
 * ------------------------------------------------------------------
 * 新增功能:檢查並更新表單的開啟/關閉狀態
 * 請設定「時間驅動」觸發器來定期執行此函式 (例如每 10 分鐘)
 * ------------------------------------------------------------------
 */
function checkFormStatus() {
  try {
    // 1. 讀取設定
    var settings = getSettings();
    var formId = settings.FORM_ID;

    // 檢查是否有設定表單 ID
    if (!formId) {
      Logger.log('錯誤:Setting 工作表中缺少 FORM_ID,無法控制表單開關。');
      return;
    }

    // 取得時間設定 (若未設定則視為 null)
    var startTime = settings.START_TIME ? new Date(settings.START_TIME) : null;
    var endTime = settings.END_TIME ? new Date(settings.END_TIME) : null;
    var closedMessage = settings.CLOSED_MESSAGE || '本表單目前不接受回應。';

    // 如果沒有設定時間,就不做任何動作,避免誤關
    if (!startTime || !endTime) {
      Logger.log('略過:未設定完整的 START_TIME 或 END_TIME。');
      return;
    }

    // 2. 判斷當前狀態
    var form = FormApp.openById(formId);
    var now = new Date(); // 取得現在時間
    var isOpen = false;

    // 邏輯:現在時間 必須在 開始與結束 之間
    if (now >= startTime && now <= endTime) {
      isOpen = true;
    }

    // 3. 執行開關動作 (只在狀態需要改變時才動作,節省資源)
    var currentStatus = form.isAcceptingResponses();

    if (isOpen && !currentStatus) {
      // 應該開啟,但目前是關閉 -> 執行開啟
      form.setAcceptingResponses(true);
      Logger.log('表單已自動【開啟】');
    }
    else if (!isOpen && currentStatus) {
      // 應該關閉,但目前是開啟 -> 執行關閉
      form.setAcceptingResponses(false);
      form.setCustomClosedFormMessage(closedMessage); // 設定關閉訊息
      Logger.log('表單已自動【關閉】');
    }
    else {
      Logger.log('檢查完畢:表單狀態無須變更 (目前狀態: ' + (currentStatus ? '開啟' : '關閉') + ')');
    }

  } catch (error) {
    Logger.log('檢查表單狀態時發生錯誤: ' + error.toString());
  }
}



2.點選 左側 ⚙️ 專案設定 - ✅在編輯器中顯示「appsscript.json」資訊清單檔案
再回到 < > 編輯器中 將程式貼到 appsscript.json 中即可。


{
  "timeZone": "Asia/Taipei",
  "dependencies": {},
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "oauthScopes": [
    "https://www.googleapis.com/auth/script.external_request",
    "https://www.googleapis.com/auth/spreadsheets",
    "https://www.googleapis.com/auth/presentations",
    "https://www.googleapis.com/auth/drive",
    "https://www.googleapis.com/auth/script.send_mail",
    "https://www.googleapis.com/auth/script.scriptapp",
    "https://www.googleapis.com/auth/forms",
    "https://www.googleapis.com/auth/documents"
  ]
}


3.設定 程式 觸發條件 



3.設定 表單 時間 觸發條件 





沒有留言:

張貼留言

歡迎大家一起留言討論!