SpringBoot+JSP水果商城系统:毕业设计实战与部署指南
发布时间:2026/8/31 2:08:44 作者:尧图编辑部 阅读量:1,286

简介本资源是一套基于SpringBootJSPMyBatis技术栈开发的水果商城系统完整实践材料面向Java Web初学者与课程设计学生解决从需求分析到可运行系统落地的学习闭环问题。压缩包共4个文件1个PNG图标、1个SQL建库脚本、1个DOCX需求分析文档、1个ZIP源码包总大小99.06MB其中SQL文件含完整表结构与初始化数据DOCX文档详述用户管理、商品浏览、购物车、订单流程等核心功能需求源码包涵盖Controller、Service、Mapper及JSP前端页面目录结构清晰、注释完整便于理解MVC分层逻辑与前后端交互机制。已有5617人学习下载配套需求文档与数据库设计可直接用于课程设计答辩或毕业项目参考显著降低环境搭建与业务建模门槛。1. 项目概述与整体设计思路1.1 这个系统到底解决了什么问题先说个实际场景。每年毕业季计算机专业的学生都在为毕业设计发愁。我见过太多人选了“商城系统”这种经典题目结果要么做得过于简陋被答辩老师追问到哑口无言要么一上来就上前后端分离、微服务、Redis缓存这些重型技术栈结果自己都讲不清楚代码逻辑答辩时漏洞百出。这个SpringBootjsp水果商城系统本质上是一个标准的中小型Java Web项目。它要解决的核心问题很明确让用户能够浏览水果商品、加入购物车、模拟下单支付同时让管理员能够维护商品信息、处理订单、管理用户。技术栈选型非常克制——SpringBoot做后端框架JSP做页面渲染MySQL存数据没有引入任何超出毕设范畴的复杂中间件。选这个题目的好处在于它覆盖了Java Web开发的主干线MVC分层架构、数据库设计、会话管理、权限控制、事务处理、文件上传、分页查询。这些都是面试官和答辩老师最关注的基础能力。你把这个项目吃透了SpringBoot常见面试题基本都能答上七八成。1.2 为什么是SpringBoot JSP而不是前后端分离现在很多学生一上来就想搞Vue SpringBoot前后端分离我不反对但得说句公道话如果你是为了学技术前后端分离没问题但如果是毕设请先想清楚答辩时怎么展示。前后端分离意味着你要准备两套代码、处理跨域问题、部署时还要考虑前端打包产物怎么和后端服务配合。答辩现场一旦网络环境不稳定前端静态资源加载不出来整个演示就垮了。而JSP方案是服务端渲染所有页面由后端直接输出演示时只需要保证一个应用在跑稳定性高得多。还有个更现实的因素JSP方案的技术链路短。从浏览器发起请求到Servlet接收再到Controller处理、Service写业务、Mapper查数据库最后把数据塞进ModelAndView返回页面渲染每一环都清晰可见。答辩老师问“一个请求从发出到返回经历了什么”你顺着代码链路从头到尾讲一遍这就是最标准的答案。SpringBoot在这套方案里的角色也很纯粹——它负责简化配置、内嵌Tomcat、提供依赖管理和自动装配。你不用像以前用SSM那样写一堆XML配置文件一个application.yml就能搞定数据源、端口、编码等关键设置省下来的时间可以专心写业务代码。1.3 项目模块划分用户端和管理员端双视角做一个商城系统第一件事不是写代码而是把“谁在用这个系统”想清楚。水果商城的用户分两类对应两种完全不同的功能集合。**用户端前台**面向普通消费者核心路径是注册登录 → 浏览商品 → 加入购物车 → 结算下单 → 查看订单。这个流程必须完整闭环任何一个环节断了演示时都会很尴尬。比如用户下了单订单列表里却看不到记录这种低级问题在答辩时会被直接扣分。**管理员端后台**面向店铺运营人员核心路径是管理员登录 → 商品分类管理 → 商品上架/下架/库存调整 → 查看和处理用户订单 → 管理用户账号。后台界面不需要花哨但功能要扎实。我见过不少同学的毕设后台只有一个商品增删改查订单管理完全是空的这从需求分析层面就站不住脚。两个角色之间通过会话中的角色字段进行区分。用户登录成功后后台根据其角色值决定跳转到哪个首页并在拦截器中校验访问权限——普通用户访问管理员页面直接拦截这个机制后面我会详细讲。2. 需求分析与功能模块拆解2.1 需求分析文档应该写成什么样标题里提到的“需求分析Word”是很多同学容易忽略但答辩时非常加分的一项材料。不少人的需求文档就是网上随便找一个模板改个标题就算完成。结果答辩老师随口问一句“你这个系统的非功能需求有哪些”直接愣住。一份及格的毕设需求分析文档至少应该包含以下内容项目背景与目标用一两段话说明为什么要做这个水果商城解决什么问题。这里不需要长篇大论重点是让老师看到你做过思考。角色分析明确系统有哪几类用户每类用户的职责和权限边界是什么。功能需求列表用表格列出功能编号、功能名称、功能描述、优先级。比如“FR-001 用户注册游客填写用户名、密码、手机号完成注册注册成功后自动登录”。非功能需求性能并发量预估、安全密码加密存储、SQL注入防护、可用性操作响应时间、兼容性浏览器适配。用例图与用例描述不用画得太复杂但每个核心用例登录、下单、商品管理最好有简单的用例描述表。我记得自己当年写需求分析时踩过一个坑把功能需求写得太细甚至把某个DAO方法都写进文档里了。后来导师说需求分析是给项目干系人看的不是给程序员看的要用业务语言描述“系统要做什么”而不是“代码怎么实现”。这个建议我一直记着写文档的核心是功能边界清晰不是技术细节堆砌。2.2 用户端功能清单与核心流程用户端的功能看起来简单但每个功能都有值得深挖的细节。注册登录是最基础的模块。注册时我们需要考虑用户名是否重复、密码怎么加密存储、手机号格式是否合法、用户填写的邮箱/手机号要不要做唯一性校验。登录成功后的Session管理也要想清楚——用户关闭浏览器后再打开是否还需要重新登录这涉及Cookie的有效期设置。商品浏览包含首页的商品列表展示、按分类筛选、关键词搜索、商品详情页查看。这里有一个容易被忽视的细节商品列表要不要做分页如果数据库里只有十几条测试数据确实可以不分页。但答辩老师很可能问“如果有10万条商品怎么处理”你至少得能说出LIMIT分页或者PageHelper分页插件的方案。购物车是商城系统的核心模块之一。它的数据模型设计很关键——购物车是存数据库还是存Session我推荐存数据库。原因很简单如果存Session用户换一台设备或者浏览器崩溃购物车数据就丢了。存数据库虽然多几次查询但用户体验可靠得多。下单结算是整个系统业务逻辑最复杂的地方。它涉及库存扣减、订单生成、购物车清空、订单金额计算这一连串操作。如果这些操作不是原子的就会出现“订单创建成功但库存没减”或者“购物车清空了但订单没生成”的数据不一致问题。这就必须在Service层加上事务控制我后面会专门讲。订单管理用户可以查看自己的订单列表查看订单详情对未支付订单进行模拟支付确认收货甚至可以取消订单和申请退款。这些状态流转在设计数据库时就要提前想清楚。2.3 管理员端功能清单与权限控制管理员端的功能设计要体现“管理”两个字不是简单的前台功能复制。商品分类管理分类是树形结构还是一个层级的列表水果商城规模不大一级分类就够用比如“国产水果”“进口水果”“时令水果”。但如果想做得更有亮点可以考虑设计成二级分类练一下父子级数据的处理。商品管理除了常规的商品增删改查一定包含图片上传。图片上传涉及文件存储位置的选择——存本地磁盘还是存数据库BLOB字段我建议存本地磁盘数据库只保存图片URL路径。如果存BLOB数据库会迅速膨胀而且图片读取性能很差相当于拿数据库当文件服务器用。库存管理后台要能看到每个商品的当前库存量库存不足时要有提示。这里可以拓展一个“库存预警”功能——设定一个阈值低于这个值就显示红色提醒。这个功能很小但体现你考虑到了真实业务场景答辩时可以说“这是我在分析真实水果店铺运营需求时发现的痛点”。订单管理管理员可以查看所有用户订单、按订单状态筛选、发货操作模拟、订单统计。如果你想让系统看起来更有含金量可以加一个简单的数据可视化——统计近7天订单量图表用ECharts就能实现工作量不大但很出彩。权限控制方面最简单的做法是用拦截器HandlerInterceptor统一拦截校验。在SpringBoot里注册一个LoginInterceptor对/admin/**路径的请求先检查Session中是否有管理员登录标记没有就重定向到管理员登录页。不要试图在Controller里一个个判断那样代码会散落得到处都是。3. 数据库设计五张核心表的建模过程3.1 需求分析到数据模型的映射思路数据库设计是毕设答辩的必考点也是很多同学最薄弱的一环。很多人上来就建表建到一半发现字段不够用又回头加反反复复浪费时间。正确的顺序应该是从需求分析中的功能列表出发推导出每张表。我画一个简单的推导路径用户注册登录功能 → 需要一张用户表user商品分类浏览功能 → 需要一张分类表category商品展示功能 → 需要一张商品表product分类和商品是一对多关系购物车功能 → 需要一张购物车表cart用户和商品通过它关联下单功能 → 需要订单表orders记录订单主信息还需要订单详情表order_item记录订单中每个商品的快照信息这样推导下来最少需要6张表。有的同学还会加一张轮播图表banner或者管理员日志表这些都是加分项但不强制。3.2 用户表和商品表的核心字段设计用户表user的关键字段如下字段名类型备注idint主键自增usernamevarchar(50)用户名唯一索引passwordvarchar(64)密码MD5加密后存储saltvarchar(32)加密盐值nicknamevarchar(50)昵称phonevarchar(20)手机号addressvarchar(200)默认收货地址avatarvarchar(200)头像图片路径roletinyint角色1-普通用户2-管理员create_timedatetime注册时间password不直接存MD5值而是要加盐后加密这一步很关键。因为简单的MD5已经被彩虹表覆盖得差不多了直接存等于白加密。加盐的做法是注册时生成一个随机UUID作为salt拼接password后做MD5把hash值和salt都存进数据库。登录验证时再用传入的密码拼接salt做MD5比对。商品表product核心字段字段名类型备注idint主键category_idint分类外键namevarchar(100)商品名称main_imagevarchar(200)主图URLsub_imagestext副图URL逗号分隔detailtext商品详情描述pricedecimal(10,2)单价stockint库存量salesint销量statustinyint商品状态1-上架0-下架create_timedatetime上架时间price必须用decimal类型不能用float或double。这个是个经典坑——浮点数在计算机中是近似表示0.10.2都不等于0.3跟金额相关的计算如果出现这种误差会在答辩时被老师一针见血地指出来。sales和stock用int就够没必要用bigint。3.3 购物车表与订单表的关联设计购物车表cart的设计比较直接字段名类型备注idint主键user_idint用户外键product_idint商品外键quantityint数量checkedtinyint是否勾选结算user_id和product_id可以加一个唯一索引防止同一用户把同一商品加入购物车两次。当用户再次添加相同商品时直接更新数量而不是插入新记录。订单表orders和订单明细表order_item是表关系中比较经典的一对多设计订单表orders记录一次订单的整体信息包括订单编号、下单用户、总金额、订单状态、收货人信息。字段名类型备注idint主键order_novarchar(32)订单编号唯一user_idint下单用户total_pricedecimal(10,2)订单总金额statustinyint1-待付款2-待发货3-待收货4-已完成5-已取消receiver_namevarchar(50)收货人姓名receiver_phonevarchar(20)收货人手机号receiver_addressvarchar(200)收货地址create_timedatetime下单时间pay_timedatetime支付时间订单明细表order_item记录订单中每个商品的快照信息。为什么要“快照”因为商品的价格、名称随时可能被管理员修改。如果订单只存了一个商品ID用户查看历史订单时商品名称和价格早就不一样了这显然不行。所以下单时要把商品名称、图片、单价都冗余到订单明细表里。字段名类型备注idint主键order_idint订单外键product_idint商品外键product_namevarchar(100)商品名称快照product_imagevarchar(200)商品主图快照pricedecimal(10,2)成交单价快照quantityint购买数量3.4 为什么外键约束我建议不用说一个可能跟教材观点相左的建议如果业务不复杂可以不用数据库外键约束而是靠应用层维护逻辑关联。外键约束能保证数据的完整性比如你不能插入一条指向不存在商品的购物车记录。但它也会带来问题插入、删除时数据库要做额外的完整性检查影响性能而且一旦涉及批量导入、数据修复等操作外键会让流程变得非常痛苦。在毕设场景下我更推荐把关联逻辑写在Service层——插入订单明细时先检查订单ID是否存在删除商品时先查有没有被订单引用。这样做的好处是代码逻辑更直观答辩时你能清楚地解释每一步的意图。不过要注意表与表之间的关联关系在表结构设计阶段仍然要画清楚。我建议在需求分析文档里画一张ER图把主外键关系标注出来表明你理解表间关系只是把约束放在了应用层实现。4. 后端核心实现从项目搭建到业务闭环4.1 项目初始化与关键依赖配置用IDEA新建SpringBoot项目时有一个很多新手容易卡住的点Spring Initializr拉取模板超时或者没有想要的版本选项。这时候不要死磕直接去start.spring.io把项目下载到本地然后用IDEA打开即可。pom.xml里最重要的依赖就那么几个dependencies !-- Web启动器 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- JSP解析引擎 -- dependency groupIdorg.apache.tomcat.embed/groupId artifactIdtomcat-embed-jasper/artifactId scopeprovided/scope /dependency !-- JSTL标签库 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId /dependency !-- MyBatis -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.0/version /dependency !-- MySQL驱动 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency /dependencies这里有个关键细节如果pom.xml不引入tomcat-embed-jasperJSP文件无法被编译解析浏览器访问时会直接看到源码或者报404。这是JSP项目最常见的启动坑我后面排查部分会专门讲。application.yml配置如下server: port: 8080 servlet: context-path: / spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/fruit_mall?useUnicodetruecharacterEncodingutf-8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 mvc: view: prefix: /WEB-INF/jsp/ suffix: .jsp mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.fruitmall.entity configuration: map-underscore-to-camel-case: true注意database url上必须加useUnicodetrue和characterEncodingutf-8否则中文数据入库后可能是乱码。serverTimezone也要指定不然新版MySQL驱动会报时区错误。map-underscore-to-camel-case这个配置太重要了它能把数据库的create_time自动映射成Java实体的createTime省去一大堆手写ResultMap的麻烦。4.2 登录注册模块加盐MD5与Session管理登录注册是每个系统的脸面也是答辩时被拷问概率最高的点。我直接贴核心代码。注册部分Service层处理逻辑Override public boolean register(User user) { // 1. 检查用户名是否已存在 User exist userMapper.findByUsername(user.getUsername()); if (exist ! null) { return false; } // 2. 生成随机盐值 String salt UUID.randomUUID().toString().replace(-, ).substring(0, 16); user.setSalt(salt); // 3. 密码加盐后MD5加密 String encryptedPwd MD5Util.md5(user.getPassword() salt); user.setPassword(encryptedPwd); // 4. 默认角色为普通用户 user.setRole(1); user.setCreateTime(new Date()); return userMapper.insert(user) 0; }你要能说清楚为什么加密时是把salt拼在password后面而不是前面。其实没有严格规定但拼接顺序一旦确定就不能变否则登录时验证永远对不上。答辩时你可以说加盐的目的在于让同一个密码在不同用户身上产生不同的哈希值避免彩虹表直接破解。登录逻辑Override public User login(String username, String password) { User user userMapper.findByUsername(username); if (user null) { return null; } String encryptedPwd MD5Util.md5(password user.getSalt()); if (!encryptedPwd.equals(user.getPassword())) { return null; } return user; }登录成功后在Controller里把用户对象放进SessionPostMapping(/login) public String login(User user, HttpSession session, Model model) { User exist userService.login(user.getUsername(), user.getPassword()); if (exist null) { model.addAttribute(error, 用户名或密码错误); return user/login; } session.setAttribute(loginUser, exist); // 根据角色跳转不同首页 if (exist.getRole() 2) { return redirect:/admin/index; } return redirect:/; }4.3 购物车实现联合唯一索引与数量更新购物车模块最容易出现的逻辑漏洞是“重复添加”。我的做法是在cart表上建立user_id和product_id的联合唯一索引然后处理逻辑分两种情况。Override public void addToCart(Integer userId, Integer productId, Integer quantity) { Cart cart cartMapper.findByUserIdAndProductId(userId, productId); if (cart null) { // 第一次加入直接新增 Cart newCart new Cart(); newCart.setUserId(userId); newCart.setProductId(productId); newCart.setQuantity(quantity); newCart.setChecked(1); cartMapper.insert(newCart); } else { // 已存在数量累加 cart.setQuantity(cart.getQuantity() quantity); cartMapper.update(cart); } }这个逻辑本身不复杂但你要能解释清楚为什么用“先查再决定插入还是更新”而不是“直接插入靠数据库索引报错再捕获异常”。前者逻辑意图明确可读性好后者虽然少了一次查询但把异常处理当作正常业务流代码可维护性差。答辩论“你怎么处理重复添加”时能说出这两种方案的对比会显得你是真的思考过。购物车页面展示时需要查出每个购物车记录对应的商品信息。简单的做法是写一个VO类把Cart和Product的关键字段组合在一起。4.4 下单事务Transactional的正确用法下单模块是整个系统技术含量最高的地方也非常适合在答辩时重点展示。它涉及四个操作创建订单主记录、创建订单明细、扣减库存、清空购物车。这四个操作必须全部成功或者全部失败回滚。Transactional(rollbackFor Exception.class) Override public Order createOrder(Integer userId, String receiverName, String receiverPhone, String receiverAddress) { // 1. 查询该用户的购物车记录勾选的 ListCart cartList cartMapper.findCheckedByUserId(userId); if (cartList null || cartList.isEmpty()) { throw new RuntimeException(购物车为空无法下单); } // 2. 计算订单总金额检查库存 BigDecimal totalPrice new BigDecimal(0); for (Cart cart : cartList) { Product product productMapper.findById(cart.getProductId()); if (product.getStock() cart.getQuantity()) { throw new RuntimeException(商品【 product.getName() 】库存不足); } totalPrice totalPrice.add( product.getPrice().multiply(new BigDecimal(cart.getQuantity())) ); } // 3. 创建订单主记录 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setTotalPrice(totalPrice); order.setStatus(1); order.setReceiverName(receiverName); order.setReceiverPhone(receiverPhone); order.setReceiverAddress(receiverAddress); order.setCreateTime(new Date()); orderMapper.insert(order); // 4. 创建订单明细 扣减库存 for (Cart cart : cartList) { Product product productMapper.findById(cart.getProductId()); OrderItem item new OrderItem(); item.setOrderId(order.getId()); item.setProductId(product.getId()); item.setProductName(product.getName()); item.setProductImage(product.getMainImage()); item.setPrice(product.getPrice()); item.setQuantity(cart.getQuantity()); orderItemMapper.insert(item); productMapper.decreaseStock(product.getId(), cart.getQuantity()); } // 5. 清空购物车 cartMapper.deleteByUserIdAndChecked(userId); return order; }这里必须注意一个细节Transactional默认只回滚RuntimeException如果方法抛出的是受检异常比如Exception事务不会自动回滚。所以需要显式指定rollbackFor Exception.class确保任何异常都触发回滚。还有个隐藏很深的坑事务失效场景。如果这个createOrder方法是在同一个类中被另一个方法调用而不是通过Controller调用Service的入口方法进入事务会失效。因为Spring的事务是基于AOP代理实现的同类内部调用不走代理。我遇到过同学把createOrder设置为private然后在同一个Service的另一个public方法里调用它结果事务完全没生效数据出现了半截。答辩前一定要自己测一下这个场景。订单编号的生成方式也值得注意。不要用时间戳当订单号高并发下会重复。我习惯用“yyyyMMddHHmmss 四位随机数”生成private String generateOrderNo() { SimpleDateFormat sdf new SimpleDateFormat(yyyyMMddHHmmss); String timeStr sdf.format(new Date()); int randomNum (int) ((Math.random() * 9 1) * 1000); return timeStr randomNum; }如果项目要求更高可以引入Redis自增序列或者雪花算法但毕设用这个级别足够。4.5 拦截器实现登录与角色权限控制权限控制是系统安全性的重要体现。SpringBoot中通过实现HandlerInterceptor接口来定义拦截器。public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); User user (User) session.getAttribute(loginUser); String uri request.getRequestURI(); // 管理员路径校验 if (uri.startsWith(/admin)) { if (user null) { response.sendRedirect(/admin/login); return false; } if (user.getRole() ! 2) { response.getWriter().write(无权访问); return false; } } // 用户中心路径校验 if (uri.startsWith(/user)) { if (user null) { response.sendRedirect(/user/login); return false; } } return true; } }注册拦截器Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/**) .excludePathPatterns(/, /index, /product/**, /login, /register, /admin/login, /css/**, /js/**, /images/**); } }这里有一个新手容易犯的错误如果没有把静态资源路径排除页面上的CSS和JS文件全都会被拦截器拦截导致页面样式丢失。我在第一次做项目时就踩过这个坑整个页面白花花的没有任何样式排查了半天才发现是拦截器把静态请求也拦了。4.6 文件上传与富文本展示商品管理功能实战商品管理中图片上传是一个绕不开的功能。实现方案是文件上传接口接收MultipartFile保存到本地磁盘指定目录然后把磁盘路径映射成URL返回。PostMapping(/admin/product/uploadImage) ResponseBody public MapString, Object uploadImage(RequestParam(file) MultipartFile file) { MapString, Object result new HashMap(); try { // 1. 原始文件名处理 String originalFilename file.getOriginalFilename(); String suffix originalFilename.substring(originalFilename.lastIndexOf(.)); // 2. 生成唯一文件名防止重名覆盖 String fileName UUID.randomUUID().toString().replace(-, ) suffix; // 3. 保存到本地磁盘 String filePath uploadDir File.separator fileName; File dest new File(filePath); if (!dest.getParentFile().exists()) { dest.getParentFile().mkdirs(); } file.transferTo(dest); // 4. 返回可访问的URL路径 String url /upload/ fileName; result.put(success, true); result.put(url, url); } catch (IOException e) { e.printStackTrace(); result.put(success, false); } return result; }文件上传有两点需要注意一是务必对文件名做处理不能直接用用户上传的原始文件名保存否则会有路径穿越的安全风险二是要在SpringBoot中配置静态资源映射把/upload/**路径指向磁盘上的上传目录。spring: web: resources: static-locations: classpath:/static/,file:${upload.dir}这里我用了一个自定义配置项upload.dir用于指定文件上传的根目录。这样做的好处是部署时只需要改配置不需要改代码。5. JSP页面渲染与前端交互细节5.1 JSP项目中页面文件应该放在哪JSP文件在SpringBoot项目中默认放在src/main/webapp/WEB-INF/jsp/目录下。放在WEB-INF下的好处是外部无法通过URL直接访问必须经过Controller转发才能渲染安全性更好。这里有个容易踩坑的地方IDEA新建SpringBoot项目默认可能没有webapp目录需要手动创建并且要在Project Structure里把webapp标记为Web资源目录否则启动后JSP会404。5.2 页面渲染EL表达式与JSTL标签JSP页面中最重要的两个技术点就是EL表达式和JSTL标签库。EL表达式用${}语法获取后端传入的数据JSTL标签用于逻辑控制和循环遍历。商品列表页面的核心代码% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % div classproduct-grid c:forEach items${productList} varproduct div classproduct-card a href/product/detail/${product.id} img src${product.mainImage} alt${product.name} /a div classproduct-info h3${product.name}/h3 p classprice fmt:formatNumber value${product.price} typecurrency pattern¥0.00/ /p p classstock库存${product.stock}/p a href/cart/add/${product.id} classbtn-add-cart 加入购物车 /a /div /div /c:forEach /divJSTL的c:forEach是JSP页面渲染列表的主力它有两个属性需要掌握items代表循环的数据集合var是循环变量的名字。很多新手分不清items和var的区别其实items是“你要遍历什么”var是“遍历出来的每个元素叫什么”。永远不要在JSP页面中编写Java代码片段就是那种% %的写法。工程上这是大忌页面会变得无法维护而且混入Java代码会导致页面逻辑与业务逻辑深度耦合。如果有复杂的格式化逻辑应该放在后端处理好JSP只负责展示。5.3 购物车页面的全选与数量修改购物车页面是前端交互相对复杂的一个页面。核心功能包括全选/取消全选、修改数量、计算总价、删除商品、结算。如果每个操作都发起一次AJAX请求代码会非常繁琐。我的建议是量大的交互用JavaScript在前端完成最终提交时一次性更新购物车。比如用户勾选商品、修改数量总价实时用JavaScript计算展示不需要请求后端只有点击“去结算”时才把选中的购物车ID列表提交到后端。数量修改涉及一个关键点库存校验。前端JavaScript只能做友好的交互提示真正的库存校验必须放在后端。比如用户在页面把数量改到1000前端可以正常显示但提交下单时后端发现库存不足要返回明确的错误提示并且在页面回显。金额计算也必须在后端重算千万不要信任前端传过来的总价。道理很简单前端的数据是用户可以篡改的一个懂技术的用户完全可以把总价从100改成0.01提交给你。后端一定要根据数据库中的当前价格重新计算。5.4 JSP文件上传前后的中文乱码问题JSP页面最常见的坑就是中文乱码。乱码产生的原因只有一个字符编码在某个环节不一致。完整的解决方案页面文件本身用UTF-8保存文件头部声明UTF-8编码SpringBoot中配置字符编码过滤器强制请求和响应都使用UTF-8数据库连接URL加上characterEncodingutf-8数据库表结构使用utf8mb4字符集Tomcat对于POST请求表单中的中文默认以ISO-8859-1解码所以必须配置characterEncodingFilterserver: servlet: encoding: charset: UTF-8 enabled: true force: trueforce: true这个配置很关键它强制所有请求和响应都使用UTF-8覆盖容器默认的编码设置。如果漏了这个配置即使页面是UTF-8POST表单提交的中文依然可能乱码。6. 部署上线与常见问题排查实录6.1 从IDEA打包到本地运行的完整流程项目写完以后如何打包部署是很多同学容易忽略的环节。其实SpringBoot项目打包非常简单mvn package就完事但有几个坑需要特别说明。如果你的项目使用了JSP打成jar包和打成war包的处理方式不一样。SpringBoot的JSP支持在jar包模式下稍微有些特殊JSP文件需要放在META-INF/resources目录下才能被识别而且内嵌Tomcat对JSP的支持不如外部Tomcat稳定。所以我建议打成war包部署到外部Tomcat这样最稳妥。具体做法分三步修改打包方式为warpackagingwar/packaging让启动类继承SpringBootServletInitializerSpringBootApplication public class FruitMallApplication extends SpringBootServletInitializer { public static void main(String[] args) { SpringApplication.run(FruitMallApplication.class, args); } Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(FruitMallApplication.class); } }执行mvn clean package把target目录下生成的war包放到外部Tomcat的webapps目录启动Tomcat就能访问了。答辩演示时用IDEA直接运行SpringBoot的主类也可以但会显得不够专业。如果你能在答辩前准备好一个独立运行的部署环境演示时直接localhost访问这种稳定可靠的状态会给你加不少印象分。6.2 IDEA和SpringBoot版本相关的经典报错结合热搜词里那些“idea创建springboot项目失败”“springboot版本太高”的痛点我整理了JSP项目中最常见的几个报错场景。场景一IDEA新建项目没有Spring Initializr选项这个问题的原因是IDEA版本不同创建项目的入口不同。比较新的IDEA版本创建项目时选择Spring Initializr的入口在New Project - Spring Boot。如果确实找不到可以直接访问start.spring.io生成项目模板然后导入IDEA。场景二JSP文件404这个报错基本可以锁定为缺少tomcat-embed-jasper依赖。检查pom.xml加上这个依赖基本就能解决。另外确认一下JSP文件是否放在src/main/webapp/WEB-INF/jsp/目录下并且SpringBoot配置中view的前缀后缀是否写对。场景三SpringBoot版本太高导致某些配置不兼容SpringBoot 3.x和2.x在依赖坐标上有较大差异比如javax.servlet变成了jakarta.servlet。如果你用的是SpringBoot 3.xJSTL依赖需要相应调整。最保守的做法是选择SpringBoot 2.7.x版本它对JSP的支持最成熟稳定网上的资料也最多。场景四MySQL驱动连不上数据库新版MySQL驱动类名从com.mysql.jdbc.Driver变成了com.mysql.cj.jdbc.Driver连接URL也要带上serverTimezone。如果项目用的MySQL 8.xmysql-connector-java应该对应改成mysql-connector-j。6.3 数据一致性问题和跨页面传参的隐蔽Bug在做这个商城系统时有几个隐蔽的逻辑Bug我花了很长时间才排查出来这里分享给读者。第一个是购物车商品被管理员下架后用户购物车里的商品如何处理。如果购物车中商品ID对应商品已经下架直接查关联时就会查出空数据页面可能直接报空指针。我的处理方案是查询购物车列表时通过INNER JOIN关联商品表如果商品不存在或已下架该条购物车记录在图谱中直接过滤掉同时清除无效的购物车记录。第二个是Session超时后表单重复提交。用户在页面停留时间过长Session过期了点击提交按钮时后端获取不到用户信息会抛出异常。我的处理方案是在提交方法里先判断Session中是否有用户如果没有就重定向到登录页并在登录页提示“登录已过期请重新登录”。第三个是金额类型的格式化展示。Java中BigDecimal在JSP页面直接输出时会显示科学计数法比如1.0E2这样的形式非常不美观。解决方法是使用fmt:formatNumber标签或者在后端把金额转换为字符串再放入Model。还有一个容易被忽视的点用户下单后再次修改购物车商品数量并不会影响已经创建的订单。这是符合业务逻辑的因为订单已经生成了快照。但如果代码编写不小心在订单详情查询时去关联当前商品表的最新价格就会导致“订单金额和明细对不上”的问题。所以订单详情展示时一定要查order_item表而不是product表。6.4 答辩演示要准备的测试数据和演示脚本最后分享一点很实在的经验。项目做完了答辩演示环节也要提前准备否则现场容易手足无措。我建议准备一份测试数据清单包含一个管理员账号role2、两个普通用户账号、至少两个商品分类、每个分类下3-5个商品、不同状态的订单各一个待支付、待发货、待收货、已完成。这些数据能让演示流程流畅。演示脚本按照这个顺序走最顺畅首页展示商品列表演示分类筛选和搜索功能注册一个新用户完成登录浏览商品详情加入购物车演示数量修改和总价计算结算下单演示订单生成和购物车清空在“我的订单”中查看刚下的订单切换到管理员账号演示商品上下架、库存修改在订单管理中找到刚刚用户下的订单演示发货操作回到用户端确认收货整个业务闭环完成按照这个流程走完系统的主要功能都展示到了答辩老师对项目的完整度会有一个直观的印象。中途如果某个功能报错也不要慌先口述这个功能的业务逻辑再说“让我看看控制台报错信息”然后快速定位修复。演示出问题不可怕慌张才可怕。7. 经验总结与个人体会做这个水果商城项目我的最大体会是技术选型不在于多新、多复杂而在于每一层你都能讲清楚。SpringBoot JSP这套组合虽然看起来不那么“现代”但它把Java Web开发的主干脉络完整地呈现出来了——从前端页面到控制器从Service到Mapper从数据库表到对象映射每一层都是经典方案每一环都能找到对应的知识点。我个人在实际操作中还有一个建议开头提到的那些热搜词比如“springboot配置”“idea设置jsp页面语法高亮”“jsp快速引入sdk”说明大家在做毕设时遇到的问题高度集中在环境配置层面。如果你也在IDEA里配置JSP语法高亮可以在Settings - Editor - File Types里把.jsp文件关联到HTML类型这能让JSP页面的编写体验好很多。这些小技巧虽然不写进论文但它们决定了你的开发效率。最后再分享一个小技巧把项目的SQL脚本和需求分析文档整理好放到源码的doc目录下。这不仅是标题里提到的交付物要求也是答辩时的加分项。老师看到你不仅写了代码还整理了数据库设计文档和需求分析文档会认为你具备完整的项目思维。这套资料在后续找工作时的项目经历描述中也能直接复用。本文还有配套的精品资源点击获取