ARTICLE DETAIL

资讯详情

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

JavaWeb图书销售系统期末项目实战:架构、购物车与事务避坑

JavaWeb图书销售系统期末项目实战:架构、购物车与事务避坑 简介一套面向高校计算机专业学生的JavaWeb在线图书销售系统完整项目资源源自评审分98分的期末大作业适合课程设计或项目实战练习。系统覆盖用户注册登录、图书检索浏览、购物车管理、订单处理及后台图书管理采用ServletJSPMVC架构结合JDBC操作MySQL数据库并集成Druid连接池与Kaptcha验证码前端配合HTML/CSS/JavaScript与Ajax实现交互典型技术栈清晰完整。压缩包共125个文件以43个Java源码、19个JSP页面、11个Jar依赖库、8个XML配置及SQL数据库脚本等为主配套图片、样式和脚本资源接近齐全整体仅5.44MB便于下载后快速部署学习。已有53人浏览学习项目经导师指导认证业务模块分层清晰借助SQL脚本可快速初始化数据库通过源码能理解连接池、验证码等实用组件适合作为课程设计与期末答辩的完整实战参考。1. 期末项目「高级版」到底高级在哪先想清楚你要交付什么很多人的 JavaWeb 期末项目做到最后就是一个「能打开首页、能登录、能查列表」的 CRUD 壳子答辩时老师一问购物车怎么存的就卡壳。「高级版」在流传的源码包里基本指在普通 CRUD 之上补全了购物车、订单、库存、后台管理这些业务闭环并且数据库表结构更完整、自带初始化 SQL。下面把架构怎么拆、IDEA 里怎么跑通、核心模块怎么写、哪些位置容易翻车一次讲透。适合正在做 JavaWeb 课程设计或期末项目、不想只交一个壳子的同学也适合想拿一套完整案例复习 Servlet 和 SSM 的从业者。2. JavaWeb图书销售系统的架构与数据库设计四张核心表怎么拆、连接池和事务怎么配2.1 三层架构怎么拆Controller、Service、Dao 各管一段市面上流传的 JavaWeb 图书销售系统源码绝大多数不是纯 JSP 一把梭而是走 Servlet JSP JavaBean 的 MVC 分层或者直接用 SSMSpring SpringMVC MyBatis。「高级版」的判断标准之一就是看它有没有把 Controller、Service、Dao 三层拆开。如果源码里所有逻辑都堆在 Servlet 里后期加需求会非常痛苦老师也会一眼看出这是「能跑但没设计」的作品。我一般会先看包结构controller、service、dao或者 mapper三个包是否独立。Controller 只做参数接收、调用 Service、跳转页面不写 SQLService 层管业务规则比如下单时校验库存、计算总价、决定要不要开事务Dao 层只做数据库的增删改查操作不写业务判断。这个分层不是摆设答辩时老师问「为什么订单和购物车要分开设计」答案就在 Service 层的业务边界里。如果是 SSM 版本还要额外注意 MyBatis 的 mapper.xml 和实体类字段的映射关系。图书表里如果有 create_time、update_time 这种字段实体类必须对应否则查询结果全是 null。拿到源码先看 mapper.xml 里有没有 resultMap有就逐行对字段没有就检查全局配置里有没有开启驼峰映射这个细节避坑章会单独展开。2.2 数据库表设计用户、图书、订单、订单明细四张表的字段与外键图书销售系统的表通常不少于五张用户表 user、图书表 book、购物车表 cart也有的直接把购物车存 Session 不建表、订单表 orders、订单明细表 order_item。高级版一般还会加分类表 category 和管理员表 admin。「高级版」和普通版在数据库上的分水岭是有没有订单明细表——只有一单对应一本书的简单记录那是玩具一个订单对应多本书、明细单独存才是完整业务。订单表常用字段如下状态用整数存不要直接存中文字段类型说明order_idint 主键自增订单号user_idint下单用户外键关联 user 表total_pricedecimal(10,2)订单总价statustinyint0待付款 1已付款 2已发货 3已完成 4已取消create_timedatetime下单时间订单明细表字段item_id 主键、order_id 关联订单、book_id 关联图书、book_name 冗余书名、price 冗余单价、quantity 购买数量。注意 book_name 和 price 做冗余不是错误这叫「快照」目的是防止图书下架或改价后历史订单查不到当时的交易信息电商订单都必须这么干。如果你拿到的是一份完整的 JavaWeb 项目案例配 MySQL 脚本一定要确认脚本里有没有这张明细表这是高级版和初级版最直观的区别。外键关联上建表脚本里可以加外键约束但实际查询代码里不推荐 JOIN而是先查订单列表再按 order_id 分两次查明细。原因是 JOIN 在数据量上来后索引优化不好做而且拆分查询逻辑更直白答辩也好讲。如果源码里用的是 JOIN能跑就不用改但你心里要清楚两种写法的差别。2.3 mysql 数据库连接池与事务Druid 配置参数和下单事务的最小写法连接池这块「高级版」源码里最常见的方案是 Druid 或者 C3P0。Druid 是阿里开源的自带监控页面配置多一个 druid.propertiesC3P0 老一些很多教材还在用。两种都能跑不用纠结哪个更好只要和源码配套一致就行。如果源码用的是 Druid通常会有这几个参数initialSize 初始连接数、maxActive 最大活跃连接数、minIdle 最小空闲连接数。提示maxActive 不要拍脑袋写 100Tomcat 默认工作线程也就 200连接池开太大没意义。期末项目场景 initialSize5、maxActive20 足够。事务这块必须讲清楚下单这个动作至少涉及三步写操作——往 orders 表插一条、往 order_item 表插多条、再把 book 表的 stock 减掉。这三步任何一个失败都不能留下「订单生成了但库存没扣」这种脏数据。所以 Service 层的下单方法必须在一个事务里。如果是 SSM直接 Transactional如果是纯 Servlet JDBC就要用 setAutoCommit(false) commit rollback 手工控制。下面是纯 JDBC 手动事务的最小写法期末项目答辩时特别好用因为老师一眼能看懂你在干什么Connection conn null; try { conn DataSourceUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交让下面所有操作共享同一个事务 orderDao.insertOrder(conn, order); // 插入订单主表 for (CartItem item : cartList) { orderItemDao.insertOrderItem(conn, item); // 插入订单明细 bookDao.decreaseStock(conn, item.getBookId(), item.getQuantity()); // 扣减库存 } conn.commit(); // 全部成功才提交 } catch (Exception e) { if (conn ! null) { conn.rollback(); // 任何一步失败整体回滚 } throw new RuntimeException(下单失败, e); } finally { if (conn ! null) { conn.close(); } }这段代码的关键点每个 DAO 方法都把 conn 作为参数传进去为的是保证它们用的是同一个数据库连接。如果你在 Dao 里每个方法都自己调用 getConnection()这个事务就白开了——三个方法各连各的commit 和 rollback 根本管不到另外两个连接。这是事务失效最常见的根源避坑章会再提一次。2.4 初始化 SQL 的执行顺序建库、建表、插数据的正确姿势拿到源码包后数据库目录下通常有一个 .sql 文件或者 db 文件夹。执行顺序很重要先建库再建表然后插数据。很多人用 Navicat 右键直接运行整个脚本报错说表不存在就是没注意脚本里可能已经有 USE 语句或者多个语句被分号分隔但没按顺序执行。标准姿势在 Navicat 里新建查询先执行 CREATE DATABASE bookdb DEFAULT CHARACTER SET utf8mb4;再执行 USE bookdb;然后执行建表语句最后执行 INSERT 数据。如果脚本是多个文件按文件名里的数字顺序执行比如 01_schema.sql、02_data.sql。数据文件里如果有中文书名一定要确认文件编码是 UTF-8否则导入后全是问号。判断编码的办法很简单用记事本打开看中文是否正常导入后如果乱码就把文件另存为 UTF-8 再导一次。3. 在 IDEA 里运行 JavaWeb 项目导入源码、修改配置、启动 Tomcat 的完整链路3.1 环境版本搭配JDK、Tomcat、MySQL 版本怎么配不打架这是期末项目卡住最多人的第一关。「高级版」源码如果是 SSMIDEA、JDK、Tomcat、MySQL 四个版本必须匹配。最保险的组合是 JDK 8 Tomcat 8.5 MySQL 5.7配套 mysql-connector-java 5.1.49 驱动。如果源码用了 MySQL 8 的驱动和语法那就整套用 JDK 8 Tomcat 9 MySQL 8.0别混搭。我的建议是先看 pom.xml 或者 lib 目录里 servlet-api.jar 的版本再决定本机装什么。在 JDK 17 的机器上直接跑 Tomcat 9 的项目大概率会碰到 javax.servlet 包不存在或者编译报错这是版本断层不是源码烂。期末项目追求的是能用宁可把本机 JDK 退到 8、在 IDEA 的 Project Structure 里把 SDK 切到 1.8也不要去改源码适配新版本。改适配的工程量比重装环境大得多而且改完不一定说得清自己改了什么。IDEA 右下角的 Event Log 也要看一眼确认项目用的是 1.8别让它自动选成 17。3.2 修改数据库连接配置jdbc.properties 四个必改项IDEA 导入工程分两种Maven 工程直接 File - Open 选 pom.xml等依赖下载完非 Maven 的普通 Web 工程要在 Project Structure 里把 src 标记为 Sources、web 目录标记为 Web Resources再在 Artifacts 里配置 war exploded。这一步最容易出错的是 lib 目录没被识别导致 Tomcat 启动时报 ClassNotFoundException。源码包里通常在 src 或 resources 下有个 jdbc.properties也叫 db.properties 或 application.properties打开后改成你自己本机的数据库账号密码jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/bookdb?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456四个必改项都在这里。password 改成本机 MySQL 密码url 里的 bookdb 改成你建库时实际用的库名serverTimezoneAsia/Shanghai 必须写MySQL 8 不写会报时区错误useSSLfalse 是避免 MySQL 8 的 SSL 握手警告刷屏。改完再去看 Tomcat 配置IDEA 的 Deployment 选项卡里要确认 Application context 是 /book 还是 /这决定了你最后访问首页的路径后面排查 404 时第一个就查它。3.3 Tomcat 启动与访问路径排查端口占用和 404 的处理启动 Tomcat 最常见的三种表现端口被占用、部署失败、启动成功但 404。端口占用说明有残留的 Tomcat 进程Windows 下命令行执行 netstat -ano | findstr 8080 查 PID再 taskkill /f /pid 进程号 干掉。部署失败多半是 Artifacts 里没把 war exploded 加进 DeploymentTomcat 日志会提示 No artifacts marked。404 最要命但也好排查。先看 Tomcat 默认首页能不能开能开说明 Tomcat 没毛病问题出在 Application context 路径不对或者 web.xml 里 servlet-mapping 和实际访问 URL 不一致。比如 web.xml 里配置了 url-pattern 为 /book/list那浏览器就要访问 http://localhost:8080/工程名/book/list少一个前缀都不行。常见做法是把登录页作为第一个测试目标因为登录页通常不依赖 Session最容易定位问题。一次 404 是路径问题所有页面都 404 基本是 context 路径问题按这个思路逐步缩小范围别一上来就怀疑源码。4. JavaWeb 图书销售系统核心功能登录拦截、购物车、订单提交的代码实现4.1 登录状态与 SessionFilter 拦截器的写法与放行规则登录功能「高级版」和「初级版」的区别在于有没有做登录拦截而不是有没有登录页。初级版是 login.jsp 提交后验证账号密码对就跳 index.jsp。高级版要把登录状态放进 Session再用一个 Filter 拦截所有需要登录才能访问的路径未登录直接重定向到登录页。Filter 在 JavaWeb 里几乎是必考核心逻辑如下public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; HttpSession session req.getSession(false); // 传 false不主动创建 Session String path req.getRequestURI(); // 登录页、登录接口、静态资源放行 if (path.endsWith(login.jsp) || path.contains(/user/login) || path.contains(/static/)) { chain.doFilter(request, response); return; } if (session null || session.getAttribute(user) null) { resp.sendRedirect(req.getContextPath() /login.jsp); // 未登录赶回登录页 return; } chain.doFilter(request, response); // 已登录放行 }这里两个细节值得抠。第一getSession(false) 而不是 getSession()前者在会话不存在时返回 null不会为匿名请求创建新的 Session后者会硬建一个导致所有请求都往服务器写 Session浪费内存。第二放行规则必须包含登录接口本身和静态资源否则用户提交登录表单时被过滤器拦回登录页形成死循环CSS、JS 也会全部被拦页面光秃秃。Cookie 在这套系统里一般只用来做「记住我」或者回显用户名不建议把用户 ID 放 Cookie 里做登录凭证。答辩时老师问「Session 和 Cookie 的区别」能说出「Session 在服务端、Cookie 在客户端Session 依赖 Cookie 保存会话 ID」就够了不用扯分布式 Session。4.2 购物车选 Session 还是数据库表Map 去重的实现购物车是图书销售系统最容易被问穿的一个模块。两种主流做法一是直接存 Session用 MapInteger, CartItemkey 是 book_id二是建一张 cart 表存数据库。期末项目两种都有人用但「高级版」源码里更多是 Session 方案因为它不改表结构、逻辑顺、答辩好讲。我倾向推荐 Session 方案理由很实际购物车是临时行为用户没下单就关浏览器数据留着没意义放数据库还得处理「清空过期购物车」的定时任务属于给自己加需求。Session 方案的核心代码很短HttpSession session req.getSession(); MapInteger, CartItem cart (MapInteger, CartItem) session.getAttribute(cart); if (cart null) { cart new HashMap(); session.setAttribute(cart, cart); } int bookId Integer.parseInt(req.getParameter(bookId)); CartItem item cart.get(bookId); if (item null) { // 首次加购数量 1价格从 bookService 查出后传入 item new CartItem(bookId, 1, currentPrice); cart.put(bookId, item); } else { item.setQuantity(item.getQuantity() 1); // 同一本书已存在数量 1 }这个实现里有一个必问的细节为什么用 Map 不用 List因为 Map 的 key 是图书 ID天然去重。同一本书加购第二次get 一次就能判断是否存在数量直接加一用 List 的话每次加购都要 O(n) 遍历找有没有同一本书。小项目看不出差别但答辩时说不出理由就显得没想清楚。数据库方案的适用场景是跨设备同步购物车、做「购物车数量角标」这类长期记录期末项目没必要上除非源码里已经带了 cart 表那就按源码跑别自己改回 Session——以能跑为第一目标。4.3 订单提交与库存扣减事务边界和防超卖 SQL订单模块「高级」体现在两个点订单状态的定义和库存扣减的时机。状态用 int 字段存数字页面显示时再映射成中文不要直接在数据库存「待付款」这种中文排序、统计、扩展都会很痛苦。库存扣减有个「超卖」概念虽然期末项目没人真来并发压测但老师问到要能答出来。正确的做法不是先查库存、判断够不够、再 UPDATE而是把判断和扣减合并成一个原子 SQLUPDATE book SET stock stock - ? WHERE book_id ? AND stock ?参数依次是本次购买数量、图书 ID、库存下限和数量同一个值。这个 SQL 返回的影响行数就是扣减成功的标志返回 1 说明扣成功了返回 0 说明库存不够。为什么不能先查再改因为两步之间有间隙两个请求同时读到库存是 1都判断够都去扣就超卖了。一个 UPDATE 把读和写合并成原子操作才是正经做法。Java 侧代码int rows bookDao.decreaseStock(bookId, quantity); if (rows 0) { throw new RuntimeException(库存不足); // 抛运行时异常触发事务回滚 }抛 RuntimeException 有两个作用一是中断后续代码二是配合事务管理器的回滚规则让整个下单事务回滚。如果你用 checked Exception 或者 catch 住异常不往外抛事务是不会回滚的这也是 SSM 项目里事务「看起来加了注解但没用」的常见原因。5. 避坑手册JavaWeb 图书系统最常见的五个翻车点5.1 Tomcat 能启动但访问首页 404artifact 和 context path 不一致现象Tomcat 日志显示启动成功Tomcat 默认首页能打开但输入 http://localhost:8080/book/ 或者根路径都 404。原因分两类一是 IDEA 的 Run/Debug Configurations 里 Deployment 选项卡没把 war exploded 加进去Tomcat 起来了但没挂任何应用二是 Application context 路径和实际访问路径不匹配源码里配置的是 /book你却访问根路径 /。解决打开 Run/Debug Configurations确认 Deployment 里有 artifactApplication context 和浏览器地址保持一致。然后分别访问根路径和 /book/哪个有内容说明路径问题定位了。如果都不行看 Tomcat 的 localhost 日志查有没有 Servlet 加载失败的异常堆栈那才是真正的根因。5.2 数据库连接报 Access denied驱动版本和认证插件不匹配现象Tomcat 启动后点登录页面报 Access denied for user rootlocalhost或者 Public Key Retrieval is not allowed。原因第一是 jdbc.properties 里的密码和本机 MySQL 不一致这个最好查。第二是 MySQL 8.0 默认用 caching_sha2_password 认证插件老版本 JDBC 驱动不认识就会报 Public Key Retrieval is not allowed。密码里带了特殊字符没转义也会连不上。解决先确认 MySQL 版本。5.7 就用 mysql-connector-java 5.1.498.0 必须用 8.x 驱动并在 URL 里加 allowPublicKeyRetrievaltrue。properties 文件里密码如果有 : # 这类字符用反斜杠转义或者干脆先改成纯数字密码跑通再说。期末项目别在密码上耗时间跑通第一。5.3 中文乱码建库字符集、页面编码、过滤器三处排查现象页面上中文全是问号数据库里存进去的中文也是乱码。原因分三个层级建库时没指定 utf8mb4用了默认的 latin1存中文就是乱码JSP 页面顶部没声明 pageEncodingUTF-8Tomcat 按 ISO-8859-1 解码提交的中文在 Servlet 里取出来就是乱码IDEA 文件编码是 GBK源码里的中文注释和字符串本身就已经乱了。解决数据库层面把库和表改成 utf8mb4注意 utf8mb4 比 utf8 多支持 emoji 和生僻字MySQL 5.5.3 之后推荐用前者。代码层面在 web.xml 里配置 CharacterEncodingFilterencoding 设为 UTF-8forceEncoding 设为 true。最后查 IDEA 的 File - Settings - File Encodings三处编码全部设 UTF-8。三处都改了还乱码是编译后的 class 缓存问题Build - Rebuild Project 一下。5.4 登录后 Session 丢失Cookie 路径和重定向的坑现象登录页验证通过了跳转到首页后马上又跳回登录页或者页面显示未登录。原因Session 的 CookieJSESSIONID默认按路径隔离在 /book 路径下创建的 Cookie跳转到 / 路径就不带过去了。另一个常见原因是代码里用了 resp.sendRedirect(/index.jsp)写死了根路径没加 getContextPath()导致跳转后路径对不上。解决打开浏览器开发者工具的 Application - Cookies看 JSESSIONID 的 Path 是不是 /。如果不是在 web.xml 的 session-config 里配置 cookie-path 为根路径。同时把代码里所有写死的跳转路径改成 req.getContextPath() /index.jsp 这种动态拼接。写死路径是 JavaWeb 项目里翻车率最高的习惯查 Session 丢失时顺手把所有重定向都过一遍。5.5 下单后库存没扣事务失效的两种典型原因现象订单主表里有记录但 book 表的 stock 没减少或者订单明细缺失。原因事务没生效典型有两种。一是 DAO 层每个方法都自己获取 ConnectionService 层虽然调用了 commit 和 rollback但每个 DAO 方法各连各的数据库事务根本覆盖不到二是 SSM 项目里 Transactional 加在非 public 方法上或者 spring 配置文件里没有配置事务管理器。解决先检查下单链路里所有 DAO 方法是否共用同一个 Connection不共用就改成参数传递。SSM 项目去 spring 配置里查有没有 DataSourceTransactionManager 和注解驱动。验证事务是否生效的土办法在 Service 方法里故意抛一个异常看数据库里是否留下半截数据。如果订单生成了但库存没扣说明事务没包住逐层往上查。6. 验证与进阶把源码「能跑」变成答辩「能讲」的三件事系统跑通只是及格线。答辩时老师看的是你能不能讲清楚三个层次数据是怎么流的、事务边界在哪、异常怎么处理。我建议花半小时做三件验证。第一把关键链路的日志打出来。在 Service 下单方法里加 System.out.println输出「开始下单、扣库存成功、插入明细成功、提交事务」四个节点。答辩现场直接把控制台日志调出来一步一步讲比空口说「我做了事务」有力得多。再故意在扣库存前抛一个异常把回滚日志打出来这就是你做过事务测试的证据。第二准备核心表的关系说明。用 Navicat 的模型功能导出 user、book、orders、order_item 四张表的关联图标注外键和关键字段。老师问「订单明细为什么冗余书名和价格」你指着表说「历史订单不能因为图书下架就查不到」这个回答比背概念强太多。第三把三个「如果」想清楚没登录直接访问订单页怎么办——过滤器拦截同一本书加购十次怎么办——Map 去重加数量累加库存只剩一本、两个人同时下单怎么办——UPDATE 条件扣减。这三个问题是 JavaWeb 项目答辩的保留节目提前在代码里标好位置到时候直接跳过去讲比现场翻代码从容。我自己的习惯是答辩前一晚把 Tomcat 冷启动一遍删掉 target 和 out 目录重新构建再走一遍「注册-登录-加购-下单-后台发货」完整流程。这套系统翻车最多的不是代码逻辑而是环境——IDEA 缓存、端口占用、MySQL 没启动任何一个都能让人现场社死。做完这三件事你手里的「高级版」源码就不再是别人的代码而是一套讲得清、改得动、答得出的交付物。希望帮到你。本文还有配套的精品资源点击获取
返回列表