為無人商店在2天內製作的自助收銀系統解說

Bohfula / ボーフラ
作者:
為無人商店在2天內製作的自助收銀系統解說

注意: 本文為從日文原文機器翻譯而來。如果您發現翻譯錯誤,請告知我們。

為了開設LOPPO畫材涉谷店(無人商店),我將解說在2天內構築的無現金完全自助收銀系統。

開始:LOPPO畫材的無人商店專案

LOPPO畫材是從高橋的興趣開始的賽璐璐畫材品牌,至今主要以網路購物為中心發展。 今年初,我們決定租借一個小店面作為LOPPO畫材的活動空間,本來計劃將其一角作為商品庫存放置場所,但後來想到「既然要放庫存,是否可以直接變成販售點」這個想法。

雖然以前就有實體店鋪的需求,但確保店鋪營運所需的人力資源是困難的。 於是我想到了「畫材無人商店」這種形態。就像蔬菜無人商店一樣,是一個可以24小時365天購買賽璐璐畫材的夢幻般(瘋狂的!)場所。

畫材無人商店(概念圖) 畫材無人商店(概念圖)

1. 既有自助收銀的課題與解決方案

要實現不需要常駐工作人員的無人商店,需要完全自助的收銀系統。這次以防盜對策為前提,僅支援無現金結帳。

最初我們考慮了市售的自助收銀解決方案,但有兩個重大課題。

  1. 除了初期成本外,月額固定費很高
  2. 到入帳的週期很長

為了提高持續性,重要的是將固定費控制在最小限度,同時保持良好的現金流。 因此我們注意到過去活動攤位時導入的Square結帳系統。

Square不僅支援多樣的結帳方式,月額固定費為零(僅收結帳手續費),最快隔日營業日入帳的迅速入帳週期很有魅力。 此外,通過使用Square API可以建立客製化應用程式。 由於已經擁有Square Terminal,透過活用這個設備也可以控制初期成本。

不過,在開發上有很大的限制。由於僅製作店鋪開店用商品的業務就已經忙不過來,自助收銀系統的開發只能分配僅僅2天的時間。

2. 系統設計:安全且易用的自助收銀

整體架構

系統構成圖

系統大致分為以下組件構成。

  • Linux應用程式伺服器:React前端和Express後端
  • 店內系統:Windows 11 Pro Kiosk模式的客戶端終端、周邊設備、Square Terminal
  • Square服務:Square API、商品主檔資料
  • 監控運營系統:監控攝影機、不斷電電源裝置、網路冗餘化

從使用者看到的只有觸控螢幕、條碼掃描器,以及Square Terminal。客戶端以Windows 11 Pro的Kiosk模式運作,主要的應用程式邏輯在店外的Linux機器上。

安全性與網路

網路位於使用Tailscale的VPN環境下,這樣保護了客戶端終端與Linux伺服器間的通訊。 此外,所有設備的電源都連接到不斷電電源裝置以對抗雷擊和停電,網路也進行了冗餘化以確保穩定運作。

透過導入Tailscale,遠端維護也變得容易。 各終端完全不保存本地資料,全部從Square資料取得的機制。

硬體構成

  • 觸控螢幕
  • USB條碼掃描器
  • Square Terminal(結帳處理・收據列印)
  • 監控攝影機(即時監控用)

前端實作

結帳方式選擇畫面 結帳方式選擇畫面

前端使用React實作,由以下主要畫面構成。

  1. 商品掃描畫面
  2. 結帳方式選擇畫面
  3. 結帳處理中畫面
  4. 結帳完成畫面

雖然使用機器翻譯但也進行了多語言化,支援日語、英語、法語、西班牙語、繁體中文、簡體中文的6種語言。 這樣一來,訪日外國遊客也能安心使用。

// 語言設定的範例
const translations = {
  ja: {
    title: 'セルフレジシステム',
    scanTitle: '商品スキャン',
    // ...略
  },
  en: {
    title: 'Self-Checkout System',
    scanTitle: 'Product Scan',
    // ...略
  },
  // 其他語言...
};

此外,設計為最優先接受條碼掃描器的輸入,致力於讓使用者不會迷惑的介面。

3. Square API活用的重點

透過Terminal API的結帳處理

Square API中特別重要的是Terminal API。透過使用這個,可以向Square Terminal請求結帳處理。

