EBOOK · 亲密关系 《别再用关系止痛》已上线:先试看,刺到再买完整版(RM15)

我如何在 48 小时内,把婚礼请柬业务做到 85% 自动化(0 SaaS、全靠 Google 生态)

我如何在 48 小时内,把婚礼请柬业务做到 85% 自动化(0 SaaS、全靠 Google 生态)|DAPHNETXG
婚礼请柬业务自动化系统架构示意图
AUTOMATION · SYSTEM DESIGN
我如何在 48 小时内,把婚礼请柬业务做到 85% 自动化(0 SaaS、全靠 Google 生态)

我如何在 48 小时内,把婚礼请柬业务做到 85% 自动化(0 SaaS、全靠 Google 生态)

这不是一篇“心路历程”,而是一份可以照着搭的实战笔记:我如何用 Google Sheets + Forms + Docs + Drive + Gmail + Apps Script, 在不到 48 小时里,搭出一套适用于婚礼请柬业务的报价 & 下单 & 发票系统, 让业务流程 85% 脱离手动操作——不付一分钱给 SaaS,所有数据都掌握在自己手里。

这篇文章适合:小型婚礼品牌、工作室、freelancer 和已经有一定订单量的个体老板。 我会从整体思路开始,拆开每一个环节,用实际案例和代码展示, 我是怎样把一个“高度定制、变量很多”的实体请柬业务,变成一台可以稳定运转的自动机。

DAPHNETXG
DAPHNETXG
Creator of MomOS · SEO & systems educator · Xamiya Wedding 创始人。 我喜欢把情感劳动、业务流程和技术,全部折叠进一个可以长期复利的「操作系统」里。
目录
  1. 一、这篇文章在解决什么问题?
  2. 二、整体架构:一台「婚礼请柬自动机」长什么样?
  3. 三、报价引擎:前台 3 个变量,后台一整套利润模型
  4. 四、订金发票:Google 表单一提交,PDF 自动生成发出
  5. 五、尾款发票:Final Orders + 勾选一个 checkbox 就完成
  6. 六、如何保护敏感数据,又让助理很好用?
  7. 七、85% 自动化是怎么来的?用一个订单生命周期算给你看
  8. 八、常见坑与风控:触发器、测试环境和错单风险
  9. 九、常见问答(FAQ)
⚠️ 提醒:这篇默认你已经在用 Google 生态(Gmail / Drive / Sheets), 并且愿意花一点时间看懂 Apps Script 的结构。如果你完全零基础,可以先收藏, 等业务单量上来、真的被手动报价格子压垮时,再回来看。

一、这篇文章在解决什么问题?

先说结论:我想解决的不是「怎么发一张发票」,而是:

  • 当订单越来越多时,老板不被琐碎信息淹没
  • 助理可以在 不掌握敏感成本 的前提下,独立完成报价和开单;
  • 每一个客户,从第一句报价到最后出货,数据都在一个系统里闭环。

以我的婚礼品牌 Xamiya Wedding 为例,现实世界长这样:

  • 客制化程度高:信封、纸张、烫金、wax、数量、运送地区… 组合很多;
  • 一开始全靠我自己算:翻 Excel、查汇率、对照成本表、写 WhatsApp 报价;
  • 忙起来时,容易:报错价、忘记改数量、没记 log、不知道利润有没有被吃掉。

所以我给自己下了一个很狠的任务:

目标

在 48 小时内,用 0 预算,把「报价 → 定金发票 → 尾款发票」做成一套可以交给助理使用的自动化系统。

二、整体架构:一台「婚礼请柬自动机」长什么样?

先给你整张图的感觉,再进细节。

  • 前台(助理视角)
    • 一个「报价界面」:输入数量、纸张类型、运输方式 → 一键生成 WhatsApp 报价文案(中英)。
    • 一个「Final Orders」表:填好最终数量、材质、选项后,勾选「Send Final Invoice PDF」即可发出尾款发票。
  • 后台(老板视角)
    • 隐藏的 engine_cost_core:所有成本、汇率、目标利润、重量参数都在这里。
    • 两个发票模板(Google Docs):订金 & 尾款。
    • 三个 Apps Script 入口:
      • 订金发票:onFormSubmit(e)
      • 订单编号:onEdit(e) 生成 Order ID
      • 尾款发票:onFinalInvoiceCheckboxEdit(e) + sendFinalInvoicesBatch()

