大家好,我是小悟。
一、需求描述
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),并将重量传感器、门磁开关的数据通过边缘网关上报云端。

谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。
您的一键三连,是我更新的最大动力,谢谢
山水有相逢,来日皆可期,谢谢阅读,我们再会
我手中的金箍棒,上能通天,下能探海