︿
Top

2025年5月29日 星期四

初探 NotebookLM + Google 表單:以 醫療機構電子病歷製作及管理辦法 為例

一. 前言

延續前一篇: 初探 NotebookLM:以 醫療機構電子病歷製作及管理辦法 為例 有提到 NotebookLM 可以產生題庫, 但後續需 自行於 Google Forms 或 Microsoft Forms 建立表單測驗卷.

這個建立表單測驗卷的工作, 有沒有辦法由 AI 協助建立; 而不需手工一題一題複製貼上呢? 答案是可行的.

本文將以 Google Forms 為例, 透過 ChatGPT o3 推理模型, 產生 Google Apps Script, 將產出貼到 Google Apps Script 的執行環境, 操作 Google 表單/文件 ... 等物件, 建置測驗卷; 並要求在學生 [提交] 之後, 可以自動批改, 將成續寫到試算表, 同時, 將成續一併以 email 寄給學生.

依前一篇的內容, 產生題庫以後 即可進行以下步驟.

二. 以 ChatGPT o3 推理模型 產生所需的 Google Apps Script

STEP 1: 提示

# 需求

請幫我用 Google App Script :

## 測驗卷需求

1. 測驗卷可能有不同的題數, 但總分為 100 分. 例如:
  - 20 題:  5 分/題
  - 10 題: 10 分/題
1. 表單必須有 email 及 姓名, 此為必要欄位, 考生必須填入.

## 考生提交考卷後的需求

1. 依答案批改考卷, 計算成績.  
2. 寫入至 "三年A班-電子病歷測驗成績" 試算表. 欄位說明如下:
  - 時間戳記
  - 姓名
  - email
  - 成績
  - 各題的回答, 假設有 10 題, 那就設置 Q1 .. Q10 共 10 個欄位 
    - 各欄位只需填入英文字母的答案.
      - 例如: 考生在第1題回答 B, 則試算表的 Q1 欄位, 只要填入 "B", 不需填入 "B. 醫療法第六十九條"
    - 如果有答錯, 該儲存格以黃底標示. 
1. 以 email 回覆學生實得分數. 

# 電子病歷測驗卷的考題及答案.

## 考題

<<< 

1. 醫療機構電子病歷製作及管理辦法是依據下列哪一條法規訂定?
  A. 醫療法第六十八條
  B. 醫療法第六十九條
  C. 個人資料保護法
  D. 電子簽章法


1. 根據本辦法規定,醫療機構以電子文件方式製作及貯存病歷,若符合辦法規定者,下列敘述何者正確?
  A. 得免另以書面方式製作
  B. 仍應將電子病歷列印成書面保存
  C. 需經中央主管機關核准後方可免書面
  D. 電子病歷僅作為輔助參考,書面病歷仍是正本


1. 醫療機構實施電子病歷應建置系統,並具備下列管理機制,何者不包含在內?
  A. 標準作業機制
  B. 權限管控機制
  C. 市場行銷機制
  D. 緊急應變機制


1. 當醫療機構發生安全事故,且影響其營運或當事人權益時,應於知悉事故發生起多久時間內通報直轄市、縣(市)主管機關?
  A. 二十四小時內
  B. 七十二小時內
  C. 一週內
  D. 一個月內


1. 醫療機構若委託機構建置及管理電子病歷系統,該受託機構應通過哪一種認證?
  A. ISO 9001 品質管理系統驗證
  B. 中央主管機關認可之資訊安全標準驗證
  C. 電子病歷系統功能驗證
  D. 醫療機構評鑑合格


1. 醫療機構使用雲端服務儲存電子病歷資料時,資料儲存地點一般應設置於何處?
  A. 國外資料中心,以分散風險
  B. 我國境內
  C. 醫療機構內部伺服器
  D. 受託機構指定地點


1. 醫療機構製作電子病歷後,醫事人員原則上應於多久時間內完成電子簽章?
  A. 八小時內
  B. 十二小時內
  C. 二十四小時內
  D. 四十八小時內


1. 醫療機構將電子病歷移轉由承接者保存時,應製作移轉紀錄,該紀錄應由承接者妥善保存至少多久?
  A. 一年
  B. 三年
  C. 五年
  D. 七年


1. 醫療機構依電子病歷交換平臺進行電子病歷交換或利用時,除緊急情形外,應符合何項規定?
  A. 經中央主管機關核准
  B. 經病人同意
  C. 經轉介醫師同意
  D. 僅限用於學術研究


1. 醫療機構將既有之紙本病歷轉錄為電子檔案保存後,原件紙本得如何處理?
  A. 仍需保存十年
  B. 應送交病歷管理單位封存
  C. 得免以書面方式保存,且不受本法第七十條第一項保存期間之限制
  D. 應於一年內銷毀
  
