JSP+Servlet+MySQL手机商城系统设计与实现全解析

📅 发布时间:2026/10/16 7:05:29
JSP+Servlet+MySQL手机商城系统设计与实现全解析
手机商城这种项目在JavaWeb课程设计里属于出场率最高的类型之一基本每个学校都能看到几个JSPServletMySQL的商城系统。但说实话很多同学拿到的成品代码质量很差要么三层结构一团乱麻要么数据库设计压根撑不起订单流程。这篇博文就以我搞过的一个JSPServletBootstrap的手机商城管理系统为例子把从项目结构、数据库设计到核心功能实现、避坑踩雷的全过程都拆开讲清楚给出的代码和思路都是可以直接照搬的。如果你是正在做课程设计、毕设预演、或者是想零框架入门JavaWeb开发的同学这篇内容应该能省下你不少查资料的功夫。1. 项目定位与技术选型思路1.1 为什么用JSPServlet而不是Spring Boot选题阶段很多人会纠结都2025年了怎么还搞JSPServlet这种老技术直接Spring Boot不香吗这里面的逻辑要分两面看第一如果从工程效率角度Spring Boot确实方便内嵌Tomcat、自动配置、开箱即用的starter半天就能把骨架拉起来。但绝大多数高校的《JavaWeb程序设计》课程教的还是Servlet的生命周期、HttpServletRequest/HttpServletResponse原理、请求转发与重定向区别、Session跟踪机制这些底层内容。课程要求是什么项目就用什么这是最稳的策略。第二JSPServlet在理解Web运行原理这件事上有不可替代的优势。你会直接感受到浏览器发出一个HTTP请求之后请求是怎么被Tomcat接住、怎么被web.xml或者注解找到对应Servlet、Servlet怎么调DAO层、最后怎么通过JSP把数据渲染成HTML返回给用户的。整条链路没有任何魔法每一步都看得见摸得着。等你以后再去学Spring MVC、Spring Boot会发现其核心流程依然是DispatcherServlet转发到Controller本质是同一套请求-处理-响应模型。所以这个项目的定位很明确不追求工程上的极致便利而是追求把Web开发核心链路吃透。1.2 整体功能范围界定手机商城听起来是个大工程但作为课程设计不能无限膨胀要给自己的工作量划好边界。我最终确定的功能列表如下前台用户端注册、登录、退出、商品分类浏览、商品列表分页、商品关键词搜索、商品详情查看、加入购物车、购物车管理改数量、删商品、清空、提交订单、个人订单列表查询、订单状态跟踪。后台管理端管理员登录、商品分类管理增删改查、商品管理增删改查、图片上传、上下架、订单管理查看订单、发货状态更新、订单详情、用户管理查询、禁用/启用。这个范围很标准既覆盖了JavaWeb的核心知识点——Servlet、JSP、Filter、Session、Cookie、JDBC、分页、文件上传又不至于做起来没完没了。对比那些动不动加秒杀、加优惠券、加评价系统的方案我觉得基础功能够用、能讲清楚就行了功能堆太多反而暴露更多bug。2. 数据库设计五张核心表的字段与关系2.1 数据库设计原则商城系统最核心的数据库表就是五张用户表(user)、分类表(category)、商品表(goods)、订单表(orders)、订单项表(order_item)。其中订单表和订单项表是一对多关系订单项和商品表是一对一关系用户表和订单表是一对多关系。有些设计还会加一张购物车表但购物车表不是必须的我这次选择把购物车数据直接存Session后面会说为什么。一个容易犯的错是初次设计时把商品价格直接存在订单项里觉得没必要。后来做订单统计和订单详情查询的时候才明白商品价格是会变的今天卖3999明天促销改成3599如果不把下单那一刻的价格快照存进订单项查历史订单时价格就会跟着商品表变这绝对不行。业务数据必须做快照这是做交易系统的基本常识。2.2 表结构设计明细用户表(user)字段如下字段名类型说明idint(11) PK AUTO_INCREMENT用户IDusernamevarchar(32) NOT NULL UNIQUE登录用户名passwordvarchar(64) NOT NULL密码MD5加密后存储phonevarchar(16)手机号addressvarchar(255)收货地址create_timedatetime注册时间statusint(1)1正常 0禁用密码用MD5加密存储但这里说一句MD5在真实生产环境是不推荐的至少要用加盐的SHA-256或者直接用BCrypt。不过课程设计阶段MD5已经比明文密码强好几个档次了老师检查时也基本认可。商品表(goods)字段如下字段名类型说明idint(11) PK AUTO_INCREMENT商品IDcategory_idint(11)所属分类ID外键关联category表goods_namevarchar(100) NOT NULL商品名称pricedecimal(10,2) NOT NULL当前售价stockint(11)库存数量imagevarchar(255)图片URL存相对路径descriptiontext商品描述statusint(1)1在售 0下架create_timedatetime上架时间注意image字段存的是相对路径比如/upload/goods/iphone16.jpg实际文件放在Tomcat的webapps项目upload目录下。这比直接存base64字符串或者外链图片靠谱得多。订单表(orders)和订单项表(order_item)字段如下orders表字段名类型说明idint(11) PK AUTO_INCREMENT订单IDuser_idint(11)下单用户order_novarchar(32) UNIQUE订单编号时间戳随机数total_pricedecimal(10,2)订单总价receiver_namevarchar(32)收货人姓名receiver_phonevarchar(16)收货人电话receiver_addressvarchar(255)收货地址statusint(1)1待付款 2待发货 3已发货 4已完成 5已取消create_timedatetime下单时间order_item表字段名类型说明idint(11) PK AUTO_INCREMENT项IDorder_idint(11)所属订单外键关联ordersgoods_idint(11)商品IDgoods_namevarchar(100)商品名称快照goods_pricedecimal(10,2)下单时价格快照buy_numint(11)购买数量sub_totaldecimal(10,2)小计金额订单表和订单项表为什么都要存收货人信息呢这个可能有人不理解订单表存一份订单项为什么不能查用户表join答案是下单后用户改了地址怎么办物流已经发出了你拿新地址去发货就错了。订单里必须固化下单时刻的收货人信息这就是快照思想。建表SQL用CREATE TABLE即可注意外键约束可以加也可以不加。我实际建表是加逻辑外键不加物理外键的因为物理外键在删除数据时容易报错而且项目代码里本来就通过JDBC一步步关联查询没必要让数据库再强制约束一遍。3. 项目工程结构零配置依赖的包组织方式3.1 三层架构的落地方式这个项目典型的包结构如下src/main/java ├── com.shop.entity // 实体类User、Category、Goods、Orders、OrderItem ├── com.shop.dao // 数据访问层UserDao、GoodsDao、OrdersDao等 ├── com.shop.service // 业务逻辑层GoodsService、OrderService等 ├── com.shop.servlet // Servlet控制层GoodsServlet、CartServlet、OrderServlet等 ├── com.shop.filter // 过滤器EncodingFilter、LoginFilter └── com.shop.util // 工具类DBUtil、MD5Util、PageUtil很多示例项目喜欢把Servlet直接叫XxxServlet这个没问题。但要注意我强烈建议每张表对应的操作不要拆成十个Servlet而是按模块聚合一个GoodsServlet通过action参数区分list、detail、search、add、update、delete、upDownShelf这些操作。这样web.xml或注解路由数量能少一大半逻辑也更内聚。看action参数分发根据不同的action值调用不同的service方法最后决定转发到哪个JSP。以前见过一个同学的项目光商品模块就写了十来个ServletGoodsListServlet、GoodsDetailServlet、GoodsSearchServlet、GoodsAddServlet……每次改需求要在web.xml里配一长串映射。这种写法不是不行但维护起来特别痛苦。相比之下一个模块一个Servlet模式在课程设计里是更主流、更清晰的选择。3.2 数据库连接工具类的写法DBUtil负责获取数据库连接我直接用原生的JDBC方式不引入MyBatis、JdbcTemplate这些因为项目越简单越好减少学习成本。下面是一个典型写法public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/phone_shop?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai; private static final String USERNAME root; private static final String PASSWORD 123456; private static final String DRIVER com.mysql.cj.jdbc.Driver; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() { try { return DriverManager.getConnection(URL, USERNAME, PASSWORD); } catch (SQLException e) { throw new RuntimeException(数据库连接失败, e); } } public static void close(Connection conn, Statement stmt, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException e) {} if (stmt ! null) { try { stmt.close(); } catch (SQLException e) {} if (conn ! null) { try { conn.close(); } catch (SQLException e) {} } }几个关键配置说明一下useSSLfalse如果不加新版MySQL驱动会频繁提示SSL警告虽然不影响连接但控制台飘一堆红字很碍眼。serverTimezoneAsia/Shanghai不加这个驱动报时区错误是家常便饭。useUnicodetruecharacterEncodingutf8必须加否则中文乱码问题会让你怀疑人生。但是要注意DBUtil里这种硬编码用户名密码的方式只适合本地课程设计。我踩过仓库分享出去的坑数据库密码直接暴露在代码里后来不得不紧急改掉。3.3 Filter解决编码和登录拦截编码过滤器是所有Servlet处理的入口必须在请求到达业务代码之前就把编码统一。写得简单一点WebFilter(/*) public class EncodingFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); chain.doFilter(request, response); } }登录过滤器一般只拦截后台管理路径就是/admin/*这样的路径。逻辑很简单从Session里取管理员取不到就重定向到登录页。但要记住一个技巧放行登录页本身、放行静态资源js/css/img这些路径否则会出现一个诡异现象明明已经到登录页了页面样式全是裸的浏览器控制台一大堆css的404。JSP页面里还要加一行% page contentTypetext/html;charsetUTF-8 languagejava %这个是JSP自己声明的编码和过滤器的request编码是两个体系容易漏掉。4. 前台购物车实现Session方案详解4.1 为什么购物车不建表前面说了我的购物车直接用Session存不建购物车表。很多同学会问正规电商网站购物车都是存数据库的不然刷新就丢了呀。课设场景下Session方案完全够用理由有三第一购物车是临时性数据用户可能加完不买就关浏览器了存数据库会产生大量脏数据还得做定时清理。第二购物车功能核心是临时存储选中商品和数量Session天然就是一个绑定用户会话的临时存储区域生命周期正好匹配。第三数据库表数量少减少联表查询和事务复杂度答辩时讲起来也轻松。但它的缺点也很明显Session会超时默认30分钟用户换浏览器购物车就没了服务端重启购物车也没了。这个方案放在生产环境是不行的但课设完全能接受答辩时还能主动指出这个优化方向反而加分。4.2 购物车的数据结构设计购物车的核心数据结构用Mapkey是商品IDvalue是购买数量。封装一个Cart类public class Cart { // key为商品idvalue为购买数量 private MapInteger, Integer items new HashMap(); public void addItem(Integer goodsId, Integer num) { if (items.containsKey(goodsId)) { items.put(goodsId, items.get(goodsId) num); } else { items.put(goodsId, num); } } public void updateItem(Integer goodsId, Integer num) { if (num 0) { items.remove(goodsId); } else { items.put(goodsId, num); } } public void removeItem(Integer goodsId) { items.remove(goodsId); } public void clear() { items.clear(); } public MapInteger, Integer getItems() { return items; } }购物车页面要展示的商品名称、价格、图片这些信息不能存Session因为商品信息变了Session里的就过期了。正确做法是购物车里只存商品ID和数量每次打开购物车页面时再拿着这些ID批量查数据库把商品实时信息查出来组装成List展示。这里用到一个SQL技巧批量查商品的where条件用动态拼IN语句SELECT * FROM goods WHERE id IN (?,?,?)JDBC工程里可以先按购物车商品数量创建占位符循环设置参数。这个方法我用得很频繁记一下对以后写批量操作也有帮助。4.3 购物车代码实现要点加入购物车请求是/cart?actionaddgoodsId1num1CartServlet的addItem方法里核心逻辑// 从Session获取购物车如果没有则创建 Cart cart (Cart) session.getAttribute(cart); if (cart null) { cart new Cart(); } cart.addItem(goodsId, num); session.setAttribute(cart, cart); response.sendRedirect(request.getContextPath() /cart?actionlist);注意最后使用sendRedirect()而不是转发forward()这背后是一个重要的Web开发原则PRG模式Post/Redirect/Get。如果不做重定向用户按F5刷新页面时浏览器会重复提交上一次的POST请求购物车里同一个商品会被反复加购翻倍叠加。用sendRedirect让浏览器发起新的GET请求刷新就只剩查询不会重复触发加购操作。这个细节非常经典老师问到“为什么不能用转发”你答出PRG模式基本就稳了。购物车列表页展示时我额外定义了一个CartItemVO的简单类把goodsId、goodsName、price、image、num、subTotal组合在一起public class CartItemVO { private Integer goodsId; private String goodsName; private BigDecimal price; private String image; private Integer num; private BigDecimal subTotal; // getter/setter省略 }为什么用VO不用Map因为JSP页面用EL表达式取数据时${item.goodsName}比${item[goodsName]}可读性强太多。用HashMap的key还得写中括号而且VO的属性类型固定不会出现Object强制类型转换的麻烦。初学者常犯的一个错是JSP页面里EL表达式取不到值了怀疑是EL写错了其实往往是因为后台放进来的数据是Map或者List${item.name}和Map的key对不上。5. 商品管理后台图片上传与分页搜索的完整实现5.1 图片上传的两种方案后台商品管理最核心的技术点是图片上传。Servlet接收文件上传可以用原生API也可以用第三方库commons-fileupload。本来想用原生API因为Tomcat 8之后自带part接口支持文件上传完全不依赖额外jar包。WebServlet(/admin/goods/upload) MultipartConfig public class UploadServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) { Part part request.getPart(file); String fileName getFileName(part); // 重命名文件避免重名和中文乱码 String ext fileName.substring(fileName.lastIndexOf(.)); String newName System.currentTimeMillis() _ new Random().nextInt(1000) ext; String basePath request.getServletContext().getRealPath(/upload/goods); part.write(basePath File.separator newName); // 数据库保存的是相对路径 String url /upload/goods/ newName; } }这个实现很简洁。核心是MultipartConfig注解让Servlet支持multipart/form-data请求request.getPart(file)拿到上传文件part.write(path)直接把文件写到磁盘。要注意的是request.getServletContext().getRealPath(/upload/goods)拿的是当前Tomcat运行时项目的真实磁盘路径如果你的Tomcat用IDE集成目录一般在out/artifacts下如果直接扔war到Tomcat的webapps则会解压在webapps/项目名/upload/goods下。图片上传的坑主要在两个地方第一是文件名重命名不重命名就容易出现两个不同图片同名覆盖的惨案。第二是上传路径的截取。如果你只保存了磁盘绝对路径万一以后换了机器、换了Tomcat部署位置图片全挂。必须存相对URL路径然后JSP页面用${pageContext.request.contextPath}拼绝对URL来展示这样项目挪哪里图片都能正常访问。5.2 商品分页查询的实现思路前端商品列表和后台商品管理列表都有分页需求。分页的核心就是两个SQL配合起来-- 查询总记录数算总页数用 SELECT COUNT(*) FROM goods WHERE category_id? AND goods_name LIKE ? -- 查询当前页数据 SELECT * FROM goods WHERE category_id? AND goods_name LIKE ? ORDER BY id DESC LIMIT ?, ?LIMIT两个参数第一个是偏移量(offset)第二个是每页大小(pageSize)。比如每页8条第2页就是从第8条开始取8条偏移量计算是(currentPage-1) * pageSize。Service层请求分页参数之外还返回一个PageUtil对象public class PageUtil { private int currentPage; // 当前页 private int pageSize; // 每页条数 private int totalCount; // 总记录数 private int totalPage; // 总页数 private List? list; // 当前页数据 // 构造时计算总页数 public PageUtil(int currentPage, int pageSize, int totalCount) { this.currentPage currentPage; this.pageSize pageSize; this.totalCount totalCount; this.totalPage (totalCount pageSize - 1) / pageSize; } }计算总页数时很多同学写totalCount / pageSize这样如果总记录数能被每页条数整除页数会少算一页。正确写法是(totalCount pageSize - 1) / pageSize相当于向上取整。总记录12条、每页8条真实应该是2页12 / 8算出来只有1而(127)/8向上取整正好是2。JSP前台翻页控件写起来也很固定c:if test${page.currentPage 1} a href${pageContext.request.contextPath}/goods?actionlistpage${page.currentPage-1}上一页/a /c:if c:if test${page.currentPage page.totalPage} a href${pageContext.request.contextPath}/goods?actionlistpage${page.currentPage1}下一页/a /c:if页码循环输出时注意越界情况currentPage最大传多少、最小传多少都要在前端校验我在GoodsServlet的list方法里做了兜底如果传的页码小于1就强制置为1如果超过总页数就置为总页数。5.3 商品搜索的关键词防坑搜索功能无非是前端form提交一个keyword参数后端SQL拼接WHERE goods_name LIKE % ? %最关键的坑在于SQL注入。很多课程设计里的搜索功能是直接字符串拼接的String sql SELECT * FROM goods WHERE goods_name LIKE % keyword %;一旦用户在搜索框输入 or 11 --这种字符串SQL语句就会变成查询所有商品在真实系统里这是灾难。正确做法是全部改用PreparedStatement预编译参数用占位符这是我写DAO时坚持的死规矩任何外部传入的参数一律不允许字符串拼接进SQL。6. 订单流程实现事务控制的完整链路6.1 下单操作的事务边界订单提交是整个项目里事务最核心的环节它不只是往orders表插一条数据就完了而是同时涉及多个表写入在orders表插入订单主表记录在order_item表批量插入订单项更新商品库存stock stock - 购买数量这三个操作必须同生共死任何一个失败都需要整体回滚否则会出现订单生成了库存没扣的bug。商品明明只剩1个结果有两个人各下单成功买走2件库存变负数了。这是最经典的并发事务问题虽然课设不会遇到高并发但事务逻辑必须正确。订单DAO层用事务的写法核心是Connection不能由DAO内部独立管理必须由Service层控制拿到同一个连接最后统一提交或回滚Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 插入订单主表 OrderDao.insertOrder(conn, order); // 2. 批量插入订单项 OrderItemDao.batchInsert(conn, orderItems); // 3. 扣减库存 GoodsDao.reduceStock(conn, goodsId, buyNum); conn.commit(); // 全部成功提交 } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } // 抛异常给Servlet层提示下单失败 } finally { DBUtil.close(conn, null, null); }这里必须强调setAutoCommit(false)默认的JDBC连接是自动提交的每执行一条SQL就立即生效不关闭它的话根本无法实现事务回滚。我当时的OrderService里还藏了一个库存校验// 先查询商品当前库存 Goods goods goodsDao.findById(goodsId); if (goods.getStock() buyNum) { throw new RuntimeException(【 goods.getGoodsName() 】库存不足); }这个校验是防君子不防小人的在并发场景下仅仅先查再更新还有竞态的漏子但课设阶段问题不大。如果你想做得更严谨一点可以把扣库存的SQL写成条件更新UPDATE goods SET stock stock - ? WHERE id ? AND stock ?执行后判断更新行数0表示库存不够整个事务直接回滚。这个方案完美解决超卖问题答辩时是可以拿出来加分的。6.2 生成订单编号的技巧订单编号order_no一般要求全局唯一。我的方案是用时间戳加随机数拼出来的String orderNo System.currentTimeMillis() String.valueOf((int)(Math.random() * 100000));这个方案简单可靠没有分布式ID那么复杂课设场景完全够用。再加一个数据库拼接前缀NO生成结果类似NO170381234567898765。如果想去掉随机性还可以用UUID.randomUUID().toString().replace(-, )但因为太长放到订单列表里比较丑。6.3 订单状态机管理订单状态我用一个数字字段维护JSP页面通过switch标签展示对应名称1待付款2待发货3已发货4已完成5已取消。后台订单管理页面的核心操作是“发货”就是管理员把订单状态从待发货改成已发货。发货时通常允许填写快递单号但课设一般免了。用户端“确认收货”操作就是把状态从已发货变为已完成这个逻辑也不复杂重点只在于更新SQLUPDATE orders SET status ? WHERE id ? AND user_id ?为什么要带user_id防止用户A操作用户B订单的越权行为。这在Web安全里叫水平越权保护。很多初学者写代码时只按id更新攻击者把订单ID改成别人的单号就能操作别人订单。虽然课设没有安全测试但这个习惯必须养起来。7. 前端集成Bootstrap页面美化与交互细节7.1 页面布局方案前端我用的是Bootstrap 3.4.1版本为什么不选Bootstrap 5因为国内几乎所有JSP课程资料、教材示例都用Bootstrap 3的栅格语法查资料方便而且Bootstrap 3对老浏览器兼容更好老师机器上即使IE都能正常显示。整个门户页面布局是一个典型的上下结构顶部导航栏 navbar放Logo、分类菜单、搜索框、购物车入口、用户登录/注册/退出。中间主体 container商品列表用row加col-md-4一行三列也可以换成col-md-3一行四列。底部footer简单的版权信息。商品卡片的结构是这样写的div classcol-md-4 div classthumbnail img src${pageContext.request.contextPath}${goods.image} alt${goods.goodsName} styleheight:200px;width:100%;object-fit:cover; div classcaption h4 classtext-ellipsisa hrefgoods?actiondetailid${goods.id}${goods.goodsName}/a/h4 p classtext-dangerstrong¥${goods.price}/strong/p p a hrefcart?actionaddgoodsId${goods.id}num1 classbtn btn-primary btn-sm加入购物车/a a hrefgoods?actiondetailid${goods.id} classbtn btn-default btn-sm查看详情/a /p /div /div /div${pageContext.request.contextPath}这个EL表达式是你的最佳伙伴它动态获取应用上下文路径避免硬编码/phone_shop这种死路径。因为项目一旦换个部署名字所有硬编码路径全部404。我在JSP里凡是引用URL、图片、css/js的一律用它拼接。这个动作做对之后项目换个war包名字都不会出任何路径问题。7.2 轮播图与静态资源处理门户首页顶部放一个Bootstrap轮播图视觉上撑场面。通常放三张banner图图片存在web/upload/banner目录下面。轮播图的Bootstrap依赖jquery.min.js和bootstrap.min.js两个脚本文件在/jsp/common/footer.jsp这个公共include文件里统一下方引入首页、列表页、详情页都include统一页脚避免每个页面重复写一大段引脚本代码。JSP里include公共片段我用的是% include filecommon/header.jsp %这种静态包含方式。它比jsp:include性能更好JSP编译期直接合并输出运行时没有额外请求。这个方案特别适合页面头部导航这种纯静态片段。7.3 非Bootstrap页面要注意的细节登录页、后台管理页我用的是独立布局。后台管理页框架采用左侧菜单右侧内容的两栏结构左侧是管理菜单列表右侧是具体管理功能的iframe或者直接包含目标JSP页面。这里我不建议用iframe因为iframe内页面的Session、跳转、样式都很容易出问题。直接用JSP include或者Servlet转发是最稳定、最传统的做法。后台登录页有一个经典坑管理员如果没登录过滤器重定向到/admin/login.jsp。但登录页本身的css和js也是通过/admin/css/bootstrap.min.css这类相对路径引用的如果不小心在LOGIN_FILTER的排除列表里写漏就会造成一个死循环页面发起的css请求也被拦截重定向到登录页然后css请求又带上新Session去请求登录页。排除路径时一定要把/admin/css/、/admin/js/、/admin/images/这些静态资源目录一并放行。8. 环境搭建与部署全过程8.1 本地开发环境我用的工具组合是JDK 1.8MySQL 5.7Tomcat 8.5IntelliJ IDEAMaven用来做依赖管理主要是JSTL、mysql-connector-java等必用jar顺便说一句IDEA里创建一个Maven Web项目选择maven-archetype-webapp骨架即可。如果没有Maven的jar包扫描问题直接用IDEA的Web Application骨架然后手动copy lib包也完全可以很多学校机房不装Maven后者更稳妥。依赖就三个核心jardependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependencyServlet-api和jsp-api不需要引入Tomcat自带引入了反而可能和容器版本冲突导致ClassCastException。8.2 数据库初始化脚本运行项目前先手动在MySQL里创建数据库DROP DATABASE IF EXISTS phone_shop; CREATE DATABASE phone_shop DEFAULT CHARACTER SET utf8mb4; USE phone_shop;然后依次执行五张表的建表SQL、插入几类测试分类、插入十几条手机商品的INSERT语句、插入一个管理员账号。注意一定要指定DEFAULT CHARACTER SET utf8mb4不是utf8因为utf8在MySQL里不是真正的全量Unicodeemoji和特殊字符会挂。课程设计里商品描述偶尔出现特殊字符的情况也不少。初始管理员密码我放的是md5加密后的字符串不能直接在SQL里写明文否则登录验证永远对不上。这里给个小提示可以通过在线MD5工具把admin123转成密文也可以直接写个测试类调用项目的MD5Util生成然后复制到SQL里。8.3 Tomcat部署细节IDEA里配置Tomcat关键是Deployment标签页选择exploded模式开发模式目录热部署Application context设成/phone_shop或者干脆设成/。如果设成根路径所有${pageContext.request.contextPath}取到的值是空字符串访问路径会变成/goods?actionlist这种看起来正常但要小心Feature写相对路径时不能用request.getContextPath()拼接后的空值来判断是否从根路径开始JSP中URL会少一个斜杠。部署完成后访问http://localhost:8080/phone_shop/应该能看到首页。第一次跑起来如果发现首页所有图片裂了百分之九十九是图片路径没拼接contextPath而不是图片本身有问题。9. 高频问题排查清单来自现场debug的血泪经验9.1 Tomcat启动无报错但页面404最典型的场景是部署后访问根路径404。原因基本有三类一类是项目没有设置欢迎页web.xml里没有配置welcome-file-list或者虽然配置了/index.jsp但index.jsp不存在。二类是Servlet的WebServlet注解路径写得不对比如映射成/goodsServlet但页面表单提交地址是/goods对不上。三类是Tomcat的webapps路径下解压war包后目录名和application context对不上URL拼的路径和实际部署路径不一致。排查顺序应该是先看IDEA日志里项目部署到哪个路径、context是什么再浏览器地址栏直接访问/projectName/index.jsp试试最后看后端Servlet的URL模式。9.2 JDBC连接报Communications link failure这个报错基本集中在三类原因连接URL写错、MySQL服务没启动、端口号不对。最常被坑的是MySQL 5.7默认端口3306但如果电脑上有两个MySQL实例比如装了MySQL 8又装了5.7端口可能是3307。检查URL参数里的端口号要和实际MySQL的监听端口号一致。还有一个很隐形的坑com.mysql.jdbc.Driver和com.mysql.cj.jdbc.Driver两个驱动类在不同版本的mysql-connector-java里兼容性不一样5.x版本用前者8.x版本必须用后者。如果控制台报ClassNotFoundException去检查jar包版本对应的驱动类名写没写对。9.3 中文乱码问题全家桶乱码要分三层排查JSP页面本身的pageEncodingJSP文件第一行指定% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %。表单提交参数Filter里request.setCharacterEncoding(UTF-8)。数据库写入建表SQL里指定utf8mb4连接串加characterEncodingutf8。任何一个掉了链子表现就非常妖有时插入数据库是乱码有时页面上直接问号有时只有搜索传参乱。以前调试一个POST请求的中文商品名写得一切都对后来发现是MySQL数据库表本身没建对字符集默认latin1怎么改代码都白搭。9.4 Session失效和购物车丢失Session的默认有效期是30分钟Tomcat的web.xml全局配置可以改。如果你做演示时拖了太久没操作购物车就空了老师说“这购物车怎么没保存”场面很尴尬。可以在Session创建后主动设置超时时间session.setMaxInactiveInterval(60*60); // 一小时对课设来说这个数字合适合适。9.5 管理员后台CSS完全裸奔后台管理页面登录后直接露出纯HTML所有样式都不加载。归根结底是CSS路径用错了不能用相对路径直接写css/bootstrap.min.css否则当前页面URL是/admin/user/list和/admin/login.jsp时的相对路径解析结果完全不同。统一改成link relstylesheet href${pageContext.request.contextPath}/admin/css/bootstrap.min.css这个教训炸过太多次我最后把所有引用都统一成绝对路径风格一次根治。10. 项目可优化方向多一点工程思维10.1 密码加密升级MD5换成加盐哈希或BCrypt。虽然课设不一定要求但如果答辩老师问“MD5有缺点你知道吗”能回答出彩虹表攻击、加盐的必要性会明显加分。10.2 引入Redis存验证码和Session如果项目想冲高一点可以把登录验证码用Redis存Session共享也用Redis。但这是毕业设计级别的优化课程设计里主要看时间够不够。10.3 分页体验优化前端分页控件加上总记录数、总页数提示加上“跳转到第几页”输入框再加商品列表排序按价格升序/降序、按销量都是工作量不大但看着很专业的细节。10.4 Controller层抽取Servlet里代码越写越长可以把公共部分抽取BaseServlet所有Servlet继承它通过反射调用action对应的方法。这是很多项目都在用的极简MVC框架思路也是向Spring MVC过渡的桥梁。一些心得JSPServletMySQL这组技术栈在当下不算新潮但作为学校里的练手项目它的价值恰恰在于“简单”和“透明”。做这个项目的过程本质上就是亲手把HTTP请求怎么进来、服务器怎么处理、数据库怎么存取、页面怎么渲染这条路完整走一遍的过程。数据库事务、分页、会话、文件上传、权限拦截这些Web开发的核心组件都能在这个项目里第一次实实在在触碰到。我个人的建议是课设阶段不要急着用框架掩盖问题先把Servlet和JDBC写明白后面学Spring Boot、MyBatis几乎是降维打击。希望上面这些经验能帮你少踩几个坑特别是像PRG模式、订单快照、PreparedStatement防注入这类细节做好了答辩时都是能讲出深度的地方。