为无人销售所用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信息终端模式的客户端终端、周边设备、Square Terminal
  • Square服务:Square API、商品主数据
  • 监控·运维系统:监控摄像头、不间断电源装置、网络冗余化

用户看到的只有触摸对应显示器、条码扫描器以及Square Terminal。客户端在Windows 11 Pro的信息终端模式下运行,主要应用程序逻辑在店外的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 制作成地图
    //------------------------------------------------------------------
    const imageMap     = {};
    const categoryMap  = {};
    const variationMap = {};

    // ...省略(地图制作处理)

    //------------------------------------------------------------------
    // ③ 展开 ITEM,用先制作的地图嵌入信息
    //------------------------------------------------------------------
    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的运营和宣传工作。