>>>


## 答案

<<<

1. 答案:B
解說:本辦法係依醫療法第六十九條規定訂定。

1. 答案:A
解說:醫療機構以電子文件方式製作及貯存之病歷,符合本辦法規定者,得免另以書面方式製作。

1. 答案:C
解說:電子病歷系統應具備標準作業、權限管控、緊急應變、系統安全、傳輸加密及安全事故處理等管理機制。市場行銷機制並非其中之一。

1. 答案:B
解說:安全事故影響醫療機構營運或當事人權益時,醫療機構應於知悉事故發生起七十二小時內通報直轄市、縣(市)主管機關。

1. 答案:B
解說:受託機構應通過中央主管機關認可之資訊安全標準驗證,並有證明文件。

1. 答案:B
解說:雲端服務之資料儲存地點,應設置於我國境內。特殊情形經中央主管機關核准者不在此限。

1. 答案:C
解說:病歷製作後,應於二十四小時內完成電子簽章。醫事人員因故無法於時限內完成者,應由醫療機構簽章代替,事後應完成補簽。

1. 答案:C
解說:移轉紀錄應製作交由承接者至少保存五年。

1. 答案:B
解說:醫療機構透過交換平臺進行電子病歷交換或利用時,應經病人同意,病人情況緊急無法取得者不在此限。

1. 答案:C
解說:將既有紙本病歷轉錄為電子檔案並簽章封存後,原件得免以書面方式保存,且不受醫療法第七十條第一項保存期間之限制。

>>>

STEP 2: Script

完整的 script, 參考 [附錄一].

STEP 3: 在 Google Apps Script 執行環境, 建立表單相關物件及事件

  1. 開啟 Google Apps Script 網頁: Google Apps Script.
  2. 建立新專案. 名稱: "三年A班-電子病歷測驗".
  3. 將產生的 javascript, 貼到 Script Editor.
  4. 將專案 [儲存] 至雲端硬碟, 再按 [執行].

01-儲存及執行 Google Apps Script

  1. 第一次執行會要求授權, 按畫面指示完成. 如遇「未經 Google 驗證」警告, 點「進階」→「前往…」即可.

02 要求授權

03 要求授權

04 要求授權

  1. 建立表單過程, 發現有錯誤.
    --> 說真的, 不太可能一次就建立成功.
    05 建立過程發生錯誤

  2. 將錯誤貼到 ChatGPT, 請它修正.
    --> 應該會有多次.

STEP 4: 第一版可以建立表單相關物件及事件的 script

完整的 script, 參考 [附錄二].

三. 開始測試. 如果有錯, 要回頭調整 Script

STEP 1: 填寫考題, 並 [提交]

21 填寫考題

22 提交考卷

STEP 2: 沒有將成績寫入試算表, 也未寄送成績單

請 ChatGPT 再修正程式.
因為過程有點多, 這個部分就不贅述.

STEP 3: 終於產出可滿足需求的版本 script

完整的 script, 參考 [附錄三].

STEP 4: 測試截圖 (考生)

  1. 填寫測驗卷, 按 [提交].

21 填寫考題

22 提交考卷

  1. 在 email 會收到成績通知.

23 收到成績單

STEP 5: 測試截圖 (老師)

  1. 查看各考生的成續

31 老師查看成績

四. 相關的程式, 表單, 檔案的存放位置

相關的程式, 表單, 檔案 預設都存在 "老師" (測試卷製作者) Google 雲端硬碟的根目錄.

41 相關程式_表單_檔案_存放位置

五. 結論

建立 script 範本的好處

經由反覆與 ChatGPT 對話, 終於產出一版可用的測驗卷 script 範本.

這樣作有什麼好處呢? 未來只要有新的測驗卷要作:

  • 1. 準備 考題及答案.
  • 2. 準備 QUESTIONS 的樣版.
const QUESTIONS = [
  {
    text: '1. 醫療機構電子病歷製作及管理辦法是依據下列哪一條法規訂定?',
    options: [
      'A. 醫療法第六十八條',
      'B. 醫療法第六十九條',
      'C. 個人資料保護法',
      'D. 電子簽章法'
    ],
    answer: 'B'
  },
  // ... 略 ...
  {
    text: '10. 醫療機構將既有之紙本病歷轉錄為電子檔案保存後,原件紙本得如何處理?',
    options: [
      'A. 仍需保存十年',
      'B. 應送交病歷管理單位封存',
      'C. 得免以書面方式保存,且不受本法第七十條第一項保存期間之限制',
      'D. 應於一年內銷毀'
    ],
    answer: 'C'
  }
];
  • 3. 請 ChatGPT 依樣畫葫蘆, 產生 QUESTIONS 的程式段.

  • 4. 自行合併為新的 script, 修改必要的常數,

