Hướng dẫn

Google Apps Script Từ Cơ Bản Đến Nâng Cao - Hướng Dẫn Toàn Diện 2026

Tuân HoangTuân Hoang
27 tháng 2, 2026
Cập nhật: 25 tháng 3, 2026
23 phút đọc
Google Apps Script Từ Cơ Bản Đến Nâng Cao - Hướng Dẫn Toàn Diện 2026

Google Apps Script là gì và tại sao bạn nên học?

Google Apps Script là nền tảng lập trình dựa trên JavaScript, cho phép bạn tự động hóa và mở rộng các ứng dụng Google Workspace (Sheets, Docs, Gmail, Drive, Calendar...). Nói đơn giản: nếu Google Sheets là "Excel trên cloud", thì Apps Script chính là "VBA trên cloud" - nhưng mạnh hơn rất nhiều.

Apps Script có thể làm gì?

Tự động hóa lặp đi lặp lại:

  • Gửi email hàng loạt theo lịch
  • Tạo báo cáo tự động mỗi tháng
  • Sao lưu dữ liệu định kỳ
  • Nhắc nhở deadline qua email/chat

Mở rộng khả năng Google Sheets:

  • Tạo hàm tùy chỉnh (custom functions)
  • Kết nối API bên ngoài (Shopee, Facebook...)
  • Xây dựng web app mini
  • Tạo sidebar và dialog tùy chỉnh

So sánh Apps Script vs VBA (Excel)

Tiêu chí Google Apps Script VBA (Excel)
Ngôn ngữ JavaScript (phổ biến nhất TG) Visual Basic (lỗi thời)
Chạy ở đâu Cloud (Google server) Local (máy tính)
Kết nối API UrlFetchApp (dễ dùng) Phức tạp (WinHTTP)
Trigger tự động Có (theo giờ, ngày, sự kiện) Hạn chế
Web App Có (deploy thành website) Không
Chia sẻ Tự động (cloud) Phải gửi file .xlsm
Bảo mật Google OAuth 2.0 Dễ bị virus macro

Phần 1: Bắt đầu với Apps Script (Cơ bản)

1.1 Mở Script Editor

Cách mở trình soạn thảo Apps Script:

  1. Cách 1: Trong Google Sheets → menu Tiện ích mở rộng (Extensions) → Apps Script
  2. Cách 2: Truy cập trực tiếp script.google.com → tạo dự án mới

Script Editor sẽ mở trong tab mới với file Code.gs mặc định.

1.2 Script đầu tiên: Hello World

function helloWorld() {
  // Lấy spreadsheet đang mở
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getActiveSheet();

  // Ghi giá trị vào ô A1
  sheet.getRange("A1").setValue("Hello World từ Apps Script!");

  // Hiển thị thông báo
  SpreadsheetApp.getUi().alert("Chào mừng bạn đến với Apps Script!");
}

Cách chạy script:

  1. Click nút ▶ Chạy (Run) trên thanh công cụ
  2. Lần đầu: Google sẽ yêu cầu cấp quyền → click "Review Permissions" → chọn tài khoản → "Allow"
  3. Script chạy và ghi "Hello World" vào ô A1

1.3 Các đối tượng cơ bản cần biết

Đối tượng Mô tả Ví dụ
SpreadsheetApp Truy cập Google Sheets SpreadsheetApp.getActiveSpreadsheet()
Sheet Một sheet (tab) cụ thể ss.getSheetByName("Dữ liệu")
Range Một ô hoặc vùng ô sheet.getRange("A1:C10")
GmailApp Gửi/đọc email GmailApp.sendEmail(to, subject, body)
DriveApp Quản lý Google Drive DriveApp.createFile(name, content)
UrlFetchApp Gọi API bên ngoài UrlFetchApp.fetch(url, options)
CalendarApp Google Calendar CalendarApp.createEvent(title, start, end)
PropertiesService Lưu trữ key-value PropertiesService.getScriptProperties()

1.4 Đọc và ghi dữ liệu

// ĐỌC dữ liệu từ Google Sheets
function docDuLieu() {
  var sheet = SpreadsheetApp.getActiveSheet();

  // Đọc 1 ô
  var giaTri = sheet.getRange("A1").getValue();
  Logger.log("Ô A1: " + giaTri);

  // Đọc nhiều ô (trả về mảng 2 chiều)
  var data = sheet.getRange("A1:C10").getValues();
  Logger.log("Hàng 1, Cột 1: " + data[0][0]);
  Logger.log("Hàng 2, Cột 3: " + data[1][2]);

  // Đọc toàn bộ dữ liệu có trong sheet
  var allData = sheet.getDataRange().getValues();
  Logger.log("Tổng số hàng: " + allData.length);
}

// GHI dữ liệu vào Google Sheets
function ghiDuLieu() {
  var sheet = SpreadsheetApp.getActiveSheet();

  // Ghi 1 ô
  sheet.getRange("A1").setValue("Hello");

  // Ghi nhiều ô cùng lúc (mảng 2 chiều)
  var data = [
    ["Tên", "Email", "SĐT"],
    ["Nguyễn Văn A", "a@gmail.com", "0901234567"],
    ["Trần Thị B", "b@gmail.com", "0912345678"],
  ];
  sheet.getRange(1, 1, data.length, data[0].length).setValues(data);
}

Mẹo tối ưu hiệu suất:

Luôn dùng getValues() / setValues() (đọc/ghi hàng loạt) thay vì getValue() / setValue() trong vòng lặp. Mỗi lần gọi API Sheets tốn ~0.5 giây, nên đọc/ghi 1 lần cho cả vùng nhanh hơn nhiều so với đọc/ghi từng ô.

