1. 无人智慧超市不是“超市装摄像头”:系统的整体目标与能力边界
无人智慧超市管理系统源码,听起来像是新零售宣传片里的概念,但真把一个无人店从图纸变成可运行的软件系统,你需要的不是一块大屏、不是几个摄像头,而是一整套覆盖商品、订单、会员、库存、设备监控的完整业务闭环。我最近基于SpringBoot+Vue+MyBatis+MySQL这套技术栈,完成了一套企业级完整版源码,其中既包括顾客扫码进店、自动结算、门禁放行的前台链路,也包括运营端商品管理、报表统计、异常单处理的后台能力。这里我把整个系统的设计过程、表结构、接口链路和上线踩坑一起复盘一遍,希望能帮到正在做同类型项目或者准备拿这份源码做二次开发的团队。
先说清楚边界:这个系统不是单纯做一个“收银台换刷脸”的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. 技术选型复盘:SpringBoot+Vue+MyBatis+MySQL这套组合怎么就把项目撑起来了
选型不一定要追逐热点,但必须能解释清楚:为什么用这套组合,它的边界在哪。SpringBoot + Vue + MyBatis + MySQL,是中小型企业级管理系统里非常成熟的组合,社区资料多,招人成本低,出问题排查快。对无人超市这种业务来说,既没有超高并发秒杀场景,也没有海量数据仓库需求,用这套组合完全撑得起来,真正需要动脑的是业务复杂度和状态管理。下面从四个角度复盘选型逻辑。
2.1 后端为什么选SpringBoot而不是Spring Cloud或手写Servlet
SpringBoot的价值在于“自动配置”和“约定优于配置”。相比传统SSH需要写大量XML配置,SpringBoot通过起步依赖和application.yml就能把Web、MySQL连接、MyBatis、Redis等组件一次性装配好。我在这套项目里用的JDK版本是17,SpringBoot 2.7.x,稳定且兼容性广。有人可能觉得现在都Spring Boot 3.x了,为什么还用2.7?原因很现实:很多企业内部还在用JDK8/11,MyBatis、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而不是全用JPA
MyBatis的优势是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的事件机制做订单支付成功后的异步处理,数据库连接池用HikariCP(SpringBoot默认),足够支撑单店或中等规模连锁的初期阶段。
如果未来门店数快速增长,需要横向扩容,可以平滑引入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用tinyint,0下架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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='SKU表';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 | 扫码识别SKU |
| inventory | 联合唯一索引 | 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 | 订单已创建 | LOCKED |
| LOCKED | 库存已冻结 | PAID, ABNORMAL |
| PAID | 已支付 | COMPLETED, REFUNDING |
| COMPLETED | 已完成 | REFUNDING |
| REFUNDING | 退款中 | REFUNDED |
| REFUNDED | 已退款 | 终态 |
| 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.com,Nginx根据路径分发到前端静态资源或后端API。
部署顺序有讲究:先初始化MySQL数据库并导入init.sql,再启动后端服务,最后更新前端静态文件。倒过来容易看到前端已上线但接口全报错,排查方向混乱。后端配置里数据库密码不要硬编码在application.yml,用环境变量注入。我习惯写一个deploy.sh,把拉代码、打包、备份旧包、重启服务的动作串起来,减少手工操作。示例脚本很简洁但很实用:
#!/bin/bash APP_NAME=smart-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调到20,minimum-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设计成无状态JWT,Redis存的是业务数据不是session,所以本身就适合多实例轮询;定时任务的分布式锁用Redis的SETNX加过期时间,保证同一时刻只有一个实例执行报表聚合。
如果未来单库撑不住,可以考虑分库分表,但千万不要在业务初期设计过度。我给这套源码的定位是:让一个中小团队用最少成本,把无人超市的业务闭环跑通并稳定运营。高可用方案本质上是成本换稳定性,门店一天就几千单,非要上两套K8s集群,运维成本比业务成本还高。架构是服务于业务的,不是拿来表演的。
7. 踩坑实录:完整版源码里相对难啃的几个问题和最终解法
这部分是整套源码调试过程中沉淀下来的经验。我尽量把每个问题的现象、根因、解法写完整,因为这些问题在后续二次开发里大概率还会遇到。这些问题都不是什么高深理论,但每一个都能让系统在真实运行中出大问题。
7.1 并发下单导致库存扣成负数
现象是第一轮联调时,两个请求同时给同一个SKU下单,库存最终变成-1。根因是我第一版扣库存时先SELECT stock,判断大于0再UPDATE stock-1,这两个操作中间有事务间隙,并发请求读到同一份库存,各自都认为够扣。修复方式就是前面提到的原子更新SQL:UPDATE inventory SET stock=stock-#{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 status='REFUNDED' WHERE order_no=#{orderNo} AND status='PAID',如果影响行数为0说明订单状态已变化,直接拒绝;第二,缩小事务范围,只把真正的状态更新和库存回补放进事务,查询和前置校验放到事务外。这个案例说明,数据库隔离级别是保护伞,但应用层还是要用条件更新和状态机做最后防线,程序不能完全依赖数据库默认行为。
7.4 前端轮询接口把数据库连接池打满
顾客端支付页面为了等支付结果,最初用了每2秒调一次订单状态接口的轮询,30个顾客同时结算时,连接池直接被打满,HikariCP报连接不可用。根因不是接口慢,而是每个轮询都开一个数据库连接查订单表,查询又没走索引,大量请求把连接池占死。解决办法:第一,订单状态查询必须走唯一索引order_no;第二,轮询改成WebSocket或SSE推送,支付回调后后端主动通知前端,不需要每2秒轮询。我在代码里用SSE做支付结果推送,开发量不大,但把数据库和网络开销都降下来了。
如果不想动前端轮询,另一个折中方案是把轮询接口全部套Redis缓存,订单状态变更时主动更新key。但既然是无人超市的收银场景,推送方案体验更好,也更符合实时性要求。后端在支付回调线程里拿到新状态,通过SSE通道推给对应顾客终端,同时门禁系统收到放行指令,整体响应在1秒内。这里也说一句:轮询在没有推送方案时是可接受的,但要注意轮询频率和查询效率,不能让一个无关紧要的状态查询打垮整个系统。
7.5 时间字段和时区问题
门店部署在不同地区或者服务器设在云上时,时间错乱非常坑。有一段时间订单列表显示的时间比实际早8小时,根因是后端JVM默认时区、MySQL连接串时区和前端JavaScript时区三者不一致。统一方案是:数据库连接串加serverTimezone=Asia/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 关于商业落地的一些体会
最后说点项目之外的经验。无人超市真正难的不是技术,是业务模型的毛利模型是否成立。一套系统源码能把识别、结算、防损、库存、会员串起来,让管理者随时知道门店挣了多少、损耗多少、哪些商品卖得快,就已经完成了信息化任务。我在实际推进类似项目时,遇到最多的不是“系统跑不起来”,而是“门店运营流程没有定义清楚”,比如谁负责补货、异常单多久处理一次、库存差异率按什么标准考核。技术系统只是把管理规则固化成软件流程,规则没定,系统再完善也救不了业务。
如果有团队想拿这套源码做二次开发,我的建议是先找一个真实小店做试点,把“进店-选购-结算-离店”全流程看一遍,记录每个让顾客犹豫的瞬间,再回头改系统的交互和异常处理。技术选型可以抄作业,业务细节必须自己磨。这套系统是我做过的比较完整的零售类单体项目之一,如果你按我说的顺序把它跑起来,再对照自己的门店场景改,应该能少走很多弯路。