
简介《JSP手机销售网设计说明书》是一份面向Java/JSP初学者、课程设计与毕业设计场景的完整项目文档围绕电商背景下手机销售网站的建设全过程展开。文档采用JSPMySQL遵循MVC模式解决传统销售模式数据处理量大、效率低等问题从项目背景、需求分析到概要设计、详细设计与实现均有清晰说明重点介绍了用户表、商品表、分类表、购物车表和订单表的表结构设计、E-R图以及系统主要模块注册、登录、购物车、商品浏览和查询的流程与功能附录还提供了界面截图与关键JSP源码便于对照学习。资源压缩包为单个docx文件文档总大小约789KB内容完整且便于查阅可帮助读者快速掌握JSP网站设计说明书的写法与数据库设计思路还可直接参考其中代码示例完善自身项目。目前已有466人学习下载适合课程设计、毕业设计备查或补全JSP项目文档撰写能力。1. 先从一张“JSP手机销售网设计说明书”说起它到底在要求你交付什么很多同学拿到“JSP手机销售网设计说明书”这个题第一反应是去找模板文档、复制一张封面再往里面塞几张截图。但“设计说明书”这四个字在课程设计和毕业设计里真正的意思是把整个手机销售网站从数据库、后端逻辑到页面展示每一个环节都讲清楚让接手的人照着说明就能把它复现出来。这不是一份文档任务是一个完整的 JSP Servlet MySQL 全栈小项目外加一篇能解释每行代码为什么这么写的交付物。它适合两类人一是正在做 Java Web 课程设计、需要选一个既有业务深度又能在两周内写完的题目的学生二是刚学完 Servlet 和 JSP、想用一个完整业务把零散知识点串起来练手的开发者。手机销售这个业务选得巧——有商品列表、用户登录、购物车、下单流程、库存扣减每个模块都是一道必考题。搞定了它你以后再看别的 Java Web 项目骨架基本都熟了。2. 技术选型与工程结构为什么偏偏是 JSP Servlet MySQL2.1 技术栈选型不选框架是为了让每一行代码都能被答辩老师问到“JSP手机销售网”这个标题里只点了 JSP但一个能跑通的销售系统不可能只有 JSP 页面。常见的配套组合是 JSP Servlet JavaBean MySQL采用 MVC 思想分层JSP 负责视图展示Servlet 负责接收请求和跳转JavaBeanDAO 实体类负责数据封装和数据库操作。这套组合在课程设计里是最稳的选择。为什么不用 Spring Boot 或者 SSM不是说框架不好而是“设计说明书”要求的是一步步讲清楚原理。你用一个 Servlet 写登录验证老师能问 Request 和 Session 的区别你直接上 Spring Boot 的拦截器老师只能看到封装后的黑匣子你自己也很难讲透。JSP Servlet 是最贴近 HTTP 底层的一套方案请求怎么进来、参数怎么取、Session 怎么保持、数据库连接怎么开关全部暴露在你面前。这套组合写完工程代码量通常在 1500 到 2500 行之间规模正好适合一个人两周完成不至于写不完也不会单薄得没有内容可写。数据库选 MySQL 而不是 Oracle 或 SQL Server主要原因只有一个——部署方便。MySQL 体积小、安装快、图形化客户端多Navicat 和 SQLyog 都能直接把建表脚本跑起来。Tomcat 加 MySQL 加 JDBC 驱动的组合已经是被验证过无数次的经典搭配遇到问题随便一搜就能找到答案这在赶进度的时候比任何技术优势都重要。2.2 说明书里的功能模块怎么映射到工程目录拿到题目后第一步不是写代码而是把说明书里的功能模块拆出来往工程目录上对应。一个标准的 JSP 手机销售网工程目录长这样PhoneShop/ ├── src/ │ ├── com.shop.entity/ 实体类User、Phone、Cart、Orders │ ├── com.shop.dao/ DAO 层UserDao、PhoneDao、OrderDao │ ├── com.shop.servlet/ Servlet 控制层LoginServlet、PhoneListServlet、CartServlet │ └── com.shop.util/ DBUtil、字符编码过滤器 ├── WebContent/ │ ├── jsp/ 业务页面login.jsp、register.jsp、phone_list.jsp │ ├── css/ 样式文件 │ ├── js/ 前端脚本 │ ├── images/ 商品图片和页面素材 │ └── WEB-INF/ │ ├── web.xml Servlet 映射和欢迎页配置 │ └── lib/ mysql-connector-java.jar我一般会先在说明书的“系统功能结构”那一章画一个树状图再把每个叶子节点对应到一个具体文件。比如“用户登录”对应LoginServlet.java加login.jsp“商品分页展示”对应PhoneDao.findByPage()加phone_list.jsp“加入购物车”对应CartServlet.java加cart.jsp。这样说明书写完的时候代码目录结构就已经定型了后面只是按图填充。WebContent 下的 WEB-INF 是 Java Web 的安全目录浏览器不能直接访问里面的文件但 Servlet 可以通过转发进入。JSP 页面我习惯放在 WEB-INF 外面的 jsp 目录因为某些页面比如登录和商品列表希望用户能直接输入地址访问而需要权限的页面比如购物车和订单确认就放到 WEB-INF 里面强制走 Servlet 转发。这个区分很有用有同学把全部 JSP 堆在外面结果用户不登录直接输地址就能看到别人购物车内容答辩时被问住。web.xml 是工程的入口配置注册 Servlet、设置编码过滤器、指定欢迎页都在这里完成。一个简洁的配置长这样?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 welcome-file-list welcome-filelogin.jsp/welcome-file /welcome-file-list servlet servlet-nameLoginServlet/servlet-name servlet-classcom.shop.servlet.LoginServlet/servlet-class /servlet servlet-mapping servlet-nameLoginServlet/servlet-name url-pattern/login/url-pattern /servlet-mapping /web-appwelcome-file 决定了访问http://localhost:8080/phoneshop/时默认打开哪个页面。Servlet 映射里的 url-pattern 是核心/login表示用户在浏览器地址栏输入.../login时会走LoginServlet。这里最常见的翻车点是把 url-pattern 写成LoginServlet少了斜杠或者 class 路径写错包名导致启动时报错或 404。写这段配置的时候Servlet 类要先用 javac 或 IDE 编译过IDE 会把 class 文件输出到 WEB-INF/classes 下Tomcat 才能识别。3. 数据库设计先行五张表怎么撑起整个销售流程3.1 建表 SQL用户、手机、购物车、订单、订单项数据库是整个系统的基础说明书里“数据库设计”这一章往往占最大篇幅。手机销售网站的核心业务是“用户浏览手机 → 加入购物车 → 下单付款”围绕这条链路至少需要五张表。下面这份建表 SQL 是我常用的结构字段命名和类型都经过实际运行验证CREATE DATABASE IF NOT EXISTS phone_shop DEFAULT CHARACTER SET utf8mb4; USE phone_shop; -- 用户表 CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT 密码(明文,演示用), real_name VARCHAR(20) DEFAULT NULL COMMENT 真实姓名, phone VARCHAR(20) DEFAULT NULL COMMENT 联系电话, address VARCHAR(200) DEFAULT NULL COMMENT 收货地址, balance DECIMAL(10,2) DEFAULT 0 COMMENT 账户余额, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 手机商品表 CREATE TABLE phone ( id INT NOT NULL AUTO_INCREMENT, brand VARCHAR(30) NOT NULL COMMENT 品牌,如华为/小米, model VARCHAR(50) NOT NULL COMMENT 型号, price DECIMAL(10,2) NOT NULL COMMENT 售价,单位元, stock INT NOT NULL DEFAULT 0 COMMENT 库存数量, image_url VARCHAR(200) DEFAULT NULL COMMENT 图片相对路径, description TEXT COMMENT 商品描述, status TINYINT DEFAULT 1 COMMENT 1在售 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_brand (brand) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 购物车表 CREATE TABLE cart ( id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL COMMENT 所属用户, phone_id INT NOT NULL COMMENT 商品ID, quantity INT NOT NULL DEFAULT 1 COMMENT 购买数量, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_phone (user_id, phone_id), CONSTRAINT fk_cart_user FOREIGN KEY (user_id) REFERENCES user(id), CONSTRAINT fk_cart_phone FOREIGN KEY (phone_id) REFERENCES phone(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单表 CREATE TABLE orders ( id INT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 订单编号, user_id INT NOT NULL COMMENT 下单用户, total_price DECIMAL(10,2) NOT NULL COMMENT 订单总金额, receiver_name VARCHAR(20) NOT NULL COMMENT 收货人, receiver_phone VARCHAR(20) NOT NULL, receiver_address VARCHAR(200) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0待发货 1已发货 2已完成, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单项表 CREATE TABLE order_item ( id INT NOT NULL AUTO_INCREMENT, order_id INT NOT NULL COMMENT 所属订单, phone_id INT NOT NULL COMMENT 商品ID, phone_model VARCHAR(50) NOT NULL COMMENT 商品名称快照, price DECIMAL(10,2) NOT NULL COMMENT 下单时价格快照, quantity INT NOT NULL COMMENT 购买数量, PRIMARY KEY (id), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几个字段设计上的关键点值得展开。user表的 password 字段我故意没加密只为了让课程设计代码更短更直观。如果在说明书里能主动写一句“密码采用 MD5 加密后存储”并配上DigestUtils.md5DigestAsHex一行代码答辩时是明显的加分项。phone表的价格用DECIMAL(10,2)而不是 FLOAT因为浮点数在计算金额时会出现 0.10.2≠0.3 的精度问题金额用十进制类型是硬规矩。status字段用TINYINT而不是枚举字符串因为数据库里数字比较比字符串快页面展示时再用数字映射到“在售/下架”。订单相关的两张表是设计的核心。orders表保存收货人和总价order_item表保存订单里每一件商品的快照。注意order_item里除了phone_id外还有phone_model和price两个冗余字段——这是故意做的。因为用户下单后如果后台改了商品价格或型号历史订单里的数据不应该跟着变快照能保留下单那一刻的真实信息。这套设计思路叫“冗余历史数据”在电商系统里很常见写进说明书会显得你考虑过实际场景。3.2 购物车表 vs Session 临时保存两条路线怎么取舍购物车这个模块有两种常见实现方式设计说明书必须明确选哪条并说明理由。第一种全部放在 Session 里用户未登录也能加购关掉浏览器就清空第二种建cart表持久化未登录用户先存 Session登录后把 Session 中的购物车合并进数据库表。我不建议第一次做就上第二种因为合并逻辑容易出 bug但也不用放弃cart表——直接在 JSP 展示阶段用 Session 模拟购物车同时预留一张cart表结构能让你在答辩时说出“生产环境会使用数据库表持久化购物车”。cart表设计里值得注意的细节是UNIQUE KEY uk_user_phone (user_id, phone_id)。这个唯一键能防止同一用户把同一个手机加入购物车两次第二次加购时会触发 DuplicateKeyException程序里捕获异常改成“数量加一”就行。没有这个约束的话购物车里出现两行相同商品下单时就得先合并行多写很多判断代码。下单的事务逻辑是另一个容易糊弄的地方。用户点击“提交订单”时系统要做三件事往orders插入一条订单主记录、往order_item批量插入订单项、扣减phone表的库存。这三件事必须要么全部成功要么全部失败。比如前两步成功了扣库存时失败就会出现“订单生成了但库存没减”超卖问题就是这么来的。所以 DAO 层下单那块必须用事务控制后面第 4 章会给出具体代码。3.3 商品图片字段路径和状态位决定了页面能不能好看phone表的image_url字段是很多新手容易忽略的。一定要存相对路径而不是完整 URL更不要把图片以 BLOB 二进制形式直接怼进数据库。存二进制会让数据库表体积爆炸查询变慢而且 JSP 页面上还要专门写个 Servlet 去把二进制流吐出来纯属自找麻烦。正确做法是把图片文件放到工程的 WebContent/images/phones/ 目录下数据库里只存文件名或者相对路径页面里用img src%request.getContextPath()%/images/phones/%phone.getImageUrl()% /拼接访问。status字段的状态位也建议多做一步商品列表页查询时默认带上AND status 1下架商品不出现在列表里但订单详情页能通过phone_id查到历史数据。这样后台做“下架某款手机”时只是改个数字不需要物理删除数据既安全又保留完整数据链路。写说明书时把这几个字段设计讲明白老师一看就知道你不是只会复制建表语句。4. 核心流程落地注册登录、商品列表、购物车与下单的代码怎么写4.1 登录与个人信息展示页面数据从数据库到 JSP 页面的完整链路登录功能是整个系统最基础的模块它的代码链路也最能体现 MVC 分层——请求打到 ServletServlet 调用 DAO 查库结果存进 Session最后转发到 JSP 页面展示。下面先看 LoginServlet 的关键部分WebServlet(/login) public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); // 1. 获取表单参数 String username request.getParameter(username); String password request.getParameter(password); // 2. 调用 DAO 查询用户 UserDao userDao new UserDao(); User user userDao.findByUsernameAndPassword(username, password); // 3. 判断结果跳转或转发 if (user null) { request.setAttribute(errorMsg, 用户名或密码错误); request.getRequestDispatcher(login.jsp).forward(request, response); return; } // 4. 登录成功保存 Session HttpSession session request.getSession(); session.setAttribute(loginUser, user); // 5. 重定向到个人信息页 response.sendRedirect(request.getContextPath() /personal); } }代码里有几个点需要说明。第 1 步的request.setCharacterEncoding(UTF-8)必须放在读取任何参数之前否则 POST 请求的中文参数会出现乱码。第 3 步登录失败时用的是 forward 转发而不是 sendRedirect 重定向目的是让request.getAttribute(errorMsg)在 login.jsp 里能直接取到值如果改成重定向request 里的属性就丢了。第 5 步登录成功后用重定向跳转是为了避免用户按 F5 刷新时浏览器弹“是否重新提交表单”的警告这是 POST-Redirect-GET 模式的标准做法。UserDao.findByUsernameAndPassword内部执行 SQL 查询JDBC 连接从工具类DBUtil获取。查询结果封装成User对象返回没有记录时返回 null。这个 DAO 方法的问题在于用户名和密码是直接拼接进 SQL 的存在 SQL 注入风险。我建议在说明书里明确指出“参数化查询是必须的”并用PreparedStatement替代 Statement就能顺手解决这个问题public User findByUsernameAndPassword(String username, String password) { String sql SELECT * FROM user WHERE username ? AND password ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { User user new User(); user.setId(rs.getInt(id)); user.setUsername(rs.getString(username)); // ... 其他字段赋值 return user; } } } catch (SQLException e) { e.printStackTrace(); } return null; }参数化的好处是?占位符把用户输入和 SQL 结构彻底隔离开无论用户输什么字符串都只会被当作参数值不会被解释成 SQL 语句。try-with-resources语法保证 Connection、PreparedStatement、ResultSet 三个资源用完后自动关闭比手写 finally 块简单又不容易漏。登录成功后的“个人信息展示页面”是 JSP 页面里 JSP 标签用得最丰富的一个地方。用户信息、余额、地址都要从 Session 里取出来回显% page pageEncodingUTF-8 % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html body h2欢迎, c:out value${sessionScope.loginUser.realName} default${sessionScope.loginUser.username} //h2 p当前余额: ${sessionScope.loginUser.balance}/p p收货地址: ${sessionScope.loginUser.address}/p p注册时间: ${sessionScope.loginUser.createTime}/p /body /html${sessionScope.loginUser.balance}这种 EL 表达式会从 Session 里取出 loginUser 对象并调用其 getter 方法。JSTL 的c:out标签用于输出变量它默认会 HTML 转义可以防止存储型 XSS。这里注意一个坑EL 表达式取不到值时页面不会报错只会显示为空所以如果数据没查到看到空白页先查 Session 里有没有存对象而不是怀疑 EL 写错了。4.2 商品列表与分页为什么每页条数不敢设成 100手机列表页是用户进入系统的第一屏一次性把所有手机全部查出来会让页面很长、加载很慢分页是必须的。查询方法在 PhoneDao 里长这样public ListPhone findByPage(int page, int pageSize) { String sql SELECT * FROM phone WHERE status 1 ORDER BY id DESC LIMIT ?, ?; ListPhone list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, (page - 1) * pageSize); ps.setInt(2, pageSize); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Phone p new Phone(); p.setId(rs.getInt(id)); p.setBrand(rs.getString(brand)); p.setModel(rs.getString(model)); p.setPrice(rs.getBigDecimal(price)); p.setImageUrl(rs.getString(image_url)); list.add(p); } } } catch (SQLException e) { e.printStackTrace(); } return list; } public int countAll() { String sql SELECT COUNT(*) FROM phone WHERE status 1; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { if (rs.next()) { return rs.getInt(1); } } catch (SQLException e) { e.printStackTrace(); } return 0; }LIMIT 后第一个参数是偏移量第二个是返回行数。第 1 页查LIMIT 0, 8第 2 页查LIMIT 8, 8偏移量计算公式是(page - 1) * pageSize。每页条数这里设 8 是因为手机列表通常用 4 列 2 行的网格布局8 条正好两屏设成 5 或 6 会让最后一排出现空位页面不美观。分页时还要把总页数算出来。定义totalPage (totalCount pageSize - 1) / pageSize这样 43 条总数据、每页 8 条时总页数 (437)/8 6 页不会出现 43/85 页但第 6 页还有 3 条数据的截断错误。页面底部用“上一页”“下一页”链接生成链接时把页码参数拼到 URL 后同时带上当前品牌的筛选条件这样用户翻到第 3 页时筛选状态不会丢。4.3 购物车加购与订单提交事务处理是最后一道防线购物车设计如果选择 Session 方案结构是一个HashMapInteger, Integerkey 是商品 idvalue 是数量。加购之后展示页面从 Map 里取出商品 id 集合循环查数据库得到商品详情。这里关键动作是当用户点“加入购物车”后用重定向刷新购物车页面而不是直接转发回商品详情页避免刷新时数量加二。订单提交是系统里最容易出错的地方因为涉及多表写入必须放在一个事务里执行。核心 service 方法大致如下public boolean createOrder(int userId, MapInteger, Integer cartMap, String receiverName, String receiverPhone, String receiverAddress) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 1. 开启事务 // 2. 生成订单主记录 String orderNo PH System.currentTimeMillis(); BigDecimal totalPrice BigDecimal.ZERO; for (Map.EntryInteger, Integer entry : cartMap.entrySet()) { Phone p phoneDao.findById(entry.getKey()); totalPrice totalPrice.add(p.getPrice().multiply(BigDecimal.valueOf(entry.getValue()))); } // 3. 插入 orders 表 orderDao.insertOrder(conn, orderNo, userId, totalPrice, receiverName, receiverPhone, receiverAddress); // 4. 插入 order_item 并扣库存 for (Map.EntryInteger, Integer entry : cartMap.entrySet()) { int phoneId entry.getKey(); int quantity entry.getValue(); orderDao.insertOrderItem(conn, orderNo, phoneId, quantity); phoneDao.decreaseStock(conn, phoneId, quantity); } conn.commit(); // 5. 全部成功才提交 return true; } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }第 1 步conn.setAutoCommit(false)关闭自动提交之后所有的 insert 和 update 都在同一个数据库事务里执行。第 5 步只有全部语句都成功才commit()中途任何异常都走rollback()回滚保证不会出现“订单有了但库存没扣”或者反过来。finally块里把连接状态恢复并关掉避免连接泄漏。整个方法里所有 DAO 方法都接收conn参数而不是自己新建连接这样才能保证它们用的是同一个事务连接——这是新手最容易犯的错误三个 DAO 各 get 一个 Connection事务自然就失效了。下单成功后要立刻清空 Session 里的购物车 Map然后重定向到订单列表页展示订单详情。“清空” 用session.removeAttribute(cartMap)而不是新建一个 Map省内存同时避免旧引用残留。5. 部署与避坑Tomcat 跑通前后的五个翻车现场5.1 从 IDEA 导出 war 包到 Tomcat 部署核对清单要在本机把项目跑起来IDE 里配置好 Tomcat 后直接启动是最省事的路径但说明书要写清楚的是“部署到独立 Tomcat”的步骤这样换台机器也能跑。常见做法是在 IDEA 右侧 Maven 面板执行package命令打出 war 包或者直接拷贝 WebContent 目录为一个 web 应用然后把 war 复制到Tomcat安装目录/webapps/下启动 Tomcat访问http://localhost:8080/手机项目名/。有几个细节容易忽略。第一MySQL 必须提前建好库和表JDBC 连接串里的用户名密码要和本机一致第二JDBC 驱动 jar 必须放在 WEB-INF/lib 里只放在 Tomcat 的 lib 目录也行但换环境容易忘第三端口占用问题——8080 被占时改 Tomcat 的 server.xml把 Connector 端口改成 8081同时访问路径也同步改。我一般会先在命令行里跑startup.bat看日志能避开一堆 IDE 隐藏的问题。5.2 五个常见坑现象、原因、解决坑一JSP 页面全部显示中文乱码现象所有页面上的中文和数据库里的中文都变成问号或“锟斤拷”。原因三个环节至少有一个编码不对——JSP 文件本身的编码、HTTP 响应输出时的编码、数据库连接和表结构的编码。解决JSP 页面第一行写% page contentTypetext/html; charsetUTF-8 pageEncodingUTF-8 %web.xml 里加一个过滤器统一设置请求和响应编码filter filter-nameencodingFilter/filter-name filter-classcom.shop.util.CharacterEncodingFilter/filter-class /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping过滤器类里做request.setCharacterEncoding(UTF-8)和response.setCharacterEncoding(UTF-8)。数据库连接串后面必须拼?useUnicodetruecharacterEncodingutf8否则 INSERT 进去的中文还是会乱。这个坑的排查顺序建议是先看页面显示乱码还是数据库存储乱码——页面乱码查 JSP 头和过滤器数据库乱码查 JDBC 连接串和建表字符集。坑二JDBC 连接数据库直接 ClassNotFoundException现象启动 Tomcat 访问任意页面的功能都报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver。原因MySQL 驱动 jar 没放进 WEB-INF/lib或者放的位置不对。IDEA 里直接导入的 jar 只是在编译期生效运行期 Tomcat 找不到。解决确认WEB-INF/lib/mysql-connector-java-x.x.x.jar存在如果用的是 mysql-connector-java 8.x驱动类名是com.mysql.cj.jdbc.Driver而不是旧版的com.mysql.jdbc.Driver同时连接串建议加serverTimezoneAsia/Shanghai。看驱动是否加载成功直接看 Tomcat 日志有 “Loading class com.mysql.jdbc.Driver” 字样就说明找到了。坑三商品图片裂了但文件确实存在现象列表页的img图标显示一个碎图右键“在新标签页打开图片”是 404。原因JSP 页面里用了相对路径而当前 URL 路径不是根路径时浏览器会把图片地址拼到当前 Servlet 路径下比如/phoneshop/phone/list/images/phones/a.jpg自然找不到。解决所有图片路径统一通过%request.getContextPath()%拼完整上下文路径例如img src%request.getContextPath()%/images/phones/phone_001.jpg width200 height200 /这样生成的地址永远是/phoneshop/images/phones/phone_001.jpg不会受当前 URL 层级影响。这个坑在说明书里值得单列一条因为几乎所有人都会踩。坑四提交订单后 F5 刷新订单重复创建现象用户点一次“提交订单”刷新一次页面数据库里出现两条相同订单。原因下单请求是 POST 提交刷新时浏览器会原样重发上一次的 POST 请求导致 createOrder 执行两次。解决下单成功后不要 forward 到成功页而是response.sendRedirect(orderSuccess.jsp)。重定向后的刷新只是刷新成功页面不会重新提交表单。更严谨的做法是在 createOrder 里判断orders.order_no是否已存在存在就返回失败从数据层面防止重复插入。坑五每次重启 Tomcat登录状态就没了现象登录成功第二天打开浏览器还要重新登录或者改了 JSP 页面刷新后 Session 对象变成新的。原因Session 默认存在服务器内存里Tomcat 重启或 web.xml 里 reloadable 配置导致应用重新加载时Session 全部失效。这属于正常现象但如果连同一个浏览器会话内刷新一下都丢登录状态就要检查 Cookie 了——Session 依赖 Cookie 里的 JSESSIONID设置为浏览器关闭即失效时关浏览器再开自然要重登。解决给 Cookie 设置有效期是这个问题最常见的处理。在登录成功时手动创建 Cookie 并写回响应Cookie cookie new Cookie(JSESSIONID, request.getSession().getId()); cookie.setMaxAge(60 * 60 * 24 * 7); // 保活7天 cookie.setPath(request.getContextPath()); response.addCookie(cookie);注意 setPath 要和应用的上下文路径一致否则 Cookie 不会随请求发到服务端。这个做法说不上多高级但能让你的系统体验接近真实网站写进说明书能明显加分。6. 上线前的三个小技巧页面刷新、图片定位、个人信息回显6.1 JSP 页面让加载完后刷新一次缓存数据更新场景商品列表页有个实际需求很常见后台管理员修改了某款手机的价格或库存但用户浏览器还停留在旧页面上。JSP 可以在body标签上挂onloadlocation.reload()让页面加载完成后自动刷新一次。但要注意这个技巧只适合用于“后台数据更新后跳转回来的确认页”如果用在列表页会让用户每次打开页面都要刷两遍体验反而差。更稳妥的写法是只刷新局部区域比如用 jQuery 的$(#phoneList).load(phoneListServlet)重新拉取列表。6.2 JSP 图片如何对坐标定位CSS 绝对定位页面里放手机展示图时用得上“坐标定位”。一个简单的做法是用 CSS 的绝对定位配合百分比或像素值把图片摆到 banner 区域指定坐标div styleposition:relative; width:800px; height:300px; img src%request.getContextPath()%/images/banner.jpg styleposition:absolute; left:120px; top:60px; width:260px; height:200px; / img src%request.getContextPath()%/images/phone_01.jpg styleposition:absolute; left:450px; top:80px; width:180px; height:150px; / /div父容器设置position:relative子图片用position:absolute后left 和 top 坐标是相对父容器的。这样调位置只需要改两个数值不用动整个布局。这个技巧在商品促销广告图和首页轮播图里很实用说明书里配一张效果截图观感更专业。6.3 个人信息展示页面的一个补强技巧登录后的个人信息页可以多展示一个“最近订单”区域把orders表里 status 等于 0待发货的记录列出来点进去能看到订单项的商品快照。做法是个人信息页的 Servlet 里多查一次orderDao.findByUserId()把结果放入 request 域页面用 JSTL 的c:forEach循环渲染。这块代码量不大却能把前面所有表关联起来让整个设计说明书闭环——从用户表到订单表再到商品表每一张表都有了实际用途。我早期做这类项目时有个毛病总喜欢把所有 JSP 和 Servlet 混在同一个包里后来才发现说明书里连“系统功能结构图”和实际目录都对不上。现在我的习惯是写代码之前先按模块把页面清单列出来登录、列表、购物车、结算、个人中心各占几个页面全部理清后再动手。这样不仅代码写起来顺写说明书也快两边不会打架。这个习惯帮我少走了很多弯路希望帮到你。本文还有配套的精品资源点击获取