Phần 2: 10 Script Thực Tế Cho Người Mới (Trung cấp)

Script 1: Tạo menu tùy chỉnh

// Chạy khi mở spreadsheet
function onOpen() {
  var ui = SpreadsheetApp.getUi();
  ui.createMenu('Công cụ tùy chỉnh')
    .addItem('Dọn dẹp dữ liệu', 'cleanData')
    .addItem('Gửi báo cáo email', 'sendReport')
    .addSeparator()
    .addSubMenu(ui.createMenu('Xuất dữ liệu')
      .addItem('Xuất PDF', 'exportPDF')
      .addItem('Xuất CSV', 'exportCSV'))
    .addToUi();
}

function cleanData() {
  var sheet = SpreadsheetApp.getActiveSheet();
  var data = sheet.getDataRange().getValues();
  var cleaned = data.map(function(row) {
    return row.map(function(cell) {
      if (typeof cell === 'string') {
        return cell.trim(); // Xóa khoảng trắng thừa
      }
      return cell;
    });
  });
  sheet.getDataRange().setValues(cleaned);
  SpreadsheetApp.getUi().alert('Đã dọn dẹp ' + data.length + ' hàng dữ liệu!');
}

Script 2: Gửi email tự động từ Sheets

/**
 * Gửi email cá nhân hóa cho danh sách trong Sheets
 * Cấu trúc sheet: Tên | Email | Sản phẩm | Số tiền | Đã gửi?
 */
function sendPersonalizedEmails() {
  var sheet = SpreadsheetApp.getActiveSheet();
  var data = sheet.getDataRange().getValues();
  var sentCount = 0;

  // Bỏ qua hàng tiêu đề (bắt đầu từ hàng 2)
  for (var i = 1; i < data.length; i++) {
    var name = data[i][0];
    var email = data[i][1];
    var product = data[i][2];
    var amount = data[i][3];
    var sent = data[i][4];

    // Bỏ qua nếu đã gửi hoặc email trống
    if (sent === "Đã gửi" || !email) continue;

    // Tạo nội dung email
    var subject = "Xác nhận đơn hàng - " + product;
    var htmlBody = '
' + '

Xin chào ' + name + ',

' + '

Cảm ơn bạn đã đặt hàng ' + product + '.

' + '

Tổng thanh toán: ' + formatCurrency(amount) + '

' + '

Đơn hàng sẽ được xử lý trong 24h.

' + '

Trân trọng,
Team SheetStore

' + '
'; try { GmailApp.sendEmail(email, subject, '', { htmlBody: htmlBody }); sheet.getRange(i + 1, 5).setValue("Đã gửi"); sheet.getRange(i + 1, 6).setValue(new Date()); sentCount++; // Nghỉ 1 giây giữa các email (tránh bị Google giới hạn) Utilities.sleep(1000); } catch (e) { sheet.getRange(i + 1, 5).setValue("Lỗi: " + e.message); } } SpreadsheetApp.getUi().alert('Đã gửi ' + sentCount + ' email thành công!'); } function formatCurrency(amount) { return new Intl.NumberFormat('vi-VN', { style: 'currency', currency: 'VND' }).format(amount); }

Script 3: Tạo hàm tùy chỉnh (Custom Function)

/**
 * Chuyển số thành chữ tiếng Việt (dùng như hàm Sheets)
 * Sử dụng: =DOC_SO(1500000) → "Một triệu năm trăm nghìn đồng"
 *
 * @param {number} so Số cần chuyển
 * @return {string} Số bằng chữ tiếng Việt
 * @customfunction
 */
function DOC_SO(so) {
  if (!so || so === 0) return "Không đồng";

  var mangSo = ["không","một","hai","ba","bốn","năm","sáu","bảy","tám","chín"];
  var donVi = ["","nghìn","triệu","tỷ"];

  var str = Math.floor(Math.abs(so)).toString();
  var nhom = [];

  // Chia thành nhóm 3 chữ số
  while (str.length > 0) {
    nhom.unshift(str.slice(-3));
    str = str.slice(0, -3);
  }

  var ketQua = [];
  for (var i = 0; i < nhom.length; i++) {
    var n = parseInt(nhom[i]);
    if (n === 0) continue;

    var viTri = nhom.length - 1 - i;
    var chu = docNhom(parseInt(nhom[i]), mangSo);
    ketQua.push(chu + " " + donVi[viTri]);
  }

  var result = ketQua.join(" ").trim();
  result = result.charAt(0).toUpperCase() + result.slice(1);
  return result + " đồng";
}

function docNhom(so, mangSo) {
  var tram = Math.floor(so / 100);
  var chuc = Math.floor((so % 100) / 10);
  var donVi = so % 10;
  var ket = "";

  if (tram > 0) ket += mangSo[tram] + " trăm ";
  if (chuc > 1) ket += mangSo[chuc] + " mươi ";
  else if (chuc === 1) ket += "mười ";
  else if (chuc === 0 && tram > 0 && donVi > 0) ket += "lẻ ";

  if (donVi > 0) {
    if (chuc > 1 && donVi === 5) ket += "lăm";
    else if (chuc > 1 && donVi === 1) ket += "mốt";
    else ket += mangSo[donVi];
  }

  return ket.trim();
}

/**
 * Tính tuổi từ ngày sinh
 * Sử dụng: =TUOI("1990-05-15") → 35
 *
 * @param {Date} ngaySinh Ngày sinh
 * @return {number} Số tuổi
 * @customfunction
 */
function TUOI(ngaySinh) {
  var today = new Date();
  var birthDate = new Date(ngaySinh);
  var age = today.getFullYear() - birthDate.getFullYear();
  var m = today.getMonth() - birthDate.getMonth();
  if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) {
    age--;
  }
  return age;
}

/**
 * Chuyển không dấu thành có dấu cho tên (xử lý tên Việt)
 * Sử dụng: =BO_DAU("Nguyễn Văn An") → "Nguyen Van An"
 *
 * @param {string} str Chuỗi cần bỏ dấu
 * @return {string} Chuỗi không dấu
 * @customfunction
 */
function BO_DAU(str) {
  return str.normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .replace(/đ/g, 'd')
    .replace(/Đ/g, 'D');
}

Lưu ý về Custom Function:

  • Phải có JSDoc comment với @customfunction tag
  • Không thể truy cập UI (alert, prompt) từ custom function
  • Không thể ghi dữ liệu vào sheet khác
  • Giới hạn 30 giây thực thi
  • Dùng trong ô Sheets như hàm bình thường: =DOC_SO(A1)

Script 4: Bảo vệ dữ liệu - Ghi log chỉnh sửa

/**
 * Trigger chạy khi có ai chỉnh sửa sheet
 * Tự động ghi lại: ai sửa, sửa ô nào, giá trị cũ/mới, thời gian
 */
function onEdit(e) {
  // Bỏ qua nếu sửa ở sheet "Log"
  if (e.source.getActiveSheet().getName() === "Log") return;

  var logSheet = e.source.getSheetByName("Log");
  if (!logSheet) {
    logSheet = e.source.insertSheet("Log");
    logSheet.appendRow(["Thời gian", "Người sửa", "Sheet", "Ô", "Giá trị cũ", "Giá trị mới"]);
  }

  logSheet.appendRow([
    new Date(),
    Session.getActiveUser().getEmail(),
    e.source.getActiveSheet().getName(),
    e.range.getA1Notation(),
    e.oldValue || "(trống)",
    e.value || "(trống)"
  ]);
}

Script 5: Tạo PDF từ Google Sheets

/**
 * Xuất sheet hiện tại thành file PDF và lưu vào Google Drive
 */
function exportToPDF() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getActiveSheet();

  // Tạo tên file theo ngày
  var today = Utilities.formatDate(new Date(), "Asia/Ho_Chi_Minh", "dd-MM-yyyy");
  var fileName = sheet.getName() + " - " + today + ".pdf";

  // URL API xuất PDF
  var url = 'https://docs.google.com/spreadsheets/d/' + ss.getId()
    + '/export?'
    + 'format=pdf'
    + '&gid=' + sheet.getSheetId()
    + '&size=A4'
    + '&portrait=true'
    + '&fitw=true'           // Fit width
    + '&gridlines=false'     // Ẩn gridlines
    + '&printtitle=false'
    + '&sheetnames=false';

  var token = ScriptApp.getOAuthToken();
  var response = UrlFetchApp.fetch(url, {
    headers: { 'Authorization': 'Bearer ' + token }
  });

  // Lưu PDF vào Drive
  var folder = DriveApp.getRootFolder();
  var pdfFile = folder.createFile(response.getBlob().setName(fileName));

  SpreadsheetApp.getUi().alert(
    'PDF đã được tạo!\n'
    + 'File: ' + fileName + '\n'
    + 'Link: ' + pdfFile.getUrl()
  );
}

