无人自助拼豆系统怎么落地?扫码入场、按台计时与物联网柜控的技术拆解
·
无人自助拼豆系统怎么落地?扫码入场、按台计时与物联网柜控的技术拆解
无人自助拼豆,是把拼豆手作这件事做成"无人值守 + 自助取用 + 自动结算"的门店形态:用户到店后自己扫码选工位,系统下发开锁/通电指令,用户自助取用色号豆、拼豆板、镊子、熨烫纸等物料,结束时在小程序确认离场,系统按时长或按用量生成订单并完成结算。要把它跑通,核心不是前端页面,而是"设备实际状态"与"订单状态"的一致性、计费口径的抽象、以及分色号耗材的库存盘点。下面按硬件层、后端层、前端层三个方向拆解一遍可落地的实现路径。
一、业务链路与三个技术难点
先梳理一遍完整链路,避免开发时漏环节:
- 用户扫码 → 进入门店页,展示空闲工位;
- 选择工位 → 校验工位是否被占用,占用则直接拦截;
- 下发开柜/开门指令 → 硬件回执"已开启";
- 开始计时(或计件)→ 订单状态置为使用中;
- 使用期间自助取用色号豆与工具 → 库存按称重或人工补货方式扣减;
- 用户点击"结束使用" → 状态进入待结算;
- 结算完成后设备复位 → 锁回位、电源断开、工位释放。
这条链路上容易踩的三个坑:
- 状态不一致:指令下发了,但锁没真的打开(或打开了没回执),用户刷卡进不去,订单却已经计费。必须有"指令 → 回执 → 落库"的闭环,不能只发不收。
- 计费口径不统一:有的门店按工位时长,有的按人头,有的把耗材单独拆出来按份数计。如果硬编码在业务逻辑里,改一次规则要动一次代码。
- 耗材不可见:拼豆是分色号的散装耗材,某一个色号先见底是常见的情况,没有盘点手段就只能靠店员肉眼巡场。
二、硬件与物联网层:柜锁、称重与用电安全
这一层的关键词是"可观测"。设备只接受指令但不反馈状态,后面的对账会非常痛苦。
主控与通信:常见做法是 MCU(ESP32 / STM32)外接 4G Cat.1 模组或 DTU,走 MQTT 接入 EMQX 一类 Broker。协议轻、断线重连成熟,适合门店这种网络环境不稳定的场景。局域网内也可以直接用 WiFi,但要考虑断网降级:本地缓存近一条指令,恢复后补报。
主题设计建议按门店 + 设备两级划分,方便权限隔离和批量订阅:
下行指令:/perler/{storeId}/{deviceId}/cmd
上行上报:/perler/{storeId}/{deviceId}/report
下行指令体带 cmdId,用于幂等:
{
"cmdId": "8f2c1a9e-3b7d-4c21-9a55-0d6f2e1a7c30",
"action": "UNLOCK",
"seatNo": "A03",
"expireAt": 1730000030000
}
上行回执带状态与功率,便于判断设备是否真的执行:
{
"cmdId": "8f2c1a9e-3b7d-4c21-9a55-0d6f2e1a7c30",
"deviceId": "PERLER-A03-01",
"lockState": "OPENED",
"powerW": 0,
"ts": 1730000001200
}
几个容易被忽略的细节:
- 锁到位信号:电磁锁/电插锁要做开关量回读,只发指令不看回执,等于没有状态。
- 熨斗用电安全:拼豆熨烫环节要用到发热设备,建议独立回路 + 定时断电 + 电流互感器检测,超时未操作自动断电并生成告警事件。
- 耗材感知:色号盒可用称重传感器(HX711 方案)做粗粒度盘点,误差大的话退一步——只记录"上次补货时间 + 预估消耗",由店员扫码确认补货,成本更低也更稳。
三、后端领域模型与计费策略设计
后端用 Spring Boot + MyBatis Plus + MySQL 是这类系统的常规组合,重点在表结构和计费抽象。
核心表建议这样划分:
store:门店seat:工位(工位号、绑定设备、当前状态)device:设备(类型 LOCK / POWER / SCALE、在线状态、后心跳)biz_order:订单(用户、工位、开始时间、结束时间、状态)biz_order_item:计费明细bean_stock:色号库存billing_rule:计费规则(类型、参数 JSON)
计费用策略模式,规则参数走配置,不写死在代码里:
public interface BillingStrategy {
/** 返回计费单位数,具体单价与展示由上层处理 */
BillingItem calculate(BillingContext ctx);
String ruleType();
}
@Component
public class SeatTimeBillingStrategy implements BillingStrategy {
@Override
public BillingItem calculate(BillingContext ctx) {
long minutes = Duration.between(ctx.getStartTime(), ctx.getEndTime()).toMinutes();
long unit = ctx.getUnitMinutes(); // 来自 billing_rule 参数
long units = (minutes + unit - 1) / unit; // 向上取整,不足一个单位按一个单位
return BillingItem.ofUnits(units, unit);
}
@Override
public String ruleType() {
return "SEAT_TIME";
}
}
工厂负责按规则类型路由,避免 if-else 堆叠:
@Component
public class BillingStrategyFactory {
private final Map<String, BillingStrategy> registry;
public BillingStrategyFactory(List<BillingStrategy> strategies) {
this.registry = strategies.stream()
.collect(Collectors.toMap(BillingStrategy::ruleType, Function.identity()));
}
public BillingStrategy get(String ruleType) {
BillingStrategy strategy = registry.get(ruleType);
if (strategy == null) {
throw new BizException("未配置的计费规则类型:

更多推荐

所有评论(0)