多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

从零实现无人超市管理系统:SpringBoot+Vue3+MyBatis全栈实践

从零实现无人超市管理系统:SpringBoot+Vue3+MyBatis全栈实践 无人智慧超市这几年吹得很猛但真正落地的系统长什么样网上能说清楚的不多。大多数文章停留在我们有刷脸进门、自动结算这种演示层面一聊到库存怎么扣、异常订单怎么退、设备掉线怎么办就含糊过去了。这篇我打算从实际开发的视角把一套基于Java SpringBootVue3MyBatis的前后端分离无人超市管理系统从头到尾拆一遍适合准备做毕业设计、正在接外包项目、或者公司想低成本自建无人零售中台的同学参考。先说清楚这套系统能做什么用户扫码或刷卡进门自助选购商品经过RFID识别区或视觉结算台自动完成商品识别与订单生成线上支付后出门全程无店员参与。后台管理员可以维护商品、管理门店、查看实时交易流水、处理异常订单、设置会员折扣运营人员可以看到热销榜和库存预警。技术栈上后端是SpringBootMyBatisMySQL前端是Vue3Element Plus接口采用RESTful风格权限用JWT做无状态认证。如果你只是想找个能跑的源码交作业那随便down一个改改名就行。但如果你想让这套系统真正具备上线能力——哪怕只是一个校园店或者公司内部便利店——那下面这些设计决策和踩坑记录应该能让你少走很多弯路。1. 需求梳理无人超市业务闭环里真正需要系统做什么无人超市和传统POS收银最大的区别在于人的角色被拆散了。传统店里收银员同时承担结算、会员识别、异常处理、库存确认这些职责而无人场景下这些职责全部要由系统自动完成。需求阶段如果没把闭环理清后面写代码一定到处打补丁。1.1 完整的用户购物链路从用户角度看一次购物包含六个节点身份核验、开门入场、商品选购、商品识别、在线支付、出门校验。每个节点都对应后端的一个或多个接口。身份核验这边常见方案有小程序扫码、人脸识别、刷卡三种。考虑到校园店和社区店的实际情况我建议主推扫码因为小程序的注册成本最低人脸和刷卡作为补充方式保留。开门入场依赖硬件联动系统下单后调用门禁控制器接口同时记录入场时间用于后续超时订单清理。商品选购本身不涉及系统交互但有两种典型路径一种是用户拿实体商品直接走到结算台由RFID或视觉设备批量识别另一种是用户在柜内自助扫码下单商品由货道自动出货类似无人售货柜的逻辑。这套系统两者都支持区别在于订单来源字段——一个是结算台设备上报一个是用户小程序端发起。商品识别之后生成待支付订单用户可以立刻支付也可以先加入购物车继续逛。这里有一个产品细节值得注意无人超市不适合先买单后取货的传统超市逻辑因为用户可能临时不想要了。所以订单状态机里必须包含待支付→已取消这条路径且取消操作要能释放库存。1.2 后台运营端的核心职能运营端不是简单做个CRUD界面它至少要覆盖商品管理、库存管理、订单管理、会员管理、门店管理、数据报表六个模块。商品管理的关键是SKU概念要清晰。同一个商品在不同门店可以有不同售价同一SKU在不同门店库存独立这是分布式库存的基础。很多初版系统把商品和库存绑死在一张表里导致后续做多门店时改到想哭。库存管理要区分实物库存和可售库存。用户在结算台识别商品后系统预占库存支付成功后扣减库存订单取消则释放库存。这三个动作的时机如果不一致就会出现超卖或者库存不准的情况。订单管理的重心是异常处理。无人超市常见的异常有用户拿了商品没支付就出门、支付成功但门禁没开、RFID识别数量和实际不符。这些都要有对应的异常单流程不能只靠人工看流水。数据报表不需要一开始就做得特别复杂但热销商品排行、分时段客流、库存周转预警这三张表必须有这是无人超市运营的基础依据。1.3 角色权限与安全边界系统涉及的角色包括超级管理员、门店运营、普通用户。超级管理员管全局配置门店运营只能看自己门店的数据用户只能操作自己的订单和余额。权限这块我强烈建议用RBAC模型不要用简单的拦截器写死角色判断。因为无人超市项目迭代很快今天可能只有三种角色三个月后可能就会有巡店人员财务审计这些新角色。RBAC在初期多花半天时间后面能省好几天的返工。安全边界还有一个容易忽略的点无人超市的支付接口和门禁接口是核心敏感接口必须加签名校验和幂等控制否则被人抓包重放就麻烦了。这块我在后面支付章节会细讲。2. 技术选型逻辑为什么是SpringBootVue3MyBatis而不是别的组合这套组合乍看很课程设计但实际用下来会发现它恰恰是中小型商业项目性价比最高的搭配。我做过不少项目简单聊聊每层选型背后的取舍。2.1 后端的层级拆解与边界控制SpringBoot在这个项目里承担的是接口层和业务层的容器角色。我见过不少团队把无人超市系统的业务逻辑全塞进Controller里一个接口几百行调试起来特别痛苦。规范的做法是分四层Controller只做参数校验和结果包装Service层写业务规则Mapper层只做数据访问外加一个独立的DTO/VO转换层。依赖注入方面SpringBoot的自动装配帮了很大忙。Redis配置、数据源配置、RabbitMQ配置全部走application.yml环境切换只需要改profile。部署的时候生产环境用生产配置本地开发用开发配置不会出现在我电脑上是好的这种尴尬。这里有个实操细节多环境配置要独立维护不要把生产数据库密码写在默认配置里。有人图省事把所有配置写在一个文件里结果代码泄露导致数据库被删这种事故我在群里见过不止一次。2.2 为什么用MyBatis而不是MyBatis-Plus或JPA说实话MyBatis-Plus在开发效率上确实更高内置了分页插件和代码生成器能少写很多CRUD。但我在设计这个系统时坚持用原生MyBatis原因是无人超市业务里有大量复杂动态SQL——比如订单列表按多条件筛选、库存按门店和SKU联合查询、销售报表按小时聚合——这些场景下原生SQL的可控性和可优化性远高于自动生成的API。JPA在实体关系映射上很方便但它的懒加载和N1查询问题在报表场景里会让人抓狂。MyBatis的好处是SQL全在你的掌控下EXPLAIN一跑哪里慢改哪里心智负担小。有人担心MyBatis工作量大其实配合代码生成器先自动生成基础CRUD然后针对复杂查询手写XML整体工作量并不会比MyBatis-Plus高太多但后期的调优空间完全是两码事。2.3 前端为什么选Vue3而不是ReactVue3和React都能做这个项目我选Vue3有几个具体原因一是Element Plus组件库和Vue3的配合非常成熟后台管理界面几乎不用写样式二是组合式API在管理复杂表单状态时比Vue2的Options API清晰很多三是如果后面要做小程序端uni-app基于Vue语法技术栈可以复用。Vue3里我重点用了script setup语法配合ref和reactive管理响应式数据状态管理用Pinia而不是Vuex。Pinia的API设计更简洁对TypeScript支持也更好而且去掉了Vuex里那些繁琐的mutation概念写起来爽快很多。前端工程上做了三个小优化路由懒加载保证首屏速度axios统一封装处理token注入和401跳转商品图片走独立的CDN路径不和后端静态资源混在一起。这三件事虽然基础但对体验和安全都很关键。2.4 数据库选型的现实考量MySQL 8.0是这套系统的主力存储。为什么不用PostgreSQL没有特别硬的理由考虑到团队成员熟悉度、云厂商支持度、以及市面上大量现成的运维经验MySQL在这个体量下完全够用。唯一需要提醒的是字符集必须在建库时就定好utf8mb4不要用utf8。因为商品名称和用户昵称里很可能出现特殊字符比如音乐符号、emoji名称utf8在MySQL里实际只能存3字节遇到生僻字和大部分表情符号会直接报错。这个坑我帮别人排查过改字符集要重建索引非常麻烦。数据库连接池用HikariCP它现在已经是SpringBoot的默认选项性能和稳定性都经过大规模验证不需要额外替换。慢查询日志在测试阶段就要打开把超过200ms的SQL全部揪出来优化不要等上线后用户投诉了再查。3. 数据库模型设计支撑无人运营的核心表结构与字段取舍无人超市的业务核心是库存-订单-支付三者的强一致性表结构设计必须围绕这条主线来展开。我直接把核心表拆出来讲比贴整份SQL更直观。3.1 用户与会员体系的表设计用户表user_account的核心字段包括主键id、openid小程序唯一标识、手机号、昵称、头像、余额、积分、会员等级、状态、创建时间。这里有个反直觉的设计余额字段直接冗余在用户表里而不是单独建一张钱包流水表再来聚合。理由是余额查询是极高频率操作每次实时SUM流水表对数据库压力很大。但冗余也意味着要保证一致性所以每次余额变动必须同时写入资金流水表wallet_log并开启本地事务。流水表只做追加不做修改这是账务系统的基本素养。会员等级不要用硬编码字段用独立的member_level表维护等级规则包括折扣率、满减门槛、升级条件。因为运营随时可能调整策略写死在代码里改一次发一次版没必要。3.2 商品与库存模型的关键取舍商品相关的表分三级product是SPU层product_sku是SKU层sku_stock是门店库存层。product表存商品名称、主图、描述、品牌、分类。product_sku表存规格、条码、成本价、销售价。sku_stock表存门店id、skuId、实物库存、预占库存、安全库存线。为什么要拆成三级因为一个商品可能有多个规格——比如同一款饮料有330ml罐装和500ml瓶装——它们的条码不同、价格可能不同、库存必须独立管理。库存字段里实物库存和可用库存是两个概念。可用库存实物库存-预占库存。用户在结算台识别商品时预占支付成功后转实扣订单取消则释放预占。这套机制保证了高并发下不会超卖。库存变动还涉及一个隐藏逻辑无人超市的库存不完全是后台手工录入的。设备端RFID或视觉结算台在完成识别后会上报商品信息后台要根据上报结果自动扣减库存。这就要求库存操作接口有设备来源标识否则报表里无法区分人工盘点和设备自动扣减。3.3 订单链路的状态机设计订单主表orders的核心字段订单号、用户id、门店id、订单状态、商品总金额、实付金额、折扣金额、支付方式、支付时间、创建时间。订单明细表order_item存每个商品的SKU信息、单价、数量、小计。订单状态我设计成待支付(0)、已支付(1)、已取消(2)、退款中(3)、已退款(4)、异常(5)。这个状态机必须支持两条核心流转路径正常路径是待支付→已支付异常路径是待支付→已取消已支付→退款中→已退款。有一个很微妙的点是已取消和超时未支付关闭要分开吗我的做法是不分开统一走取消逻辑只增加一个取消原因字段。对用户来说他只知道订单没了对运营来说取消原因必须能区分是用户主动取消还是系统超时关闭否则对账时会一头雾水。订单号的生成不要用数据库自增id直接暴露给前端我用的方案是时间戳门店id后三位随机四位保证全局唯一且能从订单号反查门店和下单时间。百度那种全局发号器对这个小项目来说过度设计了。3.4 设备与门店的边界划分设备表device记录设备编号、设备类型门禁/RFID结算/视觉结算、所属门店、在线状态、最后心跳时间。门店表shop记录门店名称、地址、营业状态、经纬度。为什么要单独做设备表因为无人超市的业务链路里设备是独立的业务参与方。用户的入场、结算、出门动作都由设备触发设备异常直接影响订单流程。没有设备表你就无法回答哪个门店的哪台设备今天掉线了几次这种最基本的运维问题。设备心跳用简单的时间戳轮询机制设备每隔30秒上报一次心跳后台判断当前时间减去最后心跳时间超过90秒则标记离线。不需要引入复杂的物联网中间件性价比不高。4. 后端核心模块拆解从用户认证到商品识别的接口设计后端代码结构我按业务域分包controller、service、mapper、entity、dto、vo、config、common。下面挑几个含金量最高的模块讲实现思路。4.1 JWT无状态认证与权限拦截的落地登录流程是标准的JWT三步走用户传openid或手机号到/api/auth/login后端验证用户存在后签发AccessToken和RefreshTokenAccessToken有效期设2小时RefreshToken有效期设7天前端axios拦截器自动刷新。为什么用双Token无人超市的用户经常一周才来一次如果AccessToken过期就强制重新登录体验太差。但AccessToken的有效期太长又有安全风险。双Token方案完美平衡攻击者即使拿到AccessToken也最多用2小时刷新Token走独立接口且绑定设备标识盗刷成本高很多。权限拦截用Spring拦截器实现在WebMvcConfigurer里注册拦截器并排除白名单路径。白名单必须包含登录接口、注册接口、商品列表接口、门店列表接口。剩下的接口全部校验token合法性并从token里解析出user_id注入ThreadLocal供Service层使用。一个小细节ThreadLocal用完必须remove否则Tomcat线程池复用时会串数据。4.2 商品识别与库存扣减的事务边界商品识别是整个系统最核心的接口。流程是结算台设备将识别到的商品条码列表POST到/api/order/recognize参数包含门店id和条码数组。后端处理分三步根据门店id条码列表查出对应SKU校验上下架状态。检查库存是否足够不足的SKU返回具体缺货信息。生成待支付订单和订单明细预占库存。这里的事务边界要控制好。生成订单和预占库存必须在一个事务里用Transactional保证同时成功或同时失败。库存不足时要抛出异常触发回滚不能让用户拿到一张半成品订单。踩过一个具体的坑商品识别接口被设备重复调用。设备网络波动导致识别结果发送了两次结果生成了两笔一模一样的待支付订单。解决方案是在识别接口上加一个简单幂等校验——同一个设备在10秒内提交相同条码集合的请求直接返回上一次订单。后来换成更严谨的方案设备端生成请求流水号后端用流水号做唯一索引。4.3 订单超时关闭与库存释放的定时任务待支付订单如果一直不支付库存就被一直占用。无人超市场景下用户在结算台识别完可能突然不想要了直接走人天天都有这种订单。所以必须有一个定时任务兜底。实现上我用Spring的Scheduled注解配置了一个每30秒执行一次的扫描任务查询超过15分钟仍未支付的订单将其置为已取消并释放预占库存。为什么是15分钟这是产品和运营讨论出来的平衡点。太短用户结算台前多停留一会订单就被取消了体验差太长库存占用太久影响正常售卖。这里有个并发问题值得注意定时任务释放库存时用户可能正在支付这笔订单。所以取消操作必须是条件更新——UPDATE orders SET statusCANCELLED WHERE order_id? AND statusPENDING受影响行数为0说明订单状态已被用户改成已支付定时任务直接放弃释放库存。这个条件更新是保证状态一致性的关键。4.4 MyBatis动态SQL与报表统计的实践商品列表查询和订单列表查询都有动态SQL需求。以订单列表为例门店运营可能按订单号、用户手机号、订单状态、时间段、支付方式中的任意组合筛选而且分页显示。MyBatis的where标签加if条件判断处理这种场景非常干净SQL只拼接实际存在的条件不会出现WHERE 11这种丑陋写法。配合PageHelper分页插件两行代码搞定分页PageHelper.startPage(pageNum, pageSize); ListOrderVO list orderMapper.selectOrderList(query); PageInfoOrderVO pageInfo new PageInfo(list);但这里要提醒PageHelper的startPage只对紧随其后的第一条查询生效。如果代码里在startPage之后有别的SQL先执行分页就会串位。所以每个方法里尽量只保留一条查询语句对应一次startPage。报表统计方面热销商品排行就是一个简单的GROUP BY加ORDER BYSELECT sku_id, SUM(quantity) AS total_sales FROM order_item WHERE create_time BETWEEN #{start} AND #{end} GROUP BY sku_id ORDER BY total_sales DESC LIMIT 20;这套SQL在小数据量下完全没问题。如果后续数据量上来了再拆离线统计或上ES都不迟前期不要过度设计。5. Vue3前端架构与关键页面实现前端项目基于Vite构建开发体验比Webpack快一个量级。目录结构按模块划分api放接口封装、views放页面组件、router放路由配置、store放Pinia状态、components放公共组件。5.1 移动端购物页面的核心交互用户端H5页面是整个系统的门面核心页面有三个门店列表页、商品列表页、购物车/结算页。虽然无人超市的核心操作在设备端但用户查看订单、充值、会员信息都在H5上完成。商品列表页我用了组合式API来管理筛选条件、商品数据、加载状态// 组合式API示例商品列表状态管理 const { proxy } getCurrentInstance() const goodsList ref([]) const shopId ref() const categoryId ref() const loading ref(false) const loadGoods async () { loading.value true try { const res await proxy.$api.getGoodsList({ shopId: shopId.value, categoryId: categoryId.value }) goodsList.value res.data } finally { loading.value false } }页面加载时调用loadGoods切换分类时重置categoryId并再次调用。这种写法比起Vue2的datamethods分散管理逻辑集中度明显更好。购物车页用Pinia管理状态因为购物车数据需要在商品页和结算页之间共享。加入购物车时只在前端维护状态真正落库是在结算台识别后生成订单的环节这种模式下前端的购物车更像一个意愿清单不能和真实订单混淆。5.2 管理后台的权限路由与动态菜单后台管理页面用Element Plus做UI框架侧边栏菜单根据用户角色动态生成。实现方案是路由表分为两种静态路由登录页、404页、无权限提示页和动态路由商品管理、订单管理、库存管理等后端登录接口返回该用户可访问的菜单权限码集合前端用router.addRoute动态注册。这里的核心难点是刷新页面后动态路由会丢失。因为路由是在登录后动态加的浏览器一刷新Vue实例重新创建需要先在Pinia里存储用户信息和权限码刷新时再根据权限码重新注册路由。这个逻辑放在路由守卫的beforeEach里处理确保任何路径的刷新都能恢复完整路由表。表格页面统一封装了一个SearchTable组件集成了搜索表单、表格、分页器三段结构通过props传入字段配置。这样商品列表、订单列表、门店列表页面的代码量减少了大概40%而且风格统一。5.3 axios拦截器的token管理与错误处理axios封装是前端工程质量的关键。我在请求拦截器里统一注入Authorization头值为Bearer ${accessToken}。响应拦截器处理两件事HTTP 401时调用刷新接口换新token并重放原请求业务错误码非0时弹出统一错误提示。这里最常见的问题是刷新token时多个请求同时返回401导致重复刷新。解决办法是加一个isRefreshing标志和待重放请求队列let isRefreshing false let pendingQueue [] service.interceptors.response.use( (response) { if (response.data.code 401) { if (!isRefreshing) { isRefreshing true return refreshToken().then((newToken) { // 更新本地token // 重放排队的请求 pendingQueue.forEach((cb) cb(newToken)) pendingQueue [] return service(response.config) }).finally(() { isRefreshing false }) } else { return new Promise((resolve) { pendingQueue.push((newToken) { response.config.headers.Authorization Bearer ${newToken} resolve(service(response.config)) }) }) } } return response } )这段代码虽然看起来稍微复杂但它是保证用户长时间使用不频繁掉线的关键建议直接抄。6. 设备对接与支付闭环无人超市的最后一公里无人超市系统能不能真跑起来取决于设备对接和支付链路是否扎实。这一章讲两个最容易出问题的环节。6.1 门禁/结算设备的接口协议设计设备对接的本质是定义一套清晰、可容错的HTTP接口协议。设备端是弱客户端它们的内存和网络环境都远不如手机所以接口设计上必须考虑以下原则敏感操作带设备签名。设备请求头里附加设备编号和签名串签名串由设备编号、时间戳、密钥做HMAC加密生成。后端验签失败直接拒绝请求防止有人伪造设备接口刷单。结算台上报商品结果后设备需要等待后台确认并返回订单号待支付金额。如果后台超时未响应设备端要有重试和降级机制——先保留商品结果在本地等网络恢复再上报不能直接把商品放行。门禁设备的下行指令走后台主动调用管理员远程开门、超时订单强制关门、异常事件远程锁定。这些指令必须有操作记录日志便于事后的安全审计。我们按事件来源和指令类型做了结构化日志排查纠纷时能快速定位。还有一条容易被忽略的经验设备接口和用户接口要分别走不同的API前缀比如/api/device/xxx和/api/user/xxx。这样既方便权限隔离又方便在网关层做限流配置。6.2 支付回调与订单状态的最终一致性支付环节选用微信支付或支付宝的Native支付业务流程是后端调用支付API获取支付链接返回给前端前端生成二维码用户扫码支付微信/支付宝服务器异步通知后端回调地址后端在回调中验签成功后将订单状态置为已支付扣减预占库存并转实扣前端轮询订单状态接口查询到已支付后跳转结果页。这里最核心的是回调幂等性。支付回调可能因为网络原因重复推送所以回调处理逻辑必须是幂等的——同一笔订单被回调100次结果也必须是正确的。我的实现是查询订单状态如果已经是已支付状态则直接返回成功不再执行扣库存逻辑。另一个容易踩的坑是回调校验必须验签且校验金额。验签保证请求确实来自支付平台金额校验保证支付平台返回的金额和订单金额一致防止出现订单改价之类的极端情况。设备端的联动也在这一步触发订单支付成功后后台调门禁接口放行用户。这个联动是异步的通过RabbitMQ发送一条支付成功消息门禁服务消费消息后调用设备接口开门。异步解耦的核心好处是即使门禁设备暂时不可用也不影响支付主流程的完成。未开门的情况由重试任务补偿超过3次仍未成功则生成异常工单运营手动介入。6.3 金额精度与财务对账的处理涉及钱的系统金额计算必须用BigDecimal禁止用double或float。这不是教条而是浮点数的二进制表示天生无法精确表达十进制小数——0.1在double里是有误差的。会员折扣、满减优惠、退款计算叠加起来误差会被放大对不上账是必然的。所有金额字段在数据库里用DECIMAL(10,2)类型Java实体用BigDecimal前端展示用toFixed(2)。MyBatis的TypeHandler会自动处理两者映射不需要额外写转换器。对账逻辑上每天的凌晨定时任务拉取前一天的所有支付流水与支付平台账单比对。只对总金额不做逐笔核对的方案也可以接受但无人超市客单价低笔数多我建议直接逐笔比对加上支付平台有而本地无和本地有而支付平台无两个方向的差异列表运营按列表核查即可。7. 关键难点复盘并发超卖、缓存穿透与事务失效开发阶段遇到的几个技术难点单独列出来复盘一下这些都是不跑真实业务碰不到的坑。7.1 高并发库存扣减的锁机制选择无人超市的秒杀场景不多但节假日促销仍然会出现同一SKU被多用户同时抢购的情况。库存扣减的SQL写成条件更新是最稳妥的方案UPDATE sku_stock SET stock stock - #{num} WHERE sku_id #{skuId} AND shop_id #{shopId} AND stock #{num}受影响行数为0说明库存不足。这个SQL靠数据库的行锁天然保证并发安全不需要在Java代码里加synchronized或Redis分布式锁性能足够且实现最简单。要注意的是MySQL默认的REPEATABLE READ隔离级别下如果事务里先查询库存再更新可能产生幻读。所以核心原则是库存操作不要先查再改直接条件更新把判断放到SQL里。这样代码更简洁并发安全也更有保障。7.2 商品查询缓存的穿透问题用户端商品列表的请求量远大于后台管理端第一次开发时没做缓存数据库压力一上来就报警。引入Redis后遇到的是穿透问题——用户频繁查询不存在的商品ID请求全部落到数据库Redis成了摆设。解决方案分两步先是在查询方法上加缓存存在则直接返回再是对查不到的商品ID也缓存一个空值并设置较短过期时间比如60秒让后续的相同查询不再穿透到数据库。更进一步的做法是用布隆过滤器拦截非法ID但在这个数据量下用空值缓存已经足够了。缓存和数据库的一致性策略是Cache Aside模式查询先走缓存写操作直接更新数据库同时删除缓存。为什么不更新缓存而是删除因为计算新缓存值需要额外查询且并发更新场景下容易出现老值覆盖新值的问题。删除缓存让下次查询自然重建简单可靠。7.3 本地事务与异步消息的事务一致性方案第6章说的支付成功后发MQ消息给门禁设备这里就涉及本地事务和消息发送的一致性。如果扣库存的事务提交了但MQ消息发送失败门禁就不会开用户就被困在里面——这在无人超市里是严重的体验事故。标准的解决方案是消息表模式在业务事务里同时写入一张mq_message表事务提交后再由独立的定时任务轮询这张表把未发送的消息推送到MQ。发送成功后更新消息状态为已发送。如果发送失败定时任务持续重试同时配合最大重试次数和人工告警。这个方案的价值在于把业务操作和消息发送变成了同一个本地事务的一部分而不是靠分布式事务框架去强撑一致性。事务消息、本地消息表、MQ的事务消息这三种方案我都考虑过最终选了本地消息表因为实现最简单不依赖特定的MQ版本特性运维成本最低。8. 部署落地与真实踩坑记录最后讲部署环节这一部分全部是从实际项目里积累的教训。代码写得再漂亮部署环节一个坑就能让你排查一整天。8.1 环境规划和资源配置建议最低配的部署架构是单台云服务器2核4G、40G SSD、带宽按需。上面跑MySQL、Redis、后端Jar包、Nginx前端静态资源也由Nginx托管。这个配置支撑一个校园超市的日常流量完全够用日订单量在3000笔以内不用升级。如果预算稍微充裕建议MySQL和Redis独立部署后端应用单独一台Nginx单独一台或直接用云负载均衡。这样做的核心原因是隔离故障域——数据库磁盘满了不会导致应用层全部不可用。服务器系统用Ubuntu 22.04 LTSMySQL 8.0通过Docker跑Redis直接宿主机安装。用Docker管理MySQL的好处是环境隔离、备份迁移方便但要注意数据目录必须挂载到宿主机否则容器删了数据全没了——这个教训我见过太多人踩过。8.2 前后端部署的具体步骤前端部署npm run build后生成dist目录把dist内容复制到Nginx的web根目录配置SPA的history模式回退规则location / { try_files $uri $uri/ /index.html; }后端部署mvn clean package -DskipTests打出可执行Jar包用systemd配置开机自启日志通过logback输出到固定目录并按大小滚动。实际操作中还有两个容易忽略的点。第一后端Jar运行时要显式指定profilejava -jar supermarket.jar --spring.profiles.activeprod确保加载生产环境的配置。第二定期备份数据库用系统cron任务每天凌晨执行mysqldump并通过scp同步到备份服务器备份保留7天。无人超市的订单数据是核心资产丢数据基本等于项目白干。8.3 跨域、端口与安全加固的细节前后端分离的跨域问题在开发环境用Vite的proxy解决生产环境则通过Nginx反向代理解决——前端请求/api路径时Nginx转发到后端服务的8080端口浏览器始终只和Nginx通信不存在跨域问题。Nginx的关键配置是location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }安全加固方面至少要完成这几件事修改MySQL默认端口和root密码Redis绑内网IP并设置访问密码云安全组只放行80/443/22端口后端Jar包不以root用户运行Nginx配置SSL证书启用HTTPS。这些做完系统才有基本的线上生存能力。9. 从能用变好用的几个运营功能扩展主体功能跑通之后再补几个运营想用但没有跟开发讲过的功能这些是决定系统能不能被接受的关键细节。9.1 库存预警与智能补货的规则配置库存预警不要只做低于阈值就告警这种简单逻辑那会导致运营被告警轰炸。我实现的规则是按SKU设置安全库存线和预警周期低于安全线且当日销量在走高的SKU才触发告警同时生成一条补货建议单关联该SKU在供应商目录里的默认采购价。预警触达方式优先是管理后台的工作台弹窗和短信。短信比较烧钱所以只对补货建议单的创建人发送。9.2 会员积分与折扣策略的规则引擎积分规则做成可配置消费1元积1分首次消费双倍积分储值充值额外送积分。折扣策略则按会员等级设置普通会员98折、银卡95折、金卡9折同时支持全场满减活动的叠加规则。这块看起来简单但实现时要注意优惠叠加顺序——先折扣后满减还是反过来结果差不少。我目前的设计是折扣和满减互斥系统默认取用户收益最大的方案并把这个选择逻辑独立成promotionEngine服务。规则多了之后这个独立服务的演进空间比写在订单Service里大得多。9.3 门店客流与购买转化分析通过门禁设备的事件流水可以统计出每日进店人数、扫码用户数、成交订单数。进店人数除以扫码数是有效进店率扫码数除以成交订单数能反映结算转化率。这三层漏斗数据对无人超市的选址和陈列非常有价值。哪个门店进店率高但转化率低说明商品布局或者结算流程有问题哪个门店进店率低则可能是引流问题。报表页做一个简单的漏斗图运营每天看这一页就够了。另外让系统24小时运转之后我意识到运营最需要的不是实时看到一切而是该处理的事情不遗漏。所以工作台应该优先推送异常订单、设备离线、库存预警、退款申请四项内容其他的信息都做折叠处理。这个设计原则后来很多需求评审里都能用上。10. 写在最后的几点项目体会如果照着这篇文章把系统搭完你得到的不仅仅是一份能跑的源码更多的是一套可以在真实场景里验证过的业务理解。我在实际开发中感受最深的一点是无人超市系统的技术难度不高真正的复杂度全部来自设备、用户、订单、库存这四个要素之间的异常状态组合。不要试图预判所有异常然后全部提前处理而是先保证主流程畅通再靠消息队列、定时任务、人工工单去兜底这样系统做出来的复杂度最低、维护性最好。第二点是权限和日志设计一定要前置。无人超市涉及资金和物品出入一旦上线运营每一次订单操作、库存调整、退款行为都要能追溯到责任人。日志不是为了应付审计而是你排查问题时最直接的线索。我在账目对不上的时候靠操作日志链恢复场景的次数远远多过靠代码Debug的次数。最后如果这篇文章能帮你少走一些弯路那最好的反馈就是你也能把自己的踩坑记录写成博客发出来。经验这东西只有流动起来才有价值。有问题可以直接留言交流看到我会及时回复。
返回列表