做校园超市这一块,最早就源于一个很实际的场景:我朋友在学校里开了一个小卖部,每天中午和晚上下课那二十分钟,收银台前面总是排着长队,学生想买包薯片、拿瓶饮料都得等半天;而另一边,货架上的临期零食没人知道,库存全靠记忆,进货凭感觉。后来我们几个做技术的聊起来,就打算做一个“校园超市小卖部零食在线购物商城系统”,让学生在线下单、到店自提,顺便把库存、订单、促销这些事全部数字化。项目最终落地用上了 Spring Boot、Node.js、Vue 三件套,这中间踩了不少坑,也沉淀了不少能直接复用的经验。
这篇文章是我从设计到开发、再到部署上线的完整复盘。核心内容包含整体架构选型逻辑、功能模块拆解、数据库设计、下单扣库存的并发处理、Node.js BFF 层的搭建、Vue 前端工程化,以及最后生产环境部署与运维。如果你正准备做一个类似的校园商城项目,或者想搞明白 Spring Boot 和 Node.js 在一个系统里到底怎么分工,这篇文章应该能给你不少可以直接照抄的答案。
1. 项目概览与方案选型
1.1 校园超市小卖部的真实痛点
做校园在线商城之前,先得搞清楚一个问题:校园里的超市和小卖部到底缺什么?
我观察下来,线下小卖部的痛点非常集中。第一是收银效率,放学那二十分钟是绝对高峰期,学生挤在柜台前,有人拿了好几样零食,有人还要问价格,后面排队的人恨不得直接放下钱就走。队伍一长,很多学生干脆不买了,这一部分丢失的销售额其实不少。第二是库存不透明,小卖部通常没有像样的进销存系统,老板靠脑子记大概还剩多少,结果经常出现某款网红薯片缺货一周没人知道,或者囤了一堆临期饮料最后只能亏本处理。第三是没有营销抓手,促销活动贴个 A4 纸在门口,效果完全无法量化,会员、复购、满减这些玩法和线下收银完全脱节。
校园在线购物商城系统要解决的,就是把这套链路搬到线上:学生在线浏览商品、加购物车、下单支付,支付后选择到店自提或者宿舍配送;商家后台维护商品、管库存、处理订单、看销售数据;平台侧还能做基础的账号管理、数据统计和推荐位运营。核心目的就两个:缓解线下的收银和排队压力,让库存和营销变得可量化、可追踪。
1.2 技术选型背后的逻辑
很多人在第一次看到“Spring Boot + Node.js + Vue”这种组合时,第一反应都是:一个项目里为什么会出现两种后端技术?是不是重复造轮子?
实际做下来你会发现,这三个技术栈在这个项目里各管一段,边界非常清晰:
- Spring Boot 负责核心业务。用户、商品、订单、购物车、支付流水这些强一致性、强事务要求的模块,全部交给 Java 这一侧。Java 生态在数据库事务、连接池管理、定时任务、数据一致性方面的成熟度是经过多年业务验证的,做商城底账系统非常合适。
- Node.js 负责 BFF 层和实时消息。前端并不直接面对 Spring Boot 的每一个接口,而是统一走 Node.js 这一层。Node.js 在这里做接口转发、字段裁剪、数据聚合,同时用 socket.io 提供 WebSocket 服务,推送订单状态变化,比如用户下单成功,商家后台立刻弹出一条新订单提醒。这类 IO 密集、实时性强的场景,Node.js 处理起来非常顺手。
- Vue 负责前端呈现层。用户端和商家后台都用 Vue3 + Vite 开发,组件化开发模式非常适合商城这种页面多、交互复杂、状态管理需求明显的项目。
选择这套方案还有一个现实原因:项目团队本身的技术背景是混合的。做后端的人熟悉 Java,做前端和工具链的人熟悉 JavaScript/Node,按业务边界切分,反而能让不同的人在自己擅长的区域发力。如果你是单人开发,BFF 层也可以理解为一道可控的胶水层,后续不管是要适配小程序端,还是想给第三方开放接口,都可以在 BFF 层做二次加工,不必频繁改动核心 Java 服务。
需要特别说清楚的是,加一个 Node.js BFF 层不是炫技。它的价值体现在三件事:第一,隐藏核心服务内部结构,前端只面对 BFF 的稳定接口,Spring Boot 内部怎么改都不影响前端;第二,可以做接口聚合和多端适配,同一个数据源,给 Web 端和小程序端裁剪不同字段;第三,可以在这一层做轻量逻辑,比如转发前的 token 校验、接口缓存,减少 Java 侧的重复代码。
注意:如果项目对数据一致性要求特别高,或者团队里没人熟悉 Node.js,也可以让前端直连 Spring Boot,Node.js 只负责 WebSocket 推送。BFF 层加不加、做到什么程度,完全取决于团队规模和维护成本,不是越复杂越好。
1.3 整个系统的数据流向
把架构落到运行视角来看,系统里的数据基本是按三条链路在跑:
- 业务链路:Vue 页面 → axios 请求 → Node.js BFF → Spring Boot REST API → MySQL / Redis。
- 实时链路:Spring Boot 订单状态变更 → 回调 Node.js 内部接口 → socket.io 推送到用户端或商家端。
- 管理链路:管理员和商家通过 Vue 后台操作商品、订单、推荐位,数据写入数据库后由前端拉取最新状态刷新。
也就是说,浏览器永远只面对 Node.js 暴露的那几个路径,不会直接看到 Spring Boot 的接口。这样做的好处,一是端口暴露面变小,二是后续后端接口重构时前端完全无感,三是为了以后接小程序的场景铺好了路。
2. 功能拆解与数据库设计
2.1 用户端、商家端、管理端功能清单
商城系统按角色可以清晰拆成三块:用户端、商家端、管理端。每一块的职责不同,实现的复杂程度差别也很大。
用户端面向学生,功能主要包括:
- 账号与登录:支持手机号或学号注册,密码 BCrypt 加密存储,JWT 维护登录态。
- 商品浏览:首页轮播推荐位、分类 Tab 筛选、关键词搜索、商品详情页。
- 购物车:加入购物车、修改数量、删除、勾选商品结算。
- 下单与支付:选择到店自提或宿舍配送,填写地址或自提点备注,提交订单后进入支付环节。
- 订单中心:按照待付款、待发货、待收货、已完成、已取消五个状态展示订单列表。
- 个人中心:展示头像昵称、收货信息、历史订单入口。
商家端面向店铺运营者,功能包括:
- 商品管理:新增商品、编辑信息、上下架,上传商品图片。
- 库存管理:手动调整库存,低库存预警提示。
- 订单处理:接单、发货、处理退款申请。
- 数据统计:今日销售额、商品销量排行、分类销售占比。
管理端面向平台运营者,功能包括:
- 用户管理:冻结或解冻违规账号。
- 商家入驻审核:开通和收回店铺权限。
- 轮播图和推荐位配置。
- 平台级订单和交易流水查询。
从工作量上看,用户端页面最多,商家端逻辑最重,管理端实现最轻。开发时建议按商家端优先,因为对校园小卖部老板来说,商品管理和订单处理才是核心价值。
2.2 表结构设计与字段要点
商城系统的核心数据表我拆了 7 张左右,用户表、商品表、分类表、购物车表、订单表、订单明细表、收货信息表。设计时有几个值得注意的字段细节。
用户表(user)建议字段:id、学号或账号、密码摘要、昵称、头像、手机号、角色类型(用户/商家/管理员)、状态(正常/冻结)、创建时间。
商品表(product)核心字段:id、商品名称、分类 id、现价、原价、库存、销量、主图、描述、上下架状态、是否推荐、创建时间、更新时间。
购物车表(cart)相对简单,存用户 id、商品 id、数量、勾选状态即可,通过商品 id 关联商品表。
订单表(orders)和订单明细表(order_item)是最需要花心思的地方。订单表要冗余接收人姓名、手机号、地址或自提点、商品总数、总金额快照,订单明细表则要记录购买那一刻的商品名称、单价、数量、图片快照。为什么一定要做快照?因为商品价格和名称随时会变,如果只存商品 id,后续商品改价或改名,历史订单展示就会错乱。用户查几个月前的订单,看到的还是当时的商品信息和价格,这才是正确行为。
数据库设计的几个细节值得反复强调:
- 金额统一用分存储,也就是整数类型,避免浮点精度问题。前端展示时再除以 100。
- 订单号建议用日期加随机序列生成,不要用自增主键直接当订单号,避免暴露系统业务量。
- 所有表都加 create_time 和 update_time 字段,排查数据问题时没有这两个字段会非常痛苦。
- 常用查询字段要紧跟索引,比如订单表的 user_id、status,商品表的 category_id、status。
2.3 订单状态机与库存扣减模型
订单从创建到完成,会经过几个明确的状态:
待付款 → 已付款/待发货 → 已发货/待收货 → 已完成
另外还有两条分支:待付款超时或主动取消 → 已取消;已付款未发货申请退款 → 退款中/已退款。
这个状态机看起来简单,但实现时有两个地方特别容易出错。
第一个是超卖问题。经典的错误写法是:先查询库存,程序里判断库存够不够,再执行 UPDATE 扣库存。在低并发下没问题,一旦多人同时下单,查询和更新之间会有时间窗口,库存会被扣成负数。正确做法是使用条件更新 SQL:
UPDATE product SET stock = stock - #{count} WHERE id = #{id} AND stock >= #{count}这条 SQL 通过 where 条件保证库存永远不会被扣成负数,受影响行数为 0 时说明库存不足,事务直接回滚,业务层提示库存不够。
第二个是订单超时未支付需要释放库存。最简单的实现是用 Spring Boot 的 @Scheduled 定时任务,每隔一段时间扫描超时待付款订单,把订单状态改为已取消,同时把商品库存加回去。这里必须注意重复回补问题:同一笔订单被定时任务扫到两次,库存就会重复加。解决办法是用一条 UPDATE 语句原子地完成状态判断和更新:
UPDATE orders SET status = 'CANCELLED', cancel_time = NOW() WHERE order_no = #{orderNo} AND status = 'PENDING_PAYMENT'如果受影响行数是 0,说明订单已经不在待付款状态,直接跳过库存回补,不要再用 Java 代码先查状态再更新,那中间一定存在并发窗口。
3. 后端服务实现与并发攻坚
3.1 Spring Boot 技术栈与登录鉴权
Spring Boot 侧的依赖,我选了 Spring Boot Web、MyBatis-Plus、MySQL Driver、Redis、JJWT、Lombok。版本方面,如果你用的是 JDK 8 环境,就老老实实选 Spring Boot 2.7.x;如果能上 JDK 17,可以直接用 Spring Boot 3.x,但要注意 MyBatis-Plus 也换成配套新版本。
鉴权这块我没引入 Spring Security,因为校园商城这种规模不需要那么重的权限框架。一个 HandlerInterceptor 加 JWT 完全够用。
登录接口校验账号密码成功后,生成包含用户 id 和角色的 JWT,返回给前端。前端把 token 存在 localStorage 里,每次请求通过 axios 请求拦截器在 header 里加上:
Authorization: Bearer <token>后端写一个拦截器,在 preHandle 里解析 token,校验通过就放行,失败就返回 401。登录、注册、商品列表这些公开接口单独配置放行规则,其余接口全部过拦截器。
统一异常处理建议从第一天就做。用 @RestControllerAdvice 捕获业务异常、参数校验异常、兜底异常,统一返回:
{ "code": 500, "message": "具体错误信息", "data": null }这样前端拦截器只需要认这一种协议,所有错误提示都可以统一处理,不用每个页面都写 try catch。
3.2 下单流程的并发处理细节
下单是商城系统里最容易出事故的环节,我把整个流程的顺序展开讲一遍。
标准下单链路是:
- 前端携带 token 和勾选好的商品清单调用下单接口。
- 后端校验用户身份,校验商品是否存在并且已上架。
- 计算订单总金额,创建订单主表,状态为待付款。
- 遍历订单明细,逐条执行库存条件更新 SQL。
- 只要有任意一条商品库存不足,整个事务回滚,接口返回库存不足。
这里有一个取舍问题:到底是先创建订单再扣库存,还是先扣库存再创建订单?我的建议是两者放在同一个事务里,哪个先执行其实不重要,关键是最后统一提交。实践中一般先写订单主表,拿到订单 id 后再写明细并同步扣库存,最后整体提交。这样一旦中途出错,所有操作都能回滚,不会出现订单建了但库存没扣,或者库存扣了但没有订单的情况。
还有一个细节是,购物车多选合并下单时,不同购物车条目里可能出现同一个商品。比如用户把某个零食加了两条记录进去,下单前一定要在服务层按商品 id 做聚合,把数量合并后再执行扣库存逻辑,否则会生成两条明细,库存扣两次。
3.3 Node.js BFF 层怎么搭
Node.js BFF 我用的框架是 Express,配合 axios 做请求转发,socket.io 做 WebSocket 推送。这个 BFF 层核心职责只有三个:转发、聚合、推送。
转发最简单,就是接收前端请求,把请求转发到 Spring Boot 对应接口,拿到响应后原样返回给前端。用 http-proxy-middleware 可以很轻量地实现,也可以手动用 axios 转发。手动转发的好处是可以自定义超时、重试和错误处理。
聚合是指 BFF 可以把多个上游接口返回的数据拼装成一个结果。比如商品详情页,除了商品基本信息,页面还要展示同类推荐商品。前端如果分别调两个接口,页面就要处理两次加载状态。在 BFF 里可以用 Promise.all 并发请求两个接口,一次性拼装好返回给前端。这样前端只面对一个接口,加载逻辑更简单。
为了区分内网调用和外部请求,Spring Boot 调用 Node.js 的通知接口一定要加内部密钥校验。比如约定 header 里带一个名叫 X-Internal-Token 的字段,Node.js 侧每次校验,防止这个端口暴露后被恶意刷。
3.4 WebSocket 实时订单推送
WebSocket 推送这部分是 Node.js 在整套系统里最有存在感的模块。
socket.io 连接默认会先尝试轮询,再升级到 WebSocket,这种机制在校园内网环境下非常稳定。连接鉴权方面,我在 socket.io 握手时解析 JWT,token 有效才允许连接建立。
用户端连接时带上 userId,商家端连接时带上 shopId。BFF 层在内存里维护一张映射表:userId 对应 socketId 列表,shopId 对应 socketId 列表。推送订单消息时,根据目标角色的 id 找到所有 socketId,逐个发送消息。
用户下单成功后,Spring Boot 通过内部接口通知 Node.js,Node.js 拿到新订单数据,根据订单归属的店铺 id,把消息推给该店铺所有在线的商家连接。商家后台页面收到消息后弹出一个新订单提醒,这种体验比前端轮询好非常多。
如果你在部署时发现 socket.io 连接不稳定,先检查这几个点:服务器防火墙是否放行了对应端口;Nginx 是否代理了 /socket.io 路径;socket.io 是否需要配置跨域源。校园内网场景通常问题不大,云服务器部署时需要仔细排查。
4. Vue 前端工程化与页面实现
4.1 工程初始化与目录组织
前端用 Vite 创建 Vue3 项目,依赖方面装了 Pinia、Vue Router、Element Plus、axios。用户端我用轻量自定义组件展示商品卡片,商家后台大量使用 Element Plus 的表格、表单、弹窗组件。
目录组织建议按业务模块拆分,不要按文件类型堆成一坨。我常用的划分方式是这样的:
- src/api:接口请求统一放这里,按业务拆成 user.js、product.js、order.js。
- src/views:页面级组件,用户端和商家端分目录放。
- src/router:路由配置,按角色权限拆分。
- src/store:Pinia 仓库,存用户信息、购物车数量、通知消息这些全局状态。
- src/components:通用业务组件,比如商品卡片、订单状态标签、空状态占位。
这样组织的核心价值是,新增一个功能页面时,视图、接口、状态三个位置各改一处,不会满项目找文件。
4.2 请求封装、鉴权与会话保持
axios 封装是前端工程质量的分水岭。我在项目里做了两块逻辑:请求拦截器和响应拦截器。
请求拦截器统一从 Pinia 仓库拿 token,塞进 Authorization header。响应拦截器统一处理后端返回的 { code, message, data } 结构:
- code 为 200 时直接返回 data,页面里拿到的就是业务数据。
- code 为 401 时清除本地登录态,跳转登录页。
- code 为其他值时用 Element Plus 弹出 message 提示。
国内团队很常见的一种约定是,后端所有接口无论业务成败都返回 HTTP 200,真正的错误码放在 body 里的 code 字段。这样前端只需要处理一种 HTTP 状态,省事很多。但前提是前后端必须严格约定 code 的枚举含义,不能出现这个接口返回 400 那个接口返回 500 的混乱局面。
路由守卫方面,我在 Vue Router 的 beforeEach 里判断用户状态。如果访问商家后台而当前角色不是商家或管理员,直接重定向到首页并提示无权限。另外要注意,刷新页面后 Pinia 里的状态会清空,需要在应用初始化时从 localStorage 恢复用户信息,或者调用一次获取当前用户信息的接口。
4.3 核心页面与交互实现
商品列表页是整个用户端的流量入口。我按分类 Tab 加载数据,配合搜索框做模糊查询。每切一个分类就拉一次接口,商品卡片用网格布局展示,点击商品进入详情页。
购物车页面是交互细节最多的一个页面。修改数量时调用后端更新接口,同时给按钮加 loading 状态防止连点触发重复请求。勾选商品参与结算,结算时把选中商品和数量传给下单接口。这里有个小建议:购物车列表的勾选状态不要只存在前端,最好在后端也存一份,否则用户刷新页面后勾选状态丢失,体验很难受。
下单页需要处理收货方式选择,到店自提就填预计取货时间,宿舍配送就填楼栋和宿舍号。提交后进入收银台页面,校园场景没有真实支付通道,我做的是一套模拟支付流程,后端创建一个待付款订单,前端点确认支付后直接调用支付回调接口模拟支付成功。后续如果要接真实支付渠道,只需要替换支付网关接口,业务流程不需要动。
商家后台的商品管理用 el-table 展示,操作列放编辑、上下架、删除按钮。新增或编辑商品用 el-dialog 弹窗包裹表单,图片上传使用 el-upload,上传成功后把返回的文件路径存进表单字段。订单列表默认按创建时间倒序,每种状态用不同颜色的标签区分,商家看到新订单提醒后点进去就能直接操作发货。
5. 联调与部署上线的完整流程
5.1 本地开发环境的快速搭建
本地联调时,三个服务分别跑在不同端口上:
- Spring Boot 主服务:8080。
- Node.js BFF:3000。
- Vue dev server:Vite 默认 5173。
本地方案的关键是 Vite 代理。前端所有请求都带 /api 前缀,Vite 配置里让 /api 转发到 Node.js 的 3000 端口:
server: { proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true } } }Node.js 再把请求转发到 Spring Boot 的 8080。三个进程都启动后,打开前端地址就可以走完整链路,整个过程不需要任何跨域配置,因为浏览器看到的永远只有前端自己的地址。
5.2 前后端联调的关键问题
联调是前后端扯皮的重灾区,这几年我遇到的最大问题集中在三处。
第一,字段名对不上。Spring Boot 返回驼峰命名,前端变量也建驼峰,本来没矛盾。但只要后端某个接口在 SQL 里用了别名,或者 BFF 层做了字段裁剪,返回值就可能缺字段,前端拿不到数据直接白屏。联调之前必须把接口文档里的响应结构敲死。
第二,错误码不统一。前端拦截器只处理统一返回结构,但不同后端开发写出来的接口,有的失败时返回空 data 而不是抛异常,有的把参数校验失败写成了 HTTP 500。联调前一定要约定好,业务失败也必须返回统一的 body 结构。
第三,本地跨域。如果前端没有配代理,直接请求 Node.js 地址,就会触发跨域。建议开发期完全依赖 Vite proxy,不要给 Node.js 单独配置 CORS,这样生产环境也只会通过 Nginx 代理,不存在两套跨域策略打架的问题。
5.3 生产环境部署与进程管理
上线时我用的最简单的单机部署方案:一台 Linux 服务器,同时跑 Nginx、Spring Boot、Node.js 和 MySQL。
前端构建命令:
npm run build构建完的 dist 目录拷贝到 Nginx 的 html/shop 目录。Nginx 配置两个 location 规则,静态文件直接返回,/api 和 /socket.io 前缀代理到 Node.js 的 3000 端口。前端路由是 history 模式,必须加 try_files 回退规则,否则刷新页面会 404:
location / { try_files $uri $uri/ /index.html; } location /api { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; } location /socket.io { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }Spring Boot 打 jar 包,用 nohup 启动:
nohup java -jar app.jar --spring.profiles.active=prod > app.log 2>&1 &Node.js BFF 我用 PM2 管理,长处是崩溃自动重启、日志集中管理、开机自启配置方便:
pm2 start ecosystem.config.js pm2 save pm2 startup提示:Spring Boot 生产环境的数据库密码、密钥这些信息不要写在 application.yml 里,建议用环境变量或者外置配置文件覆盖。服务之间的内部接口不要暴露到公网,尽量走内网通信。
5.4 数据备份与日常运维
数据备份这块,我加了一个简单的 crontab 定时任务,每天凌晨用 mysqldump 全量备份数据库,保留最近 7 天的备份文件。校园小卖部体量,完全够用。
0 2 * * * mysqldump -u root -p`密码` shop > /backup/shop_$(date +\%Y\%m\%d).sql日常运维需要关注的就三件事:磁盘空间、MySQL 慢查询、Nginx 错误日志。校园项目一般没有特别大的并发,只要数据库连接池配得合理,磁盘监控到位,基本不会出大问题。
6. 常见问题速查与避坑技巧
6.1 登录鉴权与跨域问题
现象:接口返回 401,明明刚调完登录接口,后续所有请求还是失败。
排查顺序:
- 登录接口是否真的返回了 token。
- axios 请求拦截器是否正确设置了 Authorization header。
- 后端拦截器是否放行了登录、注册这些公开接口。
- 刷新页面后立即 401,多半是 Pinia 里的 token 没有从 localStorage 恢复。
现象:本地开发打开前端页面,请求一直跨域。
排查顺序:
- 确认访问的是 Vite 提供的 5173 地址,不是直接双击 HTML 文件。
- 确认 Vite proxy 的 target 指向 Node.js BFF 地址,不是 Spring Boot 地址。
- 确认 Node.js 接收的路径前缀和前端请求的路径一致。
6.2 并发场景与库存问题
现象:同时下单时库存变成负数。
排查方向:
- 扣库存 SQL 有没有带 WHERE stock >= 条件。
- 扣库存和创建订单是不是在同一个事务里。
- 前端是不是没有做按钮防重复提交,同一请求在短时间内被发了多次。
现象:定时任务把已取消订单的库存重复加回去了。
排查方向:
- 看订单状态更新是不是用的原子 UPDATE 语句,就是 UPDATE 订单表 WHERE status = 'PENDING_PAYMENT' 这种写法。
- 检查代码里是不是先 SELECT 查状态再更新,如果是,一定要改成条件更新。
6.3 前端构建与部署问题
现象:dist 部署到 Nginx 后,刷新带路径参数的页面出现 404。
原因:Vue Router history 模式在刷新时会向后端请求这个路径的资源,Nginx 默认找不到对应文件就返回 404。配置 try_files $uri $uri/ /index.html; 就能解决。
现象:浏览器控制台报 socket.io 连接失败。
排查方向:
- 确认 socket.io 请求最终打到了 Node.js 服务。
- 确认 Nginx 代理了 /socket.io 路径,并且配置了 WebSocket 升级头。
- 确认 socket.io 的跨域源配置和前端实际域名一致。
6.4 排查工具与日志
排查问题的基础是日志。Spring Boot 侧打印 SQL 和请求日志,Node.js 侧打印转发日志和推送日志,前端侧保留请求和响应日志。生产环境排查问题时,我会第一步先看三处日志的报错时间点,再对应分析。有一个小工具建议装一下:Redis 的慢日志、MySQL 的慢查询日志,这两个对排查性能问题特别有参考价值。
7. 复盘总结与实用建议
做了这个项目之后,我对多技术栈协作的体会深了一些。Spring Boot、Node.js、Vue 这三个东西,本质上谁也不该替代谁,它们的边界本来就不同:Spring Boot 强在稳定的事务和数据一致性,Node.js 强在 IO 密集型请求和实时场景,Vue 强在快速构建复杂前端交互。真正把项目拖垮的往往不是技术栈选得不对,而是边界没有划清楚:哪个服务负责什么、接口怎么命名、错误码怎么约定,这些规则在动工前就要定下来。
最后想给准备做类似项目的人一个建议:不要一上来就让 Vue 直连 Spring Boot 的每个接口,也不要一上来就把 BFF 层写得特别重。最稳妥的路径是,先把核心业务跑通,再用 Node.js 把接口聚合和 WebSocket 推送加进去,最后根据实际情况决定要不要把 BFF 做强。项目是拿来解决问题的,不是拿来秀技术组合的。
还有一个小技巧可以分享:socket.io 的 Room 机制特别适合商家端多人同时在线的场景。一个店铺如果老板和店员两个账号同时登录,推送订单提醒时给该店铺的 room 里所有连接都发一条消息,就不需要逐个维护 socketId 列表了。这个细节在实际开发里非常省心。