ARTICLE DETAIL

资讯详情

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

JSP+Servlet+MySQL实现网上购书系统:数据库设计到部署全攻略

JSP+Servlet+MySQL实现网上购书系统:数据库设计到部署全攻略 简介这是一份面向Java毕业设计的网上购书系统完整项目资料包压缩包约99.48MB整合项目报告、答辩PPT、源代码、数据库、截图与部署视频六大类文件。项目基于JSP/Servlet构建以MVC模式组织覆盖JDBC数据库操作、用户与图书及订单表设计、HTML/CSS/JavaScript前端交互、AJAX局部更新、Session/Cookie会话管理、支付接口集成及Tomcat部署等Web开发核心知识点适合需要完成系统设计或巩固Java Web技能的学生参考。其中项目报告详细阐述架构设计与技术选型答辩PPT提炼关键实现源代码便于逐模块研读数据库文件提供完整表结构截图和部署视频则直观展示页面效果与本地运行步骤。此外资料中特别涉及SQL注入、XSS与CSRF等安全防护要点有助于培养规范编码意识。已有165人学习下载对正在准备毕业设计或复习Java Web开发的读者有较高实践参考价值。 如果毕业设计选题清单里躺着「基于JSP的网上购书系统」你的第一反应大概是「这技术太老了吧」。但在 JavaWeb 方向这套老技术恰恰是最稳的JSP 负责页面展示、Servlet 控制请求流转、MySQL 存数据一条链路把需求分析、数据库设计、编码到部署上线的完整流程全走了一遍。做完它源代码、数据库脚本、部署配置一应俱全再配上项目报告、答辩 PPT 和运行截图毕业设计的交付物是成体系的不是零散的一堆代码。系统的标准形态是前台做注册登录、分类浏览、搜索、购物车与下单后台由管理员维护图书、处理订单状态。它适合正在做数据库课程设计或基于 JSP 的毕设选题的同学也适合想搭一个 JavaWeb 全栈样板来练手的新手开发。2. 数据库设计先行六张表把网上书城的业务流程钉死这类系统最容易犯的错是一上来就写代码。页面写了七八个数据库表还没建最后代码越写越乱改一个字段要翻遍所有 Servlet。我的做法是先花半天把表结构定下来因为网上书城本质上就是几张表的增删改查用户、图书、分类、订单、订单明细、管理员。表结构定了后面所有代码都是对着表写 CRUD思路会清晰很多。2.1 用户、分类、图书三张主表字段命名与两个容易忽略的约束先看三张基础表。我习惯把所有表名前缀统一成t_主键一律叫id每张表都带create_time这个习惯在答辩时非常加分评委扫一眼就能看出你做过规范设计。t_user用户表里username必须加UNIQUE约束这是从数据库层面拦截重复注册比在 Servlet 里先查一次再插入可靠得多。status字段表示账号状态1 正常 0 禁用管理员可以禁用违规用户而不是物理删除——用户一旦被删他名下的订单记录就变成孤儿数据。password字段长度给到VARCHAR(64)因为后面要存 MD5 加盐后的摘要32 位不够用。t_book图书表里有几个字段值得注意。price用DECIMAL(10,2)绝对不能用float或double浮点数存金额会有精度问题这是答辩时的高频提问点。cover字段存封面图片的相对路径比如/images/books/java-web.jpg不要存完整 URL这样换服务器域名时图片还能正常显示。status表示上架 1 下架 0后台的「下架」操作就是改这个字段而不是删数据。t_category分类表结构最简单但作用不小。图书和分类是多对一关系分类表里放一个sort字段做排序权重。有了这张表图书分类可以动态增删前台导航菜单从表里查出来渲染而不是写死在 JSP 页面上。2.2 订单与订单明细为什么必须加“快照”字段订单相关的两张表是整个系统的核心也是设计上最容易出彩的地方。t_order订单主表记录一笔订单的整体信息订单号、总金额、支付方式、状态、下单时间、收货人信息。这里有个关键点订单号order_no不要用自增 id 直接展示给用户而是生成一个唯一业务号格式可以是B 时间戳 随机数。自增 id 会被猜到订单数量也能被别人遍历换成业务订单号是实际项目的通用做法。t_order_item订单明细表对应一笔订单里的每一本书。它除了记录book_id和quantity之外必须冗余一份「快照」字段book_title、book_cover、price。为什么因为图书信息是可变的——管理员可能改了书名、调了价格、甚至下了架。如果订单明细只存book_id用户查看历史订单时就只能查到当前的书名和现在的价格下单那一刻的信息就丢了。订单表里还有一个容易忽略的点收货人信息。receiver_name、receiver_phone、receiver_address必须在订单提交时拷贝成快照存进订单表而不是只存user_id让页面实时去查用户表。用户下单后改了地址已经生成的订单不能跟着变。把「快照」这个概念讲清楚答辩里直接多一个亮点。2.3 建表 SQL六张表一次跑通的 MySQL 脚本下面直接给一份能跑的建表脚本MySQL 5.7 和 8.0 都兼容。统一使用 InnoDB 引擎和 utf8mb4 字符集utf8mb4 支持中文和表情符号避免插入特殊字符时报错。-- 用户表 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 用户名, password VARCHAR(64) NOT NULL COMMENT 密码(MD5加盐), salt VARCHAR(16) DEFAULT NULL COMMENT 盐值, realname VARCHAR(50) DEFAULT NULL COMMENT 真实姓名, phone VARCHAR(20) DEFAULT NULL COMMENT 手机号, email VARCHAR(100) DEFAULT NULL COMMENT 邮箱, address VARCHAR(255) DEFAULT NULL COMMENT 收货地址, status TINYINT DEFAULT 1 COMMENT 1正常 0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 分类表 CREATE TABLE t_category ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 分类ID, name VARCHAR(50) NOT NULL UNIQUE COMMENT 分类名称, sort INT DEFAULT 0 COMMENT 排序权重, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 图书表 CREATE TABLE t_book ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 图书ID, title VARCHAR(200) NOT NULL COMMENT 书名, author VARCHAR(100) DEFAULT NULL COMMENT 作者, publisher VARCHAR(100) DEFAULT NULL COMMENT 出版社, isbn VARCHAR(20) DEFAULT NULL COMMENT ISBN号, cover VARCHAR(255) DEFAULT NULL COMMENT 封面图路径, price DECIMAL(10,2) NOT NULL COMMENT 售价, original_price DECIMAL(10,2) DEFAULT NULL COMMENT 原价, stock INT DEFAULT 0 COMMENT 库存, sales INT DEFAULT 0 COMMENT 销量, category_id INT NOT NULL COMMENT 分类逻辑外键, description TEXT COMMENT 图书简介, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单主表 CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 业务订单号, user_id INT NOT NULL COMMENT 下单用户逻辑外键, total_amount DECIMAL(10,2) NOT NULL COMMENT 订单总金额, pay_type TINYINT DEFAULT 0 COMMENT 0在线支付 1货到付款, status TINYINT DEFAULT 0 COMMENT 0待付款 1待发货 2待收货 3已完成 4已取消, receiver_name VARCHAR(50) NOT NULL COMMENT 收货人快照, receiver_phone VARCHAR(20) NOT NULL COMMENT 手机号快照, receiver_address VARCHAR(255) NOT NULL COMMENT 地址快照, remark VARCHAR(255) DEFAULT NULL COMMENT 订单备注, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME DEFAULT NULL, ship_time DATETIME DEFAULT NULL, receive_time DATETIME DEFAULT NULL, INDEX idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单明细表 CREATE TABLE t_order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL COMMENT 订单ID逻辑外键, book_id INT NOT NULL COMMENT 图书ID, book_title VARCHAR(200) NOT NULL COMMENT 书名快照, book_cover VARCHAR(255) DEFAULT NULL COMMENT 封面快照, price DECIMAL(10,2) NOT NULL COMMENT 下单时单价, quantity INT DEFAULT 1 COMMENT 购买数量, subtotal DECIMAL(10,2) NOT NULL COMMENT 小计, INDEX idx_order (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 管理员表 CREATE TABLE t_admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, salt VARCHAR(16) DEFAULT NULL, last_login_time DATETIME DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几个地方说明一下。所有外键都是「逻辑外键」——表和表之间靠category_id、user_id、order_id关联但不建物理外键约束。物理外键在删除数据时会牵扯级联规则导入 SQL 时也更容易失败逻辑外键在代码里控制引用关系就够了。每个表都加了必要的索引订单表按user_id建索引因为「查询我的订单」是最频繁的操作。注意如果你用的是 MySQL 8.0驱动类名必须写com.mysql.cj.jdbc.Drivercom.mysql.jdbc.Driver从 8.0 开始被移除了。这个问题在部署阶段非常常见后面避坑章节会细说。3. 后端从零搭JSPServletMySQL 的分层写法与登录分页逻辑表结构确定后后端代码的骨架也就定了。网上书城的所有功能可以拆成几个模块用户模块注册登录、图书模块列表详情搜索、购物车模块、订单模块、后台管理模块。一说到 JSP 系统很多人担心它过时但把需求拆开看没有一个是 Servlet 和 JSP 处理不了的而且这套分层思想放在今天依然是 MVC 的正确示范。3.1 包结构与请求流转照着 Model 2 划分源码Model 2 思想的落地方式很简单JSP 只做展示Servlet 做请求控制JavaBean 做数据模型。对应到工程目录上我建议按功能分包而不是按层分包混在一起com.bookstore ├── entity # 对应六张表的实体类 User Book Category Order OrderItem Admin ├── dao # 数据访问层UserDao BookDao 等接口 实现 ├── service # 业务层OrderService 处理下单事务、库存扣减 ├── controller # Servlet 层UserServlet CartServlet OrderServlet AdminServlet ├── filter # 过滤器登录校验、统一编码 ├── util # 工具类DBUtil MD5Util └── webapp ├── WEB-INF/jsp/ # 受保护的页面放在这里 ├── css/ js/ images/ ├── index.jsp └── login.jspentity 放实体类一张表对应一个类字段和表列一一对应。DAO 层只做 SQL 操作不写业务逻辑。Service 层承载业务规则比如下单时要扣库存、算总价。Controller 层只做参数接收和页面跳转。Servlet 的写法上我习惯一个模块一个 Servlet用action参数区分操作而不是给每个功能写一个 Servlet 类WebServlet(/cart) public class CartServlet extends HttpServlet { Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action request.getParameter(action); if (add.equals(action)) { addToCart(request, response); } else if (update.equals(action)) { updateCart(request, response); } else if (remove.equals(action)) { removeFromCart(request, response); } else if (clear.equals(action)) { clearCart(request, response); } } Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); doGet(request, response); } }统一入口的好处是路由集中、好排查问题前端页面里所有购物车相关请求都指向/cart?actionxxx代码量少一半。doPost里调doGet是我多年的习惯这样表单 POST 请求和链接 GET 请求走同一套处理逻辑避免两处代码不一致。这里有个 JSP 放置位置的原则所有需要登录才能访问的页面比如用户中心、购物车结算页、后台管理页一律放在WEB-INF/jsp目录下。放在这里的 JSP 无法通过 URL 直接访问只能由 Servlet 转发跳转这是 JSP 应用做权限控制最基础也最有效的手段。index.jsp和login.jsp这种公开页面才放在 webapp 根目录。3.2 登录注册密码加盐与登录态过滤器网上搜到的很多 JSP 登录代码有个通病密码明文存库、登录后把用户名拼在 URL 里到处传。这两种写法在答辩时都会被挑刺。按照 JSP 的 Model 2 思想实现用户注册功能密码这一步就得做对。注册的核心逻辑是密码加盐散列而不是直接 MD5// RegisterServlet 注册处理片段 String username request.getParameter(username); String rawPassword request.getParameter(password); // 1. 生成随机盐值长度 8 String salt UUID.randomUUID().toString().replace(-, ).substring(0, 8); // 2. 密码摘要 MD5(原文 盐) String encoded DigestUtils.md5Hex(rawPassword salt); User user new User(); user.setUsername(username); user.setPassword(encoded); user.setSalt(salt); user.setCreateTime(new Date()); userDao.insert(user);为什么不能只做一次 MD5 不加盐因为相同密码会得到相同的摘要值攻击者用彩虹表一比对就能还原出原文。加了随机盐之后即使两个用户密码相同存到数据库里的摘要也完全不同。salt字段单独存登录校验时取出该用户对应的盐重新计算摘要再比对。登录态校验用 Filter 统一处理不要在 JSP 页面里每个头部都写一遍if (session.getAttribute(loginUser) null)。我在项目里会放两个过滤器一个管用户端一个管管理端WebFilter(/user/*) public class UserLoginFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpSession session request.getSession(false); Object loginUser session null ? null : session.getAttribute(loginUser); if (loginUser null) { // 未登录重定向到登录页 ((HttpServletResponse) resp).sendRedirect(request.getContextPath() /login.jsp); return; } // 已登录放行 chain.doFilter(req, resp); } }过滤器里有个细节getSession(false)而不是getSession()。带false表示如果当前没有 session 就返回null不会强制创建一个新 session避免未登录用户每次请求都被种一个无用的会话对象。拦截路径只写/user/*这样首页、图书列表这些公开页面不需要登录也能访问符合真实网上书城的体验。3.3 图书分页与搜索PageBean 的四个字段和一条 SQL分页是 JSP 项目的必考知识点。图书列表页数据量一大一次性查出来全展示是不可接受的。标准做法是封装一个通用分页模型PageBeanTpublic class PageBeanT { private int pageNo; // 当前页码 private int pageSize; // 每页条数 private long totalCount; // 总记录数 private long totalPage; // 总页数 private ListT list; // 当前页的数据列表 // 省略 getter / setter }查询核心就一条分页 SQL 加一条统计 SQL// BookDao 分页查询实现 String countSql SELECT COUNT(*) FROM t_book WHERE status 1 AND title LIKE ?; String listSql SELECT * FROM t_book WHERE status 1 AND title LIKE ? ORDER BY id DESC LIMIT ?, ?; // 统计总记录数 long totalCount queryCount(countSql, % keyword %); // 当前页数据LIMIT 第一个参数是起始下标 (pageNo-1)*pageSize第二个参数是每页条数 ListBook list queryList(listSql, % keyword %, (pageNo - 1) * pageSize, pageSize); PageBeanBook page new PageBean(); page.setPageNo(pageNo); page.setPageSize(pageSize); page.setTotalCount(totalCount); page.setTotalPage((totalCount pageSize - 1) / pageSize); page.setList(list);两个容易出错的地方。第一LIMIT的起始下标是(pageNo - 1) * pageSize很多新手写成pageNo * pageSize结果第一页就跳过前几条数据。第二keyword来自用户输入必须用PreparedStatement的?占位符传参绝对不能拼字符串否则 or 11这类输入直接就能把你的图书表全查出来SQL 注入是答辩时的红线问题。分页逻辑抽成PageBean后后台管理员列表、用户订单列表都能复用代码量少一半答辩老师问「代码有没有做复用」时你也有得讲。4. 购物车与订单书城系统的核心业务怎么打通购物车和订单是网上书城和「图书展示网站」的分水岭也是答辩时评委最可能深挖的业务逻辑。很多人的项目卡在「加入购物车没反应」或者「下单后库存没扣」本质上都是数据模型没设计好。这一章把购物车的会话存储、下单的事务边界和订单状态流转一次说清。4.1 购物车对象模型用 Map 而不是 List购物车在 JSP 项目里的主流实现方式是放在 Session 中。购物车本身就是会话级临时数据浏览器关闭就消失这是正常的用户真正下单后数据进入订单表持久化。Session 方案代码量最小也足够应付毕设要求。购物车的数据结构我建议设计成两个类CartItem表示购物车里的一本书加数量Cart表示整个购物车内部用一个MapInteger, CartItem存储// 购物车条目一本书 数量 public class CartItem { private Book book; // 直接放 Book 对象展示时不用再查库 private int quantity; // 购买数量 public double getSubtotal() { return book.getPrice() * quantity; // 小计 单价 * 数量 } // 省略 getter / setter } // 购物车以图书ID为 key public class Cart { private MapInteger, CartItem items new LinkedHashMap(); // 加入购物车已存在则合并数量不存在则新增条目 public void add(Integer bookId, CartItem item) { CartItem old items.get(bookId); if (old ! null) { old.setQuantity(old.getQuantity() item.getQuantity()); } else { items.put(bookId, item); } } // 修改数量数量小于等于0视为非法不做更新 public void updateQuantity(Integer bookId, int quantity) { CartItem item items.get(bookId); if (item ! null quantity 0) { item.setQuantity(quantity); } } public void remove(Integer bookId) { items.remove(bookId); } // 计算购物车总价 public double getTotalPrice() { double total 0; for (CartItem item : items.values()) { total item.getSubtotal(); } return total; } }为什么用Map而不是List两个原因。第一加入购物车时如果用户连续点了两次「加入购物车」Map可以用bookId在常数时间内找到已有条目并合并数量而List需要遍历查找再更新代码繁琐且容易漏掉合并逻辑。第二LinkedHashMap能保证购物车条目的展示顺序和加入顺序一致用户体验好。Book对象直接放进CartItem渲染购物车页面时直接用book.title、book.price、book.cover不用每个条目回查数据库。4.2 下单事务扣库存与生成订单的原子性下单流程是整套系统里唯一涉及多表写操作的业务必须放在一个事务里。这一步我吃过亏早期版本先写订单明细再扣库存结果库存扣减失败时订单已经生成了用户付了钱却拿不到书。血泪经验是订单主表、订单明细、库存扣减必须同时成功或同时失败。// OrderService.createOrder 核心骨架 public Long createOrder(Integer userId, Cart cart, String receiverName, String receiverPhone, String receiverAddress) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 购物车为空直接抛异常 if (cart null || cart.isEmpty()) { throw new RuntimeException(购物车不能为空); } // 2. 生成业务订单号B 时间戳 随机数 String orderNo B System.currentTimeMillis() (int) (Math.random() * 900 100); // 3. 插入 t_order 主表拿到自增主键 orderId // 注意收货人信息在这里拷贝成快照后续用户改地址不影响已生成的订单 Long orderId orderDao.insert(orderNo, userId, cart.getTotalPrice(), receiverName, receiverPhone, receiverAddress); // 4. 遍历购物车逐条插入 t_order_item价格取 Book 的当前售价 for (CartItem item : cart.getItems().values()) { Book book item.getBook(); orderItemDao.insert(orderId, book.getId(), book.getTitle(), book.getCover(), book.getPrice(), item.getQuantity()); } // 5. 扣减库存一条 SQL 同时做校验和扣减受影响行数为 0 说明库存不足 for (CartItem item : cart.getItems().values()) { Book book item.getBook(); int rows bookDao.deductStock(book.getId(), item.getQuantity()); if (rows 0) { throw new RuntimeException(《 book.getTitle() 》库存不足); } } conn.commit(); // 全部成功提交事务 return orderId; } catch (Exception e) { if (conn ! null) { try { conn.rollback(); // 任何一步失败全部回滚 } catch (SQLException ex) { ex.printStackTrace(); } } throw new RuntimeException(下单失败 e.getMessage(), e); } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } }这里最关键的一行是扣库存 SQL 的设计。正确的 SQL 写法是UPDATE t_book SET stock stock - ?, sales sales ? WHERE id ? AND stock ?把「判断库存够不够」和「扣减库存」合并成一条更新语句。如果库存不足stock ?条件不成立更新影响行数为 0代码里检测到这个 0 就抛出异常触发回滚。这种方式在并发场景下也能避免超卖两个请求同时读到库存只剩 1数据库的行锁会让第二条更新失败而不是两条都成功。订单明细里的价格必须取book.getPrice()不能取购物车里存的任何中间值。购物车是 Session 里的对象前端无法直接改但价格信息以数据库实时数据为准是最稳妥的原则。事务的顺序也要固定先插主表拿主键再插明细最后扣库存。顺序反了极端情况下会出现订单明细缺失但库存已经扣了的问题。4.3 订单状态机六个状态与两方操作订单状态用TINYINT存储代码里做一个常量类OrderStatus把数字映射成有意义的命名不要在 Service 里裸写if (status 1)。常见状态定义如下状态值状态含义由谁触发触发动作0待付款用户提交订单1待发货用户模拟支付成功2待收货管理员后台发货3已完成用户确认收货4已取消用户/管理员取消订单状态流转要特别注意「取消订单」的分支。待付款状态下取消订单需要恢复库存并修改状态已发货状态下取消属于退款退货流程货还在途中不应该恢复库存毕设里实现到「待付款取消并恢复库存」就足够了。后台管理员的订单列表里不同状态用不同颜色标识待发货的订单要能一键跳转到发货处理页。提示状态字段不要用varchar存中文比如「待付款」。一是存储空间大二是比较慢三是前端展示要改文案时得改数据库数据。用数字存储、页面展示时映射中文是项目开发的标准姿势。5. 本地部署与常见问题排查从 IDEA 到 Tomcat 的几个翻车点这类系统答辩前最怕的不是功能没写完而是环境起不来。项目包里如果带了部署视频拿到手第一件事是先按视频把数据库脚本导进去、确认 Tomcat 能启动再去看代码。下面这几条是我在帮人调试这类 JSP 项目时反复遇到的翻车现场前三条属于配置类问题后两条属于依赖类问题每一条都有明确的排查顺序。环境问题最大的特点是玄学感很强昨天能跑今天不能跑其实都是某一个固定的原因。5.1 Maven 工程还是普通动态 Web 工程工程形态在创建项目时就要想清楚后期迁移成本不小。两种方式我都用过给你一个实在的建议如果目标是顺利通过答辩用 IDEA 的 Java Enterprise 创建普通 Web 工程把 jar 包放进WEB-INF/lib就行简单直接不折腾。如果简历上想写「熟练使用 Maven 构建项目」或者系统里要引很多第三方库用 Maven 工程。Maven 工程的核心依赖就四个pom.xml里这样配dependencies !-- Servlet APIscope 必须是 provided不能漏 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- JSP API 同理 -- dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency !-- JSTL 标签库JSP 页面里用 c:forEach 必须引 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency !-- MySQL 8 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.30/version /dependency /dependenciesservlet-api和jsp-api的scope写成provided很关键。这两个库 Tomcat 自带如果打成compile进最终包会和 Tomcat 自带的类冲突启动时报ClassNotFoundException或者类转换异常。MySQL 驱动的版本号按你本机数据库版本选MySQL 8 必须配 8.x 驱动MySQL 5.7 用 8.x 驱动向下兼容也能跑。5.2 IDEA Tomcat 本地跑通最小步骤与 db.properties不管工程是哪种形态部署到 Tomcat 的步骤都是同一套按顺序走一遍基本能起来导入数据库Navicat 或命令行执行source建表脚本确认t_user等六张表都建好测试数据也导入了。修改配置文件db.properties里的数据库地址、用户名、密码改成你本机的实际值。IDEA 里Run → Edit Configurations点左上角选Tomcat Server → Local在Deployment标签页添加你的 Artifact。启动 Tomcat访问http://localhost:8080/项目名/看到首页说明部署成功。db.properties的配置项有固定写法直接抄这份jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/bookstore?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 jdbc.usernameroot jdbc.password你自己的数据库密码 initialSize5 maxActive20三个连接串参数的含义要能讲出来。useSSLfalse是本地开发关闭 SSL 握手加快连接速度serverTimezoneAsia/Shanghai必加MySQL 8 不指定时区会直接报Server returns invalid timezone这是让无数人卡住的经典错误characterEncodingutf8保证写入数据库的中文不乱码。5.3 现场翻车记录五个高频问题的现象、原因与解法第一条最容易被忽略IDEA 里.jsp文件打开是一篇白没有语法高亮Settings → Editor → Color Scheme里也找不到 jsp 的配色选项。原因是 IntelliJ IDEA 的 JSP 支持依赖 Java EE 插件插件没启用或者工程没有关联 Web Facet。解决方法是先到Settings → Plugins搜索启用「Java EE: JSP」插件如果插件已启用但文件还是白板到File → Project Structure → Facets手动添加 Web Fact指定 web 根目录。第二条Tomcat 能启动但访问页面报Access denied for user root。原因是db.properties里的密码和数据库实际密码不一致或者 MySQL 8 的 root 账号默认用了新的认证插件caching_sha2_password驱动不识别。先改配置文件密码如果还不行在 MySQL 命令行执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码;切换认证插件。第三条中文乱码而且是「页面乱码、数据库也乱码」。乱码原因有三个独立来源要分开排查。JSP 文件头部必须有pageEncodingUTF-8Tomcat 的conf/server.xml里 Connector 节点加URIEncodingUTF-8处理 GET 请求参数编码连接串里带characterEncodingutf8保证写入数据库不乱码。三个地方全检查过一遍乱码问题基本绝迹。第四条部署后访问 404。原因八成是 Artifact 没有添加到 Tomcat 的 Deployment 配置里或者访问路径和上下文路径不一致。解决方法是确认Run → Edit Configurations → Deployment里有你的 artifact然后用 IDEA 控制台里实际显示的路径访问不要自己凭记忆敲 URL。第五条一查数据库就报ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因是 MySQL 驱动 jar 没有打进最终的部署包。普通工程检查 jar 是否在WEB-INF/lib下Maven 工程检查驱动依赖的scope如果手滑写了provided它就不会被打进 war 包改回默认的compile即可。6. 从代码到答辩 PPT把系统讲成一条业务闭环系统和文档都做完之后最后一步是把项目讲出来。很多人的 PPT 写了一堆技术名词但评委最想听的是「你这个系统解决了什么问题、业务流程怎么走通」。我的做法是放弃按功能模块平铺而是设计一条完整的演示主线。6.1 演示路径从注册到收货只走一条主线答辩演示不要把所有页面都点一遍评委没耐心看。按业务闭环走注册新用户 → 首页搜索「Java」→ 进入图书详情 → 加入购物车 → 修改数量 → 提交订单 → 模拟支付 → 用户中心看到「待发货」→ 切换管理员后台发货 → 用户端刷新变成「待收货」→ 确认收货。这条链路覆盖四张核心表用户、图书、订单、订单明细讲完它系统架构和业务逻辑就都立住了。6.2 截图和报告怎么用证据链项目包里带的运行截图不要一股脑全塞进 PPT按「前台页面 → 后台页面 → 数据库表数据 → 部署成功页面」四类归档每类挑一两张最有代表性的。写报告时 ER 图别只画表名把关键字段标上去流程图把订单的状态机画进去。这两张图放在 PPT 里能顶三页文字。我当年做 JSP 课设最大的后悔药是没准备异常场景的演示。评委问「库存不够怎么办」我现场点进代码找逻辑冷场了十几分钟。后来我习惯把「库存不足触发的回滚」这个场景录成一段几十秒的视频放在 PPT 最后一页问到了就放问不到就不提。这个习惯我保留到现在做任何系统都会提前录好一条异常路径的演示素材。希望这个系统的落地路径和踩坑清单对你有用希望帮到你。本文还有配套的精品资源点击获取
返回列表