Script 6: Import dữ liệu từ API

/**
 * Lấy tỷ giá ngoại tệ realtime và ghi vào Sheets
 */
function getTyGia() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Tỷ giá");
  if (!sheet) {
    sheet = SpreadsheetApp.getActiveSpreadsheet().insertSheet("Tỷ giá");
  }

  // Gọi API tỷ giá (miễn phí)
  var url = "https://api.exchangerate-api.com/v4/latest/USD";
  var response = UrlFetchApp.fetch(url);
  var data = JSON.parse(response.getContentText());

  // Các loại tiền cần lấy
  var currencies = ["VND", "EUR", "GBP", "JPY", "CNY", "KRW", "THB", "SGD"];

  // Ghi tiêu đề
  sheet.getRange("A1:D1").setValues([["Ngoại tệ", "Tỷ giá (so với USD)", "Quy đổi 1 USD", "Cập nhật"]]);

  // Ghi dữ liệu
  var rows = currencies.map(function(code) {
    return [code, data.rates[code], "$1 = " + data.rates[code] + " " + code, new Date()];
  });

  sheet.getRange(2, 1, rows.length, 4).setValues(rows);

  // Format
  sheet.getRange("A1:D1").setFontWeight("bold").setBackground("#e0e7ff");
  sheet.autoResizeColumns(1, 4);

  Logger.log("Đã cập nhật tỷ giá: " + currencies.join(", "));
}

Script 7: Tự động sao lưu dữ liệu

/**
 * Sao lưu spreadsheet hiện tại vào thư mục Backup trên Drive
 * Đặt trigger chạy hàng ngày lúc 23:00
 */