所有东西都只在 Google 生态里流转:

  • 数据:Google Sheets
  • 入口:Google Forms
  • 文档:Google Docs
  • 存档:Google Drive
  • 通知:Gmail
  • 胶水:Google Apps Script

没有额外 SaaS,没有月费,没有「想导出数据发现被锁在平台里」的焦虑。

三、报价引擎:前台 3 个变量,后台一整套利润模型

先从最前面的环节说起:报价

大多数人想象中的报价,是这样的:

  • Excel 里有一张成本表;
  • 助理或老板自己算:印刷 + 运费 + 设计费 + 包装 + 目标利润;
  • 算完之后,再手写一段 WhatsApp 报价给客户。

这很耗时间,也很容易出错。我的做法是把它拆成两层:

1. 后台:engine_cost_core(只给自己看的「算法心脏」)

这一张表长得很丑,但很重要。它大致分成两块:

  • 固定参数区:单卡重量、包装重量、汇率、印刷单价、每单设计费、本地快递费、不同纸张的加价系数、目标利润率等。
  • 计算区:给定数量、纸张类型、运输方式和目标利润率,计算:
    • 总成本(印刷 + 国际运费 + 本地快递 + 设计费);
    • 加上利润后的订单基础售价;
    • 每张单价、每 100 张单价、每 +10 / +20 张的追加金额;
    • 对应的中英文 WhatsApp 报价文案。

具体公式可以因人而异,这里给一个极简的伪公式思路(为了脱敏,数值都是示意):

=LET(
  qty,        $B$3,                      /* 数量,例如 100 */
  fx,         Config!B3,                 /* 汇率 */
  printCNY,   Config!B4 * qty,           /* 印刷成本(人民币) */
  intlShipRM, Config!B10,                /* 国际运费(RM) */
  designRM,   Config!B6,                 /* 设计费 */
  localRM,    Config!B7,                 /* 本地快递 + 包装 */
  paperExtra, XLOOKUP($B$4,PaperType,ExtraPerCard), /* 纸张加价 */
  baseCost,   printCNY * fx + intlShipRM + designRM + localRM + paperExtra*qty,
  margin,     Config!B8,                 /* 目标利润率,例如 0.8(示意) */
  finalPrice, baseCost * (1 + margin),
  pricePer100, finalPrice / qty * 100,
  { finalPrice, pricePer100 }
)

真正的表里会更复杂一些,但核心就是:所有复杂计算,集中在这张表做完。

2. 前台:给助理用的「简化版报价界面」

我另外做了一张「报价输入」表,助理只需要:

  • 在下拉选项里选:数量、纸张类型、运输方式;
  • 就能自动看到:预估总价、每 100 张的价格、每 +10 张 / +20 张的大致加价。

同时,这张表会从后台引用自动生成好的中英文 WhatsApp 文案,例如:

给你一个目前的单卡预估报价:100 张,纸张:Art,运输方式:Standard,
每 100 张大概是 RM 241.20 左右。
这个价格已经包含设计、印刷、国际运费、本地快递和包装成本。
如果数量多于 100 张,大致是每多 10 张加 RM 24.xx(具体我会在你确认下单前再帮你精算一次)。

助理只需要复制粘贴,完全不用知道背后到底怎么计算。

四、订金发票:Google 表单一提交,PDF 自动生成发出

当顾客同意报价、准备下订金时,我会发给对方一个 Google 表单链接,收集:

  • 姓名、电话、Email;
  • 收货地址;
  • 数量、选择的方案、运输方式;
  • 我们刚才确认好的价格。

这张表单对应的是一个 Sheets 工作表 Form responses 1,然后由 onFormSubmit(e) 触发脚本:

