做私房菜定制系统这个项目最初是因为一个开私房菜馆的朋友找我帮忙。他的店不大每天接单靠微信和电话食材采购全凭感觉最头疼的是客户总喜欢“少油”“不要香菜”“辣度减半”“下午两点送到”这类很零碎的要求靠聊天记录根本理不清。当时市面上的点餐系统全是标准化菜品加个备注栏根本撑不住定制场景。于是我就用 Spring Boot 给他搭了一套“先提交需求、再确认订单”的私房菜定制服务系统顺便把源码和文档整理了出来。这篇文章就从头梳理一下整个系统的设计思路、核心实现、部署过程和踩过的坑如果你也在做课程设计、毕业设计或者想练手 Spring Boot 项目可以直接拿来当参考。这个系统解决的问题很简单私房菜不像连锁餐厅有固定菜单它的核心是“按需供给”。系统需要支持用户自由填写口味偏好、用餐时间、人数、预算范围甚至食材忌口商家端则要根据每单的定制信息去确认能不能做、什么时候做、价格是多少。而不是传统外卖那样直接下单支付就完了。整个系统的核心流程可以概括为“提交定制意向—商家确认—用户支付—订单跟踪—评价反馈”和普通商城订单有本质区别。1. 这个系统到底在解决什么问题1.1 定制需求不是“点菜”是“约定”传统点餐系统把菜品当成标准商品SKU、库存、价格固定用户选完就下单。但私房菜定制的逻辑完全不同用户要的是“今天能不能做这道菜”“鱼能不能换成虾”“辣椒少放一点”“能不能晚上七点送到”。这些需求充满主观描述如果用数据库的固定字段去存设计出来的表会比八爪鱼还复杂。比较好的做法是把定制需求“结构化 半结构化”结合固定的基本信息用字段灵活的差异化信息用 JSON 字符串存储后端再针对关键字段做校验和回显。我做这个系统时把“定制意向”作为整个业务的主线而不是把“菜品”作为主线。菜单上的菜品只是个参考模板用户可以在模板基础上修改调料、分量、搭配、时间等。这样设计的好处是商家端能清晰看到每一单的改动点不会遗漏用户备注用户端也不必每次从头描述需求直接选模板改一改就能提交。1.2 目标用户与使用场景这类系统通常面向三类用户第一类是想把私房菜业务线上化的个体私厨第二类是社区里的家庭小饭桌第三类是学校里的 Java 课程设计、毕业设计选题学生。对个体私厨来说他们最需要的不是大而全的 ERP而是“能接单、能记录需求、能看每天排期”的小工具对学生来说最需要的是一个业务逻辑完整、技术栈主流、能讲清楚“为什么这么设计”的项目。系统里我设置了两个端用户端和管理端。用户端提供菜品浏览、定制填写、订单提交、支付模拟、评价管理端提供菜品维护、定制确认、排期管理、订单统计。两个端共用一套 Spring Boot 后端接口用户端可以做成 Vue 单页应用也可以直接用自适应 HTML 页面启动方便演示效果好。我当时为了省事用户端用了 Vue管理端也用了 Vue两边都是纯前后端分离通过 JWT 做登录认证。2. 技术选型与整体架构2.1 为什么选 Spring Boot 2.7 MyBatis Plus MySQL我在选型时没有刻意追新而是选了 Spring Boot 2.7.x原因一是稳定二是网上资料多三是和 FastJSON、PageHelper 这些常用库的兼容性最好。如果你现在手头正好是 Spring Boot 3.x也不是不能用但要注意 JDK 版本必须 17 以上javax 包要换成 jakartaMyBatis Plus 也得换成对应版本很多老项目里的代码直接迁移会报一堆编译错误。MyBatis Plus 比原生 MyBatis 省心的地方在于单表 CRUD 基本不用写 SQL自带分页插件对新手尤其友好。私房菜系统订单、菜品、用户这些表的结构比较规整用 MyBatis Plus 能让代码量少三分之一。MySQL 则不用多说开源、部署简单、纳管方便课程设计环境基本都满足。如果将来要上云把连接串换成线上数据库就行代码不用动。2.2 前后端分离与 JWT 认证前后端分离是当前 Java Web 项目的主流做法我在这个项目里也采用这种方式。后端只提供 RESTful API前端通过 axios 调用。为了保证接口安全登录成功后后端生成 JWT 令牌返回给前端前端在请求头里带上token后端写一个拦截器校验令牌有效性。这样比传统的 Session 更适合多端访问用户在小程序端、浏览器端、管理端都能共用一套接口后续想再做一个商家小程序也方便。这里有个比较隐蔽的坑JWT 是无状态的后端不能主动让 token 失效。如果管理端账号被盗只能等 token 自然过期。解决方法也很简单在内存里维护一个黑名单把登出的 token 丢进去拦截器每次先查黑名单再验签。别嫌麻烦这种细节在答辩时反而是亮点。3. 核心模块与数据库设计3.1 用户端核心流程定制填单—提交意向—商家确认—支付—完成用户端的核心链路我把它设计成一个状态机订单状态用整数表示分别是0 待确认、1 已确认待支付、2 制作中、3 待取餐/配送中、4 已完成、5 已取消、6 已退款。之所以不直接用字符串是因为整数在数据库存储更省空间索引效率更高前后端用常量对应也清晰。具体流程是用户在菜品详情页点击“我要定制”进入定制表单页。表单里除了数量、预约时间之外还有口味选项辣度、油量、葱蒜、香菜自由文本备注以及“参考菜品”的选择。提交后生成一条状态为 0 的订单商家在管理端看到后如果可以做就修改价格、填写预计完成时间把状态变成 1如果做不了填写拒绝原因状态变成 5。用户看到状态变为 1 后再点击“支付”这个项目里是模拟支付点了直接状态变成 2。制作完成后商家端点击“出餐”状态变成 3用户确认收货变 4整个流程闭环。3.2 商家端菜品模板与排期管理商家端相对简单核心就是维护菜品模板和处理订单。菜品模板包括名称、图片、默认价格、原料、口味标签、是否上架。这里要注意“价格”这个词容易混淆私房菜定制场景下菜品模板价格只是“参考价”最终价格以商家确认时填写的为准。所以订单表里要有一个“预估金额”和一个“确认金额”用户提交订单时显示预估金额商家确认时可修改用户支付以确认金额为准。排期管理是我朋友专门提的需求。私房菜产能有限一天接不了太多桌所以商家端要能查看某一天有多少待完成订单。我在订单表里加了一个expect_finish_time字段管理端按这个字段按小时分组统计一眼就能看出哪个时段排满了。这个功能扩大点说就是最简版的产能日历虽然代码只有几十行但实际经营中非常有用。3.3 数据库核心表设计数据库我总共设计了 7 张表这里挑最重要的 4 张说一下。user表用户表字段有 id、username、password、phone、avatar、role。role 字段用整数值区分普通用户和管理员0 普通用户1 管理员。密码存的是 BCrypt 加密后的密文不要存明文这是个底线问题。menu_item表菜品模板表字段有 id、name、image_url、reference_price、description、tags、status。status 用 0/1 表示上架/下架。tags 可以直接存逗号分隔的字符串也可以存 JSON 数组我用的是 JSON 数组查询时用 LIKE 也能兼容。order_main表订单主表字段有 id、order_no、user_id、menu_item_id、customization、expect_time、status、estimate_amount、confirmed_amount、created_time、pay_time、finish_time、cancel_reason。其中customization字段存 JSON 字符串内容类似{spicy:medium,no_garlic:false,note:少油米饭换成糙米}。用 JSON 而不是单独建表是因为定制项非常灵活不可能每一类菜品都预定义全部选项。order_log表订单日志表记录每一次状态变更包括 operator、from_status、to_status、remark、create_time。这张表很多人不做但强烈建议加上后期排查问题全靠它。用户说“我没支付怎么显示已支付”一查日志就能定位是谁、在什么时间做了操作。建表语句大体如下CREATE TABLE order_main ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单编号, user_id int NOT NULL, menu_item_id int DEFAULT NULL, customization json DEFAULT NULL, expect_time datetime DEFAULT NULL, status tinyint DEFAULT 0, estimate_amount decimal(10,2) DEFAULT NULL, confirmed_amount decimal(10,2) DEFAULT NULL, pay_time datetime DEFAULT NULL, finish_time datetime DEFAULT NULL, create_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段不用一次设计到位后续迭代完全可以通过ALTER TABLE加字段重点是订单号、状态、定制信息这三部分别丢。4. 关键功能实现与代码解析4.1 定制信息如何结构化存储上面说定制信息用 JSON 字段存储但在 Java 代码里不能把它当成普通字符串直接拼接否则校验和回显都会很难受。我这里用一个单独的CustomizationDTO类来接收前端传入的数据类上加了Validated注解字段用NotBlank、Min、Max等注解做基础校验。实体类里的customization字段在数据库是 JSON 类型在 Java 里我用 String 接收通过 MyBatis Plus 的TypeHandler自动完成对象与 JSON 的互转。如果不想引入额外的 TypeHandler也可以在 Service 层手动用 FastJSON 的JSON.toJSONString()和JSON.parseObject()转两种方案都行。我建议新手先用手动方式代码透明出了问题好排查。前端提交的定制参数大致是这样的{ menuItemId: 12, quantity: 2, expectTime: 2025-07-20 18:30:00, taste: { spicy: medium, oil: less, scallion: false, garlic: false }, remark: 菜不要太咸米饭换成杂粮饭 }后端收到后先做几个硬校验预约时间不能早于当前时间、数量在 1 到 20 之间、备注长度不超过 200 字。校验通过后组装成OrderMain对象并写入数据库。订单号我推荐用yyyyMMddHHmmss 4位随机数的方式生成避免使用数据库自增 ID 暴露当日订单量也让展示更正规。4.2 订单状态流转与防重复提交订单状态流转是最容易写乱的地方。很多人把状态判断全写在控制器里后期改需求时越改越乱。我在这里采用了一个非常轻量的策略在 Service 层里写一个changeOrderStatus(orderId, fromStatus, toStatus)方法每次状态变更都必须校验当前状态是否等于fromStatus不相等就抛业务异常。这个设计很像状态机里最简单的一环前置条件校验。防重复提交方面商家可能手抖连续点了两次“确认订单”导致同一条订单被处理两遍。解决办法是对订单主表加一个乐观锁字段version更新时带上匹配旧版本号影响行数为 0 就代表被别人抢先更新了终止操作。int rows orderMapper.updateStatusWithVersion(orderId, fromStatus, toStatus, version); if (rows 0) { throw new BizException(订单状态已变化请刷新后再操作); }这种更新方式比锁表优雅得多也是面试中常被问到的“乐观锁”应用场景。做课程设计时能把这个原理讲清楚基本上就甩开“只会 CRUD”的评价了。4.3 菜品图片上传与文件安全问题菜品图片是菜品模板的必要字段我用本地上传方案在后端application.yml里配置了上传路径和访问虚拟前缀file: upload-dir: /data/private-kitchen/images/ access-prefix: /upload/**然后用 Spring Boot 的WebMvcConfigurer把这个本地目录映射成 URL 访问。上传接口接收MultipartFile做文件类型校验和后缀白名单校验只允许 jpg、png、webp大小限制在 5MB 以内。文件名不直接用用户上传的原始文件名而是用UUID重新生成避免中文文件名和路径穿越问题。这里必须提醒一句如果同一个系统后续要扩展用户头像上传、PDF 上传等功能一定要在全局层面处理上传内容的 XSS 问题。我见过很多项目只在文本输入框做了转义结果有人上传了一个带恶意脚本的 HTML/PDF 文件名或富文本描述浏览器解析时就踩坑。最好实现一个全局过滤器对所有非图片类上传资源做扫描或强制下载处理图片资源经过重编码再落盘。文件安全是答辩时的加分点也是实际线上环境必须考虑的点。4.4 菜品列表分页与搜索菜品列表用 MyBatis Plus 的分页插件实现配置一个MybatisPlusInterceptor注入PaginationInnerInterceptor即可。搜索条件包括关键字、口味标签、价格区间。我对价格区间做了最低价和最高价两个参数SQL 里用BETWEEN查询注意当最低价和最高价都为 0 时不加这个条件否则会查不出数据。分页返回统一用PageResult对象封装list和total前端拿到后渲染分页器。在 MySQL 中做模糊搜索直接LIKE %关键词%就好单表数据量不大时性能完全没问题。如果以后数据量上来了再考虑引入全文索引或者 Elasticsearch现在没必要过度设计。很多人一听搜索就想上搜索引擎这是对技术选型的误解先跑起来永远比一步到位更重要。5. 部署、运行与调试5.1 本地快速启动的完整步骤项目附带完整源码和文档本地启动我已经按最简单的方式做了。假设你已经安装 JDK 1.8、Maven 3.6、MySQL 5.7按下面这套步骤就能跑起来第一步用数据库工具建一个库比如private_kitchen设置字符集为utf8mb4排序规则选utf8mb4_general_ci。然后把项目里sql/private_kitchen.sql脚本导入里面包含建表语句和测试数据。很多新手在建库时图省事选默认字符集后面插入中文变成问号排查半天才发现字符集不对。第二步打开src/main/resources/application.yml修改数据库地址、账号、密码。同一个配置文件里还会有jwt.secret和file.upload-dir把上传路径改成你的本地目录。第三步用 Maven 打包装配。在项目根目录执行mvn spring-boot:run如果网络不好依赖下载太慢把 Maven 仓库换成国内镜像。启动成功后控制台会打印端口号和项目启动耗时默认端口 8080。第四步前端项目如果是分开的进入frontend目录执行npm install和npm run serve然后把后端地址配到前端的.env.production文件里。如果前端直接放在src/main/resources/static下则不需要额外启动前端服务直接访问 8080 就行。5.2 Docker 部署时容易踩的三个坑项目文档里我写了 Docker 部署方案这里重点说说踩过的坑。第一个坑是容器内的时区问题。很多基础镜像默认是 UTC 时区你本机跑得好好的放到 docker-compose 里订单创建时间总比实际时间少 8 小时。解决方案是在 Dockerfile 里加一句ENV TZAsia/Shanghai或者在 docker-compose 里为每个容器配置TZ环境变量。第二个坑是 MySQL 容器初始化数据脚本不生效。docker-compose 里 MySQL 的官方镜像只在数据目录为空时才会执行/docker-entrypoint-initdb.d下的脚本。如果你挂载了宿主机已有的data卷脚本不会再次执行。遇到这种情况先清掉挂载目录里的旧数据或者用独立的初始化容器执行 SQL。第三个坑是容器网络互通。Spring Boot 容器里连接 MySQLjdbc:mysql://localhost:3306/private_kitchen是连不上的localhost指向的是 Spring Boot 容器自身。正确写法是jdbc:mysql://mysql:3306/private_kitchen这里的mysql是 docker-compose 里的服务名前提是两个容器在同一个自定义网络里。不少新手第一次部署被这个坑折磨一整天其实原理搞清楚就一句话的事。5.3 版本太高带来的连环问题这可能是 Spring Boot 项目最常见的问题之一。现在很多新手一上来就创建最新版 Spring Boot 3.3 或 3.4然后学着老教程写代码结果发现javax.servlet包找不到或者 MyBatis Plus 的 starter 版本不兼容又或者 Lombok 装的版本太老无法配合高版本 JDK。我在文档里特别标注了本项目建议使用 Spring Boot 2.7.18JDK 8 或 JDK 11。如果你非要用 3.x需要额外注意三件事第一依赖从javax.*变为jakarta.*所有 import 里的javax.servlet都要手动改成jakarta.servlet第二Spring Security 6 的配置方式变了WebSecurityConfigurerAdapter已经废弃要改成基于SecurityFilterChain的写法第三MyBatis Plus 要用mybatis-plus-spring-boot3-starter而不是老版 starter。这些技术债不处理好项目很难顺利跑起来。6. 常见问题与排查技巧实录6.1 数据库连接报错的排查思路“Access denied for user rootlocalhost”这类报错九成是账号密码不对。但还有一种情况是密码正确却因为 MySQL 8.0 默认认证插件是caching_sha2_password老版本驱动不支持导致连接失败。解决办法是在 JDBC URL 上增加allowPublicKeyRetrievaltrueuseSSLfalse或者把 MySQL 用户改成mysql_native_password认证。这个问题遇到一次就会记住但第一次遇到时真的容易绕进去。时区问题也会产生一个很怪的报错类似The server time zone value йʱ is unrecognized。这个错误是因为数据库返回的时区信息 Java 不认识处理办法是在 JDBC URL 参数上加上serverTimezoneAsia/Shanghai。其实我们真正要解决的永远不是“到处乱改配置”而是理解客户端和数据库之间每一层度的转换逻辑。6.2 前端调用接口出现 401 或跨域问题用户登录没问题但访问菜品列表一直跳 401多半是前端没有把 JWT 令牌放到请求头。检查 axios 拦截器里有没有这段逻辑service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config })如果登录和查询都没问题只是在浏览器里跨域就要在 Spring Boot 后端处理 CORS。配置 CorsFilter 时注意allowedOriginPatterns不要写成allowedOrigins(*)因为在使用凭证模式时allowedOrigins(*)会被浏览器拒掉。我常用的配置是允许指定来源并允许所有常用请求方法。调试阶段可以放宽成所有来源上线前再收紧。6.3 源码改成自己毕设的技巧如果你拿这个项目当毕设绝对不能原封不动交上去否则代码查重和答辩都会有问题。我的实用建议是先全局搜索替换项目名和路径。把private-kitchen替换成你自己的项目名数据库名也改掉包名最好也换一下。然后修改前端页面的标题、Logo 和文案把“私房菜”换成你自己的业务主题比如“轻食定制”“家庭简餐”“烘焙预约”。最后把默认的测试数据全部清掉重新录入符合你论文场景的数据。这样改完之后代码结构没变但一眼看过去就是一个新项目。更重要的是最好再往里面加一个小功能。哪怕加一个“收藏菜品”也好都能让指导老师看到你确实读懂了这个系统的扩展点。我在项目文档的“扩展建议”部分专门列出了 5 个可扩展方向照着加就行。6.4 维护和扩展的方向建议这个系统的框架非常简洁扩展点也清晰。想增加真实支付可以接入微信支付或支付宝核心改动只发生在支付回调接口和订单状态机之间。想增加会员积分可以在用户表和订单表上各加一个积分字段再写一个积分流水表。想做商家多店版只需要把menu_item表增加一个shop_id字段所有查询条件补上店铺维度。想做小程序端复用现在的后端接口前端单独开发即可。我自己把商家端升级过一版加了简单的销量统计和近七天趋势图后端就是一个分组统计 SQL前端用 ECharts 折线图展示。整个功能前后端加起来 150 行代码左右效果却非常明显朋友用起来直说“终于知道周末该备多少菜了”。这种小而美的功能比堆砌一堆华而不实的模块有价值得多。最后说两句实在话如果你打算照着这个项目去做我的建议是别急着从头写代码先把订单状态流转和定制字段设计想清楚再把数据库表建好最后才轮得到 Spring Boot 的 Controller 和 Service。当初做这个系统时我自己在最开始定状态机的时候就推倒重来了一次原因是遗漏了“用户取消后商家可能还想挽留”的场景。后来想明白了状态机是业务的灵魂宁可多设计几个分支也不要让用户和商家在状态上对不上账。再分享一个小技巧给项目写文档时把关键表结构、状态枚举、接口返回示例放到前面把环境搭建步骤写得越啰嗦越好。因为真正能帮你把项目跑起来的人往往并不是最高手的那批人而是第一次接触 Spring Boot 的初学者。文档如果跳过了“数据库导入脚本”这一步后面大概率会有一堆人来问你这个问题。这个项目目前已经在 GitHub 仓库里放出来了包含完整源码、SQL 脚本、接口文档和部署说明。有需要的可以直接拿去学习或二次开发。如果你在部署过程中遇到文档之外的问题评论区留言时尽量把报错日志贴全我能很快帮你定位问题。做开源项目就是这样帮别人排一次错自己对这个系统的理解也会深一层这种互相成就的感觉比单纯写完代码更让人满足。