大家好,我是小悟。
一、需求描述
1.1 业务场景
用户通过手机扫描售货柜上的二维码 → 选择商品 → 完成支付 → 柜门自动打开 → 用户取货 → 柜门关闭 → 系统自动扣款/确认完成
1.2 核心功能需求
模块 功能点
用户端 微信/支付宝扫码登录、商品浏览、下单支付、订单查询
货柜端 柜门状态监控、商品重量感应、温度监控、缺货预警
管理后台 商品管理、订单管理、补货管理、营收统计
支付接口 微信支付/支付宝支付集成、退款处理
1.3 技术架构
后端:Spring Boot 2.7 + MyBatis-Plus + MySQL + Redis
硬件模拟:用定时任务模拟柜门、重量传感器
支付:调用微信支付V3 API(沙箱环境)
API文档:Swagger/Knife4j
二、详细步骤
步骤1:项目初始化
创建Spring Boot项目,依赖如下:
xml代码解读复制代码<dependencies> <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency>
<groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version> </dependency> <dependency>
<groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version> </dependency> <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency> <dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-java</artifactId>
<version>0.2.11</version>
</dependency> <dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java
</artifactId>
<version>4.38.157.ALL
</version>
</dependency>
<dependency>
<groupId>cn.hutool
</groupId>
<artifactId>hutool-all</artifactId> <version>5.8.20</version> </dependency>
</dependencies>
步骤2:数据库设计
sql代码解读复制代码-- 商品表 CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL COMMENT '商品名称', price INT NOT NULL COMMENT '价格(分)',
stock INT NOT NULL COMMENT '库存', image_url VARCHAR(500) COMMENT '商品图片',
cabinet_no VARCHAR(20) COMMENT '货道编号',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 订单表 CREATE TABLE orders
( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '
订单号', user_id VARCHAR(50) NOT NULL COMMENT '用户ID(openId)',
product_id BIGINT NOT NULL, product_name VARCHAR(100),
amount INT NOT NULL COMMENT '金额(分)',
status TINYINT DEFAULT 0 COMMENT '0待支付 1已支付 2已完成 3已关闭 4退款中',
pay_type VARCHAR(10) COMMENT 'WECHAT/ALIPAY', prepay_id VARCHAR(100)
COMMENT '微信预支付ID', cabinet_status TINYINT COMMENT '柜门状态 0关闭 1开启',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME,
finish_time DATETIME ); -- 货柜状态日志表 CREATE TABLE cabinet_log
( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32),
door_status TINYINT COMMENT '0关闭 1开启',
weight_before INT COMMENT '取货前重量(g)',
weight_after INT COMMENT '取货后重量(g)',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP );
步骤3:核心代码实现
3.1 实体类
vbnet代码解读复制代码@Data @TableName("product") public class Product
{ @TableId(type = IdType.AUTO) private Long id;
private String name; private Integer price; private Integer stock;
private String imageUrl; private String cabinetNo; private LocalDateTime createTime; }
@Data @TableName("orders") public class Order { @TableId(type = IdType.AUTO)
private Long id; private String orderNo; private String userId;
private Long productId; private String productName; private Integer amount;
private Integer status; private String payType; private String prepayId;
private Integer cabinetStatus; private LocalDateTime createTime;
private LocalDateTime payTime; private LocalDateTime finishTime; }
3.2 统一响应封装
typescript代码解读复制代码@Data @AllArgsConstructor public class Result<T>
{ private Integer code; private String msg; private T data;
public static <T> Result<T> success(T data) { return new Result<>(200, "success", data);
} public static <T> Result<T> error(String msg) { return new Result<>
(500, msg, null); } }
3.3 商品接口
less代码解读复制代码@RestController @RequestMapping("/api/product") public class ProductController
{ @Autowired private IProductService productService; @GetMapping("/list")
public Result<List<Product>> list() { return Result.success(productService.list());
} @GetMapping("/{id}") public Result<Product> getById(@PathVariable Long id)
{ Product product = productService.getById(id); return product !=
null ? Result.success(product) : Result.error("商品不存在"); } }
3.4 订单服务(核心业务)
scss代码解读复制代码@Service public class OrderService { @Autowired private OrderMapper
orderMapper; @Autowired private ProductMapper productMapper; @Autowired private
RedisTemplate<String, String> redisTemplate; // 创建订单并生成支付二维码 @Transactional
public Result<Map<String, String>> createOrder(Long productId, String userId, String payType)
{ Product product = productMapper.selectById(productId); if (product ==
null || product.getStock() <= 0) { return Result.error("商品库存不足");
} String orderNo = "SH" + System.currentTimeMillis() + RandomUtil.
randomNumbers(4); Order order = new Order();
order.setOrderNo(orderNo); order.setUserId(userId);
order.setProductId(productId); order.setProductName(product.getName());
order.setAmount(product.getPrice()); order.setStatus(0); // 待支付
order.setPayType(payType); order.setCabinetStatus(0);
order.setCreateTime(LocalDateTime.now());
orderMapper.insert(order);
// 调用支付接口获取二维码
String qrCode = generatePayQRCode
(orderNo, product.getPrice(), payType);
Map<String, String> result = new HashMap<>();
result.put("orderNo", orderNo);
result.put("qrCode", qrCode);
// 订单存入Redis,15分钟过期
redisTemplate.opsForValue().set
("order:" + orderNo, orderNo, 15, TimeUnit.MINUTES);
return Result.success(result); }
// 支付成功回调
@Transactional
public void handlePaySuccess
(String orderNo, String
transactionId) {
Order order =
orderMapper.selectByOrderNo
(orderNo); if
(order == null ||
order.getStatus() != 0)
{ return;
}
// 更新订单状态
order.setStatus(1); // 已支付 order.setPayTime(LocalDateTime.now()); orderMapper.updateById(order); // 减库存 productMapper.decreaseStock(order.getProductId()); // 发送MQ消息通知货柜开门(这里简化,直接调用) openCabinetDoor(orderNo); } // 模拟开门(实际需要通过物联网协议控制硬件) private void openCabinetDoor(String orderNo) { // 记录开门日志 CabinetLog log = new CabinetLog(); log.setOrderNo(orderNo); log.setDoorStatus(1); cabinetLogMapper.insert(log); // 更新订单柜门状态 orderMapper.updateCabinetStatus(orderNo, 1); // 启动关门检测任务(30秒后检测) scheduleDoorCloseCheck(orderNo); } // 关门检测 private void scheduleDoorCloseCheck(String orderNo) { ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); executor.schedule(() -> { // 模拟重量传感器获取取货前后重量 int beforeWeight = getWeightBefore(); // 等待用户取货 try { Thread.sleep(5000); } catch (InterruptedException e) {} int afterWeight = getWeightAfter(); int takenWeight = beforeWeight - afterWeight; // 根据重量差判断取货是否成功 if (takenWeight > 0) { Order order = orderMapper.selectByOrderNo(orderNo); order.setStatus(2); // 已完成 order.setFinishTime(LocalDateTime.now()); orderMapper.updateById(order); // 记录重量日志 CabinetLog log = new CabinetLog(); log.setOrderNo(orderNo); log.setDoorStatus(0); log.setWeightBefore(beforeWeight); log.setWeightAfter(afterWeight); cabinetLogMapper.insert(log); } }, 30, TimeUnit.SECONDS); } private int getWeightBefore() { return 5000; } // 模拟重量 private int getWeightAfter() { return 4800; } // 支付二维码生成(伪代码,实际需集成微信/支付宝SDK) private String generatePayQRCode(String orderNo, int amount, String payType) { // 实际调用微信支付native下单接口返回code_url return "https://pay.example.com/qr/" + orderNo; } }
3.5 微信支付集成(Native支付)
typescript代码解读复制代码@Service public class WechatPayService { @Value("${wechat.appid}") private String appid; @Value("${wechat.mchid}") private String mchid; @Value("${wechat.api-v3-key}") private String apiV3Key; @Value("${wechat.private-key-path}") private String privateKeyPath; public String nativePay(String orderNo, int amount, String description) throws Exception { // 构建请求参数 Map<String, Object> params = new HashMap<>(); params.put("appid", appid); params.put("mchid", mchid); params.put("description", description); params.put("out_trade_no", orderNo); params.put("notify_url", "https://yourdomain.com/api/pay/wechat/notify"); Map<String, Object> amountMap = new HashMap<>(); amountMap.put("total", amount); amountMap.put("currency", "CNY"); params.put("amount", amountMap); // 发送请求到微信支付API String response = HttpUtil.createPost("https://api.mch.weixin.qq.com/v3/pay/transactions/native") .header("Authorization", getAuthorizationHeader()) .body(JSONUtil.toJsonStr(params)) .execute() .body(); JSONObject json = JSONUtil.parseObj(response); return json.getStr("code_url"); // 返回二维码链接 } }
3.6 支付回调处理
less代码解读复制代码@RestController @RequestMapping("/api/pay") public class PayCallbackController { @Autowired private OrderService orderService; @PostMapping("/wechat/notify") public String wechatNotify(@RequestBody String body, @RequestHeader("Wechatpay-Signature") String signature) { // 验签和解析(省略详细验签逻辑) JSONObject result = JSONUtil.parseObj(body); String orderNo = result.getByPath("resource.ciphertext.out_trade_no", String.class); String transactionId = result.getByPath("resource.ciphertext.transaction_id", String.class); orderService.handlePaySuccess(orderNo, transactionId); // 返回成功响应给微信 return "{\"code\":\"SUCCESS\",\"message\":\"成功\"}"; } }
3.7 用户扫码登录(模拟微信授权)
less代码解读复制代码@RestController @RequestMapping("/api/user") public class UserController { @GetMapping("/wxlogin") public Result<String> wxLogin(@RequestParam String code) { // 实际需调用微信登录接口获取openId String openId = "mock_openid_" + code; String token = UUID.randomUUID().toString(); // 存入Redis,有效期2小时 redisTemplate.opsForValue().set("user:token:" + token, openId, 2, TimeUnit.HOURS); return Result.success(token); } }
步骤4:配置文件
yaml代码解读复制代码server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/vending_machine?useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 redis: host: localhost port: 6379 wechat: appid: wx1234567890 mchid: 1230000109 api-v3-key: your-api-v3-key private-key-path: /cert/apiclient_key.pem logging: level: com.example: DEBUG
三、完整业务流程时序图
lua代码解读复制代码用户 → 扫描二维码 → 前端 → 后端 → 数据库 → 支付平台 → 货柜硬件 | | | | | | | 扫码获取token | | | | |----------------->| | | | | 携带token请求商品列表 | | | |----------------->| | | | | 选择商品下单 | | | | |----------------->| | | | | 创建订单 | | | | 调用支付接口 | | | 返回二维码 | | |<----------------| | | | | 用户扫码支付 | | | | |---------------------------------->| | | 支付回调 | | | 更新订单状态 | | | 发送开门指令 | | | |----> 柜门打开 | 用户取货 | | | | 关门检测 | | | | 重量对比验证 | | | 订单完成 | |
四、总结
4.1 核心难点与解决方案
难点 解决方案
支付安全性 使用微信V3签名、回调验签、幂等处理
柜门状态一致性 Redis分布式锁 + 数据库乐观锁 + 关门超时检测
防作弊机制 重量传感器 + 震动传感器 + 视频监控接口
高并发下单 使用Redis预扣库存 + 异步扣减DB + MQ削峰
4.2 可扩展性设计
多支付渠道:策略模式封装WechatPayService、AliPayService
多硬件适配:定义CabinetDriver接口,不同厂商实现
补货管理:增加补货任务,通过WebSocket推送缺货预警
4.3 运行测试
bash代码解读复制代码# 启动MySQL、Redis # 运行SpringBoot主类 mvn spring-boot:run # 测试接口 curl http://localhost:8080/api/product/list
4.4 生产环境注意事项
✅ 必须使用HTTPS + 域名备案
✅ 支付回调需要幂等处理(防止重复通知)
✅ 柜门传感器需要心跳检测(断网重连机制)
✅ 订单超时自动取消(定时任务扫描未支付订单)
✅ 接入监控系统(Prometheus + Grafana)
4.5 总结
智能售货柜的核心闭环:扫码 → 选品 → 支付 → 开门 → 取货 → 关门确认。
通过SpringBoot高效开发,结合Redis保证高并发下的订单一致性,支付模块支持扩展多种渠道。实际落地需配合硬件物联网协议(MQTT/CoAP),并将重量传感器、门磁开关的数据通过边缘网关上报云端。
作者:悟空码字
链接:https://juejin.cn/post/7628224961766850600
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。