const SHEET_NAME = 'Form responses 1';
const INVOICE_TEMPLATE_ID = '你的 Google Doc 模板 ID';
const FOLDER_ID = '你的发票 PDF 存放 Folder ID';

function onFormSubmit(e) {
  const sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME);
  const row   = e.range.getRow();

  // 1)读当前这一行的所有字段
  const dataRange = sheet.getRange(row, 1, 1, sheet.getLastColumn());
  const data      = dataRange.getValues()[0];
  const headers   = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];

  const record = {};
  headers.forEach((h, i) => record[h] = data[i]);

  // 2)生成发票 + 发邮件
  createInvoiceAndEmail(record);
}

function createInvoiceAndEmail(data) {
  const invoiceNo = 'XM-' + new Date().getTime();   // 简单稳妥的流水号

  // 1)从模板复制一份新发票 Doc
  const docCopy = DriveApp.getFileById(INVOICE_TEMPLATE_ID).makeCopy();
  const docId   = docCopy.getId();
  const body    = DocumentApp.openById(docId).getBody();

  // 2)替换占位符(按你自己的字段名调整)
  body.replaceText('{{DATE}}', new Date().toLocaleDateString());
  body.replaceText('{{INVOICE_NO}}', invoiceNo);

  body.replaceText('{{NAME}}',   data['Name'] || '');
  body.replaceText('{{EMAIL}}',  data['Email'] || '');
  body.replaceText('{{PHONE}}',  data['Phone'] || '');
  body.replaceText('{{QTY}}',    data['Quantity'] || '');
  body.replaceText('{{PRICE}}',  data['Price'] || '');
  body.replaceText('{{ADDRESS}}',data['Full Delivery Address'] || '');

  DocumentApp.openById(docId).saveAndClose();

  // 3)导出 PDF 存进指定 Folder
  const pdfBlob = DriveApp.getFileById(docId).getAs('application/pdf');
  const folder  = DriveApp.getFolderById(FOLDER_ID);
  const pdfFile = folder.createFile(pdfBlob).setName('Invoice - ' + invoiceNo + '.pdf');

  // 4)发邮件给客户(可多语言)
  const subject = 'Your Xamiya Invoice #' + invoiceNo;
  const bodyText =
    'Hi ' + (data['Name'] || '') + ',\n\n' +
    'Here is your invoice. Please check the attached PDF.\n\n' +
    'Thank you!\nXamiya Team';

  GmailApp.sendEmail(data['Email'], subject, bodyText, {
    attachments: [pdfFile]
  });

  // 5)清理临时 Doc(只保留 PDF)
  DriveApp.getFileById(docId).setTrashed(true);
}

从客户的视角,体验是:

  1. 填完表单;
  2. 几秒内收到一封带 PDF 发票的 Email;
  3. 发票抬头、数量和价格都和刚刚沟通的一致。

从我的视角,整个过程几乎不需要再碰键盘。

五、尾款发票:Final Orders + 勾选一个 checkbox 就完成

订金收完,设计走完、数量定案后,就进入「尾款发票」阶段。

这里我用的是另一张表:Final Orders,字段包含:

  • Order ID(自动生成);
  • Name / Email / Final Quantity;
  • Paper Type / Envelope Type / Sticker / Foil 等选项;
  • Price per set / Balance Due;
  • Send Final Invoice PDF(勾选用的 checkbox)。

1. 自动生成订单号:onEdit(e)

当我们开始填写某一行订单信息时,脚本会自动在第一列生成订单号:

function onEdit(e) {
  const sheet = e.source.getActiveSheet();
  if (sheet.getName() !== 'Final Orders') return;

  const row = e.range.getRow();
  if (row === 1) return; // 表头不处理

  const orderIdCell = sheet.getRange(row, 1); // A 列
  if (orderIdCell.getValue()) return;        // 已有订单号就不重复生成

  const name  = sheet.getRange(row, 2).getValue(); // Name
  const email = sheet.getRange(row, 3).getValue(); // Email
  const qty   = sheet.getRange(row, 4).getValue(); // Final Quantity

  if (!name && !email && !qty) return; // 空行不生成 ID

  const ts = Utilities.formatDate(
    new Date(),
    Session.getScriptTimeZone(),
    'yyyyMMdd-HHmmss'
  );
  const newId = 'XM-' + ts + '-' + row;

  orderIdCell.setValue(newId);
}

