ARTICLE DETAIL

资讯详情

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

JSP+MySQL图书销售管理系统:表结构、事务与避坑全流程

JSP+MySQL图书销售管理系统:表结构、事务与避坑全流程 简介这是一套基于JSPMySQL的JavaWeb图书销售管理系统网上书店源码与数据库面向计算机相关专业期末大作业、课程设计及项目实战学习者。系统包含前台图书浏览、购物车、订单与后台管理功能代码结构清晰附有完整SQL脚本部署即可运行适合作为课程设计或练手素材。压缩包共286个文件含53个Java类、52个JSP页面、40个JS脚本、19个CSS样式以及JAR库、SQL脚本和配置文件等整体约6.93MB目录按Web项目常规组织便于查阅与二次开发。已有233人学习下载该设计经导师指导认可可作为期末大作业完整参考帮助理解Servlet、JSP与MySQL整合应用快速获得可扩展的网上书店项目基础。1. 从“图书销售管理系统”到可交差的JavaWeb毕设JSPMySQL到底能做什么如果你是第一次拿到完整的JavaWeb课程设计搜“JSPMySQL”时跳出来的多半是这样一个组合基于JSPMySQL的JavaWeb图书销售管理系统也就是网上书店项目源码数据库。这套系统不算黑科技恰好把Servlet、JSP、JDBC、Session和事务串成一条线业务覆盖用户注册登录、图书检索、购物车、订单生成与库存扣减做完它JavaWeb课程设计的基本盘就齐了。对想交一份能演示、能答辩的作业的人以及想复习三层架构的从业者都合适。这篇笔记会把表设计、环境配置、核心实现和踩坑点按顺序讲清楚照着走就能在IDEA里跑通。我见过不少直接把源码扔进Tomcat就以为完事的人结果卡在MySQL 8连接、JSP乱码、静态资源404这些细节上。这些问题看着小却能让你在答辩现场翻车。所以我不打算只讲“这项目能做什么”而是讲“怎么把它稳稳当当地跑起来并且让代码看起来像是你写的”。2. 系统设计与数据库表结构先搞清网上书店的订单状态机2.1 从用户下单到库存扣减五张表的职责划分网上书店这种系统核心业务是“用户挑书→生成订单→扣库存→支付/发货”。常见的表设计是五张用户表、分类表、图书表、订单主表、订单明细表。有些项目还会加轮播图表、公告表、管理员表但主线就是这五张。我一般先画一张简单的业务图再建表用户表存登录信息分类表给图书做一级分组图书表存价格、库存和销量。订单主表存一次下单的汇总信息比如订单号、总金额、状态、下单时间。订单明细表存这个订单里的每一本书包括下单时的书名快照、单价、数量。为什么下单价要单独存一份而不是直接关联图书表因为图书售价会改动订单明细里的价格是用户付款时的价格必须固化成快照不然以后对账会差钱。订单状态用一个tinyint字段表示这是订单流程里最容易出问题的地方。我见过很多课程设计用字符串字段比如“未支付”“已支付”改起来麻烦还容易写错。更稳的做法是固定一套状态机0待付款、1已付款、2已发货、3已完成、4已取消。用户只能从待付款走到取消或者已付款发货后不能直接退回待付款。这部分逻辑最好在后端Service层统一校验而不是在前端页面里随便改按钮。图书表的库存字段也要注意。不要用int之外的东西扣库存时应该写成stock stock - ?并在同一行SQL里加stock ?条件这样并发时才不会把库存扣成负数。后面第4章会写具体代码这里先把表结构立住。2.2 SQL脚本初始化MySQL 5.7/8.0都能跑的建表语句与样例数据下面这套SQL是常见做法兼容MySQL 5.7和8.0字符集统一用utf8mb4避免中文和表情符号乱码。注意orders是MySQL保留字所以建表时写成orders并用反引号包住或者改成t_order。我在脚本里用orders习惯上更直观。-- 创建数据库网上书店项目最基础的一步 CREATE DATABASE IF NOT EXISTS bookstore DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE bookstore; -- 分类表 CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 分类ID, name VARCHAR(50) NOT NULL COMMENT 分类名称 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书分类; -- 用户表 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT 密码建议MD5/BCrypt密文, email VARCHAR(100) COMMENT 邮箱, phone VARCHAR(20) COMMENT 手机号, role TINYINT NOT NULL DEFAULT 0 COMMENT 角色0普通用户1管理员, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 图书表 CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 图书ID, category_id INT NOT NULL COMMENT 分类ID外键, name VARCHAR(200) NOT NULL COMMENT 书名, author VARCHAR(100) COMMENT 作者, price DECIMAL(10,2) NOT NULL COMMENT 售价, stock INT NOT NULL DEFAULT 0 COMMENT 库存数量, sales INT NOT NULL DEFAULT 0 COMMENT 销量, cover VARCHAR(255) COMMENT 封面图片路径, description TEXT COMMENT 图书描述, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 上架时间, KEY idx_category (category_id), CONSTRAINT fk_book_category FOREIGN KEY (category_id) REFERENCES category(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书表; -- 订单主表 CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 订单ID, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 业务订单号给用户看的, user_id INT NOT NULL COMMENT 下单用户ID, total_price DECIMAL(10,2) NOT NULL COMMENT 订单总金额, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态0待付款1已付款2已发货3已完成4已取消, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 下单时间, KEY idx_user (user_id), CONSTRAINT fk_order_user FOREIGN KEY (user_id) REFERENCES user(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表; -- 订单明细表 CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 明细ID, order_id INT NOT NULL COMMENT 所属订单ID, book_id INT NOT NULL COMMENT 图书ID, book_name VARCHAR(200) NOT NULL COMMENT 下单时的书名快照, price DECIMAL(10,2) NOT NULL COMMENT 下单时的单价快照, count INT NOT NULL COMMENT 购买数量, sub_total DECIMAL(10,2) NOT NULL COMMENT 小计, KEY idx_order (order_id), CONSTRAINT fk_item_order FOREIGN KEY (order_id) REFERENCES orders(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单明细表;这套脚本里外键约束为什么加课程设计评审老师很喜欢看到外键表明你有数据库完整性的概念。但实际开发里外键可能成为写入瓶颈很多公司会去掉外键改由应用层保证。你交作业时可以保留外键答辩时也能说出这个取舍。写完表结构别忘了插几条样例数据否则项目一启动页面空空如也。INSERT INTO category (name) VALUES (计算机), (文学), (经管); INSERT INTO user (username, password, email, role) VALUES (admin, MD5(123456), adminbookstore.com, 1), (zhangsan, MD5(123456), zhangexample.com, 0); INSERT INTO book (category_id, name, author, price, stock, sales, cover) VALUES (1, Java编程思想, Bruce Eckel, 108.00, 50, 120, images/java.jpg), (1, MySQL从入门到精通, 张三, 68.00, 80, 200, images/mysql.jpg), (2, 活着, 余华, 45.00, 30, 500, images/huo.jpg);注意user表的密码用了MD5项目代码里比对时也要先MD5再比对。如果你在页面功能里允许用户注册记得注册时同样加密而不是明文入库。数据库增删改查不难难在这些边界约定。另外图书列表页的排序通常会用到ORDER BY sales DESC, create_time DESC也就是销量优先、新品其次这一步在用JDBC写分页查询时要留好排序参数口子。3. 在IDEA里把JSPMySQL项目跑起来环境版本与启动顺序3.1 Tomcat与MySQL版本选择JDK8还是JDK11mysql-connector怎么配这个项目是典型的JavaWeb三件套适配性最好的组合是JDK 8 Tomcat 8.5/9 MySQL 5.7/8.0。不建议一上手就上Tomcat 10因为Tomcat 10把javax.servlet换成了jakarta.servlet老JSP项目里几乎所有的import都会直接NoClassDefFoundError。如果你手里拿到的是老源码优先JDK 8。MySQL版本对驱动要求很敏感。MySQL 5.7可以用mysql-connector-java 5.1.49但MySQL 8默认认证插件是caching_sha2_password老驱动会报“Public Key Retrieval is not allowed”或者SSL连接错误。我一般直接用8.0.x驱动比如8.0.33同时它向下兼容MySQL 5.7。驱动文件放到WEB-INF/lib下面IDEA里这个目录下的jar会自动进入Artifacts不用额外配Classpath。很多人在idea运行javaweb项目配置这一步卡住其实核心就三个点一、项目要能被IDEA识别成Web项目也就是有web/WEB-INF/web.xml二、Tomcat要用Local模式配置三、Deployment里要添加war exploded不是war包。war包每次改代码都要重新buildwar exploded可以热加载静态资源和JSP。3.2 导入源码和数据库从创建数据库到IDEA配置Artifacts先把数据库跑起来。如果你还没装MySQL建议去看mysql安装教程重点是安装时选好字符集和设置root密码装完以后用命令行验证连通性。# 在MySQL安装目录bin下执行或者直接使用命令行客户端 mysql -uroot -p bookstore.sql执行之后可以用show tables;检查表是否齐全。这一步做完再打开IDE配置连接参数。在源码里找到类似db.properties或jdbc.properties的文件改成你自己的连接信息jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/bookstore?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8allowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password你的密码这里的参数不是随意加的。useSSLfalse是因为本地开发不需要SSL加密连接不加这一条MySQL 8经常报SSL连接错误。serverTimezoneAsia/Shanghai解决时区不一致导致的时间差8小时问题。characterEncodingutf8保证中文字符在连接层不乱码。allowPublicKeyRetrievaltrue是因为MySQL 8 caching_sha2_password认证方式在非SSL连接下需要从服务器获取公钥很多新手项目死在这一步。然后打开IDEAFile New Project from Existing Sources选择项目目录让IDEA识别为一个普通JavaWeb工程。再打开Run/Debug Configurations点击加号选Tomcat Server Local。在Deployment选项卡里加Artifact选xxx:war explodedApplication context填/bookstore。这里的路径必须和项目里访问路径一致不然浏览器访问不到页面。启动后访问http://localhost:8080/bookstore能看到首页就算跑通。我总结一下启动顺序先保证数据库服务可用再检查properties配置里的host、port、用户名、密码最后才是Tomcat启动。因为Tomcat启动时如果连接池初始化失败往往只会打印一条异常页面还是能打开但点登录就错。4. 核心功能实现登录拦截、图书分页与购物车Session事务4.1 用户登录与Session拦截Filter把非法请求拦在门外网上书店有几个页面必须登录后才能看比如“我的订单”“结算页”。如果没有权限控制别人直接访问/order/list.jsp就能看到订单数据这在答辩时是硬伤。常见做法是写一个Filter统一拦截而不是在每个Servlet里重复判断。package com.bookstore.filter; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; public class LoginFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String path request.getServletPath(); // 放行登录、注册、静态资源、首页 if (path.contains(login) || path.contains(register) || path.contains(css) || path.contains(js) || path.contains(images) || path.equals(/index.jsp)) { chain.doFilter(req, resp); return; } HttpSession session request.getSession(); Object user session.getAttribute(user); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }这段代码的作用很直白除了登录页、注册页、静态资源和首页其他路径都检查Session里有没有user没有就跳回登录页。注意/index.jsp要放行因为首页本身不需要登录。还要注意如果你用了WebFilter(/*)注解又在web.xml里配置了同样的Filter会出现过滤两次、二次跳转的问题。我建议只在web.xml里配置这样更直观。对应web.xml里的配置filter filter-nameLoginFilter/filter-name filter-classcom.bookstore.filter.LoginFilter/filter-class /filter filter-mapping filter-nameLoginFilter/filter-name url-pattern/*/url-pattern /filter-mappingFilter顺序也值得说如果你还有CharacterEncodingFilter要把编码过滤器放在LoginFilter前面因为LoginFilter里可能通过request.getParameter获取登录名那时候如果编码还没处理用户名就会乱码登录取不到正确值。4.2 图书分页查询与数据库增删改查的边界条件网上书店的图书列表一般不会一次全查出来几十本书还好几百本时页面会明显变慢。常见做法是分页核心SQL就是LIMIT ? OFFSET ?。下面用JDBC方式演示一个最简单的分页查询。public ListBook findPage(int page, int pageSize) { String sql SELECT id, name, author, price, stock, sales FROM book ORDER BY sales DESC LIMIT ? OFFSET ?; PreparedStatement ps conn.prepareStatement(sql); ps.setInt(1, pageSize); ps.setInt(2, (page - 1) * pageSize); ResultSet rs ps.executeQuery(); ListBook list new ArrayList(); while (rs.next()) { Book book new Book(); book.setId(rs.getInt(id)); book.setName(rs.getString(name)); book.setPrice(rs.getBigDecimal(price)); list.add(book); } return list; }注意OFFSET的计算是(page - 1) * pageSize第一页的偏移是0。很多人写成page * pageSize结果第一页丢掉前几条数据。还有一个坑是LIMIT后面的值不能在SQL里直接拼字符串一定要用PreparedStatement传参否则就是SQL注入。与此同时查询总数用的是SELECT COUNT(*) FROM book不要为了省事在Java里把所有数据load出来再size数据量大时内存扛不住。分页查询是多表关联时也可以配合JOIN category获得分类名称但要注意COUNT和LIMIT要作用在同一个主表查询上。4.3 购物车和订单提交用Session存购物车用事务保证订单完整性购物车在JSP项目里最常见的做法是放在Session里而不是建一张数据库表。因为购物车是临时数据不需要持久化。通常用MapInteger, CartItemkey是图书IDvalue包含图书信息、数量、小计。用户添加图书时先判断Session里有没有购物车没有就新建一个。订单提交是另一个重点。这里涉及两张表的数据写入订单主表和订单明细同时还要扣库存。如果主表写成功但明细失败或者扣库存失败数据就是脏的。必须用事务把这几步包起来。public void submitOrder(int userId, MapInteger, CartItem cart) throws Exception { Connection conn DBUtil.getConnection(); try { conn.setAutoCommit(false); // 1. 写入订单主表 String sqlOrder INSERT INTO orders (order_no, user_id, total_price, status, create_time) VALUES (?, ?, ?, 0, NOW()); PreparedStatement psOrder conn.prepareStatement(sqlOrder, Statement.RETURN_GENERATED_KEYS); psOrder.setString(1, generateOrderNo()); psOrder.setInt(2, userId); psOrder.setBigDecimal(3, computeTotalPrice(cart)); psOrder.executeUpdate(); ResultSet keys psOrder.getGeneratedKeys(); int orderId 0; if (keys.next()) { orderId keys.getInt(1); } // 2. 写入订单明细并扣库存 for (CartItem item : cart.values()) { String sqlItem INSERT INTO order_item (order_id, book_id, book_name, price, count, sub_total) VALUES (?, ?, ?, ?, ?, ?); PreparedStatement psItem conn.prepareStatement(sqlItem); psItem.setInt(1, orderId); psItem.setInt(2, item.getBookId()); psItem.setString(3, item.getBookName()); psItem.setBigDecimal(4, item.getPrice()); psItem.setInt(5, item.getCount()); psItem.setBigDecimal(6, item.getSubTotal()); psItem.executeUpdate(); // 扣库存条件带上 stock count防止负数 String sqlStock UPDATE book SET stock stock - ? WHERE id ? AND stock ?; PreparedStatement psStock conn.prepareStatement(sqlStock); psStock.setInt(1, item.getCount()); psStock.setInt(2, item.getBookId()); psStock.setInt(3, item.getCount()); int rows psStock.executeUpdate(); if (rows 0) { throw new RuntimeException(库存不足事务回滚); } } conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { DBUtil.close(conn); } }这段代码有几个关键点。第一setAutoCommit(false)是开启事务的开关所有Statement必须使用同一个Connection否则事务无效。很多项目里DAO层各自连接数据库把事务拆成了多个独立事务下单就出问题。第二UPDATE ... WHERE stock ?是乐观锁的一种简单实现即使两个请求同时扣库存也只有一个能执行成功另一个rows0直接回滚。第三generateOrderNo()不能只用自增id最好生成一个带时间戳的字符串比如yyyyMMddHHmmss加随机数给用户查看也方便。5. 避坑/常见问题网上书店项目从导入到部署的5个翻车现场5.1 MySQL 8连接报错SSL连接错误与Public Key Retrieval问题现象Tomcat启动正常打开首页也正常但一点“登录”就报错控制台出现Communications link failure或者Public Key Retrieval is not allowed又或者SSL connection error。原因项目里的mysql驱动太老而数据库是MySQL 8。MySQL 8默认认证插件是caching_sha2_password老驱动5.1.x处理不了再加上JDBC URL里没有显式关闭SSL驱动默认尝试加密连接本地环境又没有证书就卡住了。解决把WEB-INF/lib里的mysql-connector-java换成8.x版本并且在jdbc.properties里按第3章的写法加上useSSLfalseallowPublicKeyRetrievaltrue。改完驱动后一定要重新Build Artifacts把旧jar从target目录清掉否则运行时用的还是旧包。5.2 JSP页面中文乱码与查询结果对不上现象数据库里中文正常页面显示问号或者一堆菱形乱码或者你在搜索框输入“Java”页面上搜索出来是“????”。原因这是老JSP项目最常见的病。JSP文件的pageEncoding没有设成UTF-8或者POST请求没设置request.setCharacterEncoding(UTF-8)又或者Tomcat的server.xml里Connector没有配URIEncodingUTF-8。数据库连接URL里也缺characterEncodingutf8。这几处只要有一处不一致中文就乱。解决统一所有环节的字符集。JSP第一行写% page contentTypetext/html; charsetUTF-8 pageEncodingUTF-8%在web.xml里注册一个CharacterEncodingFilter强制把request和response都设为UTF-8MySQL建库建表用utf8mb4连接URL里带characterEncodingutf8。这样一套下来基本不会再有中文问题。5.3 IDEA配置Tomcat后启动404或静态资源丢失现象Tomcat日志显示启动成功但浏览器访问http://localhost:8080/bookstore/返回404或者图片全部不显示CSS样式丢失。原因Application context配错了。你在IDEA的Deployment里填的是/bookstore访问路径就必须是/bookstore少一个斜杠、多一个路径都会404。静态资源404通常是JSP里用了相对路径比如srcimages/logo.jpg当当前URL是/bookstore/book/list.jsp时浏览器会去/bookstore/book/images/logo.jpg找自然找不到。解决在IDEA的Run Configuration里确认Application context值是/bookstoreJSP页面里一律用绝对路径即${pageContext.request.contextPath}/images/logo.jpg或者${basePath}公共变量。另外如果项目结构里css/js放在web根目录下也要检查有没有被Filter拦截静态资源路径要在LoginFilter里放行。6. 进阶把“能跑”变成“能上线”的验证方法与三个小改造能跑只是第一步。我见过很多网上书店项目演示时手动点没问题一上并发测试就崩或者两个订单同时提交把库存扣成负数。要验证这套JSPMySQL系统到底能不能交差我一般会做三件事。第一用JMeter起10个线程同时提交下单请求压测过后去查订单表数量和图书库存余量。正常情况应该是订单表增加10条库存扣10个不能出现负数也不能出现订单明细缺失。如果库存变负说明你的回滚逻辑没生效。第二打开MySQL通用日志或者给项目加上日志框架打印SQL下单流程里应该能看到INSERT INTO orders、INSERT INTO order_item、UPDATE book三条语句在同一个Connection里按顺序执行。第三验证登录Filter是否真的拦住未登录用户直接浏览器访问/bookstore/order/list.jsp应该跳回登录页。三个小改造里最值得做的是把DriverManager.getConnection换成Druid连接池。传统JSP项目自己管理Connection每次请求都创建连接数据库压力一大就报Connection refused。用Druid以后连接复用、连接监控都方便在web.xml里配置一个StartupListener初始化数据源就行。其次是给订单提交里的库存扣减加乐观锁条件也就是代码里写的WHERE stock ?它能从根上防止超卖。第三个改造是加一个统一异常页面web.xml里配置error-page把500错误跳转到专门的error.jsp否则答辩时一报错就是半屏堆栈用户体验非常差。我自己带过好几轮这样的项目最大的教训是JavaBean字段、数据库字段、JSP表单字段三处必须同步。有一次把数据库的user_id改成userId结果忘了更新SQL里的下划线映射整个订单查询直接翻车。后来每改一个模块我都会先跑一遍按字段名查询的测试用例再写页面。系统不怕老怕的是改一半留一半。希望帮到你。本文还有配套的精品资源点击获取
返回列表