const FORM_TITLE  = '三年A班-電子病歷測驗卷';
const SHEET_NAME  = '三年A班-電子病歷測驗成績';
const SENDER_NAME = '電子病歷成績自動回覆系統';
const PROP_KEY    = 'SCORE_SHEET_ID';   // 存試算表 ID 用
  • 5. 在 Googole Apps Script 建立新的專案, 貼上新的 script, [儲存] + [執行] 就有一份新的測驗卷了.

補充說明

目前各家的 AI 模型, 雖然已經算很不錯了, 但還是會犯錯, 需要人工介入; 所以, 通常不大可能一次就完成需求, 要多次對話. 當發生錯誤時, 有 2 種方式處理:

  1. 人工自行修改程式.
  2. 依使用者回報的狀況及錯誤訊息, 請 AI 自行修改.

筆者會選擇 o3 推理模型, 主要是對 Google Apps Script 及 Google 表單都不熟, 只能提出 "要作什麼 (What)" 的需求, 無法告知 "如何作 (How)" 的細節.

所以, 並不是什麼需求都要用推理模型來作, 例如: 旅行中的翻譯需求, 只需基本模型 (ex: 4o) 即可. 筆者有建立了一個 ChatGPTs ( [翻譯] 中・英・日即時翻譯助理 ), 歡迎使用.

  • 語音的部分是轉到 google 翻譯作處理 (會有 200 個字的限制)
    • 電腦版會出現 404 error.
      • ChatGPT 表示, 若是來自電腦版的 Chrome, Google 翻譯會檢查 referer 的 request header, 而 ChatGPT 應該不在名單內, 故被回應 404.
      • 只要在網址列, 再按一次 [Enter], 就可以發音了.
    • 手機版看來正常.
      • ChatGPT 表示, 若是來自手機版的 Chrome, 因為一般都沒有傳 referer, 故 Google 翻譯未檢查 referer 的 request header.

51 中英日翻譯助理

[附錄一] 無法在 Google Apps Script 執行建立表單的程式版本

這版會無法在 Google Apps Script 執行, 會發生錯誤.

ChatGPT 的回應文字, 已包含如何操作.

以下範例是一份 完整可執行的 Google Apps Script(GAS);只要把整段貼進 Script Editor(或 App Script IDE),先執行一次 setup() 就會:

  1. 自動建立測驗表單(已啟用「電子郵件收集」與「姓名」必填欄位)。
  2. 依題目數自動計算「每題配分 = 100 ÷ 題數」,並將正確解答設定為評分依據。
  3. 建立一張新的 Google 試算表 「三年A班-電子病歷測驗成績」,並在表頭排好欄位。
  4. 設定 安裝式 onFormSubmit 觸發器,讓程式在考生交卷時自動批改、寫分數、標示錯題並寄成績信。

⚠️ 第一次要先在 IDE 執行 setup(),授權後就不用再動手。 之後所有動作(批改、寄信、寫入分數)都將自動發生。

Script

/************* 使用者可自行調整的區塊 ****************/

