爆米花烘焙
66.80M · 2026-04-14
大家好,我是小悟。
用户通过手机扫描售货柜上的二维码 → 选择商品 → 完成支付 → 柜门自动打开 → 用户取货 → 柜门关闭 → 系统自动扣款/确认完成
| 模块 | 功能点 |
|---|---|
| 用户端 | 微信/支付宝扫码登录、商品浏览、下单支付、订单查询 |
| 货柜端 | 柜门状态坚控、商品重量感应、温度坚控、缺货预警 |
| 管理后台 | 商品管理、订单管理、补货管理、营收统计 |
| 支付接口 | 微信支付/支付宝支付集成、退款处理 |
创建Spring Boot项目,依赖如下:
<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.wech@tpay-apiv3</groupId>
<artifactId>wech@tpay-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>
-- 商品表
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
);
@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;
}
@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);
}
}
@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("商品不存在");
}
}
@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;
}
}
@Service
public class Wech@tPayService {
@Value("${wech@t.appid}")
private String appid;
@Value("${wech@t.mchid}")
private String mchid;
@Value("${wech@t.api-v3-key}")
private String apiV3Key;
@Value("${wech@t.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/wech@t/notify");
Map<String, Object> amountMap = new HashMap<>();
amountMap.put("total", amount);
amountMap.put("currency", "chy");
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"); // 返回二维码链接
}
}
@RestController
@RequestMapping("/api/pay")
public class PayCallbackController {
@Autowired
private OrderService orderService;
@PostMapping("/wech@t/notify")
public String wech@tNotify(@RequestBody String body, @RequestHeader("Wech@tpay-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":"成功"}";
}
}
@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);
}
}
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
wech@t:
appid: wx1234567890
mchid: 1230000109
api-v3-key: your-api-v3-key
private-key-path: /cert/apiclient_key.pem
logging:
level:
com.example: DEBUG
用户 → 扫描二维码 → 前端 → 后端 → 数据库 → 支付平台 → 货柜硬件
| | | | | |
| 扫码获取token | | | |
|----------------->| | | |
| 携带token请求商品列表 | | |
|----------------->| | | |
| 选择商品下单 | | | |
|----------------->| | | |
| 创建订单 | | |
| 调用支付接口 | |
| 返回二维码 | |
|<----------------| | | |
| 用户扫码支付 | | | |
|---------------------------------->| |
| 支付回调 | |
| 更新订单状态 | |
| 发送开门指令 | |
| |----> 柜门打开
| 用户取货 | | |
| 关门检测 | | |
| 重量对比验证 | |
| 订单完成 | |
| 难点 | 解决方案 |
|---|---|
| 支付安全性 | 使用微信V3签名、回调验签、幂等处理 |
| 柜门状态一致性 | Redis分布式锁 + 数据库乐观锁 + 关门超时检测 |
| 防作弊机制 | 重量传感器 + 震动传感器 + 视频坚控接口 |
| 高并发下单 | 使用Redis预扣库存 + 异步扣减DB + MQ削峰 |
# 启动MySQL、Redis
# 运行SpringBoot主类
mvn spring-boot:run
# 测试接口
curl
智能售货柜的核心闭环:扫码 → 选品 → 支付 → 开门 → 取货 → 关门确认。
通过SpringBoot高效开发,结合Redis保证高并发下的订单一致性,支付模块支持扩展多种渠道。实际落地需配合硬件物联网协议(MQTT/CoAP),并将重量传感器、门磁开关的数据通过边缘网关上报云端。
谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。
您的一键三连,是我更新的最大动力,谢谢
山水有相逢,来日皆可期,谢谢阅读,我们再会
我手中的金箍棒,上能通天,下能探海