1. 无人智慧超市不是“超市装摄像头”系统的整体目标与能力边界无人智慧超市管理系统源码听起来像是新零售宣传片里的概念但真把一个无人店从图纸变成可运行的软件系统你需要的不是一块大屏、不是几个摄像头而是一整套覆盖商品、订单、会员、库存、设备监控的完整业务闭环。我最近基于SpringBootVueMyBatisMySQL这套技术栈完成了一套企业级完整版源码其中既包括顾客扫码进店、自动结算、门禁放行的前台链路也包括运营端商品管理、报表统计、异常单处理的后台能力。这里我把整个系统的设计过程、表结构、接口链路和上线踩坑一起复盘一遍希望能帮到正在做同类型项目或者准备拿这份源码做二次开发的团队。先说清楚边界这个系统不是单纯做一个“收银台换刷脸”的Demo。企业级无人智慧超市管理系统强调“管理”二字。这意味着它至少要覆盖顾客进店识别、商品实时跟踪、离店自动结算、库存与供应链联动、会员营销、门店设备监控、防损告警、运营数据看板。如果只把收银功能做出来那不叫管理系统叫自助收银机。我一开始就按业务域拆模块而不是从单一功能往外凑否则代码写一半就会互相打架。开篇先把目标定清楚后面所有设计才好展开。1.1 核心需求盘点识别、结算、防损三件事决定系统形态无人超市最容易被低估的是结算环节。传统超市在收银台由收银员完成商品计价、收款、小票而无人超市要把这套动作拆成“进店识别谁→拿了什么→出门该扣多少钱→有没有漏单被盗”。商品识别可以用RFID、视觉识别、重力感应也可以让顾客自己扫码确认方案不同代码层抽象也不同。我建议第一版先把“人工扫码确认设备辅助识别”作为主链路把视觉和RFID做成可切换的Provider接口。这样系统骨架稳定后续接硬件替换识别逻辑不至于推倒重来。防损也是企业级系统绕不开的点。天真地以为“无人”就是没人管会把异常订单和货损全部压到对账阶段运营成本反而更高。这套源码里我设计了异常单状态顾客已离店但订单未支付、出门闸机前拦截、商品数量与库存不一致、设备离线超过阈值这些都要进告警中心让远程运营人员介入。防损不是冷冰冰的监控而是把“可能出错”的环节用状态机标出来让系统在任何一环出错时都有据可查、有人可找。这个认知直接影响后面的数据库设计和接口状态机。1.2 模块划分一套系统里其实住着五个子系统如果只有一个后端工程后期会很难维护。我在这套源码里按业务域拆成几个独立模块但共用一个SpringBoot应用进程通过包结构区分goods商品与SKU、inventory库存与出入库、order订单与支付、member会员与营销、device门禁与设备。每个域内部有controller、service、mapper三层跨域调用通过service接口不直接操作对方数据表。比如订单生成时要扣库存订单服务调用inventoryService的接口而不是在订单Mapper里写一条改库存的SQL。这样做的最大好处是后续库存量级大了想抽成独立服务边界是现成的。管理端和顾客端则是两套Vue前端。顾客端运行在店内自助大屏或顾客手机上面对的是C端体验要求操作路径短、反馈明确管理端跑在运营人员电脑上面对的是数据处理表格、筛选、批量操作是重点。两套前端共用同一套后端接口但鉴权方式不一样顾客端用临时token绑定顾客身份管理端用账号密码登录后拿JWT配合Spring Security做接口权限控制。这个拆分不是技术洁癖而是两端用户诉求差异太大硬塞在同一个前端工程里会让代码和样式都变得难以维护。1.3 “企业级完整版”源码意味着什么交付物不止是能跑的Demo完整版源码对我而言至少包含四样东西后端Java工程、前端Vue工程、MySQL初始化SQL脚本、部署文档。很多项目源码只给代码不给数据库脚本导致新同事用Navicat建表建到怀疑人生。这套项目里我把建库、建表、初始化门店数据、创建管理员账号都整理成init.sql下载下来按顺序执行再改配置文件的数据库连接串就能启动。前端也需要配置文件区分开发环境和生产环境API地址避免本地联调时到处改代理。另外所谓企业级还要考虑日志、统一返回体、全局异常处理、分页查询、参数校验这些基础设施。这些不是业务功能但决定系统能不能交付给真实客户。我在后面会专门展开讲这些非功能性设计的取舍这里先记住一个结论企业级源码和学校Demo的本质区别不是功能炫不炫而是异常发生时系统有没有兜底数据能不能追溯部署时有没有完整交付物。2. 技术选型复盘SpringBootVueMyBatisMySQL这套组合怎么就把项目撑起来了选型不一定要追逐热点但必须能解释清楚为什么用这套组合它的边界在哪。SpringBoot Vue MyBatis MySQL是中小型企业级管理系统里非常成熟的组合社区资料多招人成本低出问题排查快。对无人超市这种业务来说既没有超高并发秒杀场景也没有海量数据仓库需求用这套组合完全撑得起来真正需要动脑的是业务复杂度和状态管理。下面从四个角度复盘选型逻辑。2.1 后端为什么选SpringBoot而不是Spring Cloud或手写ServletSpringBoot的价值在于“自动配置”和“约定优于配置”。相比传统SSH需要写大量XML配置SpringBoot通过起步依赖和application.yml就能把Web、MySQL连接、MyBatis、Redis等组件一次性装配好。我在这套项目里用的JDK版本是17SpringBoot 2.7.x稳定且兼容性广。有人可能觉得现在都Spring Boot 3.x了为什么还用2.7原因很现实很多企业内部还在用JDK8/11MyBatis、Spring Security等配套版本在3.x上要有迁移成本我不希望读者拿到的源码环境要求过高否则光环境版本就能劝退一半人。不选Spring Cloud理由也简单无人超市门店级系统单体应用足够不需要微服务带来的注册中心、配置中心、网关、链路追踪等一大套基建。如果硬上一套微服务反而会把简单事务拆成分布式事务把单表查询拆成Feign调用性能和开发效率都下降。我见过太多团队为了简历上的“微服务经验”把一个小项目拆成八个服务最后连本地联调都费劲。等到模式验证成功需要支持几十个门店时再按业务域拆服务不迟。我在代码里刻意保证了各业务域基于service接口交互就是为将来拆分留了口子。2.2 Vue在前端交互里的价值响应式、组件化、生态成熟Vue在管理后台领域几乎是“标配套件”。这套源码里用了Vue3 Element Plus。Vue3的Composition API可以按功能逻辑组织代码比如购物车模块的“获取购物车列表、加入商品、修改数量、清空购物车”放在一个useCart函数里比Options API的data/methods分散式写法更直观。Element Plus提供了现成的表格、表单、弹窗、树形控件整套管理端80%的界面不需要自己手写样式。对无人超市这种重交互、多状态的场景组件化带来的收益是实打实的。顾客端大屏场景下Vue的响应式能力让购物车数量变化、结算金额跳动非常顺滑。设备上报状态通过WebSocket推给前端Vue组件里监听到达数据后更新视图。这一点用jQuery也能做但组件化和状态管理的优势在业务逻辑复杂后会越来越明显。前端不是单纯画页面而是要处理好与后端接口的状态同步这个我在第五章会详细说。这里先提一句如果你只会写静态页面这套源码读起来会有点费劲建议先补一下Vue的生命周期和响应式原理。2.3 MyBatis与MySQL为什么坚持写SQL而不是全用JPAMyBatis的优势是SQL可控、灵活、易优化。无人超市的业务报表涉及多张表联查比如“门店销售日报 订单表 订单明细表 商品表 门店表”这类统计SQL用MyBatis的XML写可以随时调整GROUP BY、JOIN、子查询。如果全用JPA自动生成的查询复杂统计要么拼得很别扭要么还得走原生SQL。我在项目里对简单CRUD用了MyBatis-Plus极大减少单表Mapper模板代码复杂统计自己写XML。这样既保证开发速度又不失去SQL控制力。MySQL选InnoDB核心订单、库存表必须事务支持。MyBatis的Mapper接口和XML映射文件分开查询结果用resultMap映射到POJO遇到字段命名不一致时很灵活。这里有个实操细节大字段比如商品详情图片URL不要用select *默认查出来列表查询里应该做字段裁剪只返回列表需要的字段详情接口再查完整字段。这个优化能明显减少数据库IO和网络传输尤其是商品数量上万之后select *的浪费会被放大。2.4 这套技术栈的企业级短板集群、缓存、消息队列怎么补没有技术栈是万能的。这套组合的短板在于单机MySQL性能上限、应用层无内置分布式协调、支付回调这类异步场景要靠自己设计。应对方式不是推翻选型而是加配套组件。我在这套代码里引入了Redis做缓存和分布式锁用Spring的事件机制做订单支付成功后的异步处理数据库连接池用HikariCPSpringBoot默认足够支撑单店或中等规模连锁的初期阶段。如果未来门店数快速增长需要横向扩容可以平滑引入Nacos做注册中心、Redis Cluster做缓存集群、RocketMQ或RabbitMQ做异步消息。因为现在代码的service接口边界清晰从单体升级到微服务不算推翻重来而是一步一步把某条调用链路换成RPC。这也是我把“边界清晰”看得比“某个框架酷”更重要的原因。很多团队只想着第一步就用最重的架构结果业务还没跑通先被基础设施拖垮这是我在多个项目里看到的通病。3. 数据库设计从商品表到设备表业务实体怎么落进MySQL无人超市的数据库设计核心不是字段多而是每一张表都对应真实业务锚点。我设计表的时候一直问自己这条记录从哪来到哪去如果用户质疑数据怎么自圆其说。最终MySQL里一共14张核心业务表按域划分可以分成五类商品库存类、订单支付类、会员营销类、门店设备类、报表聚合类。下面把每类表的核心字段和设计思路讲清楚。3.1 商品、SKU、库存三张核心表商品和SKU分离是必须的。同一款饮料500ml和1L是两个SKU价格、条码、库存可能都不同。product表存商品基本信息名称、品牌、分类、图片、状态sku表存具体售卖单元条码、规格、售价、成本价、预警库存。库存表inventory按门店SKU维度记录当前库存、锁定库存、安全库存。这三张表一起支撑了“商品详情页展示、加购校验、库存扣减、补货提示”的完整链路。字段设计上sku表的barcode需要唯一索引因为扫码进店和自助结算都依赖它。product表的状态字段product_status用tinyint0下架1上架不用varchar存“上架/下架”中文因为枚举数值更节省空间也方便索引。价格字段用DECIMAL(10,2)禁止用float/double否则金额精度会在累计计算时出问题。图片URL存相对路径而不是完整域名方便后续迁移到对象存储。这里给出核心建表SQL片段方便你对照CREATE TABLE sku ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, product_id bigint NOT NULL COMMENT 商品ID, sku_code varchar(64) NOT NULL COMMENT SKU编码, barcode varchar(64) NOT NULL COMMENT 条码, spec varchar(128) DEFAULT NULL COMMENT 规格, price decimal(10,2) NOT NULL COMMENT 售价, cost_price decimal(10,2) NOT NULL COMMENT 成本价, status tinyint NOT NULL DEFAULT 1 COMMENT 状态 1上架 0下架, PRIMARY KEY (id), UNIQUE KEY uk_barcode (barcode), KEY idx_product_id (product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENTSKU表;3.2 订单表与支付流水表退款、对账、幂等怎么办订单表是整个系统的核心。order表我故意不把所有业务字段塞进去而是拆成订单主表order_info和订单明细表order_item。主表记录订单号、顾客ID、门店ID、订单状态、应付金额、实付金额、支付方式、创建时间、支付时间、离店时间。明细表记录每个SKU的数量、单价、小计、快照名称和快照图片。为什么要有快照因为商品价格可能改动订单必须保留下单那一刻的原始信息后续售后对账才能依据订单快照而不是翻当前商品表。这个设计在零售系统里几乎是标配。支付流水表单独建记录每一次支付请求和回调结果核心字段是payment_transaction_no、order_no、pay_channel、notify_status、amount、callback_time。支付回调必须做幂等也就是同一笔支付通知无论来几次系统都只处理一次状态流转。我采用的方式是给payment_transaction_no建唯一索引回调先插入流水重复则直接返回成功避免重复更新订单状态。这属于我在多个支付项目里积累的通用经验第一次写代码的人很容易漏掉。3.3 会员、积分、优惠券、门店与设备表会员域包含member表、member_points_log表、coupon表和member_coupon表。会员表以手机号为唯一登录标识关联openId或人脸特征ID用于进店识别。积分流水表和优惠券领取表都是流水型业务数据会持续增长设计时就要想好按会员ID建索引分页查询必须带时间范围不然运营后台查“半年内积分变化”会越查越慢。我在积分流水表上直接做了“会员ID创建时间”的联合索引并限制查询必须传时间范围从接口层就避免全表扫描。门店和设备的模型很关键。一个公司可能有多个无人超市门店每个门店有若干门禁设备、摄像头、结算终端。store表记录门店编号、名称、地址、营业时间、状态device表记录设备编号、门店ID、设备类型、在线状态、最后心跳时间。设备在线状态不能靠数据库轮询硬刷我设计了心跳上报接口设备每隔30秒上报一次后端更新last_heartbeat_time运营看板展示“离线N分钟”。设备表还有一个config_json字段存设备自定义配置避免业务扩展时频繁加字段。3.4 索引与唯一键设计哪些字段建索引哪些不能乱建建索引不是越多越好。我在order_info表上建了order_no唯一索引、member_id普通索引、store_id普通索引、create_time普通索引。为什么create_time建索引因为日订单报表、周订单趋势是最常见查询。为什么不在order_status上建索引因为状态值只有几个可选值区分度太低索引基本没用反而拖慢写入。这是一个非常经典的索引设计教训很多初级开发者喜欢给所有字段加索引结果写性能和存储空间双重受损。表关联字段的外键我没有加物理外键约束只建了普通索引。理由物理外键在删除和更新时强制级联容易误操作而且大表上外键检查有性能开销更重要的是业务一致性由应用层事务保证数据库只做存储和查询。库存表inventory上要建联合唯一索引(store_id, sku_id)从根上避免同一门店同一SKU存在多行库存记录。下面是几张关键表的索引汇总表名索引类型索引字段用途sku唯一索引barcode扫码识别SKUinventory联合唯一索引store_id, sku_id保证门店SKU唯一order_info唯一索引order_no订单号查询、去重order_info普通索引create_time时间范围报表payment_info唯一索引payment_transaction_no支付回调解幂等device普通索引store_id, device_type门店设备列表4. 后端功能链路拆解一次“扫码进店-选购-自动扣款-出店”的完整请求流如果说数据库设计是骨架那后端接口链路就是血管。这里我完整拆解一条顾客从进店到离店的请求链路这是无人超市业务里最容易写乱的部分因为每一步都要和硬件、状态机打交道。你把这个链路读明白后面读源码会轻松很多。4.1 进店门禁识别二维码、人脸与硬件网关怎么对接顾客进店的流程是顾客在入口扫码或刷脸门禁设备向后端发起识别请求后端确认顾客身份后返回一个临时进店凭证entry_token门禁放行同时系统创建一条进店记录。这里的关键不是校验手机号而要处理“门禁设备回调和后端响应之间的超时”问题。顾客已经在门口站着系统不能让他等三秒否则体验极差。我用的方案是先快速校验身份生成凭证不等全部流程完成就返回异步再补齐进店记录。我在项目中设计了一个DeviceGateway服务把门禁设备的HTTP回调统一封装。设备请求后端/device/entry接口带上设备编号、顾客标识、时间戳和签名后端校验设备合法性然后查会员状态如果账号异常或黑名单直接返回禁止入内。门禁放行是独立事件顾客真正进门后设备再回调/device/entry/confirm后端这时才正式把顾客session置为“在店”。先给凭证再确认进门避免网络抖动造成门开了但系统以为人没进来这个异步确认机制在真实硬件对接中很关键。4.2 实时购物车RFID、视觉识别与手动确认的取舍顾客在店里拿了商品系统怎么知道现实中有三种方案RFID标签每件商品贴在包装上出门天线批量读取优点是速度快缺点是标签成本和漏读风险视觉识别用摄像头配合模型判断顾客拿了什么用户体验好但前期开发和算力投入大手动扫码确认最朴素顾客用手机或大屏扫商品条码加入购物车胜在落地快。这套源码的默认实现是“手动扫码RFID可选”因为第一版跑通业务比炫技重要先保证能结算再谈体验升级。购物车数据放Redis还是MySQL我选择Redis。理由很直接顾客在店时间半小时内购物车是高频读写、临时性极强的数据放MySQL频繁update不仅慢还会把订单表的热点放大。Redis里以cart:{memberId}:{storeId}为key用Hash结构存skuId到数量的映射顾客每扫描一件商品就调用一次加购接口。后端同时把购物车内容同步推送给顾客手机端和门店自助大屏。因为Redis没有持久化事务离店结算前会有一道“购物车快照校验”对齐最终订单避免用户说没拿某商品但系统结算了。4.3 订单生成与支付回调状态机的边界在哪里顾客点击“结算并离店”后后端要做一串动作从Redis读取购物车校验商品上下架状态和库存生成订单主表和明细表冻结锁定库存然后发起支付。订单状态我用一组常量表达状态流转必须按顺序走不允许跳变。为什么这样设计因为无人超市的结算链路涉及多个外部系统状态字段是系统间沟通的“共同语言”乱跳会让对账、退款、报表全部出错。订单主状态如下状态码含义可流转目标CREATED订单已创建LOCKEDLOCKED库存已冻结PAID, ABNORMALPAID已支付COMPLETED, REFUNDINGCOMPLETED已完成REFUNDINGREFUNDING退款中REFUNDEDREFUNDED已退款终态ABNORMAL异常终态支付模块对接的是模拟支付沙箱但代码结构和真实渠道一致。后端生成预支付单调支付通道下单接口返回支付二维码顾客手机或大屏扫码支付支付通道回调后端/pay/notify接口后端校验签名、金额、订单状态然后才把订单置为PAID并扣减库存。回调可能迟到、重复、乱序所以状态流转里必须判断当前订单状态只有LOCKED或CREATED时接受支付成功否则直接忽略并返回成功应答避免重复处理。4.4 库存扣减与超卖防护乐观锁和事务边界高并发下单时库存超卖是这个项目最容易出的恶性bug。我在代码里用了两种手段叠加数据库层面扣库存SQL写成原子更新条件里加stock #{num}当库存不足时影响行数为0上层拿到0就知道要抛库存不足异常。应用层面再对库存版本号做乐观锁校验防止业务里先查后判可能出现的并发问题。这两层防护缺一不可数据库原子操作保证最终一致性应用层乐观锁提供更友好的业务提示。事务边界也要小心。订单生成和库存扣减不能放在一个长事务里因为整个链路里还要调支付接口调用外部接口不能占着数据库连接等响应。我把事务拆成“订单创建冻结库存”一个事务支付成功回调再开新事务执行“扣减库存更新订单状态”。冻结库存和扣减库存分开的好处是如果用户支付超时取消订单释放冻结库存就行不需要动已售库存对账简单。这个“冻结-扣减”两段式设计在零售和电商系统里非常常见。4.5 异常单、防损报警与人工介入系统必须接受一个现实一定会有顾客扫码漏了商品、或者带着未结算商品直接出门。设计上离店闸机接收到RFID或视觉复核结果后如果发现购物车与实付不一致后端生成一条异常单记录同时推送告警给运营人员。异常单不是简单标记“偷盗”而是一套可跟踪的工单记录门店、设备、顾客、时间、差异明细、处理结果。没有这套机制无人超市的仓库盘点误差根本管不住。运营后台的“异常单管理”是整个系统里最容易被人忽略但也最体现企业级价值的功能。我实现了异常单列表、详情回溯、人工关闭三种操作运营能手动把一笔异常单改为“已补款”或“货损报损”报损后自动生成库存调整记录。这样财务和供应链才能接得住。技术系统只是把管理规则固化成软件流程规则没定系统再完善也救不了业务这句话在无人超市项目里尤其适用。5. Vue前端与后台管理小程序/大屏/PC管理端的功能拆分和状态管理前端部分占比不低很多后端开发者容易低估这里的工作量。无人超市有典型的三个前端场景顾客进店时的手机端或店内大屏、收银结算自助终端、运营管理PC端。我在这套源码里做了两个Vue工程把顾客端和管理端分开部署避免两套完全不同交互逻辑的页面堆在同一个工程里互相污染。5.1 顾客端进店、购物车、结算界面怎么设计顾客端的第一屏是“扫码或刷脸进店”引导。技术实现上手机端可以用手机号验证码或微信授权店内大屏则显示一个二维码顾客用手机扫一下后端换出entry_token。界面设计的原则是步骤少、反馈快不需要注册流程。进店后购物车页面是大字号展示商品名称、数量、小计加减按钮要足够大因为可能在触屏上用。这里的交互细节是加购操作要立即反馈不能等整个接口返回才更新界面我用了乐观更新策略先更新视图接口失败再回滚提示。结算界面要展示优惠明细商品原价、会员折扣、优惠券抵扣、应收金额、实付金额。这里前端不做金额计算所有金额以购物车接口返回的settlementVO为准前端只负责展示。因为任何前端算钱的操作都会在促销叠加、四舍五入上出问题。生成支付二维码后前端进入轮询订单状态接口支付成功后跳转“已支付欢迎下次光临”并调用门禁放行接口让闸机开门。支付结果页同时展示“本次购物省了XX元”这是运营侧很看重的会员价值感知。5.2 管理端商品、库存、订单、会员、营销、设备六个主菜单管理端的菜单结构我按运营人员的使用习惯排列Dashboard工作台、商品管理、库存管理、订单管理、会员管理、营销管理、设备管理、异常单管理。商品管理核心是SKU列表和上下架操作表格带批量导入导出库存管理用库存台账页展示当前库存、锁定库存、预警状态支持手工调整库存并记录调整原因。订单管理包含订单列表、详情、退款操作列表默认按创建时间倒序支持订单号、手机号、门店筛选。这几个页面是运营每天打开次数最多的交互必须顺手。营销管理用于配置会员折扣和优惠券这是无人超市能否从“尝鲜”走向“复购”的关键模块。我在后端设计了营销规则的策略接口比如“满减券”“折扣券”“会员专享价”前端管理端可以创建、发布、停用券模板顾客端购物车结算时自动匹配可用优惠。设备管理页展示所有门店设备的在线状态、心跳时间、离线告警运维人员能一键重启设备或查看设备日志。管理端界面不需要花哨但信息密度和操作效率一定得够否则运营人员宁可回Excel。5.3 状态管理为什么用了Vuex以及和后端接口的配合两个前端工程都用了Vuex做状态管理尽管Vue3社区更多推荐Pinia但考虑到这套源码要兼容一部分还在用Vue2的团队我统一选Vuex4。为什么需要集中状态管理因为顾客端页面的购物车数据、会员信息、门禁状态在多个组件间共享如果每个组件各自请求接口会出现数据不一致。比如大屏显示购物车而手机端已经结算大屏必须同步刷新不能各查各的。用Vuex把当前会话的基础数据放一份组件只负责读和派发action数据源就单一了。在管理端Vuex主要存登录用户信息、权限路由、全局筛选条件。真正高频变化的订单数据不放到Vuex里而是各自页面组件内用onMounted加载并在操作后主动刷新。这样避免把Vuex当数据库用。请求层我用Axios封装统一加JWT请求头统一处理401跳转登录和500全局错误提示。每个业务模块的API函数单独一个文件比如src/api/order.js里只有订单相关接口后续改接口路径只需动一个文件。这个分层看起来简单但在多人协作时能少吵很多架。6. 上线部署与性能调优从开发机到生产服务器的一次完整推进开发环境能跑通和线上稳定运行是两个世界。我在这套源码里放了一个生产部署文档这里把关键步骤和当时调优的思路写出来。很多人以为部署就是把jar包扔到服务器上跑起来实际上连接池、缓存、时区、Nginx代理这些环节任何一个配置错了系统都能跑但跑不稳。6.1 环境初始化JDK、MySQL、Nginx、打包脚本生产机我是按一台4核8G的云主机标准设计的。系统装JDK17、MySQL8.0、Nginx1.24前端打包成静态文件放在Nginx的html/dist目录下后端通过Spring Boot内嵌Tomcat监听8080端口Nginx做反向代理把/api前缀的请求转发到后端。为什么不用前后端完全分离的跨域方案因为生产环境直接代理更简单浏览器只认识一个域名Cookie和Token管理都方便。用户访问https://example.comNginx根据路径分发到前端静态资源或后端API。部署顺序有讲究先初始化MySQL数据库并导入init.sql再启动后端服务最后更新前端静态文件。倒过来容易看到前端已上线但接口全报错排查方向混乱。后端配置里数据库密码不要硬编码在application.yml用环境变量注入。我习惯写一个deploy.sh把拉代码、打包、备份旧包、重启服务的动作串起来减少手工操作。示例脚本很简洁但很实用#!/bin/bash APP_NAMEsmart-market.jar BACKUP_DIR/data/backup/$(date %Y%m%d%H%M%S) mkdir -p $BACKUP_DIR cp /app/$APP_NAME $BACKUP_DIR/ mv /tmp/$APP_NAME /app/$APP_NAME systemctl restart smart-market echo deploy done at $(date)6.2 数据库连接池、MyBatis二级缓存和Redis缓存取舍HikariCP连接池默认最大连接数是10对单体应用够用但接口并发峰值时可能不够。我先压测观察把maximum-pool-size调到20minimum-idle保持5。连接池不是越大越好每个连接都占用MySQL服务端线程和内存盲目调到100反而拖垮数据库。MyBatis的二级缓存默认没开因为跨Mapper缓存容易读到脏数据尤其在库存这种频繁变化的表上。我明确建议业务数据不要依赖MyBatis二级缓存统一用Redis做缓存层缓存失效策略明确不会出现“明明UPDATE了数据库但页面还是旧数据”的问题。Redis在这个项目里承担三块职责顾客购物车、分布式锁、热点数据缓存门店配置、营销规则、商品基础信息。商品列表接口加了Redis缓存key带上分页参数和查询条件比如goods:list:1:20:keyword失效时间设10分钟。商品信息变化后主动删除对应缓存而不是等它自然过期。这个“删缓存而不是等过期”的细节能避免用户看到很久之前的旧数据也是我在多个项目里强调过无数遍的实践。6.3 接口耗时监控与SQL优化实例上线后我做过一轮接口慢查询梳理最典型的是订单报表接口。运营后台按天查看门店销售数据SQL带了三个表JOIN和GROUP BY刚开始一页数据要3秒多。问题出在三处第一订单明细表没有按order_id建索引导致JOIN走了全表扫第二报表统计在MySQL层直接对全量数据SUM数据量上来后很吃力第三前端每查一次都重新聚合重复计算。这些问题在数据量小时完全感觉不到一旦跑一个月报表页就卡得没法用。修复方案是为order_item.order_id建索引报表查询加门店和日期的索引下推统计口径改成定时任务每五分钟把聚合结果写入一张report_sales_daily表运营看板直接查这张结果表。这个改动把接口耗时从3秒降到200毫秒以内。我看到不少项目败在这种“能跑就行”的报表接口上所以单独提醒及时把高频聚合查询改成预计算是最省钱的性能优化。报表表本身很小查询走索引后毫秒级返回运营体验完全不一样。6.4 高可用和横向扩展的妥协方案门店规模不大时单机部署足够。如果要做简单的高可用我的做法是两台应用服务器Nginx负载均衡MySQL做主从复制Redis用哨兵模式。这套架构是“半高可用”不是真正意义上的多活但对无人超市初期几十个门店的规模是性价比最高的。做集群前必须解决两个问题会话共享和定时任务重复执行。我把登录token设计成无状态JWTRedis存的是业务数据不是session所以本身就适合多实例轮询定时任务的分布式锁用Redis的SETNX加过期时间保证同一时刻只有一个实例执行报表聚合。如果未来单库撑不住可以考虑分库分表但千万不要在业务初期设计过度。我给这套源码的定位是让一个中小团队用最少成本把无人超市的业务闭环跑通并稳定运营。高可用方案本质上是成本换稳定性门店一天就几千单非要上两套K8s集群运维成本比业务成本还高。架构是服务于业务的不是拿来表演的。7. 踩坑实录完整版源码里相对难啃的几个问题和最终解法这部分是整套源码调试过程中沉淀下来的经验。我尽量把每个问题的现象、根因、解法写完整因为这些问题在后续二次开发里大概率还会遇到。这些问题都不是什么高深理论但每一个都能让系统在真实运行中出大问题。7.1 并发下单导致库存扣成负数现象是第一轮联调时两个请求同时给同一个SKU下单库存最终变成-1。根因是我第一版扣库存时先SELECT stock判断大于0再UPDATE stock-1这两个操作中间有事务间隙并发请求读到同一份库存各自都认为够扣。修复方式就是前面提到的原子更新SQLUPDATE inventory SET stockstock-#{num} WHERE sku_id#{skuId} AND stock#{num}数据库自己保证条件判断和更新是原子的。这个改动不仅解决负数问题还减少了SELECT再UPDATE的一次网络往返。这里有个隐藏问题多个SKU同时加入购物车扣库存时如果其中一个SKU库存不足整个订单事务回滚但Redis购物车里已经记录了数量。我在结算接口里做了“预检查”和“正式扣减”两道预检查不通过直接返回具体哪个SKU库存不足前端购物车同步清掉超量商品避免用户反复点击后弹出模糊的“库存不足”提示。这个交互细节很重要因为它直接影响结算成功率用户的购物车里有一件库存不足的商品整单都结算不了体验会非常差。7.2 支付回调重复通知造成重复发货模拟支付网关为了可靠性会没收到成功响应就重复回调。第一版我没做幂等结果一笔订单支付成功回调两次订单状态更新逻辑执行两次虽然第二次判断订单已支付就跳过了但支付流水表插入了两条记录对账时金额翻倍。后来在支付流水表加payment_transaction_no唯一索引回调处理先插入流水插成功了才继续更新订单状态重复回调插入失败直接返回“success”消费方停止通知。这个经验在很多涉及支付的项目里通用凡是外部回调第一步永远做去重和幂等。另外回调签名校验不能省。模拟网关里我故意保留了一段签名逻辑真实环境要换成渠道下发的公私钥验签。签名校验要在业务处理前否则伪造回调直接改订单状态后果很严重。开发阶段可以先打印日志看参数但生产环境必须关闭日志里的敏感字段输出比如手机号、卡号、完整token都不能打到日志里。合规意识和编码习惯是两件事但都得在项目里养成。7.3 事务隔离级别导致的订单状态错乱有一段时间运营反馈订单详情页偶尔能看到已退款的订单状态又变回已支付。查下来是MySQL默认隔离级别REPEATABLE READ配合不当的事务并发导致退款服务读到了快照版本数据。更准确地说是我在退款流程里先查订单、再改状态、最后更新库存整个过程是长事务期间支付回调线程改了订单状态退款事务基于旧的快照认为订单还是PAID就重复退款。这个坑非常隐蔽只有并发压力上来后才暴露。修复思路有几个第一把退款状态更新语句改成带条件更新UPDATE order_info SET statusREFUNDED WHERE order_no#{orderNo} AND statusPAID如果影响行数为0说明订单状态已变化直接拒绝第二缩小事务范围只把真正的状态更新和库存回补放进事务查询和前置校验放到事务外。这个案例说明数据库隔离级别是保护伞但应用层还是要用条件更新和状态机做最后防线程序不能完全依赖数据库默认行为。7.4 前端轮询接口把数据库连接池打满顾客端支付页面为了等支付结果最初用了每2秒调一次订单状态接口的轮询30个顾客同时结算时连接池直接被打满HikariCP报连接不可用。根因不是接口慢而是每个轮询都开一个数据库连接查订单表查询又没走索引大量请求把连接池占死。解决办法第一订单状态查询必须走唯一索引order_no第二轮询改成WebSocket或SSE推送支付回调后后端主动通知前端不需要每2秒轮询。我在代码里用SSE做支付结果推送开发量不大但把数据库和网络开销都降下来了。如果不想动前端轮询另一个折中方案是把轮询接口全部套Redis缓存订单状态变更时主动更新key。但既然是无人超市的收银场景推送方案体验更好也更符合实时性要求。后端在支付回调线程里拿到新状态通过SSE通道推给对应顾客终端同时门禁系统收到放行指令整体响应在1秒内。这里也说一句轮询在没有推送方案时是可接受的但要注意轮询频率和查询效率不能让一个无关紧要的状态查询打垮整个系统。7.5 时间字段和时区问题门店部署在不同地区或者服务器设在云上时时间错乱非常坑。有一段时间订单列表显示的时间比实际早8小时根因是后端JVM默认时区、MySQL连接串时区和前端JavaScript时区三者不一致。统一方案是数据库连接串加serverTimezoneAsia/Shanghai后端所有时间字段用LocalDateTime在全局Jackson配置里指定时区前端展示时不做额外转换接口返回ISO字符串足够。业务表里的时间字段统一用DATETIME而不是TIMESTAMP因为TIMESTAMP有2038年问题虽然现在还远但设计新表时没必要埋这个雷。在报表聚合里按“天”分组时也要小心如果直接按创建时间分组跨时区门店的数据会归到错误的日期。我在报表SQL里使用DATE(create_time)并依赖连接时区统一先保证服务器时区一致再处理业务逻辑。这个坑排查花了一晚上最后发现仅仅是某台服务器的操作系统时区被改成了UTC。建议所有服务器从初始化开始就固定时区为Asia/Shanghai不要给后来排查的人挖坑。8. 二次开发与总结拿到这套源码后从哪里开始改源码到手后不建议上来就改业务逻辑先花半天把项目跑起来走通主流程再按模块去读代码。很多团队拿到源码第一件事就是加功能结果一个加购流程都没跑通越改越乱。读代码和写代码一样需要顺序和方法。8.1 源码目录结构和启动顺序后端工程在根目录server下典型Maven结构controller、service、mapper按业务域分包。启动顺序是先执行doc/sql/init.sql建库建表并插入初始化数据然后修改application-local.yml里的数据库用户名密码运行SmartSupermarketApplication启动后端前端工程在web-admin和web-customer两个目录下分别执行npm install和npm run dev。第一次启动建议直接用dev环境前端代理指向localhost:8080。读代码建议按这个顺序先读device域明白门禁设备如何对接再读order域的结算流程接着读inventory域的库存操作最后读report域看定时任务怎么聚合数据。不要一上来就翻前端页面后端业务链路通了前端只是数据渲染。这套源码的注释我尽量写清楚但核心还是你自己把几个关键接口的调用链走一遍比如/device/entry到/order/settle走到哪儿断在哪儿项目就理解到哪儿。8.2 适合扩展的方向AI视觉、电子价签、机器人配送这套源码留了不少扩展点。商品识别模块的ProductIdentifyService接口目前是手动扫码和RFID两种实现后续接视觉识别只需新增一个实现类把摄像头识别结果映射成SKU列表购物车链路不用改。营销模块的策略接口可以扩展更多促销玩法。设备域支持接入更多IoT设备比如温湿度传感器、电子价签运营后台的设备类型枚举和数据模型可以平滑扩展。我在设计这些接口时刻意留了抽象层就是为了让接硬件的人不被迫改业务逻辑。如果要做更大规模的连锁无人超市建议优先关注这几个能力门店级多租户数据隔离、基于位置的服务分发、更完善的供应链系统采购、配送、盘点。这些不是这套源码默认覆盖的但新增模块不会破坏现有代码边界。你可以先给一个门店做试点跑通后再通过配置中心加门店逐步扩展。技术上的扩展永远比业务上的复盘容易先把一个店的模型做扎实。8.3 关于商业落地的一些体会最后说点项目之外的经验。无人超市真正难的不是技术是业务模型的毛利模型是否成立。一套系统源码能把识别、结算、防损、库存、会员串起来让管理者随时知道门店挣了多少、损耗多少、哪些商品卖得快就已经完成了信息化任务。我在实际推进类似项目时遇到最多的不是“系统跑不起来”而是“门店运营流程没有定义清楚”比如谁负责补货、异常单多久处理一次、库存差异率按什么标准考核。技术系统只是把管理规则固化成软件流程规则没定系统再完善也救不了业务。如果有团队想拿这套源码做二次开发我的建议是先找一个真实小店做试点把“进店-选购-结算-离店”全流程看一遍记录每个让顾客犹豫的瞬间再回头改系统的交互和异常处理。技术选型可以抄作业业务细节必须自己磨。这套系统是我做过的比较完整的零售类单体项目之一如果你按我说的顺序把它跑起来再对照自己的门店场景改应该能少走很多弯路。