这样每一单都有唯一订单号,方便对账和存档。

2. 勾选 checkbox,自动生成尾款发票 PDF 并发送

核心逻辑拆成两个函数:

  • createFinalInvoicePDF(order):给定一行订单数据,生成 PDF 并发邮件。
  • onFinalInvoiceCheckboxEdit(e):当某一行的「Send Final Invoice PDF」从 FALSE → TRUE 时,调用上面的函数。
const FINAL_SHEET_NAME          = 'Final Orders';
const FINAL_INVOICE_TEMPLATE_ID = '你的尾款发票 Doc 模板 ID';
const FINAL_FOLDER_ID           = '你的尾款 PDF Folder ID';

function createFinalInvoicePDF(order) {
  if (!order || !order.email) return;

  const templateFile = DriveApp.getFileById(FINAL_INVOICE_TEMPLATE_ID);
  const copy         = templateFile.makeCopy('Final Invoice - ' + (order.orderId || 'NO-ID'));
  const copyId       = copy.getId();
  const doc          = DocumentApp.openById(copyId);
  const body         = doc.getBody();

  const today = Utilities.formatDate(
    new Date(),
    Session.getScriptTimeZone(),
    'yyyy-MM-dd'
  );

  const invoiceNo = String(order.orderId || '');

  // 替换占位符(按你的 Doc 模板来)
  body.replaceText('{{DATE}}',       today);
  body.replaceText('{{ORDER_ID}}',   invoiceNo);
  body.replaceText('{{INVOICE_NO}}', invoiceNo);

  body.replaceText('{{NAME}}', String(order.name || ''));
  body.replaceText('{{EMAIL}}',String(order.email || ''));
  body.replaceText('{{QTY}}',  String(order.qty || ''));

  body.replaceText('{{PAPER_TYPE}}', String(order.paper || ''));
  body.replaceText('{{ENVELOPE_TYPE}}', String(order.envelope || ''));
  body.replaceText('{{STICKER_WAX}}', String(order.sticker || ''));
  body.replaceText('{{FOIL_TYPE}}',   String(order.foilType || ''));

  body.replaceText('{{PRICE_PER_SET}}', String(order.pricePerSet || ''));
  body.replaceText('{{BALANCE_DUE}}',   String(order.balance || ''));

  doc.saveAndClose();

  // 导出 PDF
  const pdfBlob = DriveApp.getFileById(copyId).getAs('application/pdf');
  const folder  = DriveApp.getFolderById(FINAL_FOLDER_ID);
  const pdfFile = folder.createFile(pdfBlob);
  pdfFile.setName('Final Invoice - ' + invoiceNo + '.pdf');

  // 发邮件
  const subject = 'Your Xamiya Final Invoice #' + invoiceNo;
  const bodyText =
    'Hi ' + (order.name || '') + ',\n\n' +
    'This is your final invoice for your Xamiya order.\n' +
    'Balance due: ' + (order.balance || '') + '\n\n' +
    'Once payment is received, we will proceed with production / shipment as discussed.\n\n' +
    'Thank you!\nXamiya';

  GmailApp.sendEmail(order.email, subject, bodyText, {
    attachments: [pdfFile]
  });

  // 清理临时 Doc
  DriveApp.getFileById(copyId).setTrashed(true);
}