function backupSpreadsheet() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var today = Utilities.formatDate(new Date(), "Asia/Ho_Chi_Minh", "yyyy-MM-dd_HHmm");
  var backupName = "[Backup] " + ss.getName() + " - " + today;

  // Tìm hoặc tạo thư mục Backup
  var folders = DriveApp.getFoldersByName("Backup_Sheets");
  var folder;
  if (folders.hasNext()) {
    folder = folders.next();
  } else {
    folder = DriveApp.createFolder("Backup_Sheets");
  }

  // Tạo bản copy
  var backupFile = DriveApp.getFileById(ss.getId()).makeCopy(backupName, folder);

  // Xóa backup cũ hơn 30 ngày
  var files = folder.getFiles();
  var cutoff = new Date();
  cutoff.setDate(cutoff.getDate() - 30);

  while (files.hasNext()) {
    var file = files.next();
    if (file.getDateCreated() < cutoff) {
      file.setTrashed(true);
      Logger.log("Đã xóa backup cũ: " + file.getName());
    }
  }

  Logger.log("Backup thành công: " + backupName);
}

Script 8: Tạo dropdown động (Dynamic Validation)

/**
 * Tạo dropdown liên kết: Chọn Tỉnh/TP → tự động cập nhật Quận/Huyện
 * Sheet "DanhMuc": Cột A = Tỉnh/TP, Cột B = Quận/Huyện
 */
function onEdit(e) {
  var sheet = e.source.getActiveSheet();
  var range = e.range;

  // Chỉ xử lý khi chỉnh sửa cột B (Tỉnh/TP) ở sheet "Đơn hàng"
  if (sheet.getName() !== "Đơn hàng" || range.getColumn() !== 2) return;

  var tinhTP = range.getValue();
  if (!tinhTP) return;

  // Lấy danh sách quận/huyện tương ứng từ sheet DanhMuc
  var dmSheet = e.source.getSheetByName("DanhMuc");
  var dmData = dmSheet.getDataRange().getValues();

  var quanHuyen = [];
  for (var i = 1; i < dmData.length; i++) {
    if (dmData[i][0] === tinhTP) {
      quanHuyen.push(dmData[i][1]);
    }
  }

  // Tạo dropdown cho cột C (Quận/Huyện) cùng hàng
  var targetCell = sheet.getRange(range.getRow(), 3);
  if (quanHuyen.length > 0) {
    var rule = SpreadsheetApp.newDataValidation()
      .requireValueInList(quanHuyen, true)
      .build();
    targetCell.setDataValidation(rule);
    targetCell.setValue(""); // Reset giá trị cũ
  }
}

Script 9: Gửi thông báo qua Google Chat

/**
 * Gửi thông báo đơn hàng mới qua Google Chat Webhook
 */
function notifyNewOrder(orderData) {
  var webhookUrl = "https://chat.googleapis.com/v1/spaces/YOUR_SPACE/messages?key=YOUR_KEY";

  var message = {
    "cards": [{
      "header": {
        "title": "Đơn hàng mới!",
        "subtitle": "Từ " + orderData.customer,
        "imageUrl": "https://img.icons8.com/color/48/shopping-cart.png"
      },
      "sections": [{
        "widgets": [
          {
            "keyValue": {
              "topLabel": "Khách hàng",
              "content": orderData.customer
            }
          },
          {
            "keyValue": {
              "topLabel": "Sản phẩm",
              "content": orderData.product
            }
          },
          {
            "keyValue": {
              "topLabel": "Tổng tiền",
              "content": formatCurrency(orderData.amount)
            }
          }
        ]
      }]
    }]
  };

  UrlFetchApp.fetch(webhookUrl, {
    method: "post",
    contentType: "application/json",
    payload: JSON.stringify(message)
  });
}

Script 10: Tự động tô màu theo điều kiện nâng cao

/**
 * Tô màu hàng dựa trên giá trị cột Trạng thái
 * Xanh: Hoàn thành, Vàng: Đang xử lý, Đỏ: Quá hạn
 */
function colorByStatus() {
  var sheet = SpreadsheetApp.getActiveSheet();
  var data = sheet.getDataRange().getValues();
  var statusCol = 4; // Cột E (0-indexed = 4)

  var colors = {
    "Hoàn thành": "#d1fae5",   // Xanh nhạt
    "Đang xử lý": "#fef3c7",   // Vàng nhạt
    "Quá hạn": "#fee2e2",       // Đỏ nhạt
    "Hủy": "#e5e7eb"            // Xám nhạt
  };

  for (var i = 1; i < data.length; i++) {
    var status = data[i][statusCol];
    var color = colors[status] || "#ffffff";
    sheet.getRange(i + 1, 1, 1, data[0].length).setBackground(color);
  }

  SpreadsheetApp.getUi().alert('Đã tô màu ' + (data.length - 1) + ' hàng!');
}

Phần 3: Triggers - Hẹn giờ chạy tự động

Triggers là cơ chế cho phép script chạy tự động mà không cần bạn mở Sheets. Đây là tính năng giúp Apps Script trở nên vô cùng mạnh mẽ.

Các loại Trigger

Loại Khi nào chạy Ví dụ
onOpen Khi mở spreadsheet Tạo menu tùy chỉnh
onEdit Khi chỉnh sửa ô Ghi log, validation
onChange Khi cấu trúc thay đổi Thêm/xóa sheet
onFormSubmit Khi nhận Google Form Gửi email xác nhận
Time-driven Theo lịch hẹn Backup hàng ngày, báo cáo hàng tuần

Tạo Trigger bằng code

/**
 * Tạo các trigger tự động (chỉ cần chạy 1 lần)
 */
