SpringBoot + 微信支付实现“扫码开门,取货自动扣款”售货柜

大家好,我是小悟。

一、需求描述

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 可扩展性设计

  1. 多支付渠道:策略模式封装WechatPayService、AliPayService
  2. 多硬件适配:定义CabinetDriver接口,不同厂商实现
  3. 补货管理:增加补货任务,通过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),并将重量传感器、门磁开关的数据通过边缘网关上报云端。

谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。

您的一键三连,是我更新的最大动力,谢谢

山水有相逢,来日皆可期,谢谢阅读,我们再会

我手中的金箍棒,上能通天,下能探海

相关推荐
小蜜蜂dry2 小时前
nestjs实战-登录、鉴权(二)
前端·后端·nestjs
全栈王校长2 小时前
Nest 文件上传 - 就是增强版的 el-upload
前端·后端·nestjs
0xDevNull2 小时前
Spring Boot 3.x WebSocket 实战教程
spring boot·后端·websocket
沐雪轻挽萤2 小时前
1. C++17新特性-序章
java·c++·算法
neoooo2 小时前
Spring AI MCP Server 开发指南
人工智能·后端·mcp
殷紫川2 小时前
Spring AI 整合火山引擎豆包向量库搭建企业知识库:我踩过的 10 个致命坑与终极解决方案
java·ai编程
呆呆在发呆.2 小时前
JavaEE初阶
java·jvm·网络协议·学习·udp·java-ee·tcp
南璋2 小时前
MySQL排序踩坑:为什么"10"比"2"小?
后端
何陋轩2 小时前
Elasticsearch搜索引擎深度解析:把搜索核心讲透,面试都是小菜
后端·面试