ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SpringBoot构建无人智慧超市系统的核心实践

SpringBoot构建无人智慧超市系统的核心实践 简介这是一套基于SpringBoot开发的无人智慧超市管理系统完整源码面向计算机、电子信息工程等专业学生及毕业设计/课程设计实践者提供从后端架构到前端交互的全栈实现方案。资源共594个文件涵盖139个Java核心业务类、99个Vue组件页面、41个JS逻辑脚本、18个MyBatis映射XML及161个SVG图标资源辅以SQL脚本、配置文件与启动脚本bat/cmd完整呈现B/S架构下MVC分层结构与前后端分离开发范式。压缩包大小17.52MB已获257人学习下载。读者可直接导入IDEA运行包含用户管理、商品库存、自助结算、后台监控等典型模块所有代码经严格测试配套清晰目录结构与基础数据库初始化脚本便于快速部署、二次开发与功能拓展是高分毕设与工程实践的优质参考范例。1. 为什么一个“无人智慧超市”系统必须用 SpringBoot 而不是直接写 Servlet想象你正在调试一台自助结算终端——顾客刚扫完三件商品系统却卡在“生成订单”环节日志里只有一行NullPointerException又或者凌晨两点库存服务突然返回 500但监控告警没触发因为库存模块和支付模块压根没做健康检查端点。这些不是边缘场景而是无人值守环境下的生死线没有收银员兜底没有人工干预窗口系统必须自己完成状态感知、异常熔断、事务补偿和分钟级自愈。SpringBoot 不是“写得快”的代名词它是把这类高可靠性诉求工程化落地的最小可行框架内建 Actuator 健康探针、自动装配 DataSource 和事务管理器、标准化的配置中心接入能力、以及可插拔的指标埋点Micrometer——这些能力不是锦上添花而是让“无人”二字真正成立的基础设施。本文面向已掌握 Java 基础、熟悉 REST 接口开发但尚未在真实业务中落地过“端到端闭环”系统的开发者重点拆解如何用 SpringBoot 构建具备设备联动、实时库存校验、离线降级和审计追溯能力的超市管理系统所有代码均可在 JDK 17 SpringBoot 3.2.x 环境下直接运行。2. 从零搭建核心模块商品、库存、订单与设备通信的四层结构设计无人智慧超市的本质是将物理世界的货架、摄像头、RFID 读写器、电子价签、自助结算台等设备映射为可编程、可编排、可验证的软件实体。SpringBoot 的优势不在于“能写接口”而在于它强制你按领域边界组织代码并提供开箱即用的粘合剂。我们采用分层架构device层负责与硬件通信如通过 MQTT 接收门禁开关事件domain层封装业务规则如“扫码结算时若某商品库存不足则拒绝下单”application层协调用例如“完成一笔订单需同步更新库存、生成物流单、通知电子价签变价”web层仅处理 HTTP 协议转换。这种分层不是教条而是为了应对无人场景下的关键约束当网络中断时device层必须支持本地缓存指令application层需提供离线事务暂存队列而domain层的规则必须独立于任何外部依赖——这意味着库存扣减逻辑不能写在 Controller 里也不能依赖 Redis 的 Lua 脚本临时凑合。2.1 商品与库存的强一致性建模为什么不用 MyBatis-Plus 的TableField(fill FieldFill.INSERT)在无人超市中“上架一件商品”不是简单插入数据库而是触发一连串设备动作电子价签需刷新价格与促销标签货架传感器需校准重量阈值AI 摄像头需加载该商品的识别模型特征。若用 MyBatis-Plus 的自动填充会导致业务逻辑与数据持久化耦合且无法在事务回滚时撤销已发送的设备指令。正确做法是定义明确的领域事件// domain/event/StockChangedEvent.java public record StockChangedEvent( String skuCode, int delta, String operator, LocalDateTime occurredAt ) implements DomainEvent {}然后在InventoryService中显式发布事件Transactional public void deductStock(String skuCode, int quantity) { Inventory inventory inventoryRepository.findBySkuCode(skuCode) .orElseThrow(() - new StockNotEnoughException(skuCode)); if (inventory.getAvailable() quantity) { throw new StockNotEnoughException(skuCode); } inventory.setAvailable(inventory.getAvailable() - quantity); inventoryRepository.save(inventory); // 显式发布领域事件由监听器处理设备联动 eventPublisher.publish(new StockChangedEvent(skuCode, -quantity, AUTO_DEDUCT, LocalDateTime.now())); }提示eventPublisher使用 Spring 的ApplicationEventPublisher确保事件在事务提交后才被消费避免设备指令发出但数据库回滚导致状态不一致。2.2 设备通信层用 Spring Integration 实现 MQTT 与 HTTP 设备协议的统一抽象无人超市的设备五花八门入口闸机用 HTTP API 上报通行记录货架重量传感器通过 MQTT 发送 JSON 数据电子价签则依赖私有 TCP 协议。若为每种设备写一套 Controller代码将迅速失控。Spring Integration 提供了消息驱动的统一管道!-- pom.xml -- dependency groupIdorg.springframework.integration/groupId artifactIdspring-integration-mqtt/artifactId /dependency dependency groupIdorg.springframework.integration/groupId artifactIdspring-integration-http/artifactId /dependency配置 MQTT 入站通道接收货架传感器数据Configuration EnableIntegration public class DeviceIntegrationConfig { Bean public MqttPahoClientFactory mqttClientFactory() { DefaultMqttPahoClientFactory factory new DefaultMqttPahoClientFactory(); factory.setUserName(device); factory.setPassword(password.getBytes()); return factory; } Bean public MessageChannel sensorInputChannel() { return MessageChannels.direct().get(); } Bean public IntegrationFlow mqttInboundFlow(MqttPahoClientFactory clientFactory) { return IntegrationFlow.from( Mqtt.messageDrivenChannelAdapter(spec - spec .clientFactory(clientFactory) .connectionFactory(mqttConnectionFactory()) .topic(sensor/#) .qos(1) ) ) .transform(Transformers.fromJson(WeightSensorData.class)) // 将 JSON 转为 POJO .filter(MessageBuilder::getPayload, p - p.getWeight() 0.1) // 过滤无效数据 .channel(c - c.channel(sensorInputChannel())) .get(); } }WeightSensorData是设备原始数据的规范映射public class WeightSensorData { private String shelfId; // 货架编号 private String skuCode; // 商品编码 private double weight; // 当前重量kg private long timestamp; // 时间戳毫秒 // getter/setter... }注意Transformers.fromJson()要求类必须有无参构造器且字段名与 JSON key 完全一致。若设备厂商使用下划线命名如shelf_id需在JsonAlias中声明而非修改数据库字段名——领域模型应保持语义清晰协议适配由转换层承担。2.3 订单创建的幂等性保障Token Redis 分布式锁的双重校验自助结算台可能因网络抖动重复提交同一笔订单。单纯在数据库加唯一索引如order_no只能防止数据重复但无法阻止下游服务如库存扣减、消息推送被多次执行。必须在应用层拦截Service public class OrderService { Resource private RedisTemplateString, String redisTemplate; Transactional public Order createOrder(CreateOrderRequest request) { // Step 1: 校验防重 Token前端每次结算生成新 token提交后失效 String tokenKey order:token: request.getClientToken(); Boolean exists redisTemplate.hasKey(tokenKey); if (!Boolean.TRUE.equals(exists)) { throw new InvalidTokenException(Token 已使用或过期); } redisTemplate.delete(tokenKey); // 立即删除确保单次有效 // Step 2: 对订单号加分布式锁防止并发创建同单号订单 String lockKey order:lock: request.getOrderNo(); Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, LOCKED, Duration.ofSeconds(30)); if (!Boolean.TRUE.equals(locked)) { throw new OrderCreationConflictException(订单创建冲突请重试); } try { // 执行实际创建逻辑含库存校验、扣减 return doCreateOrder(request); } finally { redisTemplate.delete(lockKey); } } }CreateOrderRequest必须包含两个字段clientToken由前端生成的 UUID每次结算页面加载时刷新orderNo服务端生成的全局唯一订单号如ORD20240520142300123用于锁粒度控制。提示Redis 锁的过期时间30 秒必须大于订单创建的最大耗时否则可能在扣减库存中途锁失效导致并发问题。可通过Scheduled定时任务扫描超时未完成订单并回滚作为兜底。3. 关键配置与生产就绪实践Actuator、多环境 Profile 与安全加固无人超市系统一旦上线就不能再靠System.out.println()查问题。SpringBoot Actuator 是生产环境的“听诊器”但默认暴露的端点存在安全风险必须精细化配置。3.1 Actuator 端点的最小化暴露策略在application-prod.yml中仅开放必需端点并启用认证management: endpoints: web: exposure: include: health,info,metrics,threaddump,prometheus # 严格限定 base-path: /actuator endpoint: health: show-details: when_authorized # 详情需授权 probes: enabled: true metrics: tags: application: unattended-supermarket info: git: mode: full server: servlet: context-path: /api # 所有业务接口前缀同时配置 Spring Security 限制 Actuator 访问Configuration RequiredArgsConstructor public class ActuatorSecurityConfig { private final JwtAuthenticationFilter jwtFilter; Bean public SecurityFilterChain actuatorSecurityFilterChain(HttpSecurity http) throws Exception { http .requestMatcher(EndpointRequest.toAnyEndpoint()) // 仅匹配 /actuator/** .authorizeHttpRequests(authz - authz .requestMatchers(EndpointRequest.to(health, info)).permitAll() .requestMatchers(EndpointRequest.toAnyEndpoint()).hasRole(ADMIN) ) .httpBasic(Customizer.withDefaults()); // 启用 Basic Auth return http.build(); } }3.2 多环境 Profile 的实战配置分离无人超市系统需对接不同供应商的设备测试环境用模拟 MQTT Broker预发环境连真实但隔离的设备网关生产环境则必须启用 TLS 加密。SpringBoot 的 Profile 机制可精准控制# 启动命令指定 profile java -jar supermarket.jar --spring.profiles.activeprod,device-mqtt-tls对应配置文件application-dev.ymlH2 内存数据库Mock 设备服务application-prod.ymlPostgreSQL 连接池参数调优maxPoolSize: 20,minIdle: 5application-device-mqtt-tls.yml覆盖 MQTT 配置启用 SSL# application-device-mqtt-tls.yml spring: integration: mqtt: connection: ssl: enabled: true trust-store: classpath:mqtt-truststore.jks trust-store-password: changeit注意mqtt-truststore.jks文件必须打包进 JAR 的src/main/resources目录而非放在服务器任意路径——这保证了配置与代码的原子性部署避免因运维疏忽导致证书路径错误。3.3 数据库连接池的深度调优HikariCP 的 5 个必调参数无人超市的订单峰值集中在早 7-9 点和晚 6-8 点此时库存查询 QPS 可达 2000。HikariCP 默认配置maximumPoolSize10会成为瓶颈。根据 PostgreSQL 官方建议和实测关键参数如下表参数生产推荐值说明maximumPoolSize2 * (core_count 1)例如 8 核服务器设为 18避免线程争抢connection-timeout3000030 秒防止慢 SQL 拖垮整个连接池idle-timeout60000010 分钟清理长期空闲连接释放 DB 资源max-lifetime180000030 分钟强制连接重建规避 DB 连接老化leak-detection-threshold6000060 秒检测连接泄漏开发环境设为 10 秒在application-prod.yml中配置spring: datasource: hikari: maximum-pool-size: 18 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 leak-detection-threshold: 60000 validation-timeout: 3000 connection-test-query: SELECT 1提示validation-timeout必须小于connection-timeout否则连接校验失败时会阻塞获取连接。connection-test-query在 PostgreSQL 中必须用SELECT 1而非 MySQL 的SELECT 1虽语法相同但驱动行为有差异。4. 真实场景排错库存扣减失败、设备指令丢失、订单状态不一致的三大高频问题在无人超市的灰度上线阶段最常遇到的不是功能缺失而是状态漂移用户看到“支付成功”但库存未扣减或货架传感器上报缺货但后台库存数仍为正。这些问题根源不在代码 Bug而在分布式系统固有的不确定性。以下给出可立即执行的诊断路径。4.1 库存扣减失败如何定位是数据库锁表还是事务传播失效现象调用/api/orders创建订单返回 200但数据库inventory表中对应商品available字段未减少。第一步确认事务是否生效在OrderService.createOrder()方法上添加Transactional注解后检查其所在类是否被 Spring 管理即是否用Service标记。若该类是new OrderService()手动创建则事务失效。验证方式在方法内抛出RuntimeException观察数据库是否回滚。第二步检查数据库锁等待登录 PostgreSQL执行SELECT pid, usename, query, state, wait_event_type, wait_event FROM pg_stat_activity WHERE state active AND query LIKE %UPDATE inventory%;若发现wait_event_type Lock且wait_event transactionid说明存在行锁等待。此时需查阻塞源SELECT blocked_locks.pid AS blocked_pid, blocking_locks.pid AS blocking_pid, blocked_activity.usename AS blocked_user, blocking_activity.usename AS blocking_user, blocked_activity.query AS blocked_statement, blocking_activity.query AS blocking_statement FROM pg_catalog.pg_locks blocked_locks JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid blocked_locks.pid JOIN pg_catalog.pg_locks blocking_locks ON blocking_locks.locktype blocked_locks.locktype AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid AND blocking_locks.pid ! blocked_activity.pid JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid blocking_locks.pid WHERE NOT blocked_locks.granted;第三步验证事务传播行为若InventoryService.deductStock()被OrderService调用且两者均为Service则默认REQUIRED传播行为应保证同一事务。但若deductStock()方法被标记为Transactional(propagation Propagation.REQUIRES_NEW)则会开启新事务导致外层事务回滚时库存扣减无法回滚。检查所有Transactional注解的propagation属性确保未意外设置为REQUIRES_NEW。4.2 设备指令丢失MQTT QoS 与 Spring Integration 消息确认的对齐现象调用/api/devices/price/update更新电子价签价格API 返回 200但价签屏幕未变化。根本原因MQTT 协议的 QoS 级别与 Spring Integration 的消息确认机制未对齐。QoS 0最多一次可能丢消息QoS 1至少一次可能重复QoS 2恰好一次开销大。生产环境应选 QoS 1并确保消费者端正确 ACK。在application.yml中强制指定 QoSspring: integration: mqtt: outbound: qos: 1 # 必须显式设置不能依赖 broker 默认值同时在 MQTT 出站通道中启用手动确认Bean public IntegrationFlow mqttOutboundFlow(MqttPahoClientFactory clientFactory) { return IntegrationFlow.from(priceUpdateChannel) .handle(Mqtt.outboundAdapter(spec - spec .clientFactory(clientFactory) .qos(1) // 再次确认 .topic(price/update) .encoder((payload, headers) - { // 将 PriceUpdateCommand 转为 JSON 字节 return new String(payload.toString().getBytes(StandardCharsets.UTF_8)); }) )) .get(); }注意PriceUpdateCommand的序列化必须稳定如用 Jackson 的JsonInclude(NON_NULL)避免空字段否则同一指令因序列化差异被 broker 视为不同消息导致重复下发。4.3 订单状态不一致如何用 Saga 模式修复跨服务状态分裂现象订单表status PAID但库存表available未扣减且无任何错误日志。这是典型的分布式事务问题。SpringBoot 本身不提供两阶段提交2PC强行用 Seata 等方案会增加复杂度。更务实的做法是 Saga 模式将订单创建拆分为可补偿的步骤并引入状态机追踪。定义订单状态机public enum OrderStatus { CREATED, // 已创建待支付 PAID, // 支付成功待扣库存 STOCK_LOCKED,// 库存已锁定待发货 SHIPPED, // 已发货 CANCELLED // 已取消 }在OrderService中实现补偿逻辑Transactional public void confirmPayment(String orderNo) { Order order orderRepository.findByOrderNo(orderNo); if (!order.getStatus().equals(OrderStatus.CREATED)) { throw new IllegalStateException(Order not in CREATED state); } // Step 1: 更新订单状态 order.setStatus(OrderStatus.PAID); orderRepository.save(order); // Step 2: 异步触发库存扣减带重试 try { inventoryService.deductStockAsync(order.getItems()); } catch (Exception e) { // Step 3: 若扣减失败启动补偿将订单置为 CANCELLED compensateOrderFailure(orderNo, e); } } private void compensateOrderFailure(String orderNo, Exception cause) { Order order orderRepository.findByOrderNo(orderNo); order.setStatus(OrderStatus.CANCELLED); order.setCancelReason(Stock deduction failed: cause.getMessage()); orderRepository.save(order); // 发送补偿通知如短信告知用户 notificationService.sendCancelNotice(order.getCustomerId(), orderNo); }关键点deductStockAsync()必须是异步且带重试的例如用AsyncRetryableAsync Retryable( value {StockNotEnoughException.class}, maxAttempts 3, backoff Backoff(delay 1000, multiplier 2) ) public void deductStockAsync(ListOrderItem items) { for (OrderItem item : items) { inventoryService.deductStock(item.getSkuCode(), item.getQuantity()); } }提示Async方法必须在独立的 Service 类中且调用方不能是同一类的 this 引用否则代理失效。补偿逻辑必须幂等——多次调用compensateOrderFailure()不能产生副作用。5. 进阶技巧用 Spring Boot Admin 实现无人值守环境下的可视化运维当超市遍布城市各处运维人员不可能逐台登录服务器看日志。Spring Boot Admin 是专为 SpringBoot 应用设计的集中式监控平台它能将分散的 Actuator 端点聚合为可视化仪表盘并在异常时自动告警。5.1 快速集成 Admin Server 与 ClientAdmin Server 是独立应用Client 是被监控的超市系统。在pom.xml中添加依赖!-- Admin Server 项目 -- dependency groupIdde.codecentric/groupId artifactIdspring-boot-admin-starter-server/artifactId version3.2.3/version /dependency启动类添加注解SpringBootApplication EnableAdminServer public class AdminApplication { public static void main(String[] args) { SpringApplication.run(AdminApplication.class, args); } }在无人超市系统Client中配置# application.yml spring: boot: admin: client: url: http://admin-server:8080 # Admin Server 地址 username: admin password: admin123 application: name: unattended-supermarket # 服务名将显示在 Admin UI5.2 自定义健康检查将设备在线状态纳入 Health Indicator默认的HealthIndicator只检查数据库、Redis 连接。无人超市必须监控设备网关是否存活Component public class DeviceGatewayHealthIndicator implements HealthIndicator { private final DeviceGatewayClient gatewayClient; public DeviceGatewayHealthIndicator(DeviceGatewayClient gatewayClient) { this.gatewayClient gatewayClient; } Override public Health health() { try { // 调用设备网关的健康检查接口 ResponseEntityString response gatewayClient.healthCheck(); if (response.getStatusCode().is2xxSuccessful()) { return Health.up() .withDetail(gateway_status, UP) .withDetail(last_check, Instant.now().toString()) .build(); } else { return Health.down() .withDetail(gateway_status, DOWN) .withDetail(http_status, response.getStatusCode().value()) .build(); } } catch (Exception e) { return Health.down(e) .withDetail(gateway_status, UNREACHABLE) .build(); } } }重启超市系统后在 Admin UI 的 Health 页面即可看到deviceGateway指标且支持邮件告警配置。5.3 日志聚合技巧用 Logback 的 SiftingAppender 按设备类型分离日志无人超市的日志量巨大若所有设备日志混在一起排查特定货架问题效率极低。Logback 的SiftingAppender可根据 MDCMapped Diagnostic Context动态创建日志文件!-- logback-spring.xml -- appender nameDEVICE_SIFT classch.qos.logback.core.sift.SiftingAppender discriminator keydeviceType/key defaultValueunknown/defaultValue /discriminator sift appender nameDEVICE_${deviceType} classch.qos.logback.core.rolling.RollingFileAppender filelogs/device/${deviceType}.log/file rollingPolicy classch.qos.logback.core.rolling.TimeBasedRollingPolicy fileNamePatternlogs/device/${deviceType}.%d{yyyy-MM-dd}.%i.log/fileNamePattern timeBasedFileNamingAndTriggeringPolicy classch.qos.logback.core.rolling.SizeAndTimeBasedFNATP maxFileSize100MB/maxFileSize /timeBasedFileNamingAndTriggeringPolicy /rollingPolicy encoder pattern%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n/pattern /encoder /appender /sift /appender root levelINFO appender-ref refDEVICE_SIFT/ /root在设备通信层注入 MDCService public class MqttDeviceHandler { public void handleWeightData(WeightSensorData data) { // 将设备类型放入 MDC MDC.put(deviceType, weight_sensor); try { log.info(Received weight data from shelf {}, sku {}, data.getShelfId(), data.getSkuCode()); // ... 处理逻辑 } finally { MDC.clear(); // 必须清除避免污染后续请求 } } }这样所有重量传感器日志将自动写入logs/device/weight_sensor.log而价签日志写入logs/device/price_tag.log运维人员可直接tail -f logs/device/weight_sensor.log聚焦问题。提示MDC.clear()必须放在finally块中否则线程复用时会携带上一次的 MDC 值导致日志错乱。SpringBoot 的 WebMvcConfigurer 可全局配置MDC清理但设备通信层是独立线程必须手动清理。本文还有配套的精品资源点击获取
返回列表