我最早接手这类项目是在做毕业设计辅导的时候,一个学弟拿着“餐饮财务管理系统”的题目来找我,说学校要求用Spring Boot做一套带前端页面的系统,但他连项目结构都还没理清。后来这几年,陆陆续续又帮人看过不少类似的项目,包括本科毕设、课程设计,甚至一些小餐馆老板找人做的内部记账工具。说实话,这类“基于Spring Boot的餐饮财务管理系统”在表面上看起来是个很常规的CRUD项目,但真正动手做起来,里面的门道比想象中多得多。
这套系统解决的核心问题很明确:餐饮行业每天的流水笔数多、来源杂——有前台点餐的,有外卖平台的,有会员储值的,还有供应商采购的支出。如果只靠Excel或者手工账本,月底对账能把人逼疯。而一套称职的餐饮财务管理系统,至少要把“收入、支出、菜品、库存、报表”这几条线串起来,让老板打开系统就能看清今天赚了多少、哪些菜卖得好、这个月食材成本有没有超标。
这篇文章我就以这个项目为引子,把我实际做这类系统时的完整思路写下来。内容包括业务流程怎么拆解、数据库怎么设计、Spring Boot后端怎么搭、权限和财务数据安全怎么处理,以及那些文档里不会写、但你在答辩或实际上线时一定会遇到的坑。无论你是准备拿这个题目做毕设,还是真的想给自家餐馆搞一套内部管理系统,这篇都能给你一个可以直接照着做的参考。我会尽量说人话,不堆术语,但该讲清楚的技术细节一处都不会省。
1. 别急着写代码:餐饮财务系统的业务流程拆解
很多新手拿到这个题目,第一反应就是建表、写接口。但餐饮财务系统和普通的后台管理系统有个本质区别:它的数据是流动的,从点餐到结账到入账,再到采购和成本核算,整条链路是闭环的。你如果不先把业务流程理顺,写出来的代码大概率是“能跑但没法用”。
1.1 两条核心数据链路:钱怎么进来,钱怎么出去
餐饮财务,说到底管的就是现金流。我习惯把整个系统的数据流拆成两条主链:
收入链(钱怎么进来):顾客到店 -> 服务员开台/点餐 -> 后厨出菜 -> 顾客结账(现金/扫码/会员卡) -> 订单状态变为已支付 -> 流水进入当日营收。
支出链(钱怎么出去):食材采购 -> 供应商送货 -> 验收入库 -> 库存更新 -> 后厨领料出库 -> 月末盘点 -> 成本核算。
这两条链交汇的地方是菜品。菜品既是收入端(卖出去了产生营业额),又是支出端(做一道菜要消耗食材,食材是成本)。所以菜品的毛利计算,本质上是把收入链和支出链的数据在菜品位上做了一次对齐。
我在设计系统的时候,会先画一张这样的逻辑图,而不是直接开写。哪怕你不画图,脑子里也必须有这个概念:所有财务模块的数据最终要能回溯到一张原始单据——卖出去的每一笔钱要能查到对应的订单,花出去的每一笔钱要能查到对应的采购单或费用单。这是财务系统的底线,和用什么技术框架无关。
1.2 角色权限设计:老板、收银员、后厨看到的是不同的东西
餐饮系统里至少有三类角色,他们的诉求完全不同:
| 角色 | 核心诉求 | 系统应该给他们什么 |
|---|---|---|
| 老板/财务 | 看总账、看毛利、看趋势,关心钱去哪了 | 报表、图表、日结汇总、成本分析 |
| 收银员/服务员 | 开台、点菜、结账、打印小票要够快 | 简洁的点餐界面、快速结账、桌台状态 |
| 后厨/库管 | 看今天要备什么菜、领了什么料、库存还够不够 | 菜品估清、备货清单、领料出库、库存预警 |
如果你用的是Spring Boot + Vue这种前后端分离的结构,权限这块建议直接用Spring Security + JWT来做,但别再自己手写拦截器判断角色了。我见过太多项目在Controller里写if(user.getRole().equals("admin"))这种散装权限,后期加一个角色就要改几十个接口,维护成本极高。
正确做法是:用Spring Security的注解(比如@PreAuthorize("hasAuthority('finance:report:view')"))把权限粒度细化到操作级别,前端用Vue Router的守卫控制页面跳转,后端用注解兜底校验。记住一个原则:前端控制的是用户体验,后端控制的是数据安全。
1.3 功能模块的取舍:第一版别贪多
很多参考论文里的餐饮系统恨不得把会员营销、进销存、人力排班、供应链全塞进去。但你如果是做毕设或者给小店做系统,第一版我强烈建议只做四个核心模块:
- 桌台与点餐管理:桌台状态(空闲/占用/结账中)、开台、点菜、换桌、并桌。这是收入流水的源头。
- 菜品管理:菜品分类、菜品CRUD、上下架状态、菜品估清(卖完了就标记)。
- 采购与库存管理:供应商信息、采购单录入(支出流水的源头)、食材入库、库存预警。
- 财务与报表:日结汇总(当日收入/支出/毛利)、按时间段的营业报表、菜品销售排行、库存成本核算。这是整个系统的出口,也是老板唯一真正关心的地方。
把这三个模块做扎实,比堆砌十个半成品模块要值钱得多。我辅导过的学生里,凡是主动砍掉“会员积分”“拼团秒杀”“员工排班”这些花架子的,最后答辩时都能把核心逻辑讲透,老师反而会高看一眼。
2. 数据库设计是财务系统的命根子
代码写得烂还能靠重构救,数据库设计错了直接推翻重来。餐饮财务系统涉及钱,数据的一致性和可追溯性比啥都重要。我设计数据库时有几条铁律,你可以直接抄。
2.1 核心表结构:一张图理清十来张表的关系
一个标准的餐饮财务系统,数据库表通常在12到16张左右。我把核心表列出来,你建表的时候照这个框架去扩展就行:
基础数据表:
sys_user(用户表):id, username, password, real_name, role_id, statussys_role(角色表):id, role_name, role_code, description(建议用role_code做权限判断,别用中文名)category(菜品分类表):id, name, sort, statusdish(菜品表):id, category_id, name, price, cost, image, status(status字段控制上下架和估清)dining_table(桌台表):id, table_no, capacity, status(空闲/占用/结账中)
业务流水表:
orders(订单主表):id, order_no, table_id, user_id(操作员), total_amount, pay_type, status, create_time——status建议用数字枚举:0未支付、1已支付、2已退款、3已作废order_detail(订单明细表):id, order_id, dish_id, dish_name(冗余字段,防止菜品改名后历史订单查不到), price, quantity, amountsupplier(供应商表):id, name, contact, phone, address, statuspurchase_order(采购单主表):id, purchase_no, supplier_id, total_amount, operator_id, status, create_timepurchase_detail(采购明细表):id, purchase_id, ingredient_id, quantity, price, amount——注意这里关联的是**食材(ingredient)**而不是菜品ingredient(食材表):id, name, unit, stock, warn_stock, pricefinance_record(财务流水表):id, record_no, biz_type(收入/支出), biz_no(关联订单号或采购单号), amount, pay_type, create_time, remark
最后这张finance_record表是点睛之笔。它把订单和采购单里涉及钱的数据全部冗余了一份,做日结报表的时候直接查这张表,性能又快又不容易漏。这也是为什么很多初版系统对不上账的原因:报表直接从订单表里聚合,结果退款单、作废单没有过滤,数字就脏了。
2.2 冗余字段和逻辑删除:财务系统的两个隐藏要求
在做业务表的时候,有两件事新手最容易忽略:
第一是冗余业务字段。比如订单明细表里存了dish_name,采购明细表里存了ingredient_name,而不是只用外键关联。为什么?因为菜品和食材是会被改的——厨师长觉得“宫保鸡丁”太难听,改成了“宫保鸡丁(微辣版)”,如果不冗余,三个月前的订单明细里这道菜就变成了“未知菜品”,月底对账的时候你根本没法跟店长解释。这就是典型的“业务数据要保留历史快照”思想。
第二是逻辑删除大于物理删除。财务系统的任何记录,千万不要用DELETE FROM直接物理删除。订单录错了要作废,采购单填错了要冲红,这些都是业务操作,要留下痕迹。我一般会加一个is_deleted字段,默认0,删除时置为1。查询时全局过滤掉已删除数据,但底层数据永远在。这不是矫情,是财务审计的基本要求——你删掉的每一笔账,将来都可能要回来查。
2.3 索引和字段类型:细节决定查询有多快
还有一个实际测试中才会暴露的问题。orders表数据量到十万条之后,不带索引的create_time范围查询会慢到你想骂人。我在orders、order_detail、purchase_order、finance_record这几张表上都建了这些索引:
create_time单独建索引(按日期查报表必用)order_id在order_detail上建普通索引(查订单详情必用)status和create_time建联合索引(查“某时间段内已支付的订单”时效率翻倍)
字段类型方面,所有金额字段一律用DECIMAL(10,2),禁止用FLOAT或DOUBLE。这不是老顽固,而是浮点数在二进制里存在精度误差,0.1 + 0.2 计算出来是 0.30000000000000004。财务数据出现这种误差,月底平账的时候你会疯掉。Java端对应的是BigDecimal,这个没得商量。
3. Spring Boot 后端落地:项目结构、核心接口与事务控制
技术选型上,这个项目我用的是 Spring Boot 2.7.x + MyBatis-Plus + MySQL 8.0 + Redis(可选)+ Spring Security。为什么是Spring Boot 2.7而不是3.x?因为3.x是基于Jakarta EE的,很多老教程和插件还停留在javax命名空间,毕设或小团队项目没必要冒这个兼容性的险。Spring Boot 2.7还在社区主流维护期内,资料多、坑少,稳妥优先。
3.1 项目目录结构:约定优于配置
我见过太多Spring Boot项目,Controller里塞了三百行业务逻辑,Service层形同虚设。一个清晰的项目结构长这样:
com.example.restaurant ├── controller/ # 接口层,只做参数接收和结果封装 ├── service/ # 业务层,核心逻辑都在这 │ └── impl/ ├── mapper/ # MyBatis-Plus的Mapper接口 ├── entity/ # 数据库表对应的实体类 ├── dto/ # 接收前端参数的传输对象 ├── vo/ # 返回给前端的视图对象 ├── config/ # 配置类(Security配置、MyBatisPlus配置、跨域配置等) ├── common/ # 通用类(统一返回结果、异常处理、常量类) ├── utils/ # 工具类 └── RestaurantApplication.java这个结构用一句话总结就是:Controller不写业务、Service不写SQL、Mapper只写数据库操作。分层清晰了,后面所有的问题都好排查。
3.2 核心接口示例:点餐、结账、日结的代码写法
我挑三个关键接口的实际写法给你看,都是我验证过能直接用的。
点餐接口:这里的关键是事务。一次点餐要同时更新orders表、插入order_detail明细、更新桌台状态为“占用”,任何一个环节失败都不能留下半截数据。所以必须加@Transactional注解:
@Transactional public boolean createOrder(CreateOrderDTO dto) { // 1. 校验桌台状态,占用中不能重复开台 DiningTable table = diningTableMapper.selectById(dto.getTableId()); if (table.getStatus() != 0) { throw new BusinessException("该桌台当前不可用"); } // 2. 生成订单号和明细 Orders order = new Orders(); order.setOrderNo(generateOrderNo()); // 例如:20240615 + 6位随机数 order.setTableId(dto.getTableId()); order.setStatus(0); // 未支付 order.setTotalAmount(calculateTotal(dto.getItems())); ordersMapper.insert(order); // 3. 插入订单明细 for (OrderItemDTO item : dto.getItems()) { OrderDetail detail = new OrderDetail(); detail.setOrderId(order.getId()); detail.setDishId(item.getDishId()); detail.setDishName(dishMapper.selectById(item.getDishId()).getName()); detail.setPrice(item.getPrice()); detail.setQuantity(item.getQuantity()); orderDetailMapper.insert(detail); } // 4. 桌台状态置为占用 table.setStatus(1); diningTableMapper.updateById(table); return true; }注意那个generateOrderNo(),千万不要用数据库自增ID当订单号给顾客看,不然竞对一看你的订单号就知道你一天做了多少单。我的做法是:yyyyMMddHHmmss+ 4位随机数,订单号看起来像202406151430251234,长度稳定又不泄露业务量。
结账接口:这涉及钱的变更,逻辑上要更谨慎。除了改订单状态,还要把流水写入finance_record。所以我把这两个动作放在同一个事务里:
@Transactional public boolean settleOrder(SettleDTO dto) { // 1. 校验订单存在且未支付 Orders order = ordersMapper.selectById(dto.getOrderId()); if (order == null || order.getStatus() != 0) { throw new BusinessException("订单状态异常,无法结账"); } // 2. 更新订单为已支付 order.setStatus(1); order.setPayType(dto.getPayType()); // 1现金 2微信 3支付宝 4会员卡 ordersMapper.updateById(order); // 3. 写入财务流水(收入) FinanceRecord record = new FinanceRecord(); record.setRecordNo(generateRecordNo()); record.setBizType(1); // 1收入 2支出 record.setBizNo(order.getOrderNo()); record.setAmount(order.getTotalAmount()); record.setPayType(dto.getPayType()); financeRecordMapper.insert(record); // 4. 释放桌台,状态改回空闲 DiningTable table = diningTableMapper.selectById(order.getTableId()); table.setStatus(0); diningTableMapper.updateById(table); return true; }日结报表接口:这个接口的核心是聚合查询。我直接在Mapper里写了自定义SQL,按天分组统计收入和支出:
// Mapper中自带的查询,用LambdaQueryWrapper配合日期函数实现 public DailyReportVO getDailyReport(String date) { // 查当天的总收入(已支付订单) QueryWrapper<Orders> incomeWrapper = new QueryWrapper<>(); incomeWrapper.select("IFNULL(SUM(total_amount),0) AS totalIncome") .between("create_time", date + " 00:00:00", date + " 23:59:59") .eq("status", 1); // 查当天的总支出(采购单+杂项支出) QueryWrapper<FinanceRecord> expenseWrapper = new QueryWrapper<>(); expenseWrapper.select("IFNULL(SUM(amount),0) AS totalExpense") .eq("biz_type", 2) .between("create_time", date + " 00:00:00", date + " 23:59:59"); // 剩余的就是毛利 = 总收入 - 总支出 }日结的数字要能对上当天的现金流水,这是财务系统的第一道红线。测试的时候我习惯把“下单不结账”“结账后申请退款”“采购单录入错误被作废”这三种异常数据全部造一遍,看报表数据是否会被污染。
3.3 事务的坑:自调用失效与回滚策略
事务这块,我觉得有必要单独拎出来讲一个我踩过的坑。Spring的@Transactional默认是通过AOP代理实现的,同类内部的互相调用不会走代理,注解会失效。什么意思呢?比如:
@Service public class OrderServiceImpl { public void doSomething() { this.createOrder(dto); // 这里的事务注解根本没生效 } @Transactional public boolean createOrder(CreateOrderDTO dto) { // ... } }this调用绕过了Spring的代理对象,@Transactional就废了。解决方法是把事务方法放在另一个Service里,或者注入自己的代理对象调用。我个人的习惯是:每个Service只管自己领域的原子操作,跨领域的事务(比如结账涉及订单+财务流水)单独再建一个外层Service来编排,这样既避免了自调用问题,职责也更清楚。
还有一个细节:默认情况下,Spring事务只在RuntimeException和Error时回滚,受检异常(Exception的子类)不会触法回滚。如果你在代码里吞了异常或者抛的是受检异常,事务也会失效。我一般会在事务方法里统一抛出业务运行时异常,配合一个全局异常处理器返回友好的报错信息。
4. 权限与安全:财务系统必须做对的三层防护
财务数据是敏感的,就算只是个课设,答辩老师也会追着问安全问题。这个模块我给你梳理一条从登录到数据访问的完整链路。
4.1 登录认证与密码存储:别用明文,别用MD5
说实话,我看到很多毕设项目用MD5加盐这种做法已经不多了,但确实还有人在用简单的MD5。MD5加盐虽然能挡一下,但它的运算速度太快了,GPU并行运算可以快速撞库。正规做法是使用BCrypt算法,Spring Security自带的BCryptPasswordEncoder就是干这个的,每次加密会随机生成盐,同一个密码两次加密结果不一样,安全性完全不是一个级别。
// 注册用户时加密保存 String encodedPassword = passwordEncoder.encode(rawPassword); // 登录时校验 boolean matched = passwordEncoder.matches(rawPassword, storedPassword);配置也很简单,在Security配置类里声明一个BCryptPasswordEncoder的Bean就行。
4.2 JWT生效期与Token刷新:别让用户天天重新登录
餐饮店的收银员早上开店就要登录系统,如果Token有效期设成2小时,中午高峰期收银台突然弹个“登录已过期”,后厨和收银直接乱套。我在实际项目中把JWT有效期设为12小时,后台管理端(老板看报表的)设为8小时,然后加一个Token续期的逻辑:当用户操作时,如果Token剩余有效期不足2小时,自动签发一个新Token放在响应头里返回,前端收到后替换。这样用户在无感的情况下就完成了续期。
具体实现是在JWT过滤器里,验证完Token后判断剩余时间,如果低于阈值就重新生成签名:
long remainTime = expDate.getTime() - System.currentTimeMillis(); if (remainTime < 2 * 60 * 60 * 1000) { String newToken = jwtUtils.generateToken(user); response.setHeader("New-Token", newToken); }前端在axios响应拦截器里检测到New-Token就替换本地存储。这个细节做好了,系统的使用体验会明显提升。
4.3 越权访问与数据隔离:按角色过滤返回数据
权限安全里最容易被忽略的是垂直越权和水平越权。垂直越权就是普通收银员调了老板的报表接口;水平越权是收银员A查了收银员B的某个数据。解决垂直越权用Spring Security的@PreAuthorize注解,解决水平越权要养成一个习惯:在Service层查询时,强制加上当前登录用户的ID作为过滤条件。
我一般会在项目中加一个SecurityUtils.getCurrentUserId()工具方法,从SecurityContext里拿到当前用户,然后在所有涉及个人或角色范围数据的查询里带上这个条件。这是防御性的写法,哪怕哪天前端页面写漏了,后端也会把越权请求挡下来。
5. 库存与成本的联动:容易被忽视但老板最关心的功能
前面说的收入、支出都是面上的账。餐饮老板真正的痛点其实是“钱收了,但月底一算没赚多少”——问题大多出在库存损耗和成本核算上。所以财务系统里,库存模块不能只是简单的增删改查,它必须和“菜品销售”挂钩。
5.1 菜品与食材的BOM关系:算成本的基础
一道菜用了哪些食材、各用多少克,这个对应关系在餐饮行业里叫BOM(物料清单)。比如“鱼香肉丝”的BOM是:猪肉丝150g、木耳30g、胡萝卜30g、青椒20g、调味料若干。有了BOM,系统才能在卖出这道菜的时候自动扣减对应食材的库存:
@Transactional public void deductStock(Long dishId, Integer quantity) { // 根据菜品的BOM清单,循环扣减食材库存 List<DishBom> bomList = dishBomMapper.selectByDishId(dishId); for (DishBom bom : bomList) { Ingredient ingredient = ingredientMapper.selectById(bom.getIngredientId()); BigDecimal deduction = bom.getUsage().multiply(new BigDecimal(quantity)); if (ingredient.getStock().compareTo(deduction) < 0) { throw new BusinessException("食材库存不足: " + ingredient.getName()); } ingredient.setStock(ingredient.getStock().subtract(deduction)); ingredientMapper.updateById(ingredient); } }BOM的建立是推进这类系统时最麻烦的环节——厨师长得配合着录入配方。我的建议是:第一版可以先把BOM做得粗糙一点,只维护主要食材的比例,调味料之类的可按固定比例估算。等系统跑起来了,老板看到成本数据有价值,自然会愿意花时间完善BOM的精度。
5.2 库存预警与采购建议:让系统从“记账”升级为“帮手”
库存表里的warn_stock字段就是库存预警线。当库存低于这个值,系统要能主动提醒(列表标红、首页看板提示),甚至可以生成一份采购建议单:
SELECT ingredient_id, ingredient_name, (warn_stock - stock) AS suggest_purchase_qty FROM ingredient WHERE stock < warn_stock;这一小段逻辑写起来不难,但对系统的使用价值提升是质变的。餐饮店的老板每天最头疼的问题就是“今天要进多少货”,系统能根据库存余量和菜品销量趋势给出参考值,就能真正减轻店里的决策负担。
5.3 库存盘点:账面数和实际数对不上的处理
现实中库存账永远不可能跟实物完全一致,——损耗、员工餐、赠菜、过期报废都会造成差异。所以我设计了盘点单功能:每月月底,库管把实物盘点的数量录进系统,系统自动算出盘盈盘亏的差额,并生成一条记录:
@Transactional public void checkStock(CheckStockDTO dto) { BigDecimal systemStock = ingredientMapper.selectById(dto.getIngredientId()).getStock(); BigDecimal actualStock = dto.getActualStock(); BigDecimal diff = actualStock.subtract(systemStock); if (diff.compareTo(BigDecimal.ZERO) != 0) { // 记录盘亏盘盈单 StockCheckRecord record = new StockCheckRecord(); record.setIngredientId(dto.getIngredientId()); record.setSystemStock(systemStock); record.setActualStock(actualStock); record.setDiff(diff); stockCheckRecordMapper.insert(record); // 更新库存为实物数 ingredientMapper.updateStock(dto.getIngredientId(), actualStock); } }盘亏的金额会作为一项“成本损耗”进入财务支出,这也解释了为什么月底利润比预期低——不是菜卖得不好,而是成本跑冒滴漏了。先有盘盈亏数据,老板才能有依据去查是哪个环节出了问题。
6. 报表模块实战:从SQL聚合到ECharts可视化
报表是财务系统的门面,老板打开系统第一眼看的肯定是首页看板。这一节我讲一下数据怎么查出来、怎么呈现,以及哪些坑会让查出来的数字是错的。
6.1 首页看板的四个核心指标
首页看板不需要花哨,但四个数字必须实时、准确:
- 今日营收:当天已支付订单的总金额
- 今日订单数:当天已支付订单的总数
- 本月采购支出:当月所有采购单的总金额
- 当前库存预警数:库存低于预警线的食材种类数
这四个指标分别查四张表,用SQL的SUM和COUNT就能搞定。关键是查询的性能和后端缓存。由于首页看板大概率是查当天或当月的最新数据,我一般会做一个简单的Redis缓存,缓存5到10分钟,避免每次刷新都全表聚合一次。等系统访问量大了,再考虑用Canal同步MySQL到统计库或者上ClickHouse——但一个小餐馆的系统真用不上这玩意,Redis缓存就够用了。
6.2 菜品销售排行:毛利比营收更重要
菜品销售排行榜不要只按“销售额”排,那样得出的结论可能是“贵菜卖得好”,但对老板没有指导意义。我做的排行榜多了一个维度——毛利:
SELECT d.dish_id, d.dish_name, SUM(od.quantity) AS sale_count, SUM(od.amount) AS sale_amount, SUM(od.amount) - SUM(od.quantity * d.cost) AS gross_profit FROM order_detail od LEFT JOIN dish d ON od.dish_id = d.dish_id WHERE od.create_time BETWEEN #{start} AND #{end} GROUP BY d.dish_id, d.dish_name ORDER BY gross_profit DESC LIMIT 10;有了毛利排行,老板才能真正看懂“卖得好的菜不一定赚钱,赚得多的是那些成本低毛利高的菜”。这个逻辑写起来只要多加一个字段,但对系统的业务价值提升很大。
6.3 前端可视化:ECharts的接入和坑
报表页面我一般用ECharts,Vue2用vue-echarts,Vue3用echarts的npm包直接引入。折线图展示一周或一月的营收趋势,饼图展示支付方式占比,柱状图展示各分类菜品销量。后端返回的数据结构建议直接组好给前端,不要让前端去做复杂的计算。
一个比较容易踩的坑:时间字段在前端显示时差问题。如果后端返回的是2024-06-15 00:00:00这种带时区的字符串,前端new Date()解析后按浏览器本地时区显示,如果服务器和浏览器时区不一致,可能会显示成前一天或后一天。我的解决办法是:在Spring Boot的application.yml里明确指定时区:
spring: jackson: time-zone: GMT+8 date-format: yyyy-MM-dd HH:mm:ss另外,所有涉及日期范围的查询,统一用字符串传参(比如startDate=2024-06-01、endDate=2024-06-30),后端用LocalDate解析并在SQL层做BETWEEN。这样既避免了时区混乱,又让SQL索引能正常命中。
7. 打包部署与常见坑:从本地跑通到真正上线
很多人开发时一切正常,一到部署就翻车。这一节我把Spring Boot项目部署到服务器上的完整流程和最容易出问题的地方都捋一遍。
7.1 打包配置与前端静态资源合并
如果前后端分离,后端打成JAR包,前端npm run build之后把生成的dist目录里的文件复制到后端项目的src/main/resources/static下,然后一起打包。这样部署时只需要跑一个JAR就行,不用单独配Nginx(虽然实际生产我还是建议用Nginx,但课设和小项目一个JAR最省事)。
pom.xml里记得加Spring Boot的Maven插件,否则打包出来的JAR可能不是可执行的:
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>然后执行打包命令:
mvn clean package -DskipTeststarget目录下会生成一个xxx.jar,直接传到服务器上运行。
7.2 Linux服务器上后台运行
把JAR包放到服务器上,用nohup后台启动:
nohup java -jar restaurant-system.jar --spring.profiles.active=prod > app.log 2>&1 &application-prod.yml里我一般会把生产环境的数据库连接、Redis连接单独配置。请务必不要把数据库账号密码写在代码里提交到Gitee或GitHub上,至少用application.yml里的${DB_PASSWORD}占位符配合环境变量注入。
7.3 部署时最容易遇到的三个问题
端口被占用:8080是Spring Boot默认端口,服务器上如果跑了其他Java应用,端口就会冲突。可以在启动时指定端口:
nohup java -jar restaurant-system.jar --server.port=8081 > app.log 2>&1 &MySQL时区报错:连接字符串里一定要带时区参数,不然会报Server timezone value 'CST' is unrecognized:
jdbc:mysql://localhost:3306/restaurant?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/ShanghaiJAR包外部配置文件:如果你改了配置就要重新打包一次,很麻烦。我的习惯是把application-prod.yml直接放在JAR包同级目录下,启动时会自动覆盖JAR内部的配置。这样以后只改配置就重启JAR,不用重新打包。
8. 答辩与验收视角:评审老师最爱问的问题和应对思路
最后写点应景的。如果你是用这个题目做毕业设计,答辩时老师大概率会问下面几个问题,提前准备好答案,通过率会高不少。
问题一:为什么选择Spring Boot而不是SSM?
回答思路:Spring Boot简化了SSM中大量的XML配置,内嵌了Tomcat,让项目能以独立的JAR包形式运行。自动装配机制减少了样板代码,开发效率更高。生态成熟,和MyBatis-Plus、Spring Security等组件的整合成本低。
问题二:财务数据的安全性怎么保证?
回答思路:用户密码采用BCrypt加盐哈希存储;接口访问采用JWT无状态认证,配合Spring Security做注解级权限控制;涉及金额的数据修改一律走事务;所有业务记录采用逻辑删除,保留完整的操作痕迹。如果老师追问,可以把上面的代码细节拿出来展开讲。
问题三:当前系统有哪些不足?如果继续扩展会怎么做?
回答思路:这是一道送分题,不要回答“没有不足”。可以说:目前报表维度还比较简单,后续可以考虑接入消息队列实现菜品销售数据的异步统计;没有对接真正的支付网关,现在是模拟支付流程;库存模块的BOM配方依赖人工维护,后续可以增加智能估算功能。这种回答既展示了你对自己项目的清醒认识,又体现了一定的架构视野。
最后的个人体会
做这类系统的过程中,我最大的感受是:大部分项目不是死在技术上,而是死在业务流程的认知上。很多人在开发之前根本没搞清楚“日结”和“月结”的差异,没弄明白“采购支出”和“库存成本”的区别,就直接开始写代码。等你把业务流程理清了,用Spring Boot写CRUD反而是最简单的一步。
如果你正在做类似的毕设,我的建议是先别急着打开IDE,花两天时间把业务的流程图和数据字典画出来,把每个字段的来龙去脉想明白。后面写代码的时候你会发现,之前看似枯燥的设计工作,其实帮你省掉了一百次返工。这个项目的每个模块往下挖都有不少值得扩展的地方,先把地基打牢,才是正事。