星期六, 7月 11, 2026

[GAS] API 執行檔

根據官方文章-使用 Google Apps Script API 執行函式 的實作筆記,部屬 API 執行檔後在 C# 控制台上使用 OAuth2.0 來存取該 API,該 API 會 insert 資料進入 Google Sheet 內

說明
Apps Script API 提供 scripts.run 方法,可遠端執行指定的 Google Apps Script 函式。您可以在呼叫應用程式中使用這個方法,從遠端執行其中一個指令碼專案中的函式,並接收回應。
需求條件

必須先完成下列事項,才能使用 scripts.run 方法:
  • 將 GAS 部署為 API 執行檔
  • 設定 OAuth Scopes 且必須涵蓋 GAS 使用相關服務,不能只是呼叫函式使用的服務,EX:https://www.googleapis.com/auth/spreadsheets
  • 確認 GAS 和呼叫應用程式的 OAuth2 用戶端共用使用同一個 GCP 專案。GAS 專案必須是標準 GCP 專案,GAS 專案預設 GCP 專案無法使用
  • 在 GCP 專案中啟用 Google Apps Script API。
GAS Code

該 code.gs 單純是 insert 資料進入 Google Sheet 內,可以透過 testAddCheckIn() 進行測試
const SPREADSHEET_ID = '填入 Google Sheet ID';
const SHEET_NAME = 'CheckIn';

/**
 * 本機測試用:可先在 Apps Script 編輯器內執行,確認可正常寫入資料
 */
function testAddCheckIn() {
  return addCheckIn({
    identifier: 'U001',
    meetingCode: 'MEET-001',
    deviceInfo: 'Apps Script Editor Test'
  });
}

/**
 * 給外部 API Executable 呼叫的主函式
 */
function addCheckIn(payload) {
  payload = payload || {};

  const identifier = String(payload.identifier || '').trim();
  const meetingCode = String(payload.meetingCode || '').trim();
  const deviceInfo = String(payload.deviceInfo || '').trim();

  if (!identifier || !meetingCode) {
    return {
      success: false,
      status: 'NO_REQUIRED_DATA',
      message: 'identifier 和 meetingCode 為必填'
    };
  }

  const now = new Date();
  const checkedAt = Utilities.formatDate(
    now,
    'Asia/Taipei',
    'yyyy-MM-dd HH:mm:ss'
  );

  const sheet = getCheckInSheet_();
  sheet.appendRow([
    checkedAt,
    identifier,
    meetingCode,
    deviceInfo,
    'API_EXECUTABLE'
  ]);

  return {
    success: true,
    status: 'CHECK_IN_SUCCESS',
    checkedAt: checkedAt,
    identifier: identifier,
    meetingCode: meetingCode
  };
}

function getCheckInSheet_() {
  const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
  let sheet = ss.getSheetByName(SHEET_NAME);

  if (!sheet) {
    sheet = ss.insertSheet(SHEET_NAME);
    sheet.appendRow(['時間', '識別碼', '會議代碼', '裝置資訊', '來源']);
  }

  return sheet;
}
設定 appsscript.json

點選專案設定並勾選「在編輯器中顯示 appsscript.json 資訊清單檔案」
開啟 appsscript.json 並把 oauthScopes 範圍加進去
{
  "timeZone": "Asia/Taipei",
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "oauthScopes": [
    "https://www.googleapis.com/auth/spreadsheets"
  ]
}

GCP 標準專案

建立標準專案 gas-api-executable-demo 並啟用 Google Apps Script API
利用搜尋功能尋找 Google Apps Script API 服務
在 Google Enterprise API 和 Google Workspace 分類內都可以找到
啟用 Apps Script API 服務
設定 OAuth 同意畫面
GAS 綁定 GCP 標準專案

在標準專案上取得專案編號 (如下圖)
在 GAS 專案上綁定專案標號
綁定完成

該筆記 [GAS] Goolge Cloud Logging 內有紀錄不同順序的設定流程,也可以參考看看

設定用戶端
  • 應用程式類別:選擇電腦版應用程式
  • 名稱:C# Console
按下 [下載 json] 按鈕後,可以下載 credentials.json 檔案,檔案名稱為 client_secret_用戶端ID.apps.googleusercontent.com
部署 API 執行檔

部屬 => 新增部屬 => API 執行檔,誰可以存取」選項有 「只有我自己」、「所以已登入 Google 帳號使用者」
部屬完成
部屬時假如是預設專案的話,會出現下圖並引導至設定內,要求設定為標準專案
C# Console App 呼叫

首先要先確認部屬 ID,下圖為部屬時畫面上的部屬 ID,事後要取得的話,請從管理部屬進入取的
credentials.json 檔案放在 EXE 執行檔案資料夾內
安裝 NuGet
Install-Package Google.Apis.Script.v1
Install-Package Google.Apis.Auth
第一次執行 C# 程式時,瀏覽器會跳出 Google 授權畫面,看要選擇哪一個帳號來執行,該帳號必須是在 GCP 目標對象內
執行結果
token 會儲存在本機 token.json 資料夾,後續執行就不須再次授權
C# Code,必須填入 deploymentId 和 credentialsFileName
using Google.Apis.Auth.OAuth2;
using Google.Apis.Script.v1;
using Google.Apis.Script.v1.Data;
using Google.Apis.Services;
using Google.Apis.Util.Store;