// 1. 考題(依實際題數增刪)──請保持題目與選項格式一致
const QUESTIONS = [
  {
    text: '1. 醫療機構電子病歷製作及管理辦法是依據下列哪一條法規訂定?',
    options: [
      'A. 醫療法第六十八條',
      'B. 醫療法第六十九條',
      'C. 個人資料保護法',
      'D. 電子簽章法'
    ],
    answer: 'B'
  },
  {
    text: '2. 根據本辦法規定,醫療機構以電子文件方式製作及貯存病歷,若符合辦法規定者,下列敘述何者正確?',
    options: [
      'A. 得免另以書面方式製作',
      'B. 仍應將電子病歷列印成書面保存',
      'C. 需經中央主管機關核准後方可免書面',
      'D. 電子病歷僅作為輔助參考,書面病歷仍是正本'
    ],
    answer: 'A'
  },
  {
    text: '3. 醫療機構實施電子病歷應建置系統,並具備下列管理機制,何者不包含在內?',
    options: [
      'A. 標準作業機制',
      'B. 權限管控機制',
      'C. 市場行銷機制',
      'D. 緊急應變機制'
    ],
    answer: 'C'
  },
  {
    text: '4. 當醫療機構發生安全事故,且影響其營運或當事人權益時,應於知悉事故發生起多久時間內通報直轄市、縣(市)主管機關?',
    options: [
      'A. 二十四小時內',
      'B. 七十二小時內',
      'C. 一週內',
      'D. 一個月內'
    ],
    answer: 'B'
  },
  {
    text: '5. 醫療機構若委託機構建置及管理電子病歷系統,該受託機構應通過哪一種認證?',
    options: [
      'A. ISO 9001 品質管理系統驗證',
      'B. 中央主管機關認可之資訊安全標準驗證',
      'C. 電子病歷系統功能驗證',
      'D. 醫療機構評鑑合格'
    ],
    answer: 'B'
  },
  {
    text: '6. 醫療機構使用雲端服務儲存電子病歷資料時,資料儲存地點一般應設置於何處?',
    options: [
      'A. 國外資料中心,以分散風險',
      'B. 我國境內',
      'C. 醫療機構內部伺服器',
      'D. 受託機構指定地點'
    ],
    answer: 'B'
  },
  {
    text: '7. 醫療機構製作電子病歷後,醫事人員原則上應於多久時間內完成電子簽章?',
    options: [
      'A. 八小時內',
      'B. 十二小時內',
      'C. 二十四小時內',
      'D. 四十八小時內'
    ],
    answer: 'C'
  },
  {
    text: '8. 醫療機構將電子病歷移轉由承接者保存時,應製作移轉紀錄,該紀錄應由承接者妥善保存至少多久?',
    options: [
      'A. 一年',
      'B. 三年',
      'C. 五年',
      'D. 七年'
    ],
    answer: 'C'
  },
  {
    text: '9. 醫療機構依電子病歷交換平臺進行電子病歷交換或利用時,除緊急情形外,應符合何項規定?',
    options: [
      'A. 經中央主管機關核准',
      'B. 經病人同意',
      'C. 經轉介醫師同意',
      'D. 僅限用於學術研究'
    ],
    answer: 'B'
  },
  {
    text: '10. 醫療機構將既有之紙本病歷轉錄為電子檔案保存後,原件紙本得如何處理?',
    options: [
      'A. 仍需保存十年',
      'B. 應送交病歷管理單位封存',
      'C. 得免以書面方式保存,且不受本法第七十條第一項保存期間之限制',
      'D. 應於一年內銷毀'
    ],
    answer: 'C'
  }
];

// 2. 成績試算表名稱(可依班級/測驗更換)
const SCORE_SHEET_NAME = '三年A班-電子病歷測驗成績';

// 3. 若想指定寄件者名稱,可在此修改
const MAIL_SENDER_NAME = '電子病歷測驗系統';

/************** 以下程式碼通常不需改動 ****************/

/**
 * 第一次先手動執行此函式:
 * - 建立 Google Form、設定配分與正確答案
 * - 建立「成績」試算表並設定表頭
 * - 安裝 onFormSubmit 觸發器
 */
function setup() {
  // 1. 建立表單並啟用測驗
  const form = FormApp.create('電子病歷測驗卷')
    .setDescription('請填答完畢後送出,系統將自動計分並寄送成績')
    .setIsQuiz(true)
    .setCollectEmail(true); // 系統內建 Email 欄位
  
  // 新增「姓名」必填欄
  form.addTextItem()
      .setTitle('姓名')
      .setRequired(true);
  
  // 計算每題配分
  const pointsPerQuestion = Math.floor(100 / QUESTIONS.length);
  
  // 建題目、設定正確答案與配分
  QUESTIONS.forEach(q => {
    const item = form.addMultipleChoiceItem()
                    .setTitle(q.text)
                    .setRequired(true)
                    .setPoints(pointsPerQuestion);
    const choices = q.options.map(opt => {
      const isCorrect = opt.startsWith(q.answer + '.');
      return item.createChoice(opt, isCorrect); // 第二參數 true = 正解
    });
    item.setChoices(choices);
  });
  
  // 2. 建立成績試算表並寫表頭
  const ss = SpreadsheetApp.create(SCORE_SHEET_NAME);
  const header = ['時間戳記', '姓名', 'email', '成績'];
  for (let i = 1; i <= QUESTIONS.length; i++) header.push(`Q${i}`);
  ss.getActiveSheet().appendRow(header);
  
  // 3. 安裝觸發器(避免重複安裝)
  const triggers = ScriptApp.getProjectTriggers()
                    .filter(t => t.getHandlerFunction() === 'onFormSubmit');
  if (!triggers.length) {
    ScriptApp.newTrigger('onFormSubmit')
             .forForm(form)
             .onFormSubmit()
             .create();
  }
  
  Logger.log('✅ 設定完成!表單連結:' + form.getPublishedUrl());
  Logger.log('✅ 成績試算表:' + ss.getUrl());
}

/**
 * 表單送出時自動呼叫
 * @param {GoogleAppsScript.Events.FormsOnFormSubmit} e
 */
