1. 项目背景与核心需求分析
1.1 机关食堂的痛点与“智慧化”到底要解决什么问题
这个项目是我去年带团队给一家机关单位做的智慧食堂后勤管理系统,接这个活的时候单位后勤处那边的食堂管理还处在“手工+Excel”的阶段:每天用餐人数靠各科室报数,菜品采购靠厨师经验估算,结算窗口还用的是老式刷卡机,数据没法汇总。最要命的是机关食堂有严格的用餐时段,一到饭点所有人都涌进来,经常出现排队半小时、打菜三分钟的情况。
所谓的“智慧食堂”,不是简单装几台闸机就完事。核心要解决的是三件事:第一,让职工能提前知道今天吃什么、提前订餐,减少现场打菜时间;第二,让后厨和采购能看到实时订餐数据,按需备餐,减少浪费;第三,让财务和后勤领导能看到每一笔消费、每一批食材的去向,真正做到账目可追溯。这个系统最终的技术形态采用了SpringBoot + Vue + SpringCloud的微服务分布式架构,而不是传统的单体应用,原因后面我会细说。
如果你正准备做类似的后勤管理、企业园区或机关单位的进销存/订餐类系统,这篇分享会非常有用。我会把需求拆解、技术选型、微服务拆分、关键代码、部署配置,以及我踩过的坑全部聊一遍,尽量做到你照着思路能复现一个可用的版本。
1.2 系统核心功能模块清单
先列一下这个系统最终交付的功能清单,方便后续对照:
- 用户端(微信/钉钉内嵌H5 + PC Vue后台):菜品浏览、按周菜谱预览、在线订餐、取消订餐、个人消费记录、余额充值、取餐码展示。
- 食堂管理端(Vue PC管理后台):菜品管理、菜谱管理、库存管理、采购计划生成、档口/窗口管理、员工管理、食堂公告管理。
- 后勤财务端:订单汇总、营收统计、补贴规则配置、食材成本核算、供应商对账报表。
- 运营与权限:基于RBAC的角色权限、操作日志、数据脱敏、多食堂/多组织架构支持。
- 基础能力:短信/微信模板消息通知、文件上传(菜品图片、视频展示)、报表导出、数据大屏。
你可能觉得这些模块听起来不复杂,但一旦用微服务拆开,每个模块之间要处理的数据一致性、接口权限、分布式事务问题就全冒出来了。这里我想强调:不要因为项目叫“智慧食堂”就小瞧它,机关单位对权限和审计的要求非常严格,每个操作都要有日志,账户余额变动必须强一致,这些都是硬指标。
1.3 为什么选择微服务架构而不是单体
很多朋友会问:一个食堂系统而已,单体应用不香吗?确实,如果只是支撑几百人同时订餐,单体完全够用。但当时甲方在招标文件里明确写了“系统须采用微服务分布式架构”,而且要求系统要能够横向扩展,后续可能会接入其他后勤业务,比如车辆管理、访客管理、会议室预约等。如果做成单体,后期这些业务全部堆在一个工程里,想单独扩容某个模块是不可能的。
另外,机关单位的IT环境比较特殊,开发环境、测试环境、生产环境严格隔离,网络策略复杂。用微服务可以把不同模块部署到不同的服务器甚至不同的网段,某个服务出问题不会拖垮全部功能。但代价就是架构复杂度指数级上升:服务注册发现、配置中心、网关、分布式事务、分布式锁,每一块都要搭。
我的建议是:如果你们团队没做过微服务,不要一上来就搞全套。先确定哪些模块必须独立(比如订单、支付/余额、库存),哪些暂时可以塞进一个基础服务里(比如用户、权限、公告),后期再拆。我们最终拆成了6个服务,这个拆分粒度是踩过坑后调整出来的,后面详细讲。
2. 技术选型与架构设计
2.1 技术栈总览:SpringBoot + Vue + SpringCloud 的组合逻辑
直接说结论,这套技术栈是当前国内做微服务管理系统最稳妥的组合,没有之一。
后端以SpringBoot为微服务基础框架,每个服务都是一个独立的SpringBoot应用。我们用的SpringBoot 2.7.x,没上3.x,因为当时SpringCloud Alibaba对3.x的支持还不稳定,而且大量老项目依赖的jar在JDK8下是最稳的。这里特别提醒:如果你是2025年之后才新开的项目,SpringBoot 3.x已经很成熟了,可以直接上,但一定要先确认SpringCloud、SpringCloud Alibaba、MyBatis-Plus这些中间件的版本兼容矩阵。我们当时吃过高版本的亏,后面踩坑部分会提。
SpringCloud我们使用的是SpringCloud Alibaba全家桶:
- Nacos作为注册中心和配置中心;
- Sentinel做接口限流和熔断;
- OpenFeign做服务间调用;
- Gateway做统一网关;
- Seata处理分布式事务;
- Redisson做分布式锁。
前端就是标准的Vue 3 + Vite + Element-Plus + Pinia + Vue Router。可能你会问为什么不选React或者Ant Design,原因很简单:项目团队里大多数人熟Vue,而且Element-Plus的中后台组件对管理系统极其友好,表单、表格、弹窗都是一套一套的,显著缩短开发周期。
这套组合最大的优势是“生态闭环”:Nacos负责服务发现,Gateway统一入口,Sentinel护住流量,Seata搞定数据一致,前端Vue直接把组件往下堆。对于机关后勤这种“重权限、重报表、重CRUD”的系统,效率极高。
2.2 微服务拆分与数据库设计
我们最终拆了6个微服务:
| 服务名 | 职责 | 主要业务表 |
|---|---|---|
| gateway-service | 网关,统一路由、鉴权、转发 | 无 |
| auth-service | 认证授权,用户登录、Token签发、权限校验 | sys_user, sys_role, sys_permission |
| user-service | 员工/职工信息管理,组织架构 | emp_info, dept_info |
| catering-service | 核心食堂业务:菜品、菜谱、订餐、取餐、评价 | dish, menu_week, order_info, order_detail |
| inventory-service | 食材库存、采购、供应商管理 | ingredients, stock_flow, purchase_order, supplier |
| report-service | 统计报表、财务报表、数据大屏接口 | report_data, finance_summary |
数据库我们用的是MySQL 8.0,每个服务用独立的数据库,而不是共用一个库。比如catering_db、inventory_db、auth_db等。这是微服务的一个核心原则:数据隔离。如果服务间需要关联查询,一律通过OpenFeign调用接口获取,而不是直接跨库join。刚开始会觉得很麻烦,但好处是后期每个服务可以独立扩展到不同的数据库实例,甚至可以针对报表服务挂一个只读从库。
这里插一个我们踩过的坑:机关单位的数据权限要求按科室隔离。比如某个科长只能看自己科室人员的订餐记录,因为涉及员工隐私。所以我在所有核心业务表里都加了dept_id字段,查询的时候通过MyBatis-Plus的拦截器自动拼上数据权限条件。这个功能如果你在原生的MP上配置,需要自己写拦截器,我建议直接用若依微服务Plus的底层改造,它的数据权限体系非常成熟。
2.3 前端Vue工程结构与路由设计
前端工程结构大致如下:
smart-canteen-web/ ├── src/ │ ├── api/ # 按服务模块封装的请求接口 │ ├── assets/ │ ├── components/ # 通用组件:上传、图片预览、分页等 │ ├── layout/ # 后台布局:侧边栏、头部、标签页 │ ├── router/ │ │ └── index.js # 路由配置 │ ├── store/ # Pinia状态管理 │ ├── views/ │ │ ├── system/ # 系统管理页面 │ │ ├── canteen/ # 食堂业务页面 │ │ ├── inventory/ # 库存采购页面 │ │ └── report/ # 报表页面 │ ├── utils/ # 请求封装、工具函数 │ ├── App.vue │ └── main.js路由设计这块必须说说动态路由,因为不同角色能访问的菜单不一样。我们采用了“前端静态路由+后端动态权限菜单”的方式:用户登录后从auth-service拉取当前用户的菜单列表,然后通过Vue Router的addRoute动态添加。这种实现比把所有路由写在静态表里再根据角色判断要灵活得多,尤其适合机关单位经常调整权限的场景。
简单示例:
// 登录后拉取菜单 const menuList = await getCurrentUserMenus(); menuList.forEach(menu => { const route = { path: menu.path, name: menu.name, component: () => import(`@/views/${menu.component}`), meta: { title: menu.title, icon: menu.icon } }; router.addRoute('Layout', route); });注意这里有个坑:动态路由刷新页面后会丢失,因为Pinia和Vue Router的路由表是内存态,刷新后要从后端重新拉取。我们解决的办法是在路由守卫router.beforeEach里判断用户信息,如果存在就重新拉取菜单并动态添加。
2.4 网关、认证与配置中心选型
网关我们用的SpringCloud Gateway。一开始想过Zuul,但SpringCloud官方已经主推Gateway了,它是基于WebFlux的响应式网关,性能比Zuul 1.x好得多。主要配置了全局过滤器做JWT的解析和放行,把Token中的用户ID解析出来放到请求头里,转发给下游服务。
流程是这样的:
- 前端登录后,auth-service签发JWT Token。
- 前端发起请求时在Header带
Authorization: Bearer xxx。 - Gateway的GlobalFilter解析Token,校验签名和有效期,同时把
userId和deptId写入Header,例如X-User-Id、X-Dept-Id。 - 下游微服务从Header取用户信息,不再重复解析Token。
为什么要在网关解析而不是每个服务解析?因为如果每个服务都做一遍Token校验,代码重复是一方面,更重要的是维护成本高,换个密钥你得改一堆服务。网关是唯一入口,在这里统一处理最合理。
配置中心用的Nacos。每个服务的application.yml里只保留最小配置,其余全部放到Nacos的配置列表里,例如数据库连接串、Redis地址、消息队列Topic、自定义业务参数等。这样改配置不用重新打包部署,Nacos支持配置热更新,非常方便。但要注意热更新只能对@ConfigurationProperties和@RefreshScope标注的Bean生效,不是所有配置都能热更新,这点很多新手容易忽略。
3. 核心功能实现与实操细节
3.1 菜品管理与图片/视频文件上传(MinIO+m3u8播放)
食堂点餐和普通电商不一样,用户必须先看到菜品照片才能决定要不要点,所以菜品图片是刚需。菜品图片又是典型的“文件上传”场景,我直接推荐MinIO,没有用阿里云OSS,因为机关单位的数据大部分要求内网部署,公网OSS不一定合规。MinIO是开源对象存储,可以很方便地部署在局域网内,兼容S3协议。
SpringBoot集成MinIO很简单,核心依赖和配置如下:
<dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> <version>8.4.3</version> </dependency>minio: endpoint: http://192.168.10.50:9000 access-key: canteenadmin secret-key: canteen-secret bucket: canteen-image写一个工具类,封装上传和获取预签名URL:
public String uploadFile(MultipartFile file) { String fileName = UUID.randomUUID().toString() + "." + getExtension(file.getOriginalFilename()); try { minioClient.putObject(PutObjectArgs.builder() .bucket(bucketName) .object(fileName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build()); return fileName; } catch (Exception e) { throw new RuntimeException("上传失败"); } }前端展示的时候,不要直接拼endpoint+桶名+文件名作为公开URL。MinIO的桶默认是私有的,建议生成有效期的预签名URL返回给前端。之前为了省事把桶设成public,结果任何人都能访问,而且把MinIO服务的地址暴露到了公网,内部审计的时候被批了一顿。正确做法是后端生成URL:
String url = minioClient.getPresignedObjectUrl( GetPresignedObjectUrlArgs.builder() .method(Method.GET) .bucket(bucketName) .object(fileName) .expiry(3600) .build());还有一个热词是“vue播放m3u8免安装”。这是怎么来的?因为食堂菜品除了图片,我们还做了每道菜的小视频展示,有些甲方喜欢看到厨师做菜的过程。视频我们切流成HLS(m3u8),但前端如果不做任何处理,原生video标签是播不了m3u8的(Safari除外)。在Chrome下,要让video支持m3u8播放,最常用的方案是使用hls.js这个库。它不要求额外安装播放器插件,纯JS解析m3u8并用Media Source Extensions播放。
Vue3中一个简单的封装:
<video ref="videoEl" controls></video> <script setup> import Hls from 'hls.js'; import { onMounted, ref } from 'vue'; const videoEl = ref(null); const src = 'http://your-server/live/dish.m3u8'; onMounted(() => { if (Hls.isSupported()) { const hls = new Hls(); hls.loadSource(src); hls.attachMedia(videoEl.value); } else { videoEl.value.src = src; } }); </script>这就满足了“免安装”的需求,用户打开网页就能看。
3.2 订餐与库存扣减的分布式事务
先说说一个机关单位食堂订餐的流程:用户提交订餐,系统要扣减个人账户余额,同时要减少一份菜品对应的库存,还要生成一条订单记录。这三件事分布在catering-service和inventory-service两个服务里。
在单体应用里,这个操作可以放在同一个本地事务里,一个方法加上@Transactional就完事了。但在微服务里,跨服务的两个事务没法用本地事务保证。这里我们用了Seata的AT模式,也就是自动补偿模式。AT模式的原理是:在每个分支事务执行完SQL后,Seata会记录数据快照,如果后续有分支失败,根据快照逆向补偿,把之前的修改撤销。
引入Seata后,服务间的调用只需要在Feign接口加一个注解:
@FeignClient(name = "inventory-service") public interface InventoryFeignClient { @PostMapping("/stock/deduct") boolean deductStock(@RequestParam("dishId") Long dishId, @RequestParam("count") Integer count); }然后在发起方(catering-service)的方法上加上@GlobalTransactional:
@GlobalTransactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class) public void createOrder(CreateOrderDTO dto) { // 1. 生成订单 orderMapper.insert(order); // 2. 调用库存服务扣减库存 inventoryFeignClient.deductStock(dto.getDishId(), 1); // 3. 扣减用户余额(在catering-service内) balanceMapper.deductBalance(dto.getUserId(), order.getAmount()); }使用Seata的时候有几个很关键的细节:
- 服务必须向Seata Server注册,保证每个服务的数据源是由Seata代理过的,否则分支事务不会生效。
- 不要在远程调用的方法里开本地事务,或者说,不要让被调用的方法也套上
@GlobalTransactional,只需要在发起方控制全局事务。 - Seata AT模式对数据库的隔离级别有要求,默认是读已提交,但全局事务的隔离级别是“读未提交”的,因为要允许中间状态,这对库存扣减这种强一致场景是可以接受的。
如果你不想引入Seata这么重的框架,也可以使用消息队列+最终一致性,比如用RocketMQ的事务消息。但机关食堂余额扣减和库存扣减之间,业务上要求实时一致,否则用户刚订完餐,库存已经没了,后厨也不知道该不该做。所以我们还是选择了强一致方案。
3.3 高并发下的分布式锁与缓存设计
机关食堂的饭点是“波次并发”:中午11:30开放订餐,可能同一时间几百上千人同时点击订餐。当时我们压测时发现,如果不加锁,同一个用户连续点击两次“提交订单”,可能出现两条重复订单,或者库存扣成负数。
处理方式分两层:
- 第一层:用Redis缓存菜品信息,减少对数据库的冲击。
- 第二层:用分布式锁控制同一个用户、同一个菜品的订餐并发。
Redis缓存很简单,我们用的是SpringCache + Redis:
@Cacheable(cacheNames = "dish:detail", key = "#dishId", unless = "#result == null") public Dish getDishById(Long dishId) { return dishMapper.selectById(dishId); }这里要注意缓存击穿问题。如果某道菜特别热门,第一次查库后放到缓存里,后续请求都走缓存,这是没问题的。但如果缓存在瞬间过期,恰好又有大量请求进来,就会全部打到数据库。解决办法是加“缓存空值”或“逻辑过期”。我们使用的是Caffeine本地缓存+Redis两级缓存,热点数据会被短期放在本地内存里,进一步降低压力。
分布式锁我们用的Redisson,因为它是Redis官方推荐的Java客户端,提供了封装好的RLock,可以设置锁的等待时间和自动过期时间,避免死锁。
典型的订餐逻辑加锁代码:
RLock lock = redissonClient.getLock("lock:order:" + userId); boolean locked = lock.tryLock(3, 5, TimeUnit.SECONDS); if (!locked) { throw new ServiceException("系统繁忙,请稍后重试"); } try { // 校验是否重复订餐 if (orderService.countTodayByUserId(userId) >= 1) { throw new ServiceException("今天已经订过餐了"); } // 执行下单 } finally { lock.unlock(); }这里有个心得:锁的粒度一定要细。一开始我们用了一个全局锁lock:order,结果整个订餐服务几乎串行化,几百个请求打进来,后面的全部超时。后来改成按用户加锁,只有同一个用户才会互相等待,不同用户之间完全无感。对于库存扣减,我们又单独加了按菜品的锁,比如lock:dish:42,这样多个用户订同一个菜时会排队扣减库存,避免库存超卖。
3.4 对接微信公众号通知
机关单位里,很多员工已经习惯用微信公众号接收通知。我们做了微信服务号的模板消息推送,员工订餐成功后自动发一条“订餐成功通知”,包括取餐时间、取餐窗口和菜品名称。
这里提一下“微信公众号测试号服务api对接”这个事。做开发调试时,如果你没有正式的微信服务号,可以用微信公众平台接口测试账号来练手。测试号能申请到几乎全部接口权限,AppID和AppSecret都是测试专用的,非常适合联调。
对接核心步骤:
- 在微信公众平台申请测试号,拿到AppID和AppSecret。
- 在auth-service或者单独的通知服务里,调用微信官方接口
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=xxx&secret=xxx获取access_token。 - 通过模板消息接口发送通知。
这里的坑主要在Token过期时间:微信access_token有效期是2小时,而且每天调用次数有限。我们做了一个WxTokenService,用定时任务每90分钟刷新一次,存到Redis里,多个服务共享同一个token。
模板消息发送的JSON示例:
{ "touser": "openid_xxx", "template_id": "模板ID", "data": { "first": { "value": "您的订餐已确认", "color": "#173177" }, "keyword1": { "value": "红烧排骨套餐" }, "keyword2": { "value": "2025-05-20 12:00" }, "keyword3": { "value": "二楼2号窗口" }, "remark": { "value": "请凭取餐码取餐" } } }记得把员工的微信openid在首次绑定账户时保存下来,否则模板消息发送不到人。绑定流程:员工在公众号菜单点击“绑定食堂账户”,网页授权拿到openid,然后输入工号和手机号验证,验证通过后把openid关联到用户ID。
3.5 打包部署:前端后端分离与云端部署
构建这块被不少朋友问到过,尤其是“vue打包放进springboot中”这种操作。我们的做法是:前端和后端完全分离部署,Nginx托管前端静态文件,后端服务作为独立Java进程跑在服务器上,互不干扰。但在内网小规模部署时,为了省机器,也可以把前端打包后的dist目录放到SpringBoot的resources/static下面,让SpringBoot同时承担静态资源服务。不过这只适合单机演示或数据量很小的场景,真正的生产环境不建议这样混布。
前端构建:
npm install npm run build构建产物在dist/,把里面的文件全部拷到Nginx的html目录,Nginx配置示例:
server { listen 80; server_name canteen.example.local; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }为什么加try_files $uri $uri/ /index.html?因为Vue Router用的是history模式,刷新一个子路由页面,比如/canteen/dish/list,如果Nginx不重写到index.html,会直接404。这个坑非常常见。
后端的每个微服务打包好之后,直接执行:
java -jar auth-service.jar --server.port=8081 java -jar user-service.jar --server.port=8082 java -jar catering-service.jar --server.port=8083 java -jar inventory-service.jar --server.port=8084 java -jar report-service.jar --server.port=8085如果想用脚本一键启动,可以写一个start.sh,按顺序启动Nacos、Seata、Redis、MySQL等基础设施,再启动各业务服务。
机关单位的服务器环境通常比较旧,可能会有JDK版本太老的问题。建议用Docker或K8s来隔离运行环境,我们最终是用docker-compose把所有中间件和业务服务编排起来,各服务通过容器名互相访问,具体编排文件就不贴了,太长。
4. 踩坑记录与排查技巧
4.1 SpringBoot版本过高带来的依赖冲突
开头我提到SpringBoot版本问题。我们第一版开发时急着用新特性,直接选了SpringBoot 3.2.0,然后发现SpringCloud Alibaba对应的版本还没适配好,Nacos客户端一直报“normally connect”的问题。后来查了官方版本说明,才决定整套技术栈降到SpringBoot 2.7.18 + SpringCloud 2021.0.9 + SpringCloud Alibaba 2021.0.5.0。这个组合对应的Nacos Client是2.2.1,Seata是1.6.1。
经验是:不要盲目追求SpringBoot最新版。微服务是一个组合拳,必须保证SpringBoot、SpringCloud、SpringCloud Alibaba、MyBatis-Plus、Redisson这些版本互相兼容。最稳妥的方法是去SpringCloud Alibaba官方看版本发布说明,照着他们测过的组合来。
4.2 Vue播放m3u8免安装的坑
用hls.js播放m3u8时遇到两个问题:
第一,视频流如果来自HTTPS页面,浏览器会禁止播放HTTP(非加密)的视频流,也就是Mixed Content问题。当时测试环境用的是http,一切正常,一上生产环境配了HTTPS,播放器直接报错。解决办法是把视频服务也放到HTTPS后面,或者让Nginx对视频域名做反向代理并开启SSL。
第二,m3u8的跨域问题。MinIO或者流媒体服务如果没有配置CORS,hls.js在加载ts分片时会被浏览器拦截。解决方式是在MinIO桶策略里允许跨域请求:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": ["*"] }, "Action": ["s3:GetObject"], "Resource": ["arn:aws:s3:::canteen-video/*"] } ] }再加上MinIO的CORS配置。这个坑不解决,你会看到播放器一直转圈,但抓包能看到m3u8能请求到,ts分片却全部挂了。
4.3 微服务间调用与分布式事务的边界
Seata虽然能解决跨服务事务,但不是所有场景都该上。我们一开始把“生成订单+扣库存+扣余额+发送微信通知”全部放进一个全局事务,结果每次订餐要等微信通知返回,慢得离谱。
后来我把微信通知从全局事务里拆出来,用异步消息队列去发。因为通知失败了不影响订餐成功,最多用户少收到一条消息,从业务上完全能接受。这引出一个原则:全局事务里只保留必须强一致的操作,通知、日志、报表这类弱一致操作放到异步链路。
还有一个常见问题:Feign调用的超时设置。默认OpenFeign的读取超时是10秒,但如果Seata事务里要等库存服务提交,很可能超过10秒。我们当时调大了Feign的超时时间,并且设置了重试次数,结果因为库存扣减是幂等的,重试导致库存被扣了两次。所以重试策略要非常小心,只对查询接口重试,写操作最好加上幂等控制。我们的做法是让库存服务接收一个幂等键,比如订单号加菜品ID,库存流水表用唯一索引去重。
4.4 常见问题速查表
| 现象 | 原因 | 解决办法 |
|---|---|---|
| Nacos注册不上,报“normally connect” | 版本不兼容或开放端口没开 | 检查9848/8848端口,确认版本兼容矩阵 |
| Vue刷新404 | history路由没有配置try_files | Nginx加try_files $uri $uri/ /index.html |
| m3u8播放黑屏 | 跨域或Mixed Content | 配置CORS,视频服务走HTTPS |
| 分布式锁导致接口变慢 | 锁粒度过粗 | 按用户/菜品加锁,不要用全局锁 |
| Seata事务回滚不生效 | 数据源没有被代理 | 确认seata-datasource-proxy启用 |
| 用户重复订餐成功 | 缺少唯一约束和锁 | 数据库唯一索引+分布式锁双保险 |
| 微信模板消息发送失败 | access_token过期 | 定时刷新并放到Redis共享 |
| 上传文件后图片无法访问 | MinIO桶策略限制 | 使用预签名URL或配置桶读取策略 |
5. 经验总结与扩展建议
5.1 从0到1开发微服务项目的时间线
这里给团队排期做个参考。这个项目总体工期4个月,前后端4人团队:
- 第1个月:需求调研、原型确认、技术预研、搭建脚手架。期间遇到了版本兼容问题,花了将近一周才定下技术版本组合。
- 第2个月:基础服务开发,包括auth-service、user-service、网关、Nacos、前端框架搭建。
- 第3个月:核心业务开发,菜品、订餐、库存、报表,前端页面同步开发。
- 第4个月:联调、压测、部署、修复问题。前面整体还算顺利,最后压测时发现重复订餐和库存超卖的问题,临时加分布式锁,耽误了几天。
如果你们是从单体改造为微服务,时间至少乘1.5。微服务并不是“代码拆分”那么简单,中间件的搭建和运维就得花不少时间。
5.2 后续扩展方向
这个系统交付后,甲方又提了好几个扩展想法,我觉得挺有代表性:
一是把消息通知扩展到钉钉或企业微信,因为机关单位现在也不是只用微信。
二是接入智能餐盘和称重结算设备,这需要做硬件对接,但业务逻辑基本不用动,订餐、扣款、菜单数据都会复用。
三是把食谱和营养分析结合起来,自动计算每餐的卡路里和营养元素,这对机关单位的职工健康管理是加分项。
四是把采购流程做成自动对账,供应商送货单和系统采购单自动匹配,大大减轻后勤财务的工作量。
技术层面可以考虑把报告服务做成独立的报表大屏,用Flask或Node.js单独部署,更轻量;或者引入ClickHouse做历史数据分析,不过目前MySQL加上定时汇总表已经够用了。
最后说一点个人体会,做这类项目最忌讳“技术过剩”。微服务、分布式事务、分布式锁这些技术在特定场景下确实有效,但如果你的业务只有几百个人用、一天几千笔订餐,单体应用加数据库水平分表反而更香。这个项目之所以上微服务,更多是甲方的架构要求,以及未来多业务扩展的考量。如果你的团队是第一次做微服务,建议一定先把SpringCloud Alibaba的官方Demo跑通,理解Nacos、Seata、Sentinel各自的定位,再往业务里套。中间件不能只停留在“会配置”,出问题的时候能看懂日志、定位到是哪个环节慢了,才是微服务实战的硬功夫。