// Terminal 結帳生成
app.post("/api/create-terminal-checkout", async (req, res) => {
  try {
    const { order, amountMoney, paymentType = "CARD_PRESENT" } = req.body;

    const ALLOWED = new Set([
      "CARD_PRESENT",
      "FELICA_TRANSPORTATION_GROUP",
      "FELICA_ID",
      "FELICA_QUICPAY",
      "QR_CODE"
    ]);

    if (!ALLOWED.has(paymentType)) {
      return res.status(400).json({ error: "指定了不支援的結帳方式" });
    }

    // 先建立訂單
    const orderId = await createOrder(order);

    // 建立Square Terminal Checkout
    const checkoutResponse = await squareClient.terminal.checkouts.create({
      idempotencyKey: randomUUID(),
      checkout: {
        amountMoney: {
          // 金額必須是 BigInt
          amount: BigInt(amountMoney.amount),
          currency: amountMoney.currency,
        },
        deviceOptions: {
          deviceId: SQUARE_DEVICE_ID,
          skip_receipt_screen: true,
          show_itemized_cart: false,
        },
        referenceId: orderId,
        orderId,
        note: "LOPPO自助收銀結帳",
        paymentType: paymentType
      },
    });

    res.json(checkoutResponse);
  } catch (error) {
    handleError("Terminal Checkout 建立錯誤", error, res);
  }
});

多樣的結帳方式

Square Terminal支援多樣的結帳方式,因此顧客可以用自己偏好的方法付款。

  • 信用卡・金融卡
  • 交通IC卡(Suica/PASMO等)
  • iD
  • QUICPay
  • QR碼結帳(PayPay等)

注意不支援銀聯卡。

結帳狀況確認的輪詢

由於結帳處理在Square Terminal上進行,需要透過輪詢確認結帳完成或取消等狀態。

// 透過輪詢確認結帳狀況
const checkPaymentStatus = async () => {
  try {
    const statusResponse = await fetch(`/api/get-checkout-status?checkoutId=${data.checkout.id}`);
    const statusData = await statusResponse.json();

    if (statusData.status === 'COMPLETED') {
      setPaymentStatus(t.paymentCompleted);
      // 完成處理
      setTimeout(() => {
        setStatus('complete');
        setCart([]);
      }, 2000);
    } else if (statusData.status === 'CANCELED' || statusData.status === 'CANCEL_REQUESTED') {
      setPaymentStatus(t.paymentCanceled);
      setTimeout(() => {
        setStatus('ready');
      }, 3000);
    } else {
      // 如果還沒完成則重新確認
      setPaymentStatus(t.processing);
      setTimeout(checkPaymentStatus, 2000);
    }
  } catch (error) {
    console.error(t.statusCheckFailed, error);
    setPaymentStatus(t.statusCheckFailed);
    setTimeout(() => {
      setStatus('ready');
    }, 3000);
  }
};

商品主檔資料管理

商品資訊全部在Square的管理畫面註冊,透過API取得。 這樣簡化了商品新增和價格變更等運營作業。

app.get("/api/catalog-items", async (_req, res) => {
  try {
    const TYPES = "ITEM,ITEM_VARIATION,CATEGORY,IMAGE"; // 列舉所有需要的類型
    //------------------------------------------------------------------
    // ① 全部讀取
    //------------------------------------------------------------------
    const objects = [];
    for await (const obj of await squareClient.catalog.list({ types: TYPES }))
      objects.push(obj);

    //------------------------------------------------------------------
    // ② 先將CATEGORY / IMAGE / VARIATION 製成map
    //------------------------------------------------------------------
    const imageMap     = {};
    const categoryMap  = {};
    const variationMap = {};

    // ...略(map建立處理)

    //------------------------------------------------------------------
    // ③ 展開ITEM,用先製作的map嵌入資訊
    //------------------------------------------------------------------
    const filtered = objects
      .filter((o) => o.type === "ITEM")
      .map((item) => {
        // ...略(資料轉換處理)
      })
      // -- 在此進行需求篩選 --
      .filter(
        (item) =>
          !item.isArchived &&
          item.categoryNames.includes("LOPPO畫材")
      );

    res.json(filtered);
  } catch (error) {
    handleError("目錄項目取得錯誤", error, res);
  }
});

4. 透過LLM活用的快速開發

此專案最大的特徵是在僅僅2天這樣短的期間內完成開發。 使這成為可能的是LLM(大規模語言模型)的活用。

開發時間的分配

  • 基本系統開發:約2小時
  • 改良・UI調整:約4小時
  • 測試・部署:剩餘時間

Claude 3.7 Sonnet的活用方法