function onFormSubmit(e) {
  // 1. 基本資訊
  const timestamp = new Date();
  const email     = e.namedValues['收件者電子郵件地址']?.[0] || e.values[1]; // 收集 Email
  const name      = e.namedValues['姓名']?.[0]            || e.values[2];
  
  // 2. 擷取作答(e.values = [Timestamp, Email, Name, Ans1, Ans2, ...])
  const submittedAnswers = e.values.slice(3); // 長度 = 題數
  const answerLetters    = submittedAnswers.map(a => (a || '').trim().charAt(0).toUpperCase());
  
  // 3. 批改 & 計分
  const pointsPerQuestion = Math.floor(100 / QUESTIONS.length);
  let score = 0;
  const wrongIndices = [];
  answerLetters.forEach((letter, idx) => {
    if (letter === QUESTIONS[idx].answer) {
      score += pointsPerQuestion;
    } else {
      wrongIndices.push(idx); // 待會標黃底
    }
  });
  
  // 4. 寫入成績試算表
  const ss   = SpreadsheetApp.openById(getScoreSheetId());
  const sheet = ss.getActiveSheet();
  const row  = [timestamp, name, email, score, ...answerLetters];
  const rowIndex = sheet.appendRow(row).getRow();
  
  // 5. 標示錯題為黃底
  if (wrongIndices.length) {
    const bgRange = wrongIndices.map(i => sheet.getRange(rowIndex, 4 + 1 + i)); // 4欄固定 + 第i題
    bgRange.forEach(r => r.setBackground('#FFF59D')); // 淡黃色
  }
  
  // 6. 寄送成績 Email
  GmailApp.sendEmail(
    email,
    '電子病歷測驗成績通知',
    `您好 ${name},\n\n您本次測驗成績為:${score} 分 / 100 分。\n\n感謝您的作答!\n\n── ${MAIL_SENDER_NAME}`,
    { name: MAIL_SENDER_NAME }
  );
}

/** 取得成績試算表 ID(第一次 setup 時寫在 Properties) */
function getScoreSheetId() {
  const prop = PropertiesService.getScriptProperties().getProperty('SCORE_SHEET_ID');
  if (prop) return prop;
  throw new Error(`找不到成績試算表,請先執行 setup()!`);
}

/** 在 setup() 完成建立試算表後呼叫,僅在 setup 內部使用 */
function setScoreSheetId(id) {
  PropertiesService.getScriptProperties().setProperty('SCORE_SHEET_ID', id);
}

// 把 setScoreSheetId 內嵌到 setup() 內呼叫
(function patchSetup() {
  const original = setup;
  setup = function () {
    original();
    const ssId = SpreadsheetApp.getActiveSpreadsheet().getId();
    setScoreSheetId(ssId);
  };
})();

[附錄二] 可在 Google Apps Script 執行建立表單的程式版本, 但未寫寫試算表及回覆成績

可在 Google Apps Script 執行建立表單的程式版本, 但未寫試算表及回覆成績.


/************* 使用者可自行調整的區塊 ****************/

// 1. 題庫(可增減題目、修改答案)
const QUESTIONS = [
  {
    text: '1. 醫療機構電子病歷製作及管理辦法是依據下列哪一條法規訂定?',
    options: [
      'A. 醫療法第六十八條',
      'B. 醫療法第六十九條',
      'C. 個人資料保護法',
      'D. 電子簽章法'
    ],
    answer: 'B'
  },
  {
    text: '2. 根據本辦法規定,醫療機構以電子文件方式製作及貯存病歷,若符合辦法規定者,下列敘述何者正確?',
    options: [
      'A. 得免另以書面方式製作',
      'B. 仍應將電子病歷列印成書面保存',
      'C. 需經中央主管機關核准後方可免書面',
      'D. 電子病歷僅作為輔助參考,書面病歷仍是正本'
    ],
    answer: 'A'
  },
  {
    text: '3. 醫療機構實施電子病歷應建置系統,並具備下列管理機制,何者不包含在內?',
    options: [
      'A. 標準作業機制',
      'B. 權限管控機制',
      'C. 市場行銷機制',
      'D. 緊急應變機制'
    ],
    answer: 'C'
  },
  {
    text: '4. 當醫療機構發生安全事故,且影響其營運或當事人權益時,應於知悉事故發生起多久時間內通報直轄市、縣(市)主管機關?',
    options: [
      'A. 二十四小時內',
      'B. 七十二小時內',
      'C. 一週內',
      'D. 一個月內'
    ],
    answer: 'B'
  },
  {
    text: '5. 醫療機構若委託機構建置及管理電子病歷系統,該受託機構應通過哪一種認證?',
    options: [
      'A. ISO 9001 品質管理系統驗證',
      'B. 中央主管機關認可之資訊安全標準驗證',
      'C. 電子病歷系統功能驗證',
      'D. 醫療機構評鑑合格'
    ],
    answer: 'B'
  },
  {
    text: '6. 醫療機構使用雲端服務儲存電子病歷資料時,資料儲存地點一般應設置於何處?',
    options: [
      'A. 國外資料中心,以分散風險',
      'B. 我國境內',
      'C. 醫療機構內部伺服器',
      'D. 受託機構指定地點'
    ],
    answer: 'B'
  },
  {
    text: '7. 醫療機構製作電子病歷後,醫事人員原則上應於多久時間內完成電子簽章?',
    options: [
      'A. 八小時內',
      'B. 十二小時內',
      'C. 二十四小時內',
      'D. 四十八小時內'
    ],
    answer: 'C'
  },
  {
    text: '8. 醫療機構將電子病歷移轉由承接者保存時,應製作移轉紀錄,該紀錄應由承接者妥善保存至少多久?',
    options: [
      'A. 一年',
      'B. 三年',
      'C. 五年',
      'D. 七年'
    ],
    answer: 'C'
  },
  {
    text: '9. 醫療機構依電子病歷交換平臺進行電子病歷交換或利用時,除緊急情形外,應符合何項規定?',
    options: [
      'A. 經中央主管機關核准',
      'B. 經病人同意',
      'C. 經轉介醫師同意',
      'D. 僅限用於學術研究'
    ],
    answer: 'B'
  },
  {
    text: '10. 醫療機構將既有之紙本病歷轉錄為電子檔案保存後,原件紙本得如何處理?',
    options: [
      'A. 仍需保存十年',
      'B. 應送交病歷管理單位封存',
      'C. 得免以書面方式保存,且不受本法第七十條第一項保存期間之限制',
      'D. 應於一年內銷毀'
    ],
    answer: 'C'
  }
];

