首页 / 资讯中心 / 文章详情

基于SpringBoot的私房菜上门定制系统设计与实现复盘

基于SpringBoot的私房菜上门定制系统设计与实现复盘 ★ FEATURED ARTICLE
一个私房菜上门定制的业务市面上那些外卖平台其实是接不住的。外卖平台解决的是“标准化菜品 即时配送”而私房菜的核心是“按需定制 预约上门”从菜品的口味调整、食材替换到上门时间、用餐人数、服务形式每一步都带个性化。我花了一个多月时间从需求梳理到代码落地完整做了一个基于SpringBoot的私房菜上门定制系统从源码结构、业务设计、核心代码讲解到最终的部署文档整套东西都整理齐了。这篇文章就把这个系统的设计思路、核心代码逻辑和部署过程做一个完整复盘重点讲讲订单状态机、定制报价、鉴权方案这些容易踩坑的环节。适合正在做类似业务系统、或者准备用SpringBoot做完整项目的同学参考。1. 先把业务理清楚私房菜上门定制的核心模型很多人在拿到“私房菜上门定制系统”这个需求时第一反应是直接建表写接口这其实是最容易返工的做法。上门定制和普通电商订单不一样它不是“选好商品-付款-发货”的线性流程而是一个需要多方确认的非标交易过程。我建议先花一两天时间把业务角色和核心链路理清楚再动代码。1.1 三种角色和三段核心链路这个系统里一共有三类角色分别是想吃私房菜的用户、提供上门服务的私厨、以及做运营审核的管理员。用户端的核心诉求是“找到满意的私厨并表达定制需求”私厨端的诉求是“管理自己的菜品和接单报价”管理员则负责审核私厨入驻、处理投诉和下架违规菜品。三者的关系不是简单的层级关系而是围绕一笔订单在不同阶段来回协作。核心的业务链路可以拆成三段。第一段是“发现与定制”用户浏览私厨的主页和菜品列表选择感兴趣的私厨并发起定制申请提交口味偏好、忌口食材、期望上门时间、用餐人数等要求。第二段是“报价与确认”私厨看到定制需求后根据食材成本和工时给出报价用户可以接受或者继续沟通调整。第三段是“履约与评价”确认报价后用户支付定金或全款私厨按约定时间上门服务完成后双方互相评价。这里有一个很容易忽略的点整个流程里最核心的实体是“定制申请”而不是“菜品”。很多新手设计数据库时会把菜谱表和订单表直接关联忽略了中间还有“定制需求”这层信息。我在设计时专门加了定制需求和价格确认两个环节确保任何一次交易都有据可查。换句话说菜品只是引子定制的过程和最终的确认结果才是系统要管理的重点。1.2 技术栈选型与取舍原因技术栈我选的是SpringBoot 2.7 MyBatis Plus MySQL 8.0 Redis JWT这套组合在业务系统里非常成熟也适合大多数中小型项目的快速落地。SpringBoot本身没什么好多说的约定优于配置内嵌Tomcat打jar包直接跑省去了一堆Web部署的麻烦。选MyBatis Plus而不是Spring Data JPA是因为这种业务系统的查询条件复杂多变像订单列表要按状态、按时间、按私厨ID、按用户ID多条件组合查询写SQL更直接可控。MyBatis Plus的代码生成器和Wrapper查询也很省精力复杂SQL用XML简单条件用Wrapper前后端接口的适配成本低很多。Redis在这个项目里主要做了三件事缓存验证码、存储登录态辅助数据、做接口幂等控制。验证码这块如果用单机Session存也行但考虑到后续可能要拆集群用Redis存验证码和Token的登录态更稳。另外做防重复提交时Redis的SETNX天然适合做幂等标记。JWT则是前后端分离项目最常用的无状态鉴权方案用户登录后拿到token后续请求带着token走拦截器里校验签名和有效期。这套选型没有任何炫技的成分全都是“够用、稳定、容易招人接手”的方案。如果你是自己做毕业设计或者公司内部项目我强烈不建议引入太复杂的技术栈比如Spring Cloud Gateway、分布式事务、消息队列。这个体量的系统用不上反而会把部署和排查问题的成本拉高好几倍。2. 数据库设计订单状态机与定制需求存储数据库是整个系统的地基。我见过太多项目上线后改表改到崩溃问题几乎都出在设计阶段没有想清楚核心状态的流转。私房菜上门定制系统的表结构不算复杂核心是几张表之间的关联关系以及订单状态的设计。2.1 核心表结构与字段要点我按业务边界把表拆成了用户域、菜品域、订单域、评价域四块。用户表、私厨表、管理员表属于用户域菜品表和菜品图片表属于菜品域定制订单表、订单明细表、订单状态日志表属于订单域评价表和投诉表属于评价域。下面挑几张最核心的表说下设计思路。用户表和私厨表其实可以分开也可以合在一张表里用角色字段区分。我选择分开因为私厨有自己的审核状态、服务区域、接单设置这些属性和普通用户差异很大分开之后私厨表的字段不会干扰用户表的查询。定制订单表是最核心的一张表字段设计上要特别注意。订单编号用业务号格式可以设计成“日期随机数”方便客服和用户沟通时快速定位。状态字段建议用Byte类型存储0到9的取值空间足够用。金额字段用Decimal(10,2)不要用Float或Double否则后续算总价、退款时会出现精度问题。订单明细表记录了每个菜品在本次订单中的定制要求。比如用户要求“辣度减半、不要香菜、多加一份虾”这些需求既有结构化的部分也有非结构化的备注。我的做法是把辣度、忌口、份量变化这些高频使用的字段单独拆成列把“其他额外要求”放在备注字段里。这样既方便做筛选统计又不至于为了几个低频需求建一堆列。还有一张表很容易被忽略就是订单状态日志表。所有的状态变更都往这张表里插一条记录包含订单号、变更前状态、变更后状态、操作人ID、变更原因。这张表在后期排查问题、做数据对账时价值极高。有人觉得多存一条日志是浪费但实际上等线上出现“用户说付款了但商家说没收到”这种纠纷时只有状态日志能还原真实链路。2.2 订单状态机设计从“待沟通”到“已完成”私房菜上门定制的订单状态一定要在动手写代码之前设计好。因为状态流转不只是改一个字段每个状态变更背后都对应一系列业务动作和权限校验。我设计的完整状态流是这样的待沟通 → 已报价 → 已确认 → 已支付 → 服务中 → 已完成另外有两条异常路径已取消和已退款。用户在私厨主页发起定制申请后订单进入“待沟通”状态。这时私厨可以查看用户的详细需求必要的时候在IM或者电话里和用户确认细节。私厨给出报价后订单变成“已报价”。这个状态下用户可以接受报价也可以拒绝并留言拒绝后订单回到“待沟通”让私厨重新报价。用户确认报价后订单进入“已确认”接下来就是支付环节。为了降低双方的风险这个项目我设计的是“定金 尾款”模式用户先支付20%定金私厨上门服务完成后在线收取尾款。支付成功后订单状态变成“已支付”这一步主要标记“用户已付定金私厨可以备菜了”。私厨按约定时间上门服务时在私厨端点击“开始服务”订单进入“服务中”服务完成后由私厨或用户触发进入“已完成”。异常路径上用户在“待沟通”和“已报价”状态都可以主动取消订单私厨在“已确认”之前也可以取消。一旦订单进入“已支付”状态取消订单就需要走退款流程退款金额按照协商结果由管理员介入处理。每个状态变更我都要求带上角色校验用户只能操作自己的订单私厨只能操作分配给自己的订单管理员可以查看所有订单但不能随意改状态。角色权限和状态机交叉校验是这类交易系统避免越权操作的关键。2.3 定制需求的存储方案JSON字段与结构化字段怎么取舍定制需求是私房菜业务里最特殊的数据。用户的需求可能千奇百怪“少油少盐”“不要葱姜蒜”“过敏提示”“儿童餐口味清淡”等等。直接把所有需求都塞进一个备注字段当然是最简单的但后续做统计和筛选时会非常痛苦。我的做法是分级存储。高频、可枚举的需求独立成字段比如辣度不辣/微辣/中辣/特辣、忌口食材用逗号分隔存、用餐人数、期望上门时间。低频、不可枚举的需求放进一个附加说明字段允许用户写一段自由文本。另外我建了一张定制需求标签表用来做私厨能力匹配。私厨在入驻时可以维护自己擅长处理的标签比如“川菜”“粤菜”“素食”“儿童餐”“减脂餐”用户发起定制时也可以选择这些标签作为辅助筛选条件。这张表在上菜谱推荐和私厨推荐时能派上大用场。我见过有些项目把所有定制需求都塞进JSON字段查询时用MySQL的JSON函数去匹配。短时间看确实灵活但数据量上来之后这类查询无法走索引性能会越来越差。而且JSON里嵌套的东西后期维护起来也很费劲改一个结构要排查所有写入逻辑。建议除非你有特别动态的表单需求否则还是把核心字段拆出来JSON只作为补充。3. 核心代码讲解鉴权、报价、订单流转代码讲解部分我会挑几个最有代表性的模块展开分别是登录鉴权、定制报价计算、下单接口的幂等控制、以及订单状态流转的实现。这几个点也是面试被问得最频繁的弄懂它们整个项目的骨架基本就通了。3.1 登录与接口鉴权JWT的实现套路前后端分离的项目登录鉴权我统一用JWT。流程不复杂用户输入账号密码后端校验通过后生成一个token返回前端前端存储token并在后续请求的Header里携带后端拦截器校验token合法性。JWT工具类核心是生成和解析两个方法。生成时要把用户ID、角色信息放进claims过期时间根据自己的业务定我设的是24小时。解析时用相同的密钥去验签验签失败或过期直接抛异常由全局异常处理器统一返回401。public class JwtUtil { private static final String SECRET your-secret-key-change-in-production; private static final long EXPIRE_TIME 24 * 60 * 60 * 1000L; public static String generateToken(Long userId, String role) { Date now new Date(); Date expireDate new Date(now.getTime() EXPIRE_TIME); return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setIssuedAt(now) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }拦截器这块我用Spring的HandlerInterceptor实现。preHandle方法里从Header取出token解析失败直接返回401解析成功就把用户ID放到request attribute里方便后续Controller直接取用。要注意放行路径要配好比如登录接口、注册接口、私厨列表接口是放行的其他接口都要校验。public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (StringUtils.isBlank(token)) { throw new BusinessException(401, 未登录); } try { Claims claims JwtUtil.parseToken(token); request.setAttribute(userId, Long.parseLong(claims.getSubject())); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { throw new BusinessException(401, 登录已过期); } } }明确的建议JWT的密钥在配置文件里用外部参数注入生产环境一定不要写死在代码里。写死密钥等于把系统后门公开了这属于安全问题不能心存侥幸。3.2 定制报价是怎么算出来的私房菜没有固定价格每个订单的价格都不一样这就需要一个可解释、可调整的报价规则。我设计的报价规则由三部分组成基础菜价、定制加价、上门服务费。基础菜价就是私厨在菜谱中维护的“按份计价”或者“按桌计价”的价格。定制加价根据用户的具体需求计算比如用户要求把普通鸡肉换成走地鸡那这部分差价就要算进加价里。上门服务费根据距离和人数综合计算距离通过用户地址和私厨常驻地址之间的公里数来计算。计算出公里数最直接的做法是用经纬度做球面距离计算。Haversine公式是常用的方法误差在可接受范围内。实现代码如下public static double calculateDistance(double lat1, double lng1, double lat2, double lng2) { double radLat1 Math.toRadians(lat1); double radLat2 Math.toRadians(lat2); double a radLat1 - radLat2; double b Math.toRadians(lng1) - Math.toRadians(lng2); double s 2 * Math.asin(Math.sqrt( Math.pow(Math.sin(a / 2), 2) Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2) )); return s * 6371.0; }服务费我配置成一个规则表起步3公里内收30元超出部分每公里加5元。这里要提醒一点报价计算的结果在生成“已报价”状态时要快照保存不要在下单时再动态重算一遍因为规则表后续是可以改的重算会导致历史订单金额对不上。3.3 下单接口的幂等控制交易系统里最怕的就是用户手一抖点了两次下单按钮结果生成了两笔订单。防止这个问题的思路有两个层面前端按钮置灰是体验层面的防护后端幂等才是真正的兜底。我用的是“自定义注解 Redis 拦截器”的方案。前端在发起下单请求时携带一个唯一请求号可以用UUID后端在接口处理前检查这个请求号是否已经存在。首次请求时用Redis SETNX写入请求号设置过期时间为10秒如果SETNX返回false说明是重复请求直接拒绝。public class IdempotentAspect { Around(annotation(idempotent)) public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable { HttpServletRequest request RequestContextHolderUtil.getRequest(); String requestId request.getHeader(Request-Id); if (StringUtils.isBlank(requestId)) { throw new BusinessException(缺少请求号); } Boolean success redisTemplate.opsForValue() .setIfAbsent(idempotent: requestId, 1, 10, TimeUnit.SECONDS); if (Boolean.FALSE.equals(success)) { throw new BusinessException(请勿重复提交); } return joinPoint.proceed(); } }订单表给业务订单号加上唯一索引也是必要的。如果第一次请求因为超时导致前端重试但第一次事务其实已经提交了Redis里的标记还在第二次请求会被拦截。如果Redis标记恰好过期了数据库唯一索引能兜住最后一层确保不可能生成重复订单。两层防护一起上才是稳妥的做法。3.4 订单状态流转的接口设计订单状态流转不建议用一堆if-else在Controller里写状态一多代码会越来越难维护。我采用的是状态机模式做了一层封装。核心思路是定义一个状态流转的配置表每个状态对应一个“可执行操作”集合操作执行前先校验当前状态是否允许该操作。拿“报价确认”这个操作举例当前状态必须是“已报价”角色必须是该订单的私厨本人。这样校验和业务动作就被分离开了代码结构更清晰。核心代码逻辑大致这样public Order confirmQuote(Long orderId, Long operatorId) { Order order orderMapper.selectById(orderId); // 校验状态只有已报价状态才能确认 if (order.getStatus() ! OrderStatus.QUOTED) { throw new BusinessException(当前状态不可确认报价); } // 校验角色只有接单私厨本人才能确认 if (!order.getChefId().equals(operatorId)) { throw new BusinessException(无权限操作该订单); } order.setStatus(OrderStatus.CONFIRMED); orderMapper.updateById(order); orderStatusLogMapper.insert(new OrderStatusLog(orderId, OrderStatus.QUOTED, OrderStatus.CONFIRMED, operatorId, 私厨确认报价)); return order; }用状态机的好处是后面对接小程序端时前端展示的按钮列表可以直接从状态机配置里查出来不需要后端再单独写接口。举个例子“已报价”状态下用户端显示“接受报价”和“拒绝报价”两个按钮到了“已支付”状态用户端按钮变成“申请退款”私厨端按钮变成“开始服务”。这些逻辑统一收敛前端和服务端都省事。4. 部署文档要点从源码到上线代码写好只是第一步能顺利部署上线才算真正完事。我整理部署文档的时候踩了不少坑这里把关键步骤和容易出错的地方都列出来。4.1 环境准备与配置清单部署环境我建议用一台2核4G的Linux服务器这个配置跑SpringBoot MySQL Redis完全够用。软件环境版本如下JDK1.8及以上我用的是1.8Maven3.6以上编译打包用MySQL8.0需要支持utf8mb4字符集Redis5.0以上做缓存和幂等标记Nginx可选如果用反向代理就需要application.yml里有几个配置项要特别留意。数据库连接串一定要带上时区和编码参数比如serverTimezoneAsia/Shanghai、characterEncodingutf8。Redis的host和password按实际环境填写。文件上传路径要配置成绝对路径不然jar包换一个目录启动后之前上传的图片就全找不到了。spring: datasource: url: jdbc:mysql://localhost:3306/private_kitchen?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: xxxxxx driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 password: database: 0 file: upload-dir: /data/private_kitchen/upload4.2 打包、初始化、启动流程部署全流程分四步。第一步是初始化数据库把项目的sql目录下的建库脚本导进去。第二步是修改配置文件把数据源、Redis、上传路径改成生产环境的实际值。第三步是打包在项目根目录执行mvn clean package -DskipTests生成jar包。第四步是启动把jar包上传到服务器后启动。启动命令我建议配上systemd服务来管理而不是简单地用nohup java -jar跑。因为systemd可以设置开机自启进程崩溃后会自动拉起日志轮转也方便。systemd服务配置文件示例[Unit] DescriptionPrivate Kitchen System Afternetwork.target mysql.service redis.service [Service] Userappuser ExecStart/usr/bin/java -Xms512m -Xmx1024m -jar /data/private_kitchen/private-kitchen.jar SuccessExitStatus143 Restartalways RestartSec10 [Install] WantedBymulti-user.target启动后一定要先看日志确认启动成功再看端口是否监听最后用curl调一个健康检查接口验证服务可用。不要跳过验证这一步直接上Nginx反向代理出了问题会多一层排查成本。4.3 我踩过的部署坑部署过程中遇到的最典型问题有这么几个每一个都耽误过时间。第一个问题是数据库报错“Public Key Retrieval is not allowed”。这是因为MySQL 8.0默认使用了caching_sha2_password认证方式连接串里加上allowPublicKeyRetrievaltrue就能解决。第二个问题是中文乱码根源是数据库本身字符集没建对。建库时一定要指定utf8mb4否则即使连接串写了characterEncodingutf8也没用。第三个问题是图片上传后立即访问404因为SpringBoot默认不会把外部目录映射成静态资源路径需要额外配置一个资源映射器把/data/private_kitchen/upload/这个路径映射到/image/**这个URL上。还有一个容易忽略的问题是Redis没有启动就启动jar包。SpringBoot启动时如果Redis连接失败应用会直接起不来。所以启动顺序一定是先MySQL再Redis最后才是应用服务。把这几个常见问题提前写进部署文档里能帮后来的同学省下大把排查时间。5. 项目文档怎么搭源码结构、接口文档、部署文档很多开发者写代码很猛写文档就犯懒。实际上一个项目能不能被顺利接手、能不能快速跑起来文档起的作用比代码还大。这套系统我一共整理了三类文档需求设计文档、接口文档、部署运维文档。5.1 文档目录结构建议我习惯在项目根目录建一个docs文件夹里面按用途划分docs/01-需求设计包含PRD、业务流程图、角色权限说明docs/02-数据库设计包含ER图、表结构说明、字段字典docs/03-接口文档按模块拆分每个模块一份Markdown文档docs/04-部署运维包含环境要求、部署步骤、常见问题排查数据库设计文档尤其重要每一张表都要写清楚字段的含义和取值范围尤其是状态字段。订单状态0代表什么、1代表什么、2代表什么必须有一个地方统一说明不然前端对接和后端开发会因为理解不一致反复扯皮。5.2 接口文档用哪种方式维护接口文档我推荐两种方式结合开发过程中用Swagger做调试文档正式发布时用Markdown维护一份对外接口文档。Swagger的好处是自动从代码注解生成接口参数和返回示例一目了然但Swagger不适合直接发给对接方看因为它把内部所有接口都暴露出来了信息太杂。Markdown接口文档要包含以下内容接口地址、请求方式、请求参数说明、响应结果示例、错误码说明。每个接口还需要标注权限要求比如需要用户登录还是管理员登录。对接方拿着这份文档不需要看代码就能完成联调这才是好文档的标准。5.3 给接手项目的同学的建议如果你接手的是一个SpringBoot业务系统建议按这样的顺序读代码先看数据库表结构把表之间的关联关系理清楚再看状态机配置和核心Service层理解订单是怎么一步步流转的然后看Controller层搞清楚哪些接口暴露给前端最后看拦截器和工具类理解鉴权和通用处理逻辑。读代码的时候不要只看代码本身要带着“这个接口是为谁服务的”“这个状态为什么这样变化”这些问题去看。业务系统的代码没有特别高深的技巧难的是理解业务背后的约束和取舍。把订单状态机、角色权限、幂等控制这几个点想明白了整个系统的脉络理出来后续不管是为接口调优还是加新功能心里都会有底。实际动手做的过程中我最大的感受是不要迷恋复杂技术把核心业务想透、把基础方案做扎实才是王道。这套基于SpringBoot的私房菜上门定制系统技术栈用的都是非常主流的方案整体搭建下来没有哪个环节是特别高不可攀的真正考验人的是你能否把“定制、报价、履约”这条看似简单的链路拆成一个靠谱的数据模型和一套严谨的状态流转逻辑。最后说个实际的小技巧在这个项目里加一个简单的数据看板接口每天统计新增用户数、新增订单数、订单状态分布、销售额趋势给管理员角色单独做一个首页展示。数据不是很多的时候直接一条SQL group by就能跑出来但有了这个看板整个系统的完成度和说服力能上一个台阶。你以后给朋友演示或者给面试官讲项目的时候也有一个看得见摸得着的展示入口。
阅读完成 · 觉得有帮助?
咨询建站