function onFinalInvoiceCheckboxEdit(e) {
  if (!e || !e.source) return;

  const sheet = e.source.getActiveSheet();
  if (!sheet || sheet.getName() !== FINAL_SHEET_NAME) return;

  const range = e.range;
  const row   = range.getRow();
  const col   = range.getColumn();
  if (row === 1) return;

  const headers      = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
  const idxSendFlag  = headers.indexOf('Send Final Invoice PDF');
  if (idxSendFlag === -1) return;

  const sendCol      = idxSendFlag + 1;
  if (col !== sendCol) return;

  const newValue = range.getValue();
  const shouldSend =
    newValue === true ||
    String(newValue).toLowerCase() === 'true';

  if (!shouldSend) return;

  const rowValues   = sheet.getRange(row, 1, 1, sheet.getLastColumn()).getValues()[0];

  const idxOrderId  = headers.indexOf('Order ID');
  const idxName     = headers.indexOf('Name');
  const idxEmail    = headers.indexOf('Email');
  const idxQty      = headers.indexOf('Final Quantity');
  const idxPaper    = headers.indexOf('Paper Type');
  const idxEnvelope = headers.indexOf('Envelope Type');
  const idxSticker  = headers.indexOf('Sticker / Wax');
  const idxPriceSet = headers.indexOf('Price per set');
  const idxBalance  = headers.indexOf('Balance Due');
  const idxFoilType = headers.indexOf('Foil Type');

  const email = idxEmail === -1 ? '' : rowValues[idxEmail];
  if (!email) return;

  const order = {
    orderId:     idxOrderId   === -1 ? '' : rowValues[idxOrderId],
    name:        idxName      === -1 ? '' : rowValues[idxName],
    email:       email,
    qty:         idxQty       === -1 ? '' : rowValues[idxQty],
    paper:       idxPaper     === -1 ? '' : rowValues[idxPaper],
    envelope:    idxEnvelope  === -1 ? '' : rowValues[idxEnvelope],
    sticker:     idxSticker   === -1 ? '' : rowValues[idxSticker],
    pricePerSet: idxPriceSet  === -1 ? '' : rowValues[idxPriceSet],
    balance:     idxBalance   === -1 ? '' : rowValues[idxBalance],
    foilType:    idxFoilType  === -1 ? '' : (rowValues[idxFoilType] || '')
  };

  createFinalInvoicePDF(order);

  // 标记为 SENT,避免重复触发
  sheet.getRange(row, sendCol).setValue('SENT');
}

这样一来:

  • 助理只需要在 Final Orders 里填完信息 → 勾选 checkbox;
  • 系统会自动生成带所有细节的尾款发票 PDF,发到客户邮箱;
  • 所有发票 PDF 都会累积在一个 Folder 中,方便长期对账与归档。

六、如何保护敏感数据,又让助理很好用?

对于很多小团队,最现实的问题是:我能不能把报价交给助理做,但又不想暴露全部成本和利润?

我的做法是:

  • 敏感参数(印刷成本、国际运费、目标利润率等)全部集中在 engine_cost_core
  • 这张表单独设置为:只有我有编辑和查看权限;
  • 助理使用的是:一个「纯前台」的报价界面表,里面只看到「数量→价格」的结果;
  • 所有 WhatsApp 文案和单价,都是从后台引用过来的结果,而不是公式本身。

在系统思维里,这叫做:

设计原则

前台只暴露「需要被操作的变量」,后台集中存放「算法和利润模型」,通过中间层把两者连接起来。

这样就可以实现一个很舒服的状态:

  • 你可以随时根据汇率、成本变动调整利润率,而不需要重新教育助理;
  • 助理只要对着界面,给客户报一个专业、稳定的价格;
  • 客户体验不会因为你在后台调了一点数字而混乱。

七、85% 自动化是怎么来的?用一个订单生命周期算给你看

以前一个订单的人工步骤大致是:

  1. 客户来问 → 根据需求手动算价 → 写一段报价发给他;
  2. 客户同意 → 手动在 Word/PDF 里写发票 → 存成 PDF → 附件发邮件;
  3. 设计过程中多次确认 → 最后数量 / 规格锁定;
  4. 再重新算一遍尾款金额 → 再做一张尾款发票 → 再发邮件;
  5. 手动记录:这单的应收、已收、纸张类型、物流状态。

现在同一个订单,人工只需要:

  1. 在报价引擎输入变量 → 复制自动生成的 WhatsApp 文案发给客户;
  2. 客户准备下订金 → 发表单链接给他,剩下交给系统;
  3. 设计阶段:依然是人工沟通(这一部分我刻意不自动化);
  4. Final Orders 填完 → 助理勾选 checkbox → 尾款发票自动发送。

