
简介购物商城是JavaWeb课程设计与期末大作业的高分示范项目面向正在完成相关任务的本专科学生及JavaWeb初学者。源码包含用户注册登录、商品浏览、购物车、订单处理、后台管理等典型业务模块并附有较完整代码注释重点逻辑易于对照学习即使基础偏弱也能快速理解整体实现思路。压缩包共129个文件涵盖java源码、class编译文件、jsp页面、sql数据库脚本、jar依赖库、css/js前端样式及配置文件等包体约19.29MB下载后按说明部署即可运行适合用于课程答辩、功能演示或作为毕业设计前期参考。资料位于CSDN下载频道已有350人学习/下载该热度也反映出其对接JavaWeb大作业需求的实用性。对需要兼顾完整性与易读性的学习者来说这是一份可参考、可复用、可快速落地的项目资料。1. JavaWeb课程设计购物商城这个满分大作业到底在做什么每到期末JavaWeb课程设计被选得最多的题目就是购物商城。需求看起来就三句话用户能注册登录、能浏览商品、能下订单但真拿去验收时有人拿优有人被挂。差别不在功能多少而在“有无数据库设计痕迹”和“是否有完整闭环”。一台本地 MySQL、一个 Tomcat、几个 Servlet 加上 JSP就能搭出能演示也能答辨的项目。但这个项目容易拿满分也容易翻车原因集中在数据库设计对不对、分页和购物车会话状态处理得干不干净、以及异常时页面有没有给出反馈。这篇笔记把所有关键环节拆开讲从环境准备到答辨加分点照着做期末交付没问题。适合的人群正在选课设题目或已选定商城题目的在校生时间紧想用成熟方案打底的人以及想弄明白“为什么我的项目演示时总在登录和结算之间出问题”的人。2. 开始前先重建技术选型为什么商城项目绕不开 JSP Servlet MySQL2.1 三层结构为什么是这个项目的标准答案购物商城课设最常见的架构是 JSP Servlet JavaBean MySQL中间不用 SSM 或 SpringBoot。这个选型和“偷懒”没关系而是课程考核点决定了必须这么做。课设验收一般会看代码里是否区分了页面层、控制层和数据访问层。JSP 负责渲染Servlet 接收请求并跳转DAO 负责 SQL 操作JavaBean 承载商品、用户、订单这些实体数据。把这三层在 package 结构里分开建光看工程目录就能拿基础分。具体到项目工程结构我会分成bean、dao、servlet、filter、util五个包。util里放 数据库连接工具类filter里写字符编码过滤器和登录拦截器。页面文件按admin、user、common归类放WebContent 根目录只留登录页和主页。这种结构对新手友好一眼能看出数据流向导师问起来也好答。2.2 数据库表到底建几张五张表就能支撑完整业务商城数据库不需要设计得很复杂最小可行方案是五张表用户表、商品类别表、商品表、购物车表、订单表。很多学生翻车是因为把购物车设计成数据库表然后每次加购都写一条记录结算还要清空逻辑绕且容易出脏数据。常见做法是购物车不落库用 Session 保存条目数据库表只管用户、商品和订单。订单表是较容易扣分的点需要体现订单状态和总金额。状态字段建议用 TINYINT 而不是 VARCHAR0 表示待付款1 表示已付款待发货2 表示已发货3 表示已完成。用数字存状态配合order_status字段注释这是很容易加分的点。订单项和订单之间用order_id外键关联下单时事务控制好订单头和订单明细要么同时成功要么全部回滚。2.3 数据库连接用 JDBC 还是连接池课设场景下的实际选择很多模板项目直接在 DAO 里写DriverManager.getConnection每次连接数据库都新建物理连接。演示时没问题但导师问“高并发下这个项目会怎样”就露馅了。课设里体现连接池意识是加分项但用 DBCP 或 C3P0 会增加配置复杂度。常见做法是在util包里自己维护一个简易连接池或者用 DBCP 的 BasicDataSource 封装只在工具类初识化时加载一次配置。package util; import org.apache.commons.dbcp2.BasicDataSource; import java.sql.Connection; import java.sql.SQLException; public class DBUtil { private static BasicDataSource dataSource; static { dataSource new BasicDataSource(); dataSource.setDriverClassName(com.mysql.cj.jdbc.Driver); dataSource.setUrl(jdbc:mysql://localhost:3306/shopdb?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8); dataSource.setUsername(root); dataSource.setPassword(你的密码); dataSource.setInitialSize(5); dataSource.setMaxTotal(20); dataSource.setMaxWaitMillis(3000); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(AutoCloseable... resources) { for (AutoCloseable r : resources) { if (r ! null) { try { r.close(); } catch (Exception ignored) { } } } } }这段代码每次从连接池拿连接而不是新建连接数据库压力小很多。serverTimezoneAsia/Shanghai这个参数必须加否则高版本 MySQL 驱动会报时区错误。useSSLfalse是本地开发标准配置去掉反而容易警告刷屏。全部 SQL 操作走完后用DBUtil.close(resultSet, statement, connection)释放连接会归还给池而不是真正关闭。有一点要记住把数据库密码硬编码在 DBUtil 里能通过课设验收但不代表是好习惯。如果项目要放 GitHub 或交源码包建议把配置抽到jdbc.properties用Properties类加载这样代码审查时更高分。3. 从下载到运行IDEA 里跑通购物商城的完整配置路径3.1 导入项目的两种方式与选择标准拿到源码包并解压后本质上是两种导入姿势。第一种是直接以 Web 项目方式导入适合 Servlet 版本的旧结构工程第二种是新建空项目再手动配置模块适合 Maven 改造过的版本。区别在于.iml文件是否完整。考虑到课设源码质量参差不齐我一般建议在 IDEA 里用File - New - Project from Existing Sources然后选 Eclipse 或普通目录导入而不是直接打开整个解压文件夹。导入后第一步永远不是写代码而是先解决两个基础设施问题Maven 或 Tomcat 的版本是否匹配以及 JDK 编译级别是否指向一致。我用的是 JDK 1.8 加 Tomcat 8.5 的组合这个组合同样能兼容 Tomcat 9。用了 JDK 11 以上容易碰上javax.servlet依赖不在 JDK 里的报错增加了新手排查成本课设阶段不要主动给自己加戏。3.2 先准备数据库和测试数据数据库部分要在 IDE 里导入 sql 脚本之前先建好库。打开 MySQL 客户端或 Navicat执行CREATE DATABASE shopdb DEFAULT CHARACTER SET utf8mb4;再运行源码包里的shopdb.sql。这里我必须强调SQL 脚本不要整体全选直接跑容易在中途报错后留下半成品表。规范做法是逐段运行先建库再切库再建表最后灌数据。很多项目脚本开头没有USE shopdb;导入到默认库会把表建在别的库下面。运行后用SHOW TABLES;确认下表的数量是否和文档一致常见是 6 到 7 张表。如果表名带前缀如t_user后面代码里改起来非常麻烦要花半小时对齐命名。mysql -u root -p shopdb.sql命令行导入有一个前提脚本内已经包含建库语句或用USE shopdb切换过库。如果没有需要手动进入客户端创建库再导入表数据。验证是否成功去执行SELECT * FROM goods LIMIT 5;有结果返回说明表和数据都没有问题。3.3 IDEA 里配置 Artifact 与 Tomcat 的精确顺序项目能否成功启动90% 取决于这一步做没做对。不要先点 Add Tomcat顺序应该是打开 Project Structure进 Modules 确认 Language Level 是 8再进 Facets 检查 Web 选的是不是src/main/webapp或WebContent最后配置 Artifact——类型选 Web Application: ExplodedOutput Directory 让 IDEA 填默认再把右侧 Available Elements 里的项目模块拖到左边。配置完成后进入 Run/Debug Configurations新建 Tomcat Server - Local。注意两个重点Application server 选了 Tomcat 路径后要跳进 Deployment 标签页手动加刚才建好的 artifact并在 Application context 填写/shop。这个 context 路径就是之后所有 URL 的前缀http://localhost:8080/shop/index.jsp。端口如果不是 8080 被占可以直接改成 8090。改完以后在页面访问路径和代码里的request.getContextPath()形成对应关系不要代码里硬编码/shop/而 Tomcat 配置里又写/这会让你打开主页后所有静态资源 JS、CSS 全部 404。3.4 启动失败时的三个观察点第一次启动报错不要慌按顺序查三个位置。看 IDEA 底部 Run 控制台的日志是不是Exception starting filter如果是说明 Filter 初始化时用到了数据库连接和类加载数据库没启动就会这样。再看localhost.log里有没有Unable to compile class for JSP这是 JDK 版本高过 Tomcat 支持的编译版本导致。最后检查 Tomcat 的conf/context.xml里是否有全局数据源配置如果源代码里通过 JNDI 获取连接而你跳过了这步配置项目能启动但一查数据库就 500。4. 核心业务的编码闭环注册、登录、商品列表、购物车与订单4.1 注册模块密码不能存明文很多拿满分的作业亮点之一是注册模块避免明文存密码。课设阶段不需要用 BCrypt 那种重量级加密一个MD5加盐就能把安全意识到位。常见做法是注册时取用户名的后两位作为盐拼到密码后面再整体 MD5存入数据库。虽然这在生产项目里不太算足够安全但课设阶段让导师看到“不是没考虑安全问题”就够了。private String encryptPassword(String username, String rawPassword) { String salt username.substring(username.length() - 2); String toEncrypt rawPassword salt; StringBuilder sb new StringBuilder(); try { MessageDigest md MessageDigest.getInstance(MD5); byte[] bytes md.digest(toEncrypt.getBytes(UTF-8)); for (byte b : bytes) { sb.append(String.format(%02x, b)); } } catch (Exception e) { throw new RuntimeException(密码加密失败, e); } return sb.toString(); }这里String.format(%02x, b)是把每个字节转成两位十六进制拼出的 32 位字符串就是存储值。每次登录校验时用同一套规则加密再比较不要直接用 SQL 去查明文密码匹配。注册接口里还要校验两次密码输入是否一致、用户名是否重复这些前置条件在 Servlet 里做就行不用塞给数据库异常处理。4.2 商品分页自己写别用插件商品列表是商城主页的门面。课设如果有分页印象分立刻高一层。不建议引入 PageHelper 这类插件因为逻辑被封装了导师问底层实现容易答不上来。自己用 LIMIT 写分页代码就几十行还能把currentPage、totalPage、pageSize这套概念讲清楚。int pageSize 8; int currentPage 1; String pageParam request.getParameter(page); if (pageParam ! null !pageParam.isEmpty()) { currentPage Integer.parseInt(pageParam); } int offset (currentPage - 1) * pageSize; String countSql SELECT COUNT(*) FROM goods; String pageSql SELECT * FROM goods LIMIT ?, ?; PreparedStatement ps conn.prepareStatement(pageSql); ps.setInt(1, offset); ps.setInt(2, pageSize); ResultSet rs ps.executeQuery();LIMIT ?, ?的两个参数第一个是偏移量第二个是每页条数。第一页offset 0第二页offset 8以此类推。页码溢出控制要在拿到总行数之后做currentPage大于totalPage时强制置为totalPage否则用户直接改 URL 问号后的 page 参数就能把页面打崩。页面上首页、上一页、下一页的链接直接用hrefGoodsListServlet?page2保持整个链路走 Servlet 转发不要做成死链接。4.3 购物车用 Session 存储的根本原因购物车下不下单都还在选择阶段没有持久化必要。如果每次加购都往数据库插入用户关掉浏览器再打开购物车还在反而不符合一个“游客身份也能先逛逛”的商城直觉。放在 Session 里的载体是MapInteger, Integerkey 是商品 idvalue 是数量。加购时检查 map 里有没有该商品有就加一没有就 put 一个初始值 1。这里如果用了HashMap遍历时不要直接改值先拿到当前数量再 put 新值否则并发访问时会出现ConcurrentModificationException演示现场很尴尬。购物车页面点删除时接收参数goodsId从 Session 的 map 里 remove 掉再重定向回购物车页。更新数量功能可以和加减按钮复用同一个 Servlet 接口用type参数区分是 add 还是 sub。结算时遍历 map 的 entrySet 去读数据库商品表临时计算总价而不是把价格冗余在购物车数据结构里——因为商品价格可能被管理员改过下单时要以最新数据库里的价格为准。4.4 订单提交事务里完成三步操作下单是整个项目里唯一必须用事务的功能因为它涉及“库存检查、订单头插入、订单明细插入、清空购物车”多个动作任何一个失败都要回滚。Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 1. 从 Session 获取购物车商品 id 列表 // 2. 循环里 SELECT 库存并判断是否足够 // 3. UPDATE goods SET stock stock - ? WHERE id ? // 4. INSERT INTO orders(user_id, total_price, order_status) VALUES(?, ?, 0) // 5. 循环 INSERT INTO order_item(order_id, goods_id, quantity, price) VALUES(...) conn.commit(); } catch (Exception e) { if (conn ! null) { conn.rollback(); } throw new ServletException(下单失败, e); } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } }核心是把conn.setAutoCommit(false)放在所有操作前最后 commit异常时 rollback。有人在 finally 里又写了 commit这是比较危险的习惯——如果前边已经 rollback 了再 commit 会覆盖回滚干净的连接状态。数据库的隔离级别默认即可课设阶段不用刻意调整。下单成功后别忘了request.getSession().removeAttribute(cart)清空购物车否则用户继续加购时旧数据还留在 Session 里逻辑出现串单。5. 常见避坑这样配置导致的黑匣子 500 和验证失败5.1 登录失败却没有任何报错字符集引发的“假账号错误”现象数据库里有这个账号密码也对但登录时始终提示用户名或密码错误。原因注册时页面表单提交的 UTF-8 中文和数据库表里的 utf8mb4 字符集没对齐导致用户名包含中文时插入的数据出现乱码或者查询时 WHERE 条件里的值编码不一致匹配不上。解决在 Filter 里统一设置请求和响应编码。request.setCharacterEncoding(UTF-8)必须放在读取任何参数之前执行。如果你写了多个 Filter确认这个编码过滤器在过滤链的第一个位置。另外检查数据库连接 URL 里的characterEncodingutf8参数没有就去加上。最后确认shopdb.sql建表语句里DEFAULT CHARSETutf8mb4三处统一后中文数据才不会有玄学问题。5.2 首页能打开但点商品详情就 404路径参数没拼对现象首页商品列表正常显示点击某一商品进入详情页时 URL 变成 404。原因商品详情的请求路径是GoodsDetailServlet?id3但你写的超链接是hrefGoodsDetailServlet?id%goods.getId()%没问题时正常但如果有商品对象在循环里被request.getAttribute拿来的对象里没用 EL 表达式而是脚本片段可能拿到 null 值然后报空指针。解决先在浏览器地址栏手工输入http://localhost:8080/shop/GoodsDetailServlet?id3如果能打开说明页面链接写错。如果手工访问也 500去 Console 里看底层异常是不是 SQL 查询字段名和表对应不上常见是把goods_id写成了id报Unknown column。链接用 EL 而不是脚本片段写a hrefGoodsDetailServlet?id${g.id}EL 会自动处理空值。5.3 商品列表页图片全部裂掉Context Path 没搞对现象JSP 页面里img src/images/a.jpg图片打不开。原因开头的斜杠代表的是服务器根路径而你项目部署后的上下文路径是/shop图片实际地址是/shop/images/a.jpg。直接写/images/找不到资源。解决JSP 顶部引入c:set varctx value${pageContext.request.contextPath}/然后图片地址写成img src${ctx}/images/a.jpg。类似的是 CSS 和 JS 文件的引用不要用相对路径../css/style.css因为不同层级页面跳到同一份静态资源时相对路径会算错。全部改成${ctx}/开头后在任何 Servlet 转发出来的页面都能找到静态资源。5.4 数据库连接报错 Access denied for user密码或权限被忽略现象程序里明明写了正确的用户名和密码但连接时报Access denied for user rootlocalhost。原因MySQL 8 默认使用 caching_sha2_password 认证插件而项目里的 MySQL Connector/J 版本低于 8.0新旧驱动与认证方式不匹配导致密码验证失败。解决检查WEB-INF/lib下的mysql-connector-java版本8.0 以上版本才能配合 MySQL 8 使用。如果代码里用的是旧 JDBC 驱动且不方便升级可以在 MySQL 里把 root 用户的认证插件改回 mysql_native_passwordALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码;。课设场景两种方式都能过但推荐前者——升级驱动因为改认证方式在本机演示时每次重新初始化数据库都要再来一遍。5.5 数据库操作的“死锁”在课设中的真实来源现象点击下单按钮后页面卡住过一会 Tomcat 报死锁相关异常控制台里出现Deadlock found when trying to get lock。原因下单功能里同时开启了多个连接每个连接各自执行了SELECT然后才UPDATE由于没有统一加锁顺序互相等待对方持有的锁。解决下单逻辑要尽可能缩短事务时间不要在事务里执行类似Thread.sleep或调用外部接口等待的操作。检查 DAO 里是否每个方法都新建了连接如果getConnection()在 DAO 内部执行事务就不会生效因为多个 DAO 方法各自拿到独立连接根本不是同一个事务。正确做法是把连接对象从 Servlet 传到 DAO或者让 DAO 方法接收 Connection 参数保证所有语句都在同一个事务连接里按顺序执行。库存扣减时用UPDATE goods SET stock stock - ? WHERE id ? AND stock ?这种带条件的原子更新比“先查再改”更稳。6. 答辨时的加分微调日志、拦截器和一处容易惊艳的小设计项目跑通后别急着交把下面几件事做完答辨时能明显拉开差距。第一件事是在 Filter 里加登录拦截器未登录用户访问OrderServlet、CartServlet时统一跳转登录页用代码证明你考虑了资源保护。第二件事是在全局 Servlet 里包装异常用 try-catch 把底层异常转成友好提示而不是直接把 500 页面抛给用户。还有个小设计订单提交时生成订单号可以用时间戳加随机数别用自增 id 直接展示给用户——看起来是细节导师会注意到。public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) res; HttpSession session request.getSession(false); String uri request.getRequestURI(); if (uri.contains(/login.jsp) || uri.contains(/LoginServlet)) { chain.doFilter(req, res); return; } if (session null || session.getAttribute(user) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, res); }这段拦截器要放在 WebContent/WEB-INF/web.xml 里声明或者用 WebFilter 注解。注意request.getSession(false)的 false 很关键它不会主动创建新 Session否则未登录用户也会先拿到一个空 Session导致判断条件失效。答辨时被问到“登录才能下单是怎么实现的”拿这段代码把思路讲清楚就够了。最后说一下订单号生成的写法。用System.currentTimeMillis()拼上三位随机数在课设场景没问题但同一毫秒内两个用户并发下单有可能撞号。参考答案是把随机数扩大到六位或者用UUID.randomUUID().toString().replace(-, ).substring(0, 16)截取一段。导师问“并发下的命名冲突”时你能主动说出这个思考这一分就稳了。交作业前最后的自检清单是在别人电脑上从零按步骤部署一遍确认 MySQL 初始化脚本能独立运行确认 IDEA 里的 Tomcat 配置没有写死绝对路径。这个习惯我保留了很多年因为曾经吃过亏答辩教室的电脑上 Tomcat 版本不一样JSP 编译不过整组演示卡在启动页。希望帮到你避免同样的情况发生。本文还有配套的精品资源点击获取