1. 项目概述与选题思路1.1 这个毕设项目到底在做什么基于Java Web的餐厅点餐系统——这是我见过数量最多的毕业设计题目之一也是很多同学问我要源码的常青树项目。先说清楚它是什么一个让顾客通过浏览器自助点餐、后厨实时接单、管理员统一管理菜品和订单的完整Web应用。你可以把它理解成一个迷你版的扫码点餐系统只不过没有移动端小程序而是用电脑浏览器或者手机浏览器直接访问。这个系统能解决什么问题最直观的一点就是替代传统的纸质菜单和人工传菜。顾客坐下来不需要等服务员拿菜单直接在页面里浏览菜品、加入购物车、提交订单后厨端实时查看新订单菜品做完后更新状态前台和管理员可以看每日流水。如果从课堂项目评审的角度看它同时覆盖了用户登录、权限控制、数据库操作、购物车逻辑、订单状态流转这些高频考点能完整展示你掌握了Java Web全链路开发能力。适合谁来参考两类人。第一类是真要拿这个题目做毕设、时间紧任务重、需要一个靠谱骨架作为起点的人第二类是刚开始学Java Web、想找一个完整案例把SSM框架、会话管理、数据库设计串起来理解的人。这篇文章我会按实际开发顺序把手写这套系统时需要想清楚的设计决策、数据库怎么建模、核心接口怎么实现、哪些坑我踩过一条条讲清楚。1.2 技术选型背后的为什么选Java Web技术栈如果不是特殊要求我建议这学期你定下来就不要再摇摆。核心组合是Spring SpringMVC MyBatis也就是常说的SSM配MySQL数据库和Tomcat容器前端用JSP Bootstrap来做后台管理页面和点餐页面。用这套组合是有原因的。对比纯ServletJSP方案SSM的优点是分层清晰Spring负责对象管理和事务SpringMVC负责请求路由MyBatis负责SQL操作三层各管一段。这不是为了看起来高级而是等你写到订单订单明细两张表插入的时候事务回滚的处理会舒服很多。对比Spring BootSSM反而更贴近很多高校Java Web课程的教学内容评阅老师看到用SSM实现会觉得你对基础框架理解扎实当然如果你学校明确要求Spring Boot那直接把MyBatis换成Spring Data JPA或者保留MyBatis也行业务逻辑基本不受影响。数据库选MySQL 5.7或者8.0都行。我实际开发时用MySQL 5.7多一点因为新版8.0的时区配置在本地和服务器之间容易出幺蛾子5.7基本零配置。JDK用1.8这是SSM框架组合最稳妥的版本别追求新装JDK 17框架配置会让你多花很多时间去适配。这里有一个容易被忽略的点是为什么不做前后端分离。其实毕设场景里做到页面直出、服务端渲染逻辑链路更短调试也直观。前后端分离单独写一套Vue接口文档的时间可能比整个系统逻辑再写一遍还多性价比不高。JSP配合JSTL标签库在页面里直接渲染订单列表简单直接答辩时讲起来也顺畅。2. 系统功能设计与数据库建模2.1 角色权限与功能边界划分点餐系统的功能设计如果没有提前把角色理清楚写代码的时候十有八九会一团浆糊。我用下来最舒服的划分是三端顾客端、后厨端、管理端。顾客端面向普通用户核心功能是注册登录、浏览菜品、按分类检索、查看菜品详情、加入购物车、提交订单、查看自己的历史订单和订单状态。这里面有两个细节值得注意购物车是存储在Session里的订单提交成功后就清空菜品列表要有分页不能一次查200条全渲染到页面上。后厨端面向厨师或者餐饮店的制作人员它只关心有哪些新订单、每单点了什么菜、做到哪一步了。所以后厨页面要有一个自动刷新或者手动刷新的新订单列表订单卡片上突出显示菜品名称、数量、备注、桌号或者取餐号厨师做完后把订单状态从已支付/待制作改为制作中再改成已完成。这个流程看着简单却是很多同学容易漏掉的关键环节——只做了顾客端和管理端答辩时老师一定会问后厨怎么知道有新订单。管理端功能最多包括菜品分类管理、菜品信息管理名称、价格、图片、描述、上下架状态、订单管理查看所有订单、按状态筛选、用户管理禁用/启用账号、数据统计今日订单数、今日营收、菜品销量排行。管理端是体现你综合能力的地方也是数据表设计的主要动力来源。桌号或者取餐号的问题我在设计时纠结了一下。堂食场景下应该有桌号字段顾客在点餐页面选择桌号后厨按桌号出餐外卖或者自提场景则是取餐号。为了避免复杂化我统一做了桌号字段默认值为1自提场景用户手动改成手机尾号即可。这个小改动让系统既覆盖了堂食也不妨碍外卖演示答辩时灵活很多。2.2 数据库表结构设计详解数据库设计是这套系统的核心骨架表结构设计好了后面的Mapper和Service写起来就是顺水推舟。我总共设计了六张核心表字段设计遵循够用、不冗余、能讲清楚三个原则。用户表t_userid主键、username、password、phone、status启用/禁用、create_time。密码存的是MD5加密后的密文不是明文这一点答辩时是加分项。status字段用于管理端禁用违规用户默认0表示正常1表示禁用。菜品分类表t_categoryid、name、sort_order。sort_order是排序权重数值越小越靠前。这张表单独拎出来是为了让分类管理成为管理端一个独立的可演示模块区分于菜品表。菜品表t_dishid、category_id、name、price小数两位、image图片URL、description、status1上架/0下架、sales销量数、create_time。这里要注意price字段用DECIMAL(10,2)千万不要用FLOAT——浮点计算订单总额时会出现0.999999这种诡异结果用Decimal类型就稳了。订单表t_orderid、order_no订单编号、user_id、table_no、total_amount、status、remark、create_time。order_no是给用户看的订单号我用时间戳随机三位数生成保证唯一status是关键字段我用整数表示状态机0待付款、1已付款/待制作、2制作中、3已完成、4已取消。这个状态枚举是后端写业务逻辑的核心。订单明细表t_order_itemid、order_id、dish_id、dish_name冗余字段防止菜品删改后订单历史失真、price下单时的单价同样冗余、quantity。为什么要冗余dish_name和price因为菜品表的价格和名称是会变的而订单一旦生成就是历史事实必须快照当时的数据。这个设计向答辩老师解释时会很有说服力。管理员表t_adminid、username、password。管理员和普通用户分开两张表权限控制上逻辑更干净。管理端登录校验走admin表用户端登录校验走user表各不相干避免一个LoginService里写两套判断。一张可选的扩展表是购物车表t_cart但如果用Session实现购物车这张表可以不建。我推荐Session方案因为减库存逻辑简单、不需要考虑持久化用户刷新页面也不容易出bug。2.3 订单状态机的流转设计状态机是这套系统业务逻辑里最值得讲透的部分。订单状态我用整数存储在Java后端定义常量枚举0表示用户提交订单后未付款注意普通演示场景可以不真正对接支付直接提供一个模拟支付按钮点击后状态变成1。1表示已付款待制作此时后厨端能刷出新订单这是整个系统的消息枢纽。2表示制作中厨师点击开始制作后由1变成2。3表示已完成厨师制作完成后由2变成3此时顾客端显示订单完成。4表示已取消用户在未付款状态下可以取消订单由0变成4已付款订单不允许用户自行取消需要管理员在后台取消。状态流转的核心是只能相邻转换比如从1直接跳到3是允许的但严谨一点应该1→2→3每一步操作留痕。我在Service层对每一次状态修改都做了校验如果不满足前置状态直接抛出业务异常。例如后厨直接把订单从0改成3后端会报错订单未付款不能开始制作。数据库层面不需要额外加触发器全靠Java代码控制。但为了排查方便建议在订单表增加一个operate_time字段记录最近一次状态变更的时间管理员排序时能看出哪些订单长时间卡在某个状态。3. 核心功能实现与代码拆解3.1 后端分层架构与项目结构SSM项目的经典分层是Controller→Service→MapperDAO再加上实体层model、工具类util、拦截器interceptor。实际项目结构我习惯这样组织src/main/java ├── com.xxx.restaurant │ ├── controller // 控制层接收请求、返回页面或JSON │ │ ├── UserController │ │ ├── DishController │ │ ├── OrderController │ │ ├── AdminController │ │ └── LoginController │ ├── service // 业务层订单事务、登录校验 │ │ ├── UserService │ │ ├── DishService │ │ ├── OrderService │ │ └── impl/ // 实现类 │ ├── mapper // MyBatis接口 │ ├── model // 实体类 扩展VO │ ├── interceptor // 登录拦截器 │ └── utils // MD5工具、订单号生成器等 src/main/resources ├── spring-mvc.xml ├── spring-mybatis.xml ├── mybatis-config.xml └── jdbc.properties src/main/webapp ├── WEB-INF/jsp │ ├── user/ // 顾客端页面 │ ├── admin/ // 管理端页面 │ └── kitchen/ // 后厨端页面一个很重要的经验是登录拦截器必须在Spring配置文件里配置好拦截路径。拦截器拦截/user/**和/admin/**、/kitchen/**放行登录页面、注册页面、菜品列表接口。我当时在这里踩过一次坑只配置了admin的拦截器用户端所有人都能直接访问订单列表管理员发现后帮我改了拦截路径配置才解决。3.2 购物车逻辑与下单事务购物车我放在Session里数据结构是Map。每道菜的Key是dishIdValue是一个CartItem对象CartItem包含dish信息、数量、小计金额。为什么用Map而不用List因为Map天然支持按照菜品ID去重累加对同一道菜再次加入购物车时只需要取出来把quantity加1时间复杂度为O(1)。Controller层的购物车操作有三个加入购物车、修改数量加/减、清空购物车。加入购物车的核心代码逻辑如下PostMapping(/cart/add) ResponseBody public Result addCart(HttpSession session, Integer dishId) { Dish dish dishService.findById(dishId); if (dish null || dish.getStatus() ! 1) { return Result.error(菜品不存在或已下架); } MapInteger, CartItem cart (MapInteger, CartItem) session.getAttribute(cart); if (cart null) { cart new HashMap(); session.setAttribute(cart, cart); } CartItem item cart.get(dishId); if (item null) { item new CartItem(dish); cart.put(dishId, item); } item.setQuantity(item.getQuantity() 1); return Result.success(已加入购物车); }这段代码有几个细节值得展开讲。第一每次购物车操作都去数据库查一次菜品信息防止Session里保存的菜品数据过期比如管理员中途改了价格第二查询菜品时做了状态判断下架菜品不能加入购物车第三ResponseBody返回JSON而不是直接跳转页面这样前端用Ajax请求就算局部刷新不影响用户体验。下单的核心是事务处理。这里必须用Transactional注解因为下单会同时往订单表和订单明细表插入数据还会更新菜品表的销量字段任何一步失败都要回滚。我曾经遇到过一个问题订单表插入成功但明细表插入失败结果用户看到订单但订单里没有任何菜品。加上事务后这类问题彻底消失。Transactional(rollbackFor Exception.class) public Integer createOrder(OrderVO orderVO) { // 1. 生成订单主记录 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(orderVO.getUserId()); order.setTableNo(orderVO.getTableNo()); order.setRemark(orderVO.getRemark()); order.setStatus(0); order.setCreateTime(new Date()); // 计算总金额 BigDecimal total BigDecimal.ZERO; ListCartItem items orderVO.getItems(); // 2. 循环插入订单明细 for (CartItem cartItem : items) { OrderItem orderItem buildOrderItemFromCartItem(cartItem); orderMapper.insertOrderItem(orderItem); total total.add(cartItem.getSubtotal()); } order.setTotalAmount(total); orderMapper.insertOrder(order); // 3. 菜品销量累加 for (CartItem cartItem : items) { dishMapper.increaseSales(cartItem.getDishId(), cartItem.getQuantity()); } return order.getId(); }这里的重点是插入订单明细时每一条都从购物车对象构建而不是再查一次数据库。购物车在提交时快照了当前菜品的名称和价格这样即使菜品的实时价格发生了变化订单里的价格也不会被修改。这个细节体现出对订单是历史快照的理解不是所有人都会想到。3.3 菜品图片上传与访问菜品图片上传是毕设答辩的高频追问点。我的实现方案是图片上传到本地磁盘目录路径保存到数据库然后在SpringMVC配置静态资源映射让上传的图片通过访问URL在浏览器中显示。上传的核心代码PostMapping(/admin/dish/upload) ResponseBody public Result upload(RequestParam(file) MultipartFile file, HttpServletRequest request) { if (file.isEmpty()) { return Result.error(请选择图片文件); } String originalFilename file.getOriginalFilename(); String suffix originalFilename.substring(originalFilename.lastIndexOf(.)); String newFileName System.currentTimeMillis() _ UUID.randomUUID().toString().replace(-, ).substring(0, 8) suffix; String uploadPath request.getServletContext().getRealPath(/) upload/; File dir new File(uploadPath); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(uploadPath newFileName)); String imageUrl /upload/ newFileName; return Result.successWithData(imageUrl); }文件名用时间戳随机8位重命名是为了防止文件名冲突同时也避免用户上传的文件名里包含中文或者特殊字符引起Tomcat解析异常。存储路径用的是getRealPath(/)也就是部署后目录下的upload文件夹这个方案在本地IDE运行没问题但部署到Linux服务器后需要注意目录权限。如果用了Nginx做静态服务更推荐把图片存到服务器绝对路径再配置Nginx的静态资源访问规则这里不做展开。前端显示图片时直接用img src${dish.image}即可因为返回的就是以/upload/开头的URL。同时要记得在SpringMVC配置中加静态资源映射mvc:resources mapping/upload/** location/upload/ /没有这一步浏览器直接访问/upload/xxx.jpg会报404我已经见过太多朋友在这个问题上折腾了一晚上。3.4 登录验证与用户状态管理登录模块我用了两个独立的ControllerUserLoginController和AdminLoginController。用户登录成功后将用户对象放入Sessionkey为loginUser管理员登录成功后将admin对象放入Sessionkey为loginAdmin两个会话数据互不影响。密码加密我选择了MD5加盐。纯MD5在CSDN泄露数据库时代早就显示出了不安全性但作为毕设演示已经足够。加盐的含义是把固定字符串拼在密码后面再做哈希比如salt myblog2024md5(password salt)这样至少比裸MD5好一点。实际上我建议用更稳妥的做法public static String encrypt(String password) { // 在实际项目中推荐使用BCrypt但毕设用MD5更便于展示 return DigestUtils.md5DigestAsHex((password SALT).getBytes()); }登录拦截器是防止未登录用户访问受保护页面的第一道防线。自定义MyInterceptor实现HandlerInterceptor接口前置方法里判断Session是否存在登录用户不存在就重定向到登录页。public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); Object loginUser session.getAttribute(loginUser); if (loginUser null) { response.sendRedirect(request.getContextPath() /login); return false; } return true; }另一个需要注意的点是用户禁用状态实时校验。很多系统的实现是登录成功后就把用户对象塞进Session之后再也不查数据库这样一来即使管理员禁用了某个用户该用户仍然可以访问系统。为了避免这个问题在核心接口里比如提交订单、查看订单都调用了userService.checkStatus(userId)如果发现status是1直接抛出账号已被禁用异常并清空Session。4. 实操过程与关键配置记录4.1 环境搭建与骨架初始化步骤先列一个我反复使用的基础环境组合按这个组合踩坑最少JDK 1.8Apache Maven 3.6.x也用3.8实测没问题Tomcat 8.59.0也可以但8.5对老项目兼容性最好MySQL 5.78.0需额外处理时区IDE随便IntelliJ IDEA或Eclipse都行创建Maven项目的时候如果是从零开始我建议直接建war包类型。在pom.xml里引入SSM相关依赖时要注意版本一致性Spring统一用5.2.xMyBatis用3.5.xmybatis-spring用2.0.x。mall目常看到有人混搭Spring 4和MyBatis 3.4虽然能跑但配合Java 8时偶尔会有莫名奇妙的警告。数据库初始化SQL脚本可以提前准备好项目是一套建库脚本加一套样例数据脚本。建库脚本里我会创建所有表和索引样例数据则准备好十几个分类、四五十道菜、两三个测试用户。需要特别注意字符集设置CREATE DATABASE restaurant_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;使用utf8mb4而不是utf8是有讲究的。utf8在MySQL里最多存3字节的字符遇到部分生僻字或者特殊符号时会报错utf8mb4是真正的完整UTF-8兼容性更好。注意是加上InnoDB引擎支持事务。4.2 Spring与MyBatis的整合配置整合配置是SSM项目最容易出错的一层报错信息通常不直观得靠经验。我的spring-mybatis.xml配置里核心是数据源、SqlSessionFactory、Mapper扫描三件事。数据源我用的是阿里巴巴Druid连接池。为什么不用Spring自带的DriverManagerDataSource因为Druid提供了数据库连接池监控能力可以在管理后台看到当前连接数、活跃连接数、SQL执行耗时答辩时展示一下有锦上添花的效果。配置bean iddataSource classcom.alibaba.druid.pool.DruidDataSource init-methodinit destroy-methodclose property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ property nameinitialSize value5/ property namemaxActive value20/ /beanjdbc.properties文件里数据库连接串写成这样jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/restaurant_db?useUnicodetruecharacterEncodingutf8useSSLfalse jdbc.usernameroot jdbc.password你的密码这里有一个大坑useSSLfalse必须加上。新版MySQL连接器默认会尝试SSL握手本地环境没有配置证书时会导致连接超时或报错加上这个参数直接跳过。Mapper扫描方式我推荐用注解扫描而不是逐个声明bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.example.restaurant.mapper/ /bean4.3 Tomcat部署与本地联调流程项目开发完成后的推进路线我习惯分四步走第一步本地通过IDE集成Tomcat跑通。IDEA里配置Artifact为war exploded模式Application context设为/restaurant然后启动。启动成功后访问http://localhost:8080/restaurant能看到登录页面。第二步数据库初始化脚本导入。这一步做好后登录样例管理员进入管理端后台能看到样例菜品的图片和列表。如果图片显示裂图优先检查图片是否真的存在于upload目录以及静态资源映射是否配好。第三步完整业务闭环测试。我用一个测试账号模拟顾客登录→浏览菜品→加番茄炒蛋进购物车→提交订单→选模拟支付→订单状态变成1。然后切换管理员账号或直接用后厨端口查看新订单→开始制作→完成整个流程走通说明核心链路没问题。第四步导出war包部署到干净的Tomcat环境。用Maven的package命令打出war文件复制到Tomcat的webapps目录下启动Tomcat验证。这一步是为了模拟真正的部署环境排除IDE里一切正常但换环境就挂的特殊依赖。4.4 后厨自动刷新与运营数据统计实现后厨页面我用了最简单的轮询刷新方式在JSP页面上通过meta http-equivrefresh content5每5秒自动刷新整个页面或者用Ajax定时请求新订单接口局部刷新订单区域。后一种方案体验更好因为整个页面刷新会导致厨师正在查看的长订单列表跳回顶部局部刷新区块则不影响阅读。数据统计模块则是管理端的重要亮点。我在dashboard页面上展示了三块核心数据今日订单总数、今日营收额、菜品销量Top10。查询SQL分别如下-- 今日订单数 SELECT COUNT(*) FROM t_order WHERE DATE(create_time) CURDATE() AND status ! 4; -- 今日营收不含取消订单 SELECT IFNULL(SUM(total_amount), 0) FROM t_order WHERE DATE(create_time) CURDATE() AND status 3; -- 菜品销量Top10 SELECT d.name AS dish_name, SUM(oi.quantity) AS total_sales FROM t_order_item oi JOIN t_dish d ON oi.dish_id d.id JOIN t_order o ON oi.order_id o.id WHERE o.status IN (1, 2, 3) GROUP BY oi.dish_id ORDER BY total_sales DESC LIMIT 10;5. 常见问题与排查技巧实录5.1 Maven依赖冲突和版本不匹配这是毕设项目里出现概率最高的报错类型。典型症状启动Tomcat时报NoClassDefFoundError或者ClassNotFoundException明明代码编译通过一运行就挂。排查思路分三步。第一步在IDEA终端里跑mvn dependency:tree看当前项目的依赖树找出重复依赖或者版本冲突的jar包。第二步优先在pom.xml中统一Spring版本号用properties定义spring.version所有Spring依赖引用同一个变量。第三步针对冲突的传递依赖用exclusions排除掉不需要的旧版本jar包。还有一种情况是MyBatis的版本和mybatis-spring的版本不匹配。例如mybatis-spring 2.0.x要求MyBatis至少3.5.x如果pom里引入的是MyBatis 3.4.x启动时SqlSessionFactory初始化就会报错。解决方法是保持mybatis和mybatis-spring大版本对齐。5.2 数据库连接类的经典错误连接MySQL时报Access denied for user rootlocalhost第一直觉通常是密码错但有一个隐蔽原因可能你本机有两个MySQL实例比如5.7和8.0并存一个跑在3306端口另一个跑在3307端口IDE的数据库面板和项目配置连接的不是同一个实例。我实际接手过一个项目jdbc.properties里连接的是3306端口而真正启用的MySQL服务在3307检查了半天才定位到问题。排错最快的办法数据库工具里手动执行SELECT 1测试连接然后把同样的host、端口、用户名、密码填到jdbc.properties里。另外URL的characterEncoding后面不要拼错单词是characterEncodingutf8不是charSetutf8。5.3 中文乱码问题的三层排查我在开发中遇到过三种乱码形态整理成一张对照表乱码位置原因解决方法页面显示中文变成??数据库连接串没有指定编码URL加characterEncodingutf8表单提交中文乱码没有配置CharacterEncodingFilterweb.xml加Spring编码过滤器数据库存储乱码表字符集不是utf8mb4建表时指定CHARSETutf8mb4最隐蔽的是第三种。曾经有一次页面显示完美但一存进数据库就成了乱码查了半天发现是建表语句里没写CHARSET表格沿用了MySQL默认的latin1编码。解决办法是用ALTER TABLE把表转成utf8mb4ALTER TABLE t_dish CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;5.4 图片上传的常见诡异问题图片上传报FileNotFoundExceptionupload目录不存在导致transferTo失败。通常是因为Tomcat部署时目录还没有生成程序里需要先mkdirs()创建目录而不是直接transferTo。另一种情况是上传的图片过大超过了Tomcat默认的postSize限制报413 Request Entity Too Large需要修改server.xml中Connector的maxPostSize值。文件上传后通过浏览器访问404除了前面提到的静态资源映射之外还有一个原因IDEA的Tomcat配置里Application context上下文路径和上传路径拼接到一起后实际访问URL和页面里的/upload/开头不一致。解决方案是始终用绝对路径例如${pageContext.request.contextPath}/upload/xxx.jpg。5.5 购物车数据丢失和Session过期购物车存在Session里在开发调试中容易出现加了几道菜一刷新或跳转页面就没了的场景。常见原因有两个一个是使用Chrome无痕窗口浏览器关闭后Session销毁这个不算bug但演示时容易吓到自己另一个是Tomcat和IDE配置的Session超时太短默认20分钟一般不存在太短问题但如果网页长时间挂机再操作Session会失效。解决思路把购物车对象在添加成功时同时放到Cookie里存储序列化之后每次请求从Cookie恢复。这个方案能增强用户体验但Cookie体积限制在4KB左右菜品多时会超限所以我在毕设里没有采用只在Session里维护。另外一个实用小技巧调试时把Tomcat的reloadable设置为true修改JSP和Java文件后就会自动热部署不用手动重启但注意热部署有时会导致Session丢失所以调完代码如果发现登录态消失不用慌张重新登录即可。5.6 订单金额对不上问题的排查思路订单总额与明细累加不一致是另一类高频问题。出现这种现象通常不是计算逻辑bug而是数据类型问题。如果price字段用了FLOAT或DOUBLE在MySQL里日积月累的浮点误差会让金额在两位小数上产生微小偏移。解决方法一句话所有涉及金额的字段全部用DECIMAL类型Java代码中用BigDecimal进行计算绝对不要用double去相加。如果治理金额用的BigDecimal还要注意除法时的精度设置BigDecimal result total.divide(new BigDecimal(100), 2, RoundingMode.HALF_UP);这里的2是保留两位小数RoundingMode.HALF_UP是四舍五入。不加scale参数时BigDecimal除法可能抛ArithmeticException异常。6. 答辩要点与个人经验总结6.1 扩展方向从Web端到小程序端做完这套系统如果时间充裕我建议你至少做一个小规模的扩展功能这会让项目在答辩考核中脱颖而出。最常见的扩展方向是把顾客端适配成微信小程序或者移动端H5。小程序端不需要重写后端只需要复用现有Controller接口把返回的JSP页面替换成JSON格式数据小程序端负责渲染。为此后端代码要支持JSP和JSON双模式一种做法是为小程序单独写一套REST接口RestController返回Result对象JSP页面继续用原有Controller渲染。Controller里的业务逻辑基本复用只需要写一套新的接口层。另一个性价比高的扩展是简单的数据看板。之前统计模块只有数字数据可以参考常见数据可视化方案新增一个柱状图或者饼图展示最近7天的营收趋势和分类销量占比。后端提供一个聚合SQL接口返回每天的订单总数和总金额前端用图表组件渲染。别小看这个改动它把系统从管理工具提升到了经营决策工具的层次答辩效果立竿见影。我的亲身体会是评估一个毕设项目的优劣核心不在于它用了多少新技术而在于它把一个场景讲透了多少。一个点餐系统订单状态机的严谨性能体现后端功底购物车的会话管理能体现Web开发经验金额处理的细节能体现工程素养——把这些点理顺、说出为什么这么做比堆砌一堆华丽框架更能打动评委。整个项目从无到有大概需要三到四周时间第一周做设计建库、框架搭建第二周做登录、菜品和购物车第三周做订单流转和管理后台第四周集中测试、补漏、准备答辩材料。时间分配上不要前松后紧尤其是图片上传和订单状态两个模块留出充足时间调细节因为这两个地方最容易出现连锁bug。最后给你一条实际开发中最有用的建议每改完一个功能立刻完整跑一遍业务闭环。登录、加购、下单、支付、后厨完成这一条路走通再去做下一个功能。很多人到最后联调时发现几十个逻辑互相冲突本质上是前期没有做持续集成式的手工冒烟测试。先让主流程通畅再填充边角功能你会发现这个项目比想象中要顺利得多。