// 2. 成績試算表名稱
const SCORE_SHEET_NAME = '三年A班-電子病歷測驗成績';

// 3. Email 寄件者名稱
const MAIL_SENDER_NAME = '電子病歷測驗系統';

/************** 以下程式碼通常不需改動 ****************/

/**
 * 初始設定:
 * - 建立 Google Form(含姓名必填與 Email 收集)
 * - 設定題目及配分
 * - 建立成績試算表並寫表頭
 * - 安裝 onFormSubmit 觸發器
 */
function setup() {
  /* ---------- 建立並設定表單 ---------- */
  const form = FormApp.create('電子病歷測驗卷')
    .setDescription('請填答完畢後送出,系統將自動計分並寄送成績')
    .setIsQuiz(true)
    .setCollectEmail(true);   // 系統自帶 Email 欄位

  // 新增「姓名」必填欄
  form.addTextItem()
      .setTitle('姓名')
      .setRequired(true);

  // 每題配分
  const pointsPerQuestion = Math.floor(100 / QUESTIONS.length);

  // 新增所有題目
  QUESTIONS.forEach(q => {
    const item = form.addMultipleChoiceItem()
                     .setTitle(q.text)
                     .setRequired(true)
                     .setPoints(pointsPerQuestion);
    const choices = q.options.map(opt => {
      const isCorrect = opt.startsWith(q.answer + '.');
      return item.createChoice(opt, isCorrect);
    });
    item.setChoices(choices);
  });

  /* ---------- 建立成績試算表 ---------- */
  const ss = SpreadsheetApp.create(SCORE_SHEET_NAME);
  const header = ['時間戳記', '姓名', 'email', '成績'];
  for (let i = 1; i <= QUESTIONS.length; i++) header.push(`Q${i}`);
  ss.getActiveSheet().appendRow(header);

  // 將試算表 ID 存入屬性,後續查閱
  setScoreSheetId(ss.getId());

  /* ---------- 安裝觸發器(若尚未安裝) ---------- */
  if (!ScriptApp.getProjectTriggers()
        .some(t => t.getHandlerFunction() === 'onFormSubmit')) {
    ScriptApp.newTrigger('onFormSubmit')
             .forForm(form)
             .onFormSubmit()
             .create();
  }

  /* ---------- 結束 ---------- */
  Logger.log('✅ 表單連結:' + form.getPublishedUrl());
  Logger.log('✅ 成績試算表:' + ss.getUrl());
}

/**
 * 表單送出時自動呼叫
 * @param {GoogleAppsScript.Events.FormsOnFormSubmit} e
 */