function setupTriggers() {
  // Xóa trigger cũ (tránh trùng lặp)
  var triggers = ScriptApp.getProjectTriggers();
  triggers.forEach(function(trigger) {
    ScriptApp.deleteTrigger(trigger);
  });

  // Trigger 1: Chạy mỗi ngày lúc 8h sáng
  ScriptApp.newTrigger("dailyReport")
    .timeBased()
    .everyDays(1)
    .atHour(8)
    .create();

  // Trigger 2: Chạy mỗi thứ 2 hàng tuần
  ScriptApp.newTrigger("weeklyBackup")
    .timeBased()
    .onWeekDay(ScriptApp.WeekDay.MONDAY)
    .atHour(7)
    .create();

  // Trigger 3: Chạy mỗi giờ
  ScriptApp.newTrigger("checkNewOrders")
    .timeBased()
    .everyHours(1)
    .create();

  // Trigger 4: Chạy khi có ai chỉnh sửa
  ScriptApp.newTrigger("onEditHandler")
    .forSpreadsheet(SpreadsheetApp.getActive())
    .onEdit()
    .create();

  Logger.log("Đã tạo 4 triggers!");
}

Giới hạn Trigger cần biết:

  • Tối đa 20 triggers cho mỗi project
  • Mỗi lần chạy tối đa 6 phút (tài khoản miễn phí) hoặc 30 phút (Workspace)
  • Tối đa 90 phút/ngày tổng thời gian chạy trigger
  • Gmail: tối đa 100 email/ngày (miễn phí) hoặc 1500/ngày (Workspace)

Phần 4: Kỹ thuật nâng cao

4.1 Kết nối REST API nâng cao

/**
 * Module kết nối API tổng quát
 * Hỗ trợ GET, POST, PUT, DELETE với authentication
 */
var API = {
  // Cấu hình
  baseUrl: "",
  apiKey: "",

  init: function(baseUrl, apiKey) {
    this.baseUrl = baseUrl;
    this.apiKey = apiKey;
    return this;
  },

  // GET request
  get: function(endpoint) {
    return this._request("get", endpoint);
  },

  // POST request với body
  post: function(endpoint, data) {
    return this._request("post", endpoint, data);
  },

  // Request handler
  _request: function(method, endpoint, data) {
    var options = {
      method: method,
      headers: {
        "Authorization": "Bearer " + this.apiKey,
        "Content-Type": "application/json"
      },
      muteHttpExceptions: true // Không throw lỗi HTTP
    };

    if (data) {
      options.payload = JSON.stringify(data);
    }

    try {
      var response = UrlFetchApp.fetch(this.baseUrl + endpoint, options);
      var code = response.getResponseCode();
      var body = JSON.parse(response.getContentText());

      if (code >= 200 && code < 300) {
        return { success: true, data: body };
      } else {
        return { success: false, error: body, code: code };
      }
    } catch (e) {
      return { success: false, error: e.message };
    }
  }
};

// Ví dụ sử dụng: Lấy danh sách sản phẩm từ API
function getProducts() {
  var api = API.init("https://api.example.com", "your-api-key");
  var result = api.get("/products?limit=100");

  if (result.success) {
    var products = result.data;
    var sheet = SpreadsheetApp.getActiveSheet();

    // Ghi header
    sheet.getRange(1, 1, 1, 4).setValues([["Mã SP", "Tên", "Giá", "Tồn kho"]]);

    // Ghi data
    var rows = products.map(function(p) {
      return [p.id, p.name, p.price, p.stock];
    });
    sheet.getRange(2, 1, rows.length, 4).setValues(rows);
  } else {
    Logger.log("Lỗi API: " + JSON.stringify(result.error));
  }
}

4.2 Xây dựng Web App từ Apps Script

/**
 * Web App: Form nhập liệu đẹp cho Google Sheets
 * Deploy: Triển khai → Triển khai mới → Web app
 */