粗略估算:

  • 以前每单纯手动作业时间 ≈ 40–60 分钟;
  • 现在每单 ≈ 5–10 分钟(主要是人工确认和少量特殊情况处理)。

这就是我说的「85% 自动化」:不是没有人,而是把人从重复的机械动作里抽出来,把注意力放在设计、客户体验和高杠杆决策上。

八、常见坑与风控:触发器、测试环境和错单风险

最后讲一些实话:任何自动化系统都有坑,尤其是当你开始用 onFormSubmitonEdit 这种触发器时。

1. 一定要有「测试环境」

  • 在正式使用前,建议复制一份「测试表」和「测试表单」,连到同一套脚本;
  • 先用虚拟订单跑 10–20 单,确认发票内容、邮件标题都正常;
  • 确认没有错发给真实客户之后,再切到正式表。

2. 避免触发器被误触发

  • onEdit(e) 的条件判断要写严谨,比如确认是指定 sheet、指定列;
  • 勾选 checkbox 触发发送时,一定要有状态标记(例如改成 SENT),避免重复发送;
  • 在脚本里多写几层「缺少 Email 就直接 return」这样的防护。

3. 跟会计 / 对账系统的衔接

  • 发票 PDF 尽量用统一命名,如 Invoice - XM-订单号.pdf
  • 必要时可以再做一张「财务对账表」,让收入与订单号一一对应;
  • 如果未来接支付网关,这套订单编号体系也可以直接沿用。

九、常见问答(FAQ)

Q1:我不会写代码,可以直接照抄你的脚本吗?
不建议完全照抄,但很适合拿来当「骨架」。你可以先画出自己的业务流程: 报价 → 开单 → 发票 → 出货,然后把这篇文章中的几个函数名称和结构照搬, 再根据自己的字段名(比如客户姓名、产品名称、数量)去修改。 即使你现在不会写 Apps Script,也可以从「看懂变量名」开始,慢慢练习。
Q2:为什么你坚持不用任何第三方 SaaS,而是自己搭系统?
一方面是成本——对于小团队来说,每个月几个美金、十几个美金,很容易在一整年累积成一笔不小的固定支出; 更重要的是数据主权:我希望所有客户数据、报价逻辑、利润模型,都掌握在自己手里。 Google 本身就是一个成熟的生态,利用好 Sheets + Forms + Docs + Apps Script, 完全可以做出接近 SaaS 级体验的系统。
Q3:这样做会不会影响客户体验?他们会不会觉得「太冷冰冰」?
反而会更专业。客户看到的是:报价清晰、发票规范、信息一致。 真正需要温度的地方——例如设计沟通、婚礼细节、文字打磨——我是刻意保留人工的; 而那些不需要情绪劳动力的环节,比如填信息、发 PDF,就交给系统做。
Q4:如果以后订单量暴增,这套系统还撑得住吗?
对于中等体量(例如每月几百单)的业务,Google 生态完全能撑住。 真正会卡住的往往不是工具,而是你有没有一套清晰的数据结构和流程。 一旦未来真的需要上更专业的 CRM,这套「表结构 + 字段设计 + 流程逻辑」也可以直接迁移过去。
Q5:如果我也想在 48 小时内搭出一版,应该从哪一步开始?
很实用的一条路线是:
① 先画出自己的「订单人生」流程(客户从第一句询问到出货的一整条路);
② 用一张表,把所有需要记录的字段列出来;
③ 先做「订金发票」这一个环节:表单 → 表格 → 发票模板 → 脚本;
④ 跑通之后,再加上「尾款发票」和「报价引擎」;
⑤ 最后再考虑:哪些数据需要隐藏、哪些可以交给助理操作。
不必一口气做完全部,先跑通一个闭环,你就会对整个自动化的可能性有完全不一样的感觉。

数字创作者:一个人的系统化生存实验

更多数字创作者内容在这里

回到主题 Hub ↗