function onFormSubmit(e) {
  // 抓基本欄位
  const timestamp = new Date();
  const email = e.namedValues['收件者電子郵件地址']?.[0] || e.values[1];
  const name  = e.namedValues['姓名']?.[0]            || e.values[2];

  // 擷取作答(索引 3 起)
  const submittedAnswers = e.values.slice(3);
  const answerLetters = submittedAnswers.map(a => (a || '').trim().charAt(0).toUpperCase());

  // 批改
  const pointsPerQuestion = Math.floor(100 / QUESTIONS.length);
  let score = 0;
  const wrongIdx = [];
  answerLetters.forEach((letter, idx) => {
    if (letter === QUESTIONS[idx].answer) score += pointsPerQuestion;
    else wrongIdx.push(idx);
  });

  // 寫入試算表
  const ss   = SpreadsheetApp.openById(getScoreSheetId());
  const sheet = ss.getActiveSheet();
  const row  = [timestamp, name, email, score, ...answerLetters];
  const rowIndex = sheet.appendRow(row).getRow();

  // 錯題標黃
  wrongIdx.forEach(i => {
    sheet.getRange(rowIndex, 4 + 1 + i) // 第 4 欄後是 Q1...
         .setBackground('#FFF59D');
  });

  // 寄成績信
  GmailApp.sendEmail(
    email,
    '電子病歷測驗成績通知',
    `您好 ${name},\n\n您本次測驗成績為:${score} 分 / 100 分。\n\n感謝您的作答!\n\n── ${MAIL_SENDER_NAME}`,
    { name: MAIL_SENDER_NAME }
  );
}

/* --------- 試算表 ID 儲存 / 讀取工具 --------- */
function setScoreSheetId(id) {
  PropertiesService.getScriptProperties().setProperty('SCORE_SHEET_ID', id);
}
function getScoreSheetId() {
  const id = PropertiesService.getScriptProperties().getProperty('SCORE_SHEET_ID');
  if (!id) throw new Error('找不到成績試算表 ID,請先執行 setup()');
  return id;
}

[附錄三] 可在 Google Apps Script 執行建立表, 並寫試算表及回覆成績的版本

可在 Google Apps Script 執行建立表, 並寫試算表及回覆成績的版本.

/***********  可依需求自行調整的常數  ****************/
const FORM_TITLE  = '三年A班-電子病歷測驗卷';
const SHEET_NAME  = '三年A班-電子病歷測驗成績';
const SENDER_NAME = '電子病歷成績自動回覆系統';
const PROP_KEY    = 'SCORE_SHEET_ID';   // 存試算表 ID 用

// 題庫(若需增刪題目,只改這裡即可)
const QUESTIONS = [
  {
    text: '1. 醫療機構電子病歷製作及管理辦法是依據下列哪一條法規訂定?',
    options: [
      'A. 醫療法第六十八條',
      'B. 醫療法第六十九條',
      'C. 個人資料保護法',
      'D. 電子簽章法'
    ],
    answer: 'B'
  },
  {
    text: '2. 根據本辦法規定,醫療機構以電子文件方式製作及貯存病歷,若符合辦法規定者,下列敘述何者正確?',
    options: [
      'A. 得免另以書面方式製作',
      'B. 仍應將電子病歷列印成書面保存',
      'C. 需經中央主管機關核准後方可免書面',
      'D. 電子病歷僅作為輔助參考,書面病歷仍是正本'
    ],
    answer: 'A'
  },
  {
    text: '3. 醫療機構實施電子病歷應建置系統,並具備下列管理機制,何者不包含在內?',
    options: [
      'A. 標準作業機制',
      'B. 權限管控機制',
      'C. 市場行銷機制',
      'D. 緊急應變機制'
    ],
    answer: 'C'
  },
  {
    text: '4. 當醫療機構發生安全事故,且影響其營運或當事人權益時,應於知悉事故發生起多久時間內通報直轄市、縣(市)主管機關?',
    options: [
      'A. 二十四小時內',
      'B. 七十二小時內',
      'C. 一週內',
      'D. 一個月內'
    ],
    answer: 'B'
  },
  {
    text: '5. 醫療機構若委託機構建置及管理電子病歷系統,該受託機構應通過哪一種認證?',
    options: [
      'A. ISO 9001 品質管理系統驗證',
      'B. 中央主管機關認可之資訊安全標準驗證',
      'C. 電子病歷系統功能驗證',
      'D. 醫療機構評鑑合格'
    ],
    answer: 'B'
  },
  {
    text: '6. 醫療機構使用雲端服務儲存電子病歷資料時,資料儲存地點一般應設置於何處?',
    options: [
      'A. 國外資料中心,以分散風險',
      'B. 我國境內',
      'C. 醫療機構內部伺服器',
      'D. 受託機構指定地點'
    ],
    answer: 'B'
  },
  {
    text: '7. 醫療機構製作電子病歷後,醫事人員原則上應於多久時間內完成電子簽章?',
    options: [
      'A. 八小時內',
      'B. 十二小時內',
      'C. 二十四小時內',
      'D. 四十八小時內'
    ],
    answer: 'C'
  },
  {
    text: '8. 醫療機構將電子病歷移轉由承接者保存時,應製作移轉紀錄,該紀錄應由承接者妥善保存至少多久?',
    options: [
      'A. 一年',
      'B. 三年',
      'C. 五年',
      'D. 七年'
    ],
    answer: 'C'
  },
  {
    text: '9. 醫療機構依電子病歷交換平臺進行電子病歷交換或利用時,除緊急情形外,應符合何項規定?',
    options: [
      'A. 經中央主管機關核准',
      'B. 經病人同意',
      'C. 經轉介醫師同意',
      'D. 僅限用於學術研究'
    ],
    answer: 'B'
  },
  {
    text: '10. 醫療機構將既有之紙本病歷轉錄為電子檔案保存後,原件紙本得如何處理?',
    options: [
      'A. 仍需保存十年',
      'B. 應送交病歷管理單位封存',
      'C. 得免以書面方式保存,且不受本法第七十條第一項保存期間之限制',
      'D. 應於一年內銷毀'
    ],
    answer: 'C'
  }
];