// Xử lý GET request (hiển thị form)
function doGet() {
  return HtmlService.createHtmlOutput(getFormHTML())
    .setTitle("Form Đặt Hàng - SheetStore")
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

// Xử lý POST request (nhận dữ liệu từ form)
function doPost(e) {
  var sheet = SpreadsheetApp.openById("SPREADSHEET_ID").getSheetByName("Đơn hàng");

  sheet.appendRow([
    new Date(),
    e.parameter.customerName,
    e.parameter.phone,
    e.parameter.email,
    e.parameter.product,
    e.parameter.quantity,
    e.parameter.note,
    "Mới"
  ]);

  // Gửi email xác nhận
  GmailApp.sendEmail(e.parameter.email,
    "Xác nhận đơn hàng",
    "Cảm ơn " + e.parameter.customerName + " đã đặt hàng!"
  );

  return HtmlService.createHtmlOutput(
    '<h2>Đặt hàng thành công!</h2>'
    + '<p>Chúng tôi sẽ liên hệ bạn trong 24h.</p>'
  );
}

function getFormHTML() {
  return ''
  + ''
  + ''
  + '

Form Đặt Hàng

' + '
' + '' + '' + '' + '' + '' + '' + '' + '
'; }

4.3 Sidebar tùy chỉnh trong Google Sheets

/**
 * Tạo sidebar tùy chỉnh cho Google Sheets
 */
function showSidebar() {
  var html = HtmlService.createHtmlOutput(getSidebarHTML())
    .setTitle('Công cụ quản lý')
    .setWidth(300);
  SpreadsheetApp.getUi().showSidebar(html);
}

// Thêm vào menu
function onOpen() {
  SpreadsheetApp.getUi().createMenu('SheetStore')
    .addItem('Mở công cụ', 'showSidebar')
    .addToUi();
}

// Hàm xử lý phía server (được gọi từ sidebar)
function processData(action, params) {
  var sheet = SpreadsheetApp.getActiveSheet();

  switch(action) {
    case 'countRows':
      return sheet.getLastRow() - 1; // Trừ header

    case 'sumColumn':
      var col = params.column;
      var data = sheet.getRange(2, col, sheet.getLastRow() - 1).getValues();
      var sum = data.reduce(function(acc, row) { return acc + (Number(row[0]) || 0); }, 0);
      return sum;

    case 'filterData':
      var allData = sheet.getDataRange().getValues();
      var filtered = allData.filter(function(row) {
        return row[params.column - 1].toString().includes(params.keyword);
      });
      return filtered.length;

    default:
      return 'Unknown action';
  }
}

function getSidebarHTML() {
  return ''
  + ''
  + '

Công cụ nhanh

' + '' + '' + '' + ''; }

4.4 Xử lý lỗi chuyên nghiệp

/**
 * Module xử lý lỗi và ghi log chuyên nghiệp
 */
var ErrorHandler = {
  logSheet: null,

  init: function() {
    var ss = SpreadsheetApp.getActiveSpreadsheet();
    this.logSheet = ss.getSheetByName("ErrorLog");
    if (!this.logSheet) {
      this.logSheet = ss.insertSheet("ErrorLog");
      this.logSheet.appendRow(["Thời gian", "Hàm", "Lỗi", "Chi tiết", "Dòng"]);
      this.logSheet.getRange("1:1").setFontWeight("bold").setBackground("#fecaca");
    }
    return this;
  },

  // Wrapper chạy hàm với try-catch tự động
  run: function(functionName, func) {
    try {
      return func();
    } catch (e) {
      this.log(functionName, e);

      // Gửi email thông báo lỗi cho admin
      try {
        GmailApp.sendEmail(
          "admin@company.com",
          "[Apps Script Error] " + functionName,
          "Lỗi: " + e.message + "\nDòng: " + (e.lineNumber || "N/A")
          + "\nThời gian: " + new Date().toLocaleString("vi-VN")
        );
      } catch(mailError) {
        Logger.log("Không gửi được email lỗi: " + mailError.message);
      }

      return null;
    }
  },

  log: function(functionName, error) {
    if (!this.logSheet) this.init();
    this.logSheet.appendRow([
      new Date(),
      functionName,
      error.message,
      error.stack || "",
      error.lineNumber || ""
    ]);
  }
};

// Ví dụ sử dụng:
function myFunction() {
  ErrorHandler.init().run("myFunction", function() {
    // Code của bạn ở đây
    var data = getExternalData();
    processAndSave(data);
  });
}

Phần 5: Dự án thực tế hoàn chỉnh

Kết hợp tất cả kiến thức trên, đây là một dự án thực tế: Hệ thống quản lý đơn hàng tự động với đầy đủ tính năng.

Dự án: Hệ thống quản lý đơn hàng tự động

Tính năng:

  • Web form đặt hàng (doGet/doPost)
  • Tự động ghi đơn vào Google Sheets
  • Gửi email xác nhận cho khách
  • Thông báo đơn mới qua Telegram/Google Chat
  • Tự động tạo hóa đơn PDF
  • Dashboard báo cáo doanh thu theo ngày/tháng
  • Backup dữ liệu hàng ngày
/**
 * HỆ THỐNG QUẢN LÝ ĐƠN HÀNG TỰ ĐỘNG
 * Tổng hợp: Web App + Email + API + Trigger + PDF
 */

// ========== CẤU HÌNH ==========
var CONFIG = {
  SPREADSHEET_ID: "your-spreadsheet-id",
  ORDER_SHEET: "Đơn hàng",
  PRODUCT_SHEET: "Sản phẩm",
  TELEGRAM_BOT_TOKEN: "your-bot-token",
  TELEGRAM_CHAT_ID: "your-chat-id",
  ADMIN_EMAIL: "admin@company.com",
  COMPANY_NAME: "SheetStore"
};

// ========== XỬ LÝ ĐƠN HÀNG ==========
function processNewOrder(orderData) {
  var ss = SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID);
  var sheet = ss.getSheetByName(CONFIG.ORDER_SHEET);

  // Tạo mã đơn hàng
  var orderCode = "DH" + Utilities.formatDate(new Date(), "Asia/Ho_Chi_Minh", "yyyyMMddHHmmss");

  // Tính tổng tiền
  var product = getProductInfo(orderData.productId);
  var total = product.price * orderData.quantity;

  // Ghi vào Sheets
  sheet.appendRow([
    orderCode,
    new Date(),
    orderData.name,
    orderData.phone,
    orderData.email,
    product.name,
    orderData.quantity,
    product.price,
    total,
    "Mới",
    orderData.note || ""
  ]);

  // Gửi email xác nhận
  sendOrderConfirmation(orderData.email, orderCode, orderData.name, product.name, total);

  // Thông báo Telegram
  sendTelegramNotification(orderCode, orderData.name, product.name, total);

  return { success: true, orderCode: orderCode };
}

// ========== THÔNG BÁO TELEGRAM ==========
function sendTelegramNotification(orderCode, customer, product, total) {
  var message = "🛒 *ĐƠN HÀNG MỚI*\n\n"
    + "📋 Mã: " + orderCode + "\n"
    + "👤 Khách: " + customer + "\n"
    + "📦 SP: " + product + "\n"
    + "💰 Tổng: " + formatCurrency(total);

  var url = "https://api.telegram.org/bot" + CONFIG.TELEGRAM_BOT_TOKEN
    + "/sendMessage?chat_id=" + CONFIG.TELEGRAM_CHAT_ID
    + "&text=" + encodeURIComponent(message)
    + "&parse_mode=Markdown";

  UrlFetchApp.fetch(url);
}

// ========== BÁO CÁO TỰ ĐỘNG ==========
function dailyReport() {
  var ss = SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID);
  var sheet = ss.getSheetByName(CONFIG.ORDER_SHEET);
  var data = sheet.getDataRange().getValues();

  var today = Utilities.formatDate(new Date(), "Asia/Ho_Chi_Minh", "dd/MM/yyyy");

  // Lọc đơn hàng hôm nay
  var todayOrders = data.filter(function(row) {
    var orderDate = Utilities.formatDate(new Date(row[1]), "Asia/Ho_Chi_Minh", "dd/MM/yyyy");
    return orderDate === today;
  });

  var totalRevenue = todayOrders.reduce(function(sum, row) {
    return sum + (Number(row[8]) || 0);
  }, 0);

  var report = "📊 *BÁO CÁO NGÀY " + today + "*\n\n"
    + "🛒 Số đơn: " + todayOrders.length + "\n"
    + "💰 Doanh thu: " + formatCurrency(totalRevenue) + "\n"
    + "📈 TB/đơn: " + formatCurrency(todayOrders.length > 0 ? totalRevenue / todayOrders.length : 0);

  // Gửi qua Telegram
  var url = "https://api.telegram.org/bot" + CONFIG.TELEGRAM_BOT_TOKEN
    + "/sendMessage?chat_id=" + CONFIG.TELEGRAM_CHAT_ID
    + "&text=" + encodeURIComponent(report)
    + "&parse_mode=Markdown";
  UrlFetchApp.fetch(url);

  // Gửi email cho admin
  GmailApp.sendEmail(CONFIG.ADMIN_EMAIL, "Báo cáo ngày " + today, report.replace(/\*/g, ""));
}

function formatCurrency(amount) {
  return new Intl.NumberFormat('vi-VN', { style: 'currency', currency: 'VND' }).format(amount);
}

function getProductInfo(productId) {
  var ss = SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID);
  var sheet = ss.getSheetByName(CONFIG.PRODUCT_SHEET);
  var data = sheet.getDataRange().getValues();
  for (var i = 1; i < data.length; i++) {
    if (data[i][0] == productId) {
      return { name: data[i][1], price: data[i][2] };
    }
  }
  return { name: "Không rõ", price: 0 };
}