internal class Program
{
    private static async Task Main()
    {
        string deploymentId = "請填入 API Executable 的 Deployment ID";
        string credentialsFileName = "請填入 credentials.json 檔案名稱或是完整檔案路徑";

        string[] scopes =
        {
            "https://www.googleapis.com/auth/spreadsheets"
        };

        UserCredential credential;

        using (var stream = new FileStream(credentialsFileName, FileMode.Open, FileAccess.Read))
        {
            credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
                GoogleClientSecrets.FromStream(stream).Secrets,
                scopes,
                "user",
                CancellationToken.None,
                new FileDataStore("token.json", true)
            );
        }

        var service = new ScriptService(new BaseClientService.Initializer
        {
            HttpClientInitializer = credential,
            ApplicationName = "GAS API Executable Demo"
        });

        var payload = new Dictionary<string, object>
        {
            { "identifier", "U001" },
            { "meetingCode", "MEET-001" },
            { "deviceInfo", "C# Console App" }
        };

        var request = new ExecutionRequest
        {
            Function = "addCheckIn",
            Parameters = new List<object> { payload }
        };

        Operation operation = await service.Scripts.Run(request, deploymentId).ExecuteAsync();

        if (operation.Error != null)
        {
            Console.WriteLine("GAS 執行失敗:");
            Console.WriteLine(operation.Error.Message);
            return;
        }

        if (operation.Response != null && operation.Response.ContainsKey("result"))
        {
            Console.WriteLine("GAS 回傳結果:");
            Console.WriteLine(operation.Response["result"]);
        }
        else
        {
            Console.WriteLine("GAS 執行完成,但沒有回傳 result。");
        }
    }
}
Google Sheet 內資料

星期六, 7月 04, 2026

[GAS] 服務配額-MailApp.getRemainingDailyQuota

之前以為 Google Apps Script 服務配額是沒有內建函數可以查詢,上課時老師提到可寄發 mail 數量是可以查詢的,查發現是透過 getRemainingDailyQuota() 來取得,官方文件說明
Returns the number of recipients you can send emails to for the rest of the day. The returned value is valid for the current execution and might vary between executions.
function sendEmailQuotaQuery()
{
  console.log(MailApp.getRemainingDailyQuota());
}

星期三, 7月 01, 2026

[Forms] 顯示連結以傳送更多回覆

上課時聽到老師介紹才發現原來表單送出後,點選 [提交其他回應] 連結可以重新開始,該連結是可以取消,一直以為是常駐功能

設定 => 簡報 => 顯示連結以傳送更多回覆

表單送出後畫面,畫面上會有 [提交其他回應] 連結

星期日, 6月 28, 2026

[SSRS] 框線

之前都是在屬性視窗內設定框線,這次無意中進入 IDE 內進行設定,發現原來框線 IDE 設定是有操作順序,以矩形為例來記錄

框線 IDE 設定

矩形 => 滑鼠右鍵 => 矩形屬性 => 框線
上圖橘框內的樣式、寬度、色彩,要先設定後,選擇黃框外框會套用該設定,而黃框外框選擇 [無] 後離開的話,會一併恢復橘框內的三個設定預設值。

屬性視窗設定

點選控件後理論上右側屬性視窗會自動出現,或是按 F4 快捷鍵
屬性視窗內設定會比較直覺,設定就是直接套用該效果

星期六, 6月 27, 2026

[SSRS] 在頁首或頁尾中顯示每筆資料

在 SSRS 報表內,資料來源為 DataSet 的話,要顯示每一筆資料必須把資料放在報表主體 (body) 內搭配清單 (List) 或 Tablix 等控件來顯示,而頁首、頁尾只能插入文字方塊 (TextBox)、線條、矩形、影像 (Image) 這四種控件,直接從 DataSet 字串欄位拉進頁首、頁尾的話,文字方塊值會有彙總運算式如下
=First(Fields!Data.Value, "DataSet 名稱")
運算式會自帶 First() 並只顯示第一筆資料的欄位資料,跳至其他分頁的話,該文字方塊不會跟著變化,有嘗試把 First() 移除,SSRS 雖然不會報錯,但仍然維持相同結果。

在官方文章 - 在頁首或頁尾中顯示變數資料 有提到繞道做法,必須在頁首、頁尾上透過 ReportItems (Report 控件) 來顯示 DataSet 內的每一筆資料、圖片

資料來源 DataSet

只塞入兩筆資料為別為 1234567890、ABCDEFGHIJ,在 AP 端是透過 Xing 套件把這兩筆資料轉成 Barcode 並用 byte[] 來儲存
public class ReportModel
{
        public string Data { get; set; }
        public byte[] Barcode { get; set; }
}

報表 layout 設計

黃框為頁首、綠框為 Body

在頁首、頁尾顯示文字

在頁首、頁尾內插入 TextBox 後,值設定值直接指定 Boby 內的 txtData 控件,設定如下
=ReportItems!txtData.Value
從下圖執行結果內的頁碼可以發現,跳至第二筆資料後,直接抓 txtData 控件可以顯示第二筆資料 ABCDEFGHIJ

在頁首、頁尾顯示圖片

ReportItems 是 TextBox 控件集合,不包含 Image 控件,沒有辦法像文字一樣,直接指定控件,所以圖片要特別處理,首先要在 Body 插入 TextBox (命名為 txtHiddenBarcode),並把圖片轉為字串如下
=Convert.ToBase64String(Fields!Barcode.Value)
頁首、頁尾的 Image 控件,則是抓取 ReportItems 的 txtHiddenBarcode 值再透過 Convert.FromBase64String 轉回圖片來顯示
=Convert.FromBase64String(ReportItems!txtHiddenBarcode.Value)
實務上使用會把 txtHiddenBarcode 隱藏起來,畢竟它只是傳遞資料媒介