ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot校园食堂订餐系统全套实战:从设计到答辩

SpringBoot校园食堂订餐系统全套实战:从设计到答辩 “米果智能食堂管理系统”——这个名字估计很多正在做Java毕设的同学都翻到过。说到底它就是一套基于JavaSpringBoot的Web版校园食堂线上订餐管理系统覆盖了学生登录、浏览菜品、线上下单支付、食堂接单出餐、管理员统计营收的完整链路。我完整地从选题、设计、开发到答辩走了一遍这里把项目背后真正的分析思路和技术实现细节整理出来包括数据库怎么建、接口怎么写、前端怎么打包、答辩怎么讲全部说透。这篇内容适合三类人。第一类是正在纠结毕设选题和实现难度的计算机专业本专科学生需要的是一个能完成、能讲清楚、能演示的完整系统第二类是已经选定类似题目、但不知道从哪下手的同学可以直接照着这里的工程结构和代码思路做第三类是想通过一个完整JavaWeb项目来巩固SpringBoot、Redis、MyBatis、Spring Security这些实战技能的开发者。文中的代码和配置都能直接抄作业但更重要的是搞清楚每一步背后的取舍逻辑。1. 项目整体设计与思路拆解1.1 校园食堂线上化到底解决什么问题教学楼的课表和铃声把午饭时间牢牢压缩在了一个小时里。当几千名学生同时涌向食堂排队就成了每天必然经历的过程。这种背景下校园食堂线上订餐系统的价值就非常明确把“人挤在窗口前点餐”变为“人提前点餐、食堂提前备餐、到店即取”效率提升是立竿见影的。除了效率还有信息透明的问题。在线系统可以把今日菜品、剩余数量、价格、评价全部前置展示学生点餐不再靠到窗口前碰运气。对食堂经营者而言订单数据沉淀下来以后可以做非常有价值的分析哪道菜是销量王、哪个窗口在哪个时段客流最大、菜品定价是否合理这些都能从过去的经验判断变成数据决策。我在需求分析阶段还专门去本校食堂蹲了几个用餐高峰期记了一些真实数据高峰时段一个窗口平均排队12人每个学生从排队到结算平均耗时4分钟以上。这些数据后来被我写进了论文的研究背景和答辩PPT的前几页答辩老师明显对这部分更感兴趣。很多同学论文开头喜欢写“随着互联网技术的快速发展”这类套话但最打动人的往往是来自真实场景的调查数据。系统分析不是喊口号得让人看到你真的理解了这个场景。1.2 三类角色与核心用例梳理用例建模是毕业设计的标配环节但也是最容易被糊弄过去的部分。简单画几个椭圆连几条线就完事答辩老师一问“系统有哪些角色每个角色能做什么”就答不上来。我的系统划分为三类角色对应不同的登录入口和操作集合角色登录入口核心功能关键页面学生用户学生端首页菜品浏览、加入购物车、在线下单、订单查询、菜品评价首页、菜品列表、购物车、我的订单食堂管理员食堂管理后台菜品上架/下架、库存维护、接单出餐、每日营收统计菜品管理、订单管理、统计报表系统管理员总后台用户管理、食堂信息维护、公告发布、全系统订单监管用户列表、食堂管理、公告管理毕业设计的角色模型不需要太复杂但每个用例都必须能闭环。以学生下单为例主流程是登录 → 浏览菜品 → 加入购物车 → 提交订单 → 模拟支付 → 查看订单状态异常流程同样要画出来比如购物车里的菜品被下架导致库存校验失败、提交订单时Redis中该菜品库存不足、支付超时后订单自动取消。在论文里把主流程和异常流程都讲清楚是“你理解业务复杂性”的最直接证据。我特别不建议把角色拆得太碎比如再增加一个“配送员”角色、一个“财务角色”。每多一个角色就要多一套权限逻辑、多一组测试用例、多几张数据表代码量和文档量都会指数级上涨。毕设项目的核心标准是“功能完整、能演示、能讲清楚”而不是“功能多到能开公司”。2. 技术选型解析为什么SpringBoot成为主角2.1 技术栈全景图这个系统最终确定的技术栈如下后端基础框架SpringBoot 2.7.18基于Spring 5.3.x稳定且资料全网最多持久层框架MyBatis-Plus 3.5.3.2单表CRUD零SQL复杂查询用注解或XML自己写数据库MySQL 8.0字符集utf8mb4支持中文排序和emoji字符缓存层Redis 6.x用于验证码、token版本、热门菜品缓存认证与鉴权Spring Security JWTjjwt 0.11.5前端方案Vue3 Vite Element Plus最终打包进SpringBoot的static目录项目管理Maven 3.8.x单模块工程开发环境IntelliJ IDEAJDK 1.8这套技术栈的“性价比”很高。组件全部是当前企业开发的主流组合学完之后可以直接平移到真实项目对毕设而言每一块又都有海量资料可以查。选择这套技术不是因为它看起来很厉害而是因为它在“可实现性”和“技术深度”之间取了平衡。2.2 关键取舍为什么不是SSM也不是微服务很多同学觉得SSM比SpringBoot更“底层”、更能体现水平于是纠结要不要用传统SSM框架做。实际上SpringBoot并没有重新发明一套东西它底层依然是Spring容器的IOC和AOP机制只是通过自动配置把原本要在web.xml、spring.xml里反复写的重复配置全部收敛了。你用SpringBoot反而能把时间精力放在业务逻辑和系统设计上。另一个常见的误区是一上来就上微服务。微服务架构需要有明确的业务驱动团队拆分的需要、独立部署的需求、差异化技术选型的需求、流量峰值隔离的需求。校园食堂管理系统这种规模单体应用在开发效率、调试便利性、部署成本上全面占优。我在论文的技术选型章节专门写了一段论证解释为什么当前规模选择单体架构是合理的。这个论证比堆砌技术名词更能体现架构判断力。还有一点必须提醒版本别追新。SpringBoot 2.7.x是最稳妥的路线网上资料多、坑少、插件兼容好。SpringBoot 3.x对Java版本有硬性要求很多旧版本的代码生成器、插件和教程都不适配会把大量时间花在环境问题上。毕设的底线是稳定跑通不是版本号最新。2.3 工程构建Maven配置与依赖管理Maven是JavaWeb项目的“地基工程”。我最开始从网上抄了一份pom.xml结果因为版本冲突调了一下午。后来摸清了依赖管理的基本思路才发现问题完全可以避免。下面是我这个项目的核心依赖配置parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent properties java.version1.8/java.version mybatis-plus.version3.5.3.2/mybatis-plus.version jjwt.version0.11.5/jjwt.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version${mybatis-plus.version}/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version${jjwt.version}/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version${jjwt.version}/version scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies依赖冲突的排查思路很重要如果编译或运行时提示某个类找不到、某个方法不存在先不要怀疑你的代码逻辑打开IDEA的Maven窗口跑一遍mvn dependency:tree八成是传递依赖把版本覆盖了。我做订单导出Excel功能时就是POI依赖引入了一个旧版本的commons-collections导致另一个模块运行时报错。这种情况不解决哪怕源码一行不改换台电脑也会炸。解决办法是在引入POI时排除冲突项或者全局统一指定版本号。3. 数据库设计与核心功能实现3.1 核心数据表设计数据库是业务的地基表结构设计得差后面所有业务代码都在给表“还债”。我最终设计了核心的8张表用户表、食堂表、菜品表、购物车表、订单表、订单明细表、评价表、公告表。下面挑最关键的几张详细讲。用户表的核心是角色和密码。密码不用多说必须是用BCryptPasswordEncoder加盐哈希之后的结果不能明文存储更不能用什么简单的MD5直接存。role字段用数字表示0-学生、1-食堂管理员、2-系统管理员这是一个典型的RBAC简化方案权限判断放在拦截器里。CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 用户ID, username varchar(50) NOT NULL COMMENT 登录名, password varchar(100) NOT NULL COMMENT bcrypt加密后的密码, real_name varchar(50) DEFAULT NULL COMMENT 姓名, phone varchar(20) DEFAULT NULL COMMENT 手机号, role tinyint(4) NOT NULL DEFAULT 0 COMMENT 0-学生 1-食堂管理员 2-系统管理员, avatar varchar(255) DEFAULT NULL COMMENT 头像地址, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 1-正常 0-禁用, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;菜品表要特别讲一下“库存”的设计。我采用“每日库存模式”每天凌晨由定时任务把菜品库存重置为当日备餐数量当天售罄即自动下架。表里stock表示当天剩余可售数量sold表示当天已售数量两者之和就是当日总备餐量。菜品表不存累计销量累计销量从订单明细表里统计避免菜品表无限膨胀。订单表必须有一个独立的业务主键order_no。很多同学直接用自增id对外展示这有两个问题一是订单号规律容易被猜二是排查问题时难以快速定位。我用的订单号格式是“yyyyMMddHHmmss 用户id后四位 4位随机数”比如20241208123045000123一眼就能看出下单时间配送和售后都方便。订单明细表是订单与菜品多对多关系的解耦。一个订单包含多个菜品一个菜品出现在多个订单如果不拆明细表订单表里就要存逗号拼接的菜品id字符串后面统计销量、计算金额、退款拆分全是噩梦。明细表里还有一个关键细节下单时要保存菜品价格的“快照”。菜品价格后来改了历史订单金额不能跟着变所以冗余一份下单时的价格在明细里。核心表结构整理如下表名核心字段设计要点userid, username, password, role, status密码bcrypt加盐role区分三类权限canteenid, name, location, phone食堂基础信息location用于取餐点展示dishid, canteen_id, name, category, price, stock, sold, status每日库存模式售罄自动下架cartid, user_id, dish_id, quantity, selected以用户维度存储和前端购物车联动ordersid, order_no, user_id, canteen_id, total_amount, status, create_time独立订单号状态机流转order_itemid, order_id, dish_id, dish_name, price, quantity, subtotal价格快照防止改价影响历史订单commentid, order_id, user_id, dish_id, content, rating评分限制1-5星展示在菜品详情页noticeid, title, content, author, create_time首页公告轮播这八张表覆盖了系统的全部业务闭环。答辩时如果被问到“为什么这么设计”可以从数据冗余、业务封闭、查询效率三个角度去讲尤其是“价格快照”和“订单号独立”这两个设计点很容易让老师眼前一亮。3.2 登录鉴权与权限控制的完整链路登录认证我选择的是JWT Spring Security Redis三方配合的方案。整体流程是这样的用户提交用户名和密码后端用BCryptPasswordEncoder的matches方法校验密码校验通过后用jjwt生成tokenpayload里包含userId、role、tokenVersion生成token的同时在Redis里以login:token:{userId}为key保存tokenVersion并设置与token相同的过期时间前端收到token存到localStorage后续请求都在Authorization头携带Bearer {token}后端拦截器解析并校验token再比对Redis里的版本号一致才放行如果只在拦截器里校验JWT的签名和过期时间不禁用Redis的版本号功能上确实也能跑通但存在一个明显的安全缺口用户退出登录之后token本身仍然是有效的只要有人截获了token依然能冒充该用户继续请求。Redis版本号机制就是为了解决“服务端主动让token失效”的问题——退出登录或者管理员禁用用户时只需删除或改变Redis中的版本号该token立刻失效。JwtAuthenticationInterceptor是整套认证逻辑的核心参考实现如下Component public class JwtAuthenticationInterceptor implements HandlerInterceptor { private final StringRedisTemplate stringRedisTemplate; public JwtAuthenticationInterceptor(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate stringRedisTemplate; } Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } // 从请求头获取token String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { throw new BizException(401, 未登录或登录已过期); } token token.substring(7); // 解析token Claims claims JwtUtil.parseToken(token); if (claims null) { throw new BizException(401, 无效的登录凭证); } // 校验Redis中的token版本号 Long userId Long.valueOf(claims.get(userId).toString()); String cachedVersion stringRedisTemplate.opsForValue().get(login:token: userId); String currentVersion claims.get(tokenVersion).toString(); if (cachedVersion null || !cachedVersion.equals(currentVersion)) { throw new BizException(401, 登录状态已失效请重新登录); } // 把用户信息放入上下文 UserContext.set(UserContext.UserInfo.builder() .userId(userId) .role(Integer.valueOf(claims.get(role).toString())) .build()); return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.clear(); } }Spring Security配置里还要关闭csrf和session机制。前后端分离项目主要靠token认证不需要session保存登录态csrf防护在这种模式下反而会增加联调复杂度直接关掉。3.3 点餐下单与库存扣减的核心逻辑下单接口是系统里业务逻辑最重的部分也是答辩老师最爱追问的地方。整个流程我拆成五步生成订单号使用时间戳拼接用户id和随机数保证高并发下不重复对所有参与下单的菜品id依次获取Redis分布式锁避免并发下单导致超卖校验菜品状态和库存库存不足直接抛业务异常用数据库原子UPDATE语句扣减库存而不是先SELECT再UPDATE在同一个事务里插入订单表和订单明细表全部成功才提交否则整体回滚用MyBatis-Plus时原子扣减的写法很关键/** * 扣减库存使用 set stock stock - #{quantity} 保证原子性 * 并通过 stock #{quantity} 条件防止库存扣成负数。 */ Update(UPDATE dish SET stock stock - #{quantity}, sold sold #{quantity} WHERE id #{dishId} AND stock #{quantity}) int decreaseStock(Param(dishId) Long dishId, Param(quantity) Integer quantity);为什么不用“先查后改”因为“先查后改”在并发场景下存在经典超卖问题两个请求同时查到库存还剩1份然后都通过校验分别执行库存减1最终库存变成-1。而上面的SQL让数据库自己判断stock #{quantity}如果不满足条件UPDATE影响行数为0service层判断影响行数为0就说明库存被抢完直接抛异常。利用数据库的行锁和原子操作处理并发比在应用层用synchronized更靠谱因为synchronized只对单个JVM实例有效多实例部署后就失效了。订单创建完成后需要支付。真实接入微信或支付宝支付对个人开发者门槛很高需要营业执照和商户资质所以毕设一般用模拟支付前端点击“模拟支付”后调用后端接口把订单状态从“待付款”改为“待出餐”同时记录支付时间。这个方案在论文里写明“模拟支付”即可不需要真正对接第三方网关。3.4 基于Redis的查询性能优化学生端首页的点击量最大菜品列表、食堂列表这些数据“读多写少、实时性要求不高”非常适合用Redis做缓存。我设计的策略是热门菜品缓存key为cache:hotDish:{canteenId}value为菜品DTO的JSON列表过期时间30分钟公告缓存key为cache:notice过期时间1小时菜品分类缓存key为cache:category:{canteenId}过期时间30分钟但这里必须克制。很多同学看了一篇“缓存提升性能”的文章就恨不得把每个查询都包一层缓存。订单状态、购物车数量这类高频变化、个性强、实时性要求高的数据加了缓存反而会引入缓存更新、缓存穿透、缓存和DB不一致的麻烦。我的经验标准很朴素只要接口平均响应时间在100ms以内且数据库压力不大就不要为了在PPT里写“用到Redis”而强行加缓存。技术是服务业务的不是用来凑字数的。4. 实操过程从项目初始化到前后端整合4.1 工程结构与初始化步骤很多人拿到项目第一件事就是埋头写代码结果写到一半发现包结构乱成一团前端代码和后端代码混在一起。我强烈建议先想清楚目录结构再动手。下面是我最终使用的工程结构miguo-canteen/ ├── pom.xml ├── src/main/java/com/miguo/canteen/ │ ├── CanteenApplication.java // 启动类 │ ├── config/ // SecurityConfig, RedisConfig, WebMvcConfig │ ├── controller/ // UserController, DishController, OrderController │ ├── service/ // 业务接口及实现 │ ├── mapper/ // MyBatis Mapper接口 │ ├── entity/ // 与表结构对应的实体类 │ ├── dto/ // 入参对象 │ ├── vo/ // 出参对象 │ ├── common/ // Result封装、常量、工具类 │ └── exception/ // 全局异常处理器 └── src/main/resources/ ├── application.yml // 核心配置文件 ├── mapper/ // XML文件存放目录 └── static/ // 放置Vue打包后的前端文件Controller一定要越薄越好。我给自己定的规矩是Controller里不允许出现超过10行的业务逻辑只负责取参数、调Service、把结果封装成Result对象返回。这样做的直接好处是答辩时被问到“某个业务怎么实现”我可以直接从Service层找到对应方法讲不用在一堆视图代码里翻找。初始化步骤大致是在IDEA里用Spring Initializr创建项目骨架选Web、Redis、Security依赖接着写application.yml配置数据源、Redis连接、MyBatis-Plus的mapper-locations然后创建数据库并执行初始化SQL脚本最后写一个测试接口验证项目能启动。这一步顺利的话后面的开发会轻松很多。关于返回结果的封装我用统一的Result类Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }统一返回结构的好处是前端可以写一个公共的axios拦截器统一处理响应码。401统一跳到登录页200直接取data接口层面的错误处理逻辑只维护一份就行。4.2 核心接口与前端页面整合前后端联调是最容易翻车的地方。我选择把Vue打包后放进SpringBoot的static目录这种方式最大的好处是毕设演示时只需要单机启动一个jar包浏览器直接访问http://localhost:8080页面和接口都在同一个服务里不需要额外配置Nginx或者处理跨域。步骤很简单前端执行npm run build得到dist目录把dist目录下所有文件复制到SpringBoot的src/main/resources/static/目录下重新打包运行访问8080端口即看到完整系统这里有一个非常关键的坑如果前端用了Vue Router的history模式刷新页面或直接输入深层路由地址时后端会返回404因为SpringBoot的静态资源映射找不到对应的物理文件。最简单的方案是把路由改成hash模式URL上多一个#符号但刷新、前进、后退都不会发真实资源请求完全规避404问题。毕设演示追求稳定不需要为了美观硬撑history模式。如果你确实想用history模式就需要给SpringBoot加一个转发规则把非接口的路径转发到index.html但要小心别把真正的接口路径误伤。我的实测经验是这个兜底配置在不同SpringBoot版本下行为有差异容易顺手把静态资源路径也一起转发了处理起来非常煎熬。对毕设而言用hash模式是性价比最高的选择。4.3 模拟数据与演示脚本毕设演示最怕遇到点一个按钮页面白屏、报错的情况。为了确保现场不翻车我在答辩前准备了一套固定的模拟数据和演示路线提前准备好3个食堂、20个菜品、5个测试账号2个学生、2个食堂管理员、1个系统管理员让每个食堂都存在不同状态的订单待付款、待出餐、已完成、已取消各一条用固定路线演练先用学生账号完成“浏览 → 下单 → 支付 → 评价”全流程再切换食堂管理员账号演示“接单 → 出餐”最后用系统管理员账号查看统计报表额外录一段屏幕操作视频作为备份万一现场网络出问题可以播放这种预演的价值非常大。很多同学觉得自己代码没问题就进场演示一遇到小问题就开始紧张越紧张手越抖演示效果大打折扣。固定脚本加充分预演能把不可控因素降到最低。5. 常见问题排查与避坑清单5.1 环境与配置类问题速查环境类问题占了开发期报错的一大半每个都遇到过一次这里直接给你排查路径。端口被占用是最常见的启动报错Port 8080 was already in use。排查方法很简单Windows执行netstat -ano | findstr 8080Linux或Mac执行lsof -i:8080找到PID后强制结束进程。如果是系统里常驻了别的服务占端口可以换一个应用端口但别养成瞎换端口的习惯更推荐找出占用的真凶。数据库连接失败Communications link failure通常不是代码问题。90%的原因有三个MySQL服务没启动、连接地址或端口写错、账号权限不对。先去命令行用mysql -u root -p -h 127.0.0.1 -P 3306验证一遍如果命令行能连上而应用连不上再去检查application.yml。特别要注意MySQL 8.x必须配置时区参数否则会报Server returns invalid timezone。我的配置如下spring: datasource: url: jdbc:mysql://localhost:3306/miguo_canteen?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的数据库密码 driver-class-name: com.mysql.cj.jdbc.DriverMyBatis的Invalid bound statement (not found)报错也有固定排查顺序确认mapper接口是否加了Mapper注解或启动类是否加了MapperScan确认XML文件里的namespace是否和接口全限定名一致确认XML文件放到了resources/mapper/目录并在配置中声明了mybatis-plus.mapper-locations。5.2 业务与数据层问题实录业务层的坑比环境配置更有技术含量特别是这几个。库存变成负数几乎可以肯定是用了“先查再改”模式。解决办法就是前面说的原子UPDATE写法外加Redis分布式锁做初步拦截。这套组合在毕设场景下完全够用。金额计算出现奇怪的小数那一定是用Double或Float存了金额。Java的浮点运算0.10.2并不等于0.3这是二进制存储的天然缺陷。金额计算切换到BigDecimal并且注意用BigDecimal.valueOf(0.1)或new BigDecimal(0.1)不要用new BigDecimal(0.1)后者会把浮点数的二进制近似值原封不动带进去。如果金额始终以“分”为单位直接用Long类型最省心。列表接口响应很慢先怀疑N1查询。用MyBatis-Plus做一对多查询时很多人会在循环里逐条查子表本来一条SQL能解决的问题变成了几十条。正确做法是用selectBatchIds一次性查出关联数据再在内存中按主键分组组装。前端如果单独起开发服务器比如Vite默认的5173端口请求SpringBoot的8080端口必然触发浏览器跨域拦截。解决方案是后端配置CORS允许对应来源或者使用Vite的proxy配置做服务端代理转发。部署时因为前后端同源这个问题自然消失所以不要浪费大量时间在联调阶段折腾跨域。5.3 避坑清单速查表场景最容易犯的错正确做法金额存储用double/floatBigDecimal或分单位Long密码存储明文 / 简单MD5BCrypt加盐哈希库存扣减先查库存再UPDATE原子UPDATE 影响行数判断订单号直接暴露自增ID独立order_no字段缓存使用所有查询都加缓存只缓存读多写少、一致性要求低的数据前端部署强行前后端分离部署整合进static目录 hash路由异常处理catch后吞掉不处理全局异常处理器统一返回业务错误6. 个人实操体会与建议6.1 时间节奏与论文联动毕设最容易犯的错是把编码和论文完全割裂开前两个月闷头写代码最后两周突击写论文结果论文里的描述和实际代码多处对不上。我的安排是第一周做需求和原型第二周画数据库ER图和用例图第三周开始写代码时就同步把系统设计章节的初稿写好后面每完成一个模块就补一小节实现说明。到了写论文的阶段几乎只是润色和补充截图。整个项目从开始到答辩用了八周最后一周还能从容地打磨PPT。6.2 答辩演示的加分细节我在答辩前重点准备了三个可能被追问的问题每个都跟项目内部机制强相关库存超卖怎么解决答Redis分布式锁防止并发下单超卖数据库层用原子UPDATE语句做最终兜底双重保险缓存和数据库一致性如何保证答对一致性要求高的订单数据不缓存只缓存热门菜品和公告这类读多写少的数据缓存设置了短过期时间允许秒级延迟如果用户量再大十倍系统哪里会成为瓶颈答单机MySQL的连接数、Redis内存容量、Web应用的线程池大小对应的解法是分库分表、Redis集群、水平扩展实例这三个答案是文末部分了……但都基于真实的理解才可信。只要代码确实是你自己写的这些问题反而是展示深度理解的最好机会。如果是背网络上的答辩题库一追问就露馅。6.3 最后分享一个小技巧开发过程中我把git reflog用得很熟。这个命令可以查看所有分支的历史操作记录包括已经被reset掉的那些commit。有一次我在联调时不小心改坏了一个关键的金额计算工具类发现时已经过了好几天但靠着git reflog找到了改动前的commit用cherry-pick把它恢复回来省下了大量重写时间。做毕设的同学一定要养成频繁commit的习惯哪怕一天只提交一次。你的每一次commit都是在给自己买一份后悔药。
返回列表