function sendOrderConfirmation(email, orderCode, name, product, total) {
  var html = '
' + '
' + '

' + CONFIG.COMPANY_NAME + '

' + '
' + '

Xác nhận đơn hàng #' + orderCode + '

' + '

Xin chào ' + name + ',

' + '

Đơn hàng của bạn đã được tiếp nhận:

' + '' + '' + '' + '' + '
Sản phẩm:' + product + '
Tổng tiền:' + formatCurrency(total) + '
Trạng thái:Đang xử lý
' + '

Chúng tôi sẽ liên hệ trong 24h. Cảm ơn bạn!

' + '
'; GmailApp.sendEmail(email, "Xác nhận đơn hàng #" + orderCode, '', { htmlBody: html }); } // ========== SETUP ========== function setupAll() { // Tạo trigger báo cáo hàng ngày lúc 20h ScriptApp.newTrigger("dailyReport") .timeBased().everyDays(1).atHour(20).create(); // Tạo trigger backup lúc 23h ScriptApp.newTrigger("backupSpreadsheet") .timeBased().everyDays(1).atHour(23).create(); Logger.log("Setup hoàn tất!"); }

Phần 6: Tối ưu hiệu suất Apps Script

Apps Script có giới hạn thời gian chạy (6 phút/lần), nên tối ưu hiệu suất rất quan trọng:

SAI (Chậm)

// Đọc từng ô → N lần gọi API
for (var i = 1; i <= 1000; i++) {
  var val = sheet.getRange(i, 1).getValue();
  sheet.getRange(i, 2).setValue(val * 2);
}
// Mất ~17 phút (timeout!)

ĐÚNG (Nhanh)

// Đọc/ghi 1 lần → 2 lần gọi API
var data = sheet.getRange(1,1,1000).getValues();
var result = data.map(function(row) {
  return [row[0] * 2];
});
sheet.getRange(1,2,1000).setValues(result);
// Mất ~2 giây!

Checklist tối ưu hiệu suất

  • Batch operations: Dùng getValues/setValues thay vì getValue/setValue trong loop

  • Cache dữ liệu: Dùng CacheService cho dữ liệu ít thay đổi (VD: tỷ giá, danh mục)

  • Tránh SpreadsheetApp.flush(): Chỉ dùng khi thực sự cần cập nhật UI ngay

  • Xử lý dữ liệu lớn: Chia thành batch, dùng PropertiesService lưu vị trí đã xử lý

  • Logger vs console.log: Dùng Logger.log() để debug, tránh console.log

Phần 7: Debug và xử lý lỗi thường gặp

Lỗi: "Exception: You do not have permission"

Nguyên nhân: Script chưa được cấp quyền truy cập Gmail, Drive, hoặc dịch vụ khác.

Cách sửa: Chạy script thủ công 1 lần → Google sẽ yêu cầu cấp quyền → Click "Allow".

Lỗi: "Exceeded maximum execution time"

Nguyên nhân: Script chạy quá 6 phút (giới hạn miễn phí).

Cách sửa: Tối ưu batch operations, chia nhỏ tác vụ, dùng PropertiesService lưu checkpoint.

Lỗi: "TypeError: Cannot read property 'xxx' of null"

Nguyên nhân: Sheet hoặc range không tồn tại.

Cách sửa: Luôn kiểm tra null: if (!sheet) { Logger.log("Sheet không tồn tại"); return; }

Lỗi: "Service invoked too many times in a short time"

Nguyên nhân: Vượt quota API (VD: gửi quá nhiều email).

Cách sửa: Thêm Utilities.sleep(1000) giữa các lần gọi, batch requests.

Lỗi: "This script requires authorization"

Nguyên nhân: Trigger installable cần re-authorize sau khi thêm quyền mới.

Cách sửa: Xóa trigger cũ → chạy thủ công (authorize) → tạo trigger mới.

Tài nguyên học tập và tham khảo

Nguồn tài liệu chính thức:

  • Google Apps Script Reference: developers.google.com/apps-script/reference
  • Codelabs (bài tập thực hành): codelabs.developers.google.com
  • Stack Overflow: Tag [google-apps-script]

Lộ trình học đề xuất:

  1. Tuần 1-2: JavaScript cơ bản (biến, hàm, vòng lặp, mảng)
  2. Tuần 3-4: SpreadsheetApp - đọc/ghi dữ liệu, format
  3. Tuần 5-6: GmailApp, DriveApp - email & file
  4. Tuần 7-8: UrlFetchApp - kết nối API
  5. Tuần 9-10: Triggers, Web App, Sidebar
  6. Tuần 11-12: Dự án thực tế (như phần 5)

Câu hỏi thường gặp (FAQ)

Apps Script có miễn phí không?

Có. Apps Script hoàn toàn miễn phí cho tài khoản Google cá nhân và Workspace. Chỉ có giới hạn quota: 6 phút/lần chạy, 90 phút/ngày cho triggers, 100 email/ngày (cá nhân) hoặc 1500/ngày (Workspace).

Tôi không biết lập trình, có học được Apps Script không?

Có. Apps Script dựa trên JavaScript - ngôn ngữ phổ biến nhất thế giới với rất nhiều tài liệu tiếng Việt. Bạn có thể bắt đầu bằng việc copy-paste các script trong bài này, sau đó chỉnh sửa theo nhu cầu. Sau 2-4 tuần, bạn sẽ tự viết được script cơ bản.

Apps Script có an toàn không? Script có thể truy cập dữ liệu của tôi?

An toàn. Mỗi script chỉ truy cập được các dịch vụ bạn cho phép (Google sẽ hỏi khi chạy lần đầu). Script chạy trên server Google, không lưu ở máy cá nhân. Bạn có thể xem và thu hồi quyền tại myaccount.google.com/permissions.

Có thể dùng Apps Script cho doanh nghiệp production không?

Có, nhưng có giới hạn. Với doanh nghiệp nhỏ (dưới 50 người dùng, dưới 10K giao dịch/ngày), Apps Script rất phù hợp. Khi quy mô lớn hơn, nên chuyển sang Google Cloud Functions hoặc backend riêng. Apps Script phù hợp nhất cho: tự động hóa nội bộ, prototype, và tool phụ trợ.

Script có chạy khi tôi tắt máy tính không?

Có. Triggers (time-driven) chạy trên cloud server của Google, hoàn toàn độc lập với máy tính của bạn. Script sẽ chạy đúng lịch ngay cả khi bạn không online.

Nên dùng V8 runtime hay Rhino?

Luôn chọn V8. V8 là runtime mới, hỗ trợ ES6+ (let, const, arrow functions, template literals, async/await). Rhino là bản cũ chỉ hỗ trợ ES5. Kiểm tra trong Project Settings → Runtime: "V8".

Tổng kết

Google Apps Script là công cụ cực kỳ mạnh mẽ giúp bạn biến Google Sheets từ một bảng tính đơn giản thành một hệ thống quản lý hoàn chỉnh. Với JavaScript làm nền tảng, khả năng kết nối API, và chạy tự động trên cloud - Apps Script giúp bạn:

  • Tiết kiệm hàng giờ mỗi tuần bằng tự động hóa công việc lặp đi lặp lại

  • Kết nối mọi thứ - từ Gmail, Drive đến API bên ngoài

  • Xây dựng ứng dụng web app đơn giản mà không cần hosting

  • Hoàn toàn miễn phí - không tốn thêm bất kỳ chi phí nào

Hãy bắt đầu bằng 1 script đơn giản (VD: gửi email từ Sheets), rồi dần dần nâng cao. Sau 1-2 tháng, bạn sẽ có thể xây dựng những hệ thống tự động phức tạp mà trước đây cần phần mềm đắt tiền mới làm được!

Muốn có sẵn hệ thống quản lý chuyên nghiệp?

SheetStore cung cấp phần mềm quản lý bán hàng tích hợp Google Sheets với đầy đủ tính năng: quản lý đơn hàng, CRM, báo cáo tự động, và nhiều hơn nữa.

Truy cập sheet.com.vn để dùng thử miễn phí ngay hôm nay.

Chia sẻ bài viết:

Tuân Hoang

Tuân Hoang

Đội ngũ SheetStore

Bạn thấy bài viết hữu ích?

Đăng ký nhận thông báo khi có bài viết mới.

Nhận thông báo khi có bài viết mới. Không spam, hứa luôn! 😊

Bình luận (0)

Vui lòng đăng nhập để tham gia thảo luận