開發中主要活用Claude 3.7 Sonnet,提高了實作效率。 不僅是應用程式邏輯,UI設計也能輕鬆處理,連設定文件都準備好了等,真是至極周到。 特別是多語言對應的程式碼和與Square API的連接部分,LLM的提案非常有用。

另外也組合使用了ChatGPT 4o和ChatGPT o3等,但在對WEB應用程式的理解度上完全不及3.7 Sonnet。

LLM活用的具體例子

在開發中活用LLM時的理所當然注意點,生成的程式碼無法直接使用,必須在理解後加上必要的修正這點很重要。 例如,與Square Terminal API的連接部分需要以下修正。

  1. 結帳方式的追加:LLM生成的程式碼僅支援信用卡結帳,需要追加選擇結帳方式的畫面
  2. 錯誤處理:Terminal的結帳取消事件的處理方式有誤,根據API文件修正為正確的處理
  3. 安全性:一部分使用脆弱的協定進行應用程式間通訊,因此構築私人VPN確保通訊路徑的安全性

LLM提供了基本的程式碼結構,但為了能承受正式營運的調整需要手工作業。

5. 國際化與使用便利性

多語言對應的實作

為了讓訪日外國遊客也能使用,系統支援日語、英語、法語、西班牙語、繁體中文、簡體中文的6種語言。 在React組件內管理語言設定,採用從翻譯物件取得畫面上所有文字的方式。

// 語言選擇的狀態管理
const [language, setLanguage] = useState('ja'); // 將預設語言設為日語
// 取得語言設定
const t = translations[language];

// 使用例
<h1 className="text-4xl font-bold">{t.title}</h1>
<p className="text-lg text-gray-700 mb-6">
  {t.scanDescription}
</p>

使用便利性的巧思

以超市和便利商店的自助收銀相同的使用便利性為目標,進行了以下巧思。

  1. 條碼掃描優先:在頁面任意位置接受鍵盤輸入,常時優先條碼掃描器的輸入
  2. 大按鈕:容易觸控操作的大按鈕尺寸
  3. 明確的回饋:容易理解操作結果的訊息顯示

透過這些巧思,我認為實現了使用者不會迷惑就能操作的介面。

6. 運營面的巧思

即時監控與故障對應

在店內設置網路攝影機,能即時確認商店的狀況。 發生問題時,顧客可聯絡店頭張貼的電話號碼進行對應。

檢測到營運上的異常時,整備了30分鐘~2小時內趕到現場對應的體制。 此外,萬一結帳終端無法運作時,也準備了事後交付結帳連結讓顧客完成付款的替代手段。

為了穩定運作,也組建了不斷電電源裝置的電源備份、網路的冗餘化、無操作時的定期重啟等。

只有1次發生客戶端電源線插入不牢而脫落的問題,之後系統非常穩定。

7. 實際成果與效果

銷售機會的擴大

透過開設無人商店,獲得了都心車站附近24小時營業的珍貴銷售機會。 最大的成果是在回應實體店鋪開設要求的同時,也克服了人力資源的限制。

結帳方法的傾向

導入的所有結帳方法都被廣泛使用,但特別受歡迎的順序如下。

  1. 交通IC卡
  2. QR碼結帳(PayPay等)
  3. 信用卡(感應式結帳)

8. 今後的展望與擴張計劃

網購的展開

目前的LOPPO畫材EC網站是用BASE構築的,但預計變更為使用Square API的版本。 這樣不僅能節約結帳手續費,還能改善商品購買流程,並且能一元化管理店鋪和網路銷售的庫存・銷售。

此外,還想提供線上購買店頭取貨的手段。

總結:既然能製作顏料,收銀系統當然也能製作

LOPPO手工復刻了各種賽璐璐相關畫材,這次手工製作了收銀系統。

軟體應用程式像這樣透過活用既有API和LLM的支援,有時可以相對簡單地構築。 話雖如此,沒想到收銀系統竟然能如此簡單地製作,真是學到很多。

注力的重點

  1. 活用既有服務:最大限度活用Square API等既有平台
  2. 活用LLM等支援工具:積極導入提高開發效率的工具
  3. 集中於必要最小限的範圍:集中於必要功能簡單實作

希望對有興趣構築自助收銀系統,或考慮活用Square API的人有所參考。

這次構築的系統全部原始碼已在GitHub上公開。 loppo-llc/loppo-register - GitHub

Bohfula / ボーフラ

Bohfula / ボーフラ

一個擁有茶壺形狀奇特頭部的獨立遊戲開發者。經常被高橋叫去幫忙處理LOPPO的營運和宣傳工作。