简介:基于JSP+MySQL的JavaWeb图书销售管理系统网上书店项目源码与数据库,是一套可直接运行的完整工程。项目涵盖前台图书展示、购物车、订单管理及后台图书、分类、用户管理等功能模块,适合计算机相关专业学生用于期末大作业、课程设计答辩,也适合需要真实项目经验以巩固JavaWeb技能的开发者参考。压缩包共包含286个文件,大小约6.93MB,其中Java源码53个,JSP页面52个,JS脚本40个,CSS样式表19个,另有SQL数据库脚本、JAR依赖库及前端UI组件(如bootstrap、layui)等。资源目录结构与典型JavaWeb项目一致,方便对照学习前端交互与后端逻辑的实现。目前已有233人学习该资源,内容经过导师指导并认可,代码完整、可直接部署,可作为课程设计或期末大作业的核心参考范本。
1. JSP+MySQL图书销售管理系统:课程设计里最稳的JavaWeb满分组合
期末JavaWeb课设的经典需求长这样:要出一个完整的管理系统,要带源码、带数据库、能演示、能答辩。很多同学随手抄一个SpringBoot项目,结果答辩时连“请求从页面到数据库经过了哪些类”都说不清,反而被扣分。这套基于JSP+MySQL的图书销售管理系统,走的是最传统的Servlet+JSP+JDBC路线,登录、注册、图书分类浏览、分页、购物车、下单、后台管理全部齐全,代码按分层结构组织,每一行都能对着讲出原理。对准备交期末大作业、课程设计的人,以及想先把JavaWeb底层链路跑通再学框架的人来说,这套源码是少见的“能白盒运行”的参考资料。
2. 拿到源码先看结构:分包规范、依赖清单与JDK/Tomcat/MySQL版本匹配
2.1 技术栈选型:为什么这套“老”组合反而最稳
这个项目没有引入Spring、MyBatis那套框架,核心就是JSP+Servlet+JDBC+MySQL,前端样式用了Bootstrap和Layui。不要觉得“老”就等于“没用”,恰恰是这套组合把JavaWeb最核心的几个考点全部覆盖了:Servlet生命周期、HttpSession会话跟踪、JDBC手工管理连接、JSP九大内置对象、请求转发与重定向的区别。
我之前带过一个学生做课设,他坚持用SpringBoot写,结果老师让他现场说“Servlet怎么处理请求的”,他答不上来。后来换回这套JSP+Servlet的项目,反而轻松过了。原因很简单:框架把细节封装掉了,你讲不清楚“数据到底怎么从浏览器走到数据库的”;而JSP+Servlet这套,请求路径、过滤器、Session、JDBC事务全部摆在明面上,老师问什么你都能指着代码说。
整个请求链路可以概括为:浏览器提交表单 → Servlet接收参数 → Service层处理业务 → DAO层通过JDBC操作MySQL → 结果返回给JSP渲染。这个链路在后面学SSM、SpringBoot时就是底层原型。
2.2 目录结构:从web.xml到src,每一层放什么
BookStore/ ├─ src/ │ ├─ com.bookstore.entity/ # 实体类:User、Book、Category、Order │ ├─ com.bookstore.dao/ # 数据访问接口,定义增删改查方法 │ ├─ com.bookstore.dao.impl/ # DAO接口实现,内部写JDBC代码 │ ├─ com.bookstore.service/ # 业务逻辑层,事务边界基本在这一层 │ ├─ com.bookstore.servlet/ # 控制器层,等价于Controller │ ├─ com.bookstore.util/ # 工具类:DBUtil、PageBean、编码过滤器 │ └─ db.properties # MySQL连接参数统一配置 ├─ WebContent/ │ ├─ admin/ # 后台管理页面:图书管理、订单管理 │ ├─ css/ js/ images/ # Bootstrap、Layui等静态资源 │ ├─ index.jsp # 前台首页 │ ├─ login.jsp register.jsp # 登录和注册页 │ ├─ bookList.jsp # 图书列表页(带分页) │ ├─ cart.jsp # 购物车页面 │ ├─ order.jsp # 订单确认页 │ └─ WEB-INF/ │ ├─ web.xml # 部署描述符,配置Servlet映射与欢迎页 │ └─ lib/ # mysql-connector、jstl等依赖jar包 └─ sql/ └─ bookstore.sql # 建库建表+初始化数据脚本src下的分层是典型的“接口+实现”模式:dao包只定义接口,dao.impl包写真正的JDBC代码。这样改数据库实现时不用动Servlet层,答辩时能多讲一句“面向接口编程”。WebContent下面的admin目录单独放后台页面,配合登录时写入Session的角色字段做区分,算是最简单的权限控制雏形。
2.3 环境版本匹配:JDK、Tomcat、MySQL三方怎么对齐
不少新手拿到源码第一件事就是双击启动,然后被版本问题炸一脸。这个项目我自己实际跑通的组合是:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 | 最稳,不涉及模块化限制,Tomcat 8.5完全兼容 |
| Tomcat | 8.5.x | 支持Servlet 3.1/JSP 2.3,注解和web.xml两种写法都能跑 |
| MySQL | 5.7.x(如5.7.44) | 默认字符集好处理,与驱动兼容性最好 |
| 驱动 | mysql-connector-java-5.1.49 | 配5.7经典组合,Class.forName用com.mysql.jdbc.Driver |
| IDE | Eclipse / IDEA | 都能直接Import,后面细说 |
MySQL换成8.0也能跑,但有两个点要改:驱动类名换成com.mysql.cj.jdbc.Driver,JDBC连接串里要加serverTimezone=Asia/Shanghai。如果原项目的db.properties里还是老驱动写法,跑起来大概率报ClassNotFoundException或时区错误。先按上表把环境对齐,能省掉后面一半以上的报错。
3. 数据库设计与初始化:六张表、外键关系与一次性导入
3.1 六张表都长什么样
数据库脚本是整个项目的地基,建议先打开sql/bookstore.sql看一遍再启动项目。核心是六张表,关系为:用户表对应购物车和订单,图书挂在分类下,订单明细挂在订单下。
| 表名 | 关键字段 | 作用 |
|---|---|---|
| user | id、username、password、role、create_time | 用户与管理员共用,role区分权限 |
| category | id、name、description | 图书分类 |
| book | id、category_id、name、author、publisher、price、stock、sales、cover | 图书主表,category_id关联分类 |
| cart_item | id、user_id、book_id、quantity | 购物车落库表,有些版本购物车存Session,这张表可留作扩展 |
| orders | id、order_no、user_id、total_price、status、create_time | 订单主表,status表示订单状态 |
| order_item | id、order_id、book_id、book_name、price、quantity | 订单明细,冗余了book_name和price |
order_item里冗余图书名称和价格不是败笔,而是刻意的。下单后图书可能改价或下架,订单明细必须保留下单那一刻的快照。回答老师“为什么订单表里要存冗余字段”时,这个点能加分。
外键关系上,book表的category_id指向category.id,order_item的order_id指向orders.id。实际创建时外键约束可以加,但要注意后续做图书删除会受限制。
3.2 初始化脚本:命令行与Navicat两种导入法
源码里带的bookstore.sql通常包含建库、建表和INSERT初始化数据。命令行导入最直接:
mysql -uroot -p < /path/to/bookstore.sql执行前先确认编码,Windows下CMD默认GBK容易把中文注释弄乱。我用Navicat比较多:连接MySQL后,右键“运行SQL文件”,选择bookstore.sql,编码选UTF-8,运行完刷新就能看到bookstore库。
mysql -uroot -p -e "SHOW TABLES FROM bookstore;"导入完成后用上面命令确认表数量,出现6张业务表加1张初始化表就说明脚本执行完整。整个过程有个常见问题:如果提示“Unknown database 'bookstore'”,说明脚本里没有CREATE DATABASE语句,需要手动先建库再导入。
3.3 乱码根源:JDBC连接参数与建库字符集必须一致
中文乱码是JSP项目里最常见的“翻车”,而且经常是页面、数据库、控制台三处乱。根源在于字符集在三层之间不一致:JSP页面编码、MySQL表字符集、JDBC连接参数任意一个不是UTF-8,就会出现乱码。
CREATE DATABASE bookstore DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;建库时用utf8mb4而不是utf8,因为utf8mb4完整支持中文和生僻字。连接参数里对应的是:
jdbc:mysql://localhost:3306/bookstore?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/ShanghaiuseUnicode=true和characterEncoding=utf8告诉驱动以UTF-8编码向MySQL发送SQL和读取结果,useSSL=false是避免5.7版本出现SSL握手告警,serverTimezone只在MySQL 8.0驱动下必填。这套参数建议直接复制到db.properties里,别自己缩写。
4. 核心功能实现:登录会话、分页、购物车与下单的代码走读
4.1 登录与Session:LoginServlet完整流程
登录模块是答辩提问重灾区。很多课设的登录只是查一次数据库,但这套源码里你会看到完整的Session处理和角色分流。
@WebServlet("/login") public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); UserService service = new UserService(); User user = service.login(username, password); if (user == null) { req.setAttribute("msg", "用户名或密码错误"); req.getRequestDispatcher("login.jsp").forward(req, resp); } else { HttpSession session = req.getSession(); session.setAttribute("loginUser", user); if ("admin".equals(user.getRole())) { resp.sendRedirect("admin"); } else { resp.sendRedirect("index.jsp"); } } } }setAttribute("loginUser", user)把整个用户对象放进会话,后面的JSP页面直接取sessionScope.loginUser.username就能显示当前登录人。失败时用forward而不是sendRedirect,是为了把请求域里的msg错误信息带到login.jsp渲染出来;如果改用重定向,req.setAttribute的内容会丢失。成功时用sendRedirect是为了避免表单重复提交,这是两个最经典的面试考点。
4.2 图书列表与分页:LIMIT参数与PageBean封装
分页查询几乎是所有JavaWeb课设的必考功能。这套项目用PageBean统一封装分页数据,DAO层用LIMIT实现物理分页。
public class PageBean<T> { private int pageNo = 1; // 当前页码 private int pageSize = 8; // 每页显示条数 private int totalCount; // 总记录数 private List<T> data; // 当前页数据列表 public int getTotalPage() { return (totalCount + pageSize - 1) / pageSize; } }// BookDaoImpl 中 public List<Book> findByPage(int categoryId, int pageNo, int pageSize) { String sql = "SELECT * FROM book WHERE category_id = ? ORDER BY id DESC LIMIT ?,?"; int offset = (pageNo - 1) * pageSize; Object[] params = {categoryId, offset, pageSize}; // 执行查询并封装到List<Book> }LIMIT的两个参数含义一定要弄清:第一个是偏移量offset,第二个是取多少行。第一页offset=0,第二页offset=8。所以代码里必须用(pageNo - 1) * pageSize计算,而不是直接把pageNo传进去。这里如果写错,表现就是第一页正常、第二页取到了重复数据。JSP页面里再配合c:forEach遍历pageBean.data,底部渲染页码链接即可。
4.3 购物车:Session保存Map结构,不建表更省事
购物车有两种做法:存数据库,或者存Session。这套源码采用Session保存,好处是免去建表和联表查询,坏处是关闭浏览器购物车清空。课设场景下推荐前者,实现清晰且能讲得明白。
@WebServlet("/addCart") public class AddCartServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session = req.getSession(); Integer bookId = Integer.valueOf(req.getParameter("bookId")); Map<Integer, Integer> cart = (Map<Integer, Integer>) session.getAttribute("cart"); if (cart == null) { cart = new HashMap<Integer, Integer>(); } cart.put(bookId, cart.getOrDefault(bookId, 0) + 1); session.setAttribute("cart", cart); resp.sendRedirect("cart.jsp"); } }Map的key是bookId,value是该图书的购买数量。点击一次加一本,getOrDefault取出旧数量加1后放回。cart.jsp里遍历这个Map,拿key去查图书信息,value显示数量。这个方法比用ArrayList存Book对象更省内存,也天然做到“同一本书只出现一行”。注意getParameter返回字符串,Integer.valueOf转换要放在try-catch里,否则bookId参数为空时会抛NumberFormatException。
4.4 下单事务:扣库存和插订单不能拆开
这套源码最值得读的地方在订单模块,因为这是全项目唯一一个多处写数据的地方。下单要做三件事:扣减库存、插入订单主表、插入订单明细表。任何一步失败,其余两步必须回滚,否则会出现“订单生成了但库存没减”或“库存减了但订单没了”。
public boolean createOrder(int userId, Map<Integer, Integer> cart) { Connection conn = DBUtil.getConnection(); conn.setAutoCommit(false); try { // 1. 循环购物车扣减库存,加 stock >= ? 条件防超卖 String updateStock = "UPDATE book SET stock = stock - ? WHERE id = ? AND stock >= ?"; // 执行时依次传入 quantity、bookId、quantity // 2. 插入订单主表,生成订单号 String insertOrder = "INSERT INTO orders(order_no, user_id, total_price, status) VALUES(?,?,?,1)"; // order_no可用当前时间戳生成,total_price累计每条图书金额 // 3. 获取刚插入订单的自增id Long orderId = 0L; ResultSet rs = stmt.getGeneratedKeys(); if (rs.next()) { orderId = rs.getLong(1); } // 4. 遍历购物车插入订单明细 String insertItem = "INSERT INTO order_item(order_id, book_id, book_name, price, quantity) VALUES(?,?,?,?,?)"; conn.commit(); return true; } catch (SQLException e) { conn.rollback(); throw new RuntimeException("下单失败,已回滚", e); } finally { conn.setAutoCommit(true); DBUtil.close(conn); } }setAutoCommit(false)开启手动事务,全部成功后commit,任何异常rollback。扣库存的SQL里加stock >= ?条件,用受影响行数为0判断库存不足,这在并发场景下能防超卖。获取自增订单编号用getGeneratedKeys(),这个API必须紧跟INSERT语句执行,中间不能再插别的SQL。这套代码如果在答辩时能被老师追问到“为什么手动提交事务”,基本就是高分答案了。
5. 部署与避坑:导入步骤、连接配置与五个稳定踩坑点
5.1 Eclipse与IDEA的导入启动步骤
Eclipse导入最省心:File → Import → General → Existing Projects into Workspace,选源码根目录。导入后检查Project Facets里的Dynamic Web Module版本是否为3.0或更高,再把mysql-connector打头的jar确认在WEB-INF/lib下。右键项目 → Run As → Run on Server,选择本机Tomcat 8.5。
IDEA稍微费事一点:
- New → Project from Existing Sources,选项目根目录,一路Next
- 确认src标记为Sources Root,WebContent标记为Web Resource Directory
- File → Project Structure → Artifacts,看输出目录里是否有WEB-INF/lib下的jar包,没有就手动添加
- Run → Edit Configurations → 左上角加号 → Tomcat Server → Local
- Deployment里Add Artifact,Server页面设置HTTP端口8080
- 启动,浏览器访问http://localhost:8080/项目名/
IDEA导入老Web项目最常翻车的点就是Artifact里没有打包依赖jar,结果运行时一直报找不到驱动类,项目本身没编译问题但就是启动失败。在Artifacts的Available Elements里右键lib目录,选择Put into WEB-INF/lib就能解决。
5.2 数据库连接配置在哪里改
所有涉及数据库的参数都集中在src目录下的db.properties文件里,改这个文件就行,不用翻代码。
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/bookstore?useUnicode=true&characterEncoding=utf8&useSSL=false jdbc.username=root jdbc.password=123456password改成你自己MySQL的密码。如果MySQL装在别的机器,把localhost换成对应IP。注意db.properties改动后要重新发布到Tomcat,Eclipse里就是重新Run on Server,IDEA里是Build → Rebuild Project后再启动。
5.3 避坑记录:五个稳定踩坑点
坑一:页面404,路径找不到
现象:启动后浏览器报404,页面文件存在但访问不到。
原因:Servlet映射路径不一致。用@WebServlet注解时路径必须以/开头,而JSP里form的action或超链接href往往漏写了斜杠。
解决:到LoginServlet、AddCartServlet等类头看@WebServlet("/xxx")注解,再回JSP里核对action="/xxx",两者必须完全一致。
坑二:500错误ClassNotFoundException驱动类
现象:启动后第一个涉及数据库的请求直接500,日志里写着ClassNotFoundException: com.mysql.jdbc.Driver。
原因:mysql-connector-java的jar虽然加到了项目的Build Path里,但没有真正复制到WEB-INF/lib目录,Tomcat运行时找不到。
解决:把jar文件物理复制到WebContent/WEB-INF/lib下,然后刷新项目、清理Tomcat缓存后重启。
坑三:中文显示成问号或乱码
现象:页面中文正常,但写入MySQL后变成????;或者页面直接乱码。
原因:三层字符集不统一,JSP页面、JDBC连接串、MySQL表字符集至少有一层不是UTF-8。
解决:JSP头部加<%@ page contentType="text/html;charset=UTF-8" %>,db.properties里保留characterEncoding=utf8,MySQL建库用utf8mb4。改完把表里已有的乱码数据删掉重新插入。
坑四:登录报Access denied for user
现象:输入正确账号密码,报Access denied for user 'root'@'localhost' (using password: YES)。
原因:db.properties里写的密码与本地MySQL root账号密码不一致。
解决:改db.properties里的jdbc.password为正确密码;如果是MySQL刚装好没设密码,填空字符串。
坑五:Tomcat端口被占用启动失败
现象:启动Tomcat时控制台报Port 8080 required by Tomcat v9.0 Server is already in use。
原因:之前运行过的Tomcat实例没完全关闭,或者其它程序占了8080端口。
解决:Win下命令行执行netstat -ano | findstr :8080查PID,再taskkill /PID 端口号 /F结束进程;也可以改server.xml里Tomcat的端口,但要同时改HTTP和AJP两个端口。
6. 验证方法:一条条功能走查,再推一步就能当毕设
6.1 验收清单:功能一个个跑还是出问题
项目跑通后,按下面清单完整走一遍,每项功能都验证到。这套清单本身就是答辩前自测的最佳脚本。
| 模块 | 操作路径 | 预期现象 | 数据库侧验证 |
|---|---|---|---|
| 注册 | 首页→注册→提交 | 跳转登录页 | user表多一条记录 |
| 登录 | 登录页输入正确账号 | 首页显示用户名 | session里loginUser非空 |
| 登录失败 | 输入错误密码 | 同页提示错误信息 | user表无变化 |
| 图书分页 | 首页点分类或翻页 | 每页8本,页码正常 | 观察SQL中LIMIT参数 |
| 加入购物车 | 图书列表点“加入购物车” | 购物车页数量+1 | session中cart的Map变大 |
| 提交订单 | 购物车→结算 | 订单生成,库存减少 | orders与order_item各一条 |
| 库存不足 | 把书stock改为1再买2本 | 提示库存不足 | stock保持原值 |
| 后台管理 | admin登录→图书管理 | 可增删改图书 | book表同步变化 |
| 退出登录 | 点击退出 | 回首页,Session失效 | session中无用户信息 |
推荐用Navicat边操作边看表数据,比如下单成功后立刻刷新orders表,能看到订单号、总价和状态字段。这一步能帮你确认事务是否真的提交了。
6.2 值得动手的三个扩展方向
课程设计交完不是终点,这套源码再往前走三步就能当毕业设计。
第一个扩展:用Filter做登录拦截和角色控制。现在未登录用户直接访问admin下的JSP页面也能打开,加一个LoginFilter,在web.xml里配置过滤规则,Session里没有loginUser就跳转登录页;role不是admin的禁止访问/admin路径。这个知识点和实现难度,足以把项目从“及格”拉到“良”。
第二个扩展:订单状态流转。orders表现有status字段,可以把1定义为待发货、2为已发货、3为已签收,后台订单管理中提供“发货”按钮,点击后更新status。再给订单列表加时间排序和按状态筛选,就是一个完整的订单状态机。
第三个扩展:销售数据可视化。用分组聚合SQL统计每种图书的销量排行:SELECT book_id, SUM(quantity) FROM order_item GROUP BY book_id ORDER BY SUM(quantity) DESC,把结果用ECharts渲染成柱状图,JSP页面里放一个canvas容器,数据通过Ajax从Servlet以JSON格式返回。这一套下来直接从一个练习项目变成一个“管理系统+数据分析”的综合项目。
我自己在这套源码上栽过一次跟头:拿到手直接改代码,没先看原项目的web.xml和db.properties,结果折腾了一下午才发现问题出在路径映射上。从那以后我每次拿到新项目,都强制自己先读web.xml、db.properties、SQL脚本这三个文件,摸清边界再动手改。照着这个习惯走,这套代码你可以用得很顺,希望帮到你。
本文还有配套的精品资源,点击获取