/************  一次性初始化:建立表單與試算表  ************/
function setup() {
  /* ------ 建立表單 ------ */
  const form = FormApp.create(FORM_TITLE)
    .setIsQuiz(true)
    .setCollectEmail(true) // 內建收集 Email
    .setDescription('填答完畢送出後,系統將自動計分並寄送成績');

  // 必填「姓名」欄
  form.addTextItem()
      .setTitle('姓名')
      .setRequired(true);

  // 平分 100
  const pts = Math.floor(100 / QUESTIONS.length);

  // 建題目並設定解答
  QUESTIONS.forEach(q => {
    const item = form.addMultipleChoiceItem()
                     .setTitle(q.text)
                     .setRequired(true)
                     .setPoints(pts);
    const choices = q.options.map(opt => {
      const correct = opt.startsWith(q.answer + '.');
      return item.createChoice(opt, correct);
    });
    item.setChoices(choices);
  });

  /* ------ 建立試算表 ------ */
  const ss = SpreadsheetApp.create(SHEET_NAME);
  const head = ['時間戳記', '姓名', 'email', '成績'];
  for (let i = 1; i <= QUESTIONS.length; i++) head.push(`Q${i}`);
  ss.getSheets()[0].setName('成績').appendRow(head);

  // 存試算表 ID 供 onFormSubmit 使用
  PropertiesService.getScriptProperties().setProperty(PROP_KEY, ss.getId());

  /* ------ 安裝觸發器 ------ */
  ScriptApp.newTrigger('onFormSubmit')
           .forForm(form)
           .onFormSubmit()
           .create();

  Logger.log('✅ 表單編輯連結:%s', form.getEditUrl());
  Logger.log('✅ 表單填寫網址:%s', form.getPublishedUrl());
  Logger.log('✅ 成績試算表:%s', ss.getUrl());
  Logger.log('✅ 建立完成!');  
}

/************  交卷自動呼叫:批改+寄信+寫分數  ************/
function onFormSubmit(e) {
  try {
    const resp  = e.response;
    const email = resp.getRespondentEmail();             // 收件者
    const items = resp.getItemResponses();

    const name  = items[0].getResponse();                // 第一題是「姓名」
    const answers = items.slice(1);                      // 其餘為考題

    // 將作答取首字母(A/B/C/D…)
    const letters = answers.map(r => {
      const a = (r.getResponse() || '').trim();
      return a.charAt(0).toUpperCase();
    });

    /* ------ 批改計分 ------ */
    const pts = Math.floor(100 / QUESTIONS.length);
    let score = 0;
    const wrong = [];
    letters.forEach((l, i) => {
      if (l === QUESTIONS[i].answer) score += pts;
      else wrong.push(i);
    });

    /* ------ 寫入試算表 ------ */
    const sheetId = PropertiesService.getScriptProperties().getProperty(PROP_KEY);
    if (!sheetId) throw new Error('找不到成績試算表 ID,請先重新執行 setup()');

    const sheet = SpreadsheetApp.openById(sheetId).getSheetByName('成績');
    const rowData = [new Date(), name, email, score, ...letters];
    sheet.appendRow(rowData);
    const rowIdx = sheet.getLastRow();

    // 標錯題黃底
    wrong.forEach(i => sheet.getRange(rowIdx, 5 + i).setBackground('#FFF59D'));

    /* ------ 寄送成績信 ------ */
    GmailApp.sendEmail(
      email,
      '電子病歷測驗成績通知',
      `您好 ${name}:\n\n您本次測驗成績為 ${score} 分(滿分 100)。\n\n感謝您的作答!\n\n── ${SENDER_NAME}`,
      { name: SENDER_NAME }
    );
  } catch (err) {
    Logger.log('onFormSubmit Error: ' + err);
    throw err; // 讓錯誤顯示在執行記錄中,方便除錯
  }
}

沒有留言:

張貼留言