
1. 项目概述与核心价值又到了一年一度的课程设计季后台收到不少私信都在问同一个问题“老师Java Web的课设到底怎么做才能拿高分” 尤其是“网上购物系统”这个选题几乎成了计算机相关专业的“国民项目”。说实话这个项目看似简单无非是用户注册、浏览商品、加入购物车、下单支付但真想把它做得扎实、做出亮点让老师眼前一亮里面门道可不少。它绝不仅仅是一个增删改查的练习而是一个检验你能否将Java Web知识体系Servlet/JSP、MVC、数据库、前端融会贯通的绝佳试金石。我带了这么多届学生看过无数个购物系统课设最大的感触是很多同学把精力全花在了前端页面的美观上或者盲目追求使用最时髦的框架却忽略了系统最核心的业务逻辑严谨性、数据一致性和架构的清晰度。一个能稳定处理并发下单、正确计算库存和金额、拥有清晰分层结构的系统远比一个花哨但漏洞百出的界面更有价值。本次设计我们将采用经典的Java Web 技术栈不盲目堆砌新技术而是专注于把每个环节的原理吃透实现一个功能完整、逻辑严密、代码规范的购物系统。无论你是正在为课设发愁的同学还是想夯实Java Web基础的开发者这篇近万字的实操指南都将带你从零开始一步步构建出一个可用于答辩和简历的优质项目。2. 技术选型与整体架构设计在做任何编码之前定好技术栈和架构是事半功倍的关键。网上购物系统是一个典型的Web应用我们需要从前端展示、后端逻辑、数据存储三个层面来考虑。2.1 后端技术栈解析对于课程设计而言稳定、经典、资料丰富是首要原则。因此我强烈推荐以下组合核心语言与平台Java 8/11 Tomcat 9为什么是JavaJava的强类型、面向对象特性非常适合构建复杂的企业级业务逻辑。其庞大的生态和社区保证了你在开发中遇到的几乎所有问题都能找到解决方案。为什么是Tomcat作为轻量级的Servlet容器它足够简单便于本地调试和部署是学习Java Web的首选。Web层Servlet JSP JSTL/EL虽然Spring MVC是主流但对于课程设计从原生的Servlet和JSP入手更能深刻理解HTTP请求/响应、MVC模式的内涵。Servlet作为控制器ControllerJSP作为视图ViewJavaBean作为模型Model构成了最经典的MVC实现。实操心得很多同学觉得JSP过时了但恰恰是这种“过时”的技术能让你明白页面是如何动态生成的避免了对框架黑盒的依赖。使用JSTL标签库和EL表达式可以避免在JSP中写大量Java代码保持页面整洁。数据访问层JDBC 数据库连接池如Druid直接使用JDBC能让你彻底掌握SQL执行、结果集处理、事务控制等底层操作。在此基础上配合Druid这类连接池可以高效管理数据库连接这是生产级应用的必备知识。工具选型数据库选择MySQL 5.7/8.0因为它免费、通用且与Java生态结合极好。图形化工具推荐Navicat或MySQL Workbench。辅助工具与框架项目管理与构建Maven。它帮你管理项目依赖Jar包规范项目结构是现代Java项目的标配。开发工具IntelliJ IDEA 2024。它的智能提示、代码重构和强大的Maven集成能力能极大提升开发效率。社区版完全够用。版本控制Git。务必使用Git来管理你的代码即使是一个人开发。这不仅是好习惯也是答辩时展示项目管理能力的加分项。2.2 系统核心架构设计我们采用典型的三层架构确保职责分离便于维护和扩展。用户浏览器 | v 表示层 (Presentation Layer) - JSP页面 (视图View) - Servlet (控制器Controller) | v 业务逻辑层 (Business Logic Layer) - Service类 (处理核心业务如订单生成、库存扣减) | v 数据访问层 (Data Access Layer) - DAO类 (Data Access Object 封装所有数据库操作) | v 数据库 (MySQL)各层职责详解表示层负责与用户交互。Servlet接收请求调用业务层方法根据结果转发到不同的JSP页面。JSP页面负责展示数据通过EL表达式从请求域或会话域中获取数据。业务逻辑层这是系统的“大脑”。它包含复杂的业务规则例如“下单时检查库存”、“计算订单总价需考虑优惠券、运费”、“更新用户积分”。Service层的方法通常涉及多个DAO操作并且需要在事务管理下进行以保证数据一致性。数据访问层纯粹负责与数据库打交道。每个DAO类对应一个实体如UserDAO, ProductDAO, OrderDAO里面封装了CRUD增删改查方法。它不应该包含任何业务逻辑。注意很多初学者容易犯的错误是把业务逻辑写在Servlet或DAO里导致代码混乱不堪后期难以修改。务必严格遵守分层原则Servlet只做请求转发和简单数据校验Service处理业务DAO只操作数据库。3. 数据库设计与核心表结构数据库设计是系统的基石设计得好后续开发顺风顺水设计得差bug频出寸步难行。网上购物系统的核心实体包括用户、商品、订单、购物车。3.1 核心表结构定义以下是经过精简和优化的核心表结构每个字段的设置都有其考量1. 用户表 (t_user)CREATE TABLE t_user ( id int(11) NOT NULL AUTO_INCREMENT COMMENT 用户ID主键, username varchar(50) NOT NULL UNIQUE COMMENT 用户名唯一用于登录, password varchar(255) NOT NULL COMMENT 密码务必加密存储如MD5加盐, email varchar(100) DEFAULT NULL COMMENT 邮箱, phone varchar(20) DEFAULT NULL COMMENT 手机号, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;设计要点username设唯一索引防止重复注册。password字段长度要足够255为未来升级加密算法留空间。create_time和update_time是审计字段便于追踪用户行为强烈建议保留。2. 商品表 (t_product)CREATE TABLE t_product ( id int(11) NOT NULL AUTO_INCREMENT COMMENT 商品ID, name varchar(200) NOT NULL COMMENT 商品名称, description text COMMENT 商品详情富文本, price decimal(10,2) NOT NULL COMMENT 商品单价精确到分, stock int(11) NOT NULL DEFAULT 0 COMMENT 库存数量, category_id int(11) DEFAULT NULL COMMENT 分类ID外键关联分类表, image_url varchar(500) DEFAULT NULL COMMENT 主图URL, status tinyint(4) DEFAULT 1 COMMENT 状态1-上架0-下架, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品表;设计要点价格使用DECIMAL类型避免浮点数精度问题。stock库存是业务核心字段后续并发操作的关键。status字段用于软删除或上下架管理。3. 订单表 (t_order) 与 订单明细表 (t_order_item)这是重点和难点。通常将订单头信息和订单项信息分表存储遵循数据库设计范式。-- 订单主表 CREATE TABLE t_order ( order_id varchar(32) NOT NULL COMMENT 订单号唯一可使用时间戳随机数生成, user_id int(11) NOT NULL COMMENT 下单用户ID, total_amount decimal(10,2) NOT NULL COMMENT 订单总金额, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 状态0-待付款1-已付款/待发货2-已发货3-已完成4-已取消, payment_time datetime DEFAULT NULL COMMENT 支付时间, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 下单时间, PRIMARY KEY (order_id), KEY idx_user (user_id), KEY idx_create_time (create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表;-- 订单明细表 CREATE TABLE t_order_item ( id int(11) NOT NULL AUTO_INCREMENT, order_id varchar(32) NOT NULL COMMENT 关联的订单号, product_id int(11) NOT NULL COMMENT 商品ID, product_name varchar(200) NOT NULL COMMENT 商品名称快照下单时的名称, product_price decimal(10,2) NOT NULL COMMENT 商品单价快照下单时的价格, quantity int(11) NOT NULL COMMENT 购买数量, total_price decimal(10,2) NOT NULL COMMENT 该项总价 product_price * quantity, PRIMARY KEY (id), KEY idx_order (order_id), CONSTRAINT fk_item_order FOREIGN KEY (order_id) REFERENCES t_order (order_id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单明细表;设计要点订单号不使用自增ID而用自定义字符串如202411220001避免暴露业务量也便于线下沟通。数据快照t_order_item中存储了商品下单时的名称和价格 (product_name,product_price)。这是关键技巧因为商品信息未来可能会修改但订单历史必须保持原样所以不能直接关联t_product表的当前数据。外键约束t_order_item通过order_id关联t_order并设置级联删除保证数据完整性。4. 购物车表 (t_cart)CREATE TABLE t_cart ( id int(11) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL COMMENT 用户ID, product_id int(11) NOT NULL COMMENT 商品ID, quantity int(11) NOT NULL DEFAULT 1 COMMENT 商品数量, selected tinyint(1) DEFAULT 1 COMMENT 是否选中用于结算, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_product (user_id, product_id), -- 同一用户的同一商品只存一条记录 KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT购物车表;设计要点设置唯一索引uk_user_product确保同一个用户的同一个商品在购物车里只有一条记录数量变化时更新quantity字段即可而不是新增记录。3.2 数据库操作中的核心问题事务与并发这是课程设计能否深入的关键考察点。以“下单”这个核心业务为例它至少包含以下步骤查询购物车选中商品及库存。检查库存是否充足。扣减商品库存。生成订单主记录。生成订单明细记录。清空购物车中已下单的商品。这六步必须在一个数据库事务中完成要么全部成功要么全部回滚。否则可能出现“库存扣了但订单没生成”的严重数据不一致。在ServletJDBC中实现事务Connection conn null; try { conn dataSource.getConnection(); // 1. 关闭自动提交开启事务 conn.setAutoCommit(false); // 2. 执行一系列数据库操作调用各个DAO的方法需传入同一个conn cartService.checkout(conn, userId); orderService.createOrder(conn, orderInfo); cartService.clearCheckedItems(conn, userId); // 3. 提交事务 conn.commit(); } catch (SQLException e) { // 4. 发生异常回滚事务 if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } throw new RuntimeException(下单失败事务回滚, e); } finally { // 5. 恢复自动提交关闭连接实际中连接会归还连接池 if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }踩坑实录DAO层的方法需要接收一个Connection参数而不是自己创建。这样才能保证多个DAO操作在同一个数据库连接和事务内。这是很多同学初次设计时容易忽略的架构细节。4. 核心功能模块实现详解有了清晰的架构和数据库设计我们就可以开始编码了。这里重点剖析几个最具代表性的功能模块。4.1 用户登录与会话管理用户登录后我们需要一种机制来记住用户的登录状态这就是会话Session。1. 登录逻辑实现 (LoginServlet)WebServlet(/login) public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username request.getParameter(username); String password request.getParameter(password); // 1. 参数校验略 // 2. 密码加密例如MD5(password salt) String encryptedPwd MD5Util.encrypt(password); // 3. 调用Service查询用户 UserService userService new UserServiceImpl(); User user userService.login(username, encryptedPwd); if (user ! null) { // 4. 登录成功将用户信息存入Session HttpSession session request.getSession(); session.setAttribute(currentUser, user); // 5. 重定向到首页或目标页面 response.sendRedirect(request.getContextPath() /index.jsp); } else { // 6. 登录失败返回错误信息 request.setAttribute(errorMsg, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); } } }2. 登录状态拦截器过滤器不是所有页面都能直接访问例如“我的订单”页面需要用户先登录。我们可以使用Servlet Filter实现一个简单的权限拦截器。WebFilter(/*) // 拦截所有请求 public class LoginFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String uri request.getRequestURI(); // 排除不需要拦截的路径登录、注册、静态资源等 if (uri.contains(/login) || uri.contains(/register) || uri.contains(.css) || uri.contains(.js) || uri.contains(.jpg)) { chain.doFilter(req, resp); return; } // 检查Session中是否有用户信息 HttpSession session request.getSession(false); // false表示如果不存在则不创建新session if (session null || session.getAttribute(currentUser) null) { // 未登录重定向到登录页 response.sendRedirect(request.getContextPath() /login.jsp); return; } // 已登录放行 chain.doFilter(req, resp); } }实操心得在web.xml中配置Filter的拦截路径时顺序很重要。通常字符编码过滤器放在最前登录过滤器次之。使用WebFilter注解更便捷。务必记得放行静态资源css, js, images否则页面样式会失效。4.2 商品展示与分页查询商品列表页通常数据量很大必须支持分页。1. 分页工具类设计我们需要一个类来封装分页的所有信息。public class PageInfoT { private int pageNum; // 当前页码 private int pageSize; // 每页条数 private long total; // 总记录数 private int pages; // 总页数 private ListT list; // 当前页的数据列表 // 构造方法、getter/setter 略 // 计算总页数的方法 public int getPages() { if (pageSize 0) return 0; this.pages (int) (total / pageSize); if (total % pageSize ! 0) { this.pages; } return this.pages; } }2. 带分页的商品查询DAO方法public class ProductDaoImpl implements ProductDao { public PageInfoProduct findProductByPage(int pageNum, int pageSize, String keyword) throws SQLException { PageInfoProduct pageInfo new PageInfo(); pageInfo.setPageNum(pageNum); pageInfo.setPageSize(pageSize); Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DbUtil.getConnection(); // 1. 查询总记录数 String countSql SELECT COUNT(*) FROM t_product WHERE name LIKE ? AND status1; ps conn.prepareStatement(countSql); ps.setString(1, % keyword %); rs ps.executeQuery(); if (rs.next()) { pageInfo.setTotal(rs.getLong(1)); } rs.close(); ps.close(); // 2. 查询当前页数据 String dataSql SELECT * FROM t_product WHERE name LIKE ? AND status1 LIMIT ?, ?; ps conn.prepareStatement(dataSql); ps.setString(1, % keyword %); ps.setInt(2, (pageNum - 1) * pageSize); // 计算起始偏移量 ps.setInt(3, pageSize); rs ps.executeQuery(); ListProduct list new ArrayList(); while (rs.next()) { Product p new Product(); // 从ResultSet中填充Product对象属性... list.add(p); } pageInfo.setList(list); } finally { DbUtil.close(rs, ps, conn); } return pageInfo; } }3. 在JSP页面中展示分页导航在JSP页面底部根据PageInfo对象生成分页链接。div classpagination c:if test${pageInfo.pageNum 1} a hrefproductList?pageNum${pageInfo.pageNum-1}keyword${param.keyword}上一页/a /c:if c:forEach begin1 end${pageInfo.pages} vari c:choose c:when test${i pageInfo.pageNum} span classcurrent${i}/span /c:when c:otherwise a hrefproductList?pageNum${i}keyword${param.keyword}${i}/a /c:otherwise /c:choose /c:forEach c:if test${pageInfo.pageNum pageInfo.pages} a hrefproductList?pageNum${pageInfo.pageNum1}keyword${param.keyword}下一页/a /c:if /div注意事项分页查询一定要先查总数再查数据。LIMIT的参数计算是(pageNum-1) * pageSize。在高并发场景下大数据量的COUNT(*)可能很慢需要考虑优化但对于课程设计级别的数据量此方法完全可行。4.3 购物车与订单生成的完整流程这是系统最复杂的业务链涉及前端交互、后端业务逻辑和数据库事务。1. 购物车添加商品 (AddToCartServlet)protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpSession session request.getSession(); User user (User) session.getAttribute(currentUser); if (user null) { response.getWriter().write(请先登录); return; } int productId Integer.parseInt(request.getParameter(productId)); int quantity Integer.parseInt(request.getParameter(quantity)); CartService cartService new CartServiceImpl(); boolean success cartService.addItem(user.getId(), productId, quantity); response.getWriter().write(success ? 添加成功 : 添加失败); }Service层逻辑检查商品是否存在且状态为上架然后检查用户购物车是否已有该商品。若有则更新数量若无则插入新记录。这里利用了数据库表的唯一索引uk_user_product来保证幂等性。2. 下单结算 (CheckoutServlet)这是核心中的核心我们结合时序图来理解文字描述请求入口用户点击“结算”提交选中的购物车商品ID列表到CheckoutServlet。参数校验Servlet接收参数验证用户登录状态。调用业务层Servlet调用OrderService.createOrder(userId, cartItemIds)。开启事务在OrderService方法内部首先获取数据库连接并setAutoCommit(false)。库存预检查遍历商品ID查询当前库存并与购买数量对比。任何一件商品库存不足立即抛出异常事务回滚返回“库存不足”提示。扣减库存预检查通过后执行UPDATE语句扣减库存。这里存在并发问题两个用户同时购买最后一件商品。解决方案是使用乐观锁或悲观锁。悲观锁实现在查询时加锁SELECT ... FROM t_product WHERE id? FOR UPDATE。这会锁定该行数据直到事务结束。简单有效但并发度低。乐观锁实现使用版本号或条件更新UPDATE t_product SET stockstock-?, versionversion1 WHERE id? AND stock?。通过stock?这个条件在更新时再次检查库存。如果受影响行数为0说明库存不足或已被修改则回滚事务。这是更推荐的方式。生成订单生成唯一的订单号插入t_order表。生成订单项循环插入t_order_item表同时保存商品快照信息。清理购物车删除已下单的购物车项。提交事务所有步骤成功提交事务。返回结果Service返回生成的订单号Servlet将订单号传递给“订单成功”页面。3. 前端交互与防重复提交用户点击“提交订单”按钮后前端需要防止因网络延迟或用户误操作导致的重复提交。JavaScript禁用按钮点击后立即将按钮置为disabled状态并显示“处理中...”。Token机制更可靠在进入结算页时后端生成一个唯一的Token存入Session并传到页面隐藏域。提交订单时将这个Token一并提交。后端校验Token如果有效则处理订单并清除Session中的Token。这样同一个Token只能使用一次即使重复请求也会被后端拦截。5. 项目部署、测试与常见问题排查开发完成后如何让老师或同学访问你的系统进行演示如何确保系统稳定运行5.1 本地部署与远程访问打包项目使用Maven的package命令生成一个项目名.war文件。部署到Tomcat将.war文件复制到Tomcat的webapps目录下启动Tomcat运行bin/startup.bat或startup.shTomcat会自动解压并部署。本地访问浏览器打开http://localhost:8080/你的项目名。让局域网内其他电脑访问确保你的电脑和老师/同学的电脑在同一个Wi-Fi或局域网下。查询你电脑的局域网IPWindows用ipconfigMac/Linux用ifconfig然后将访问地址改为http://[你的局域网IP]:8080/你的项目名。注意可能需要关闭电脑的防火墙或为Tomcat的8080端口添加入站规则。数据库连接配置确保部署环境的数据库IP、端口、用户名、密码与你的代码中配置的一致。通常这些配置会写在db.properties文件中。5.2 核心功能测试用例不要只测“ happy path ”一切顺利的情况要重点测试边界和异常情况。测试功能测试场景预期结果检查点用户注册1. 输入已存在的用户名注册失败提示“用户名已存在”唯一约束生效2. 密码强度不足如纯数字前端JS校验或后端返回错误数据安全性用户登录1. 用户名密码正确跳转首页Session中有用户信息登录状态保持2. 用户名密码错误停留在登录页显示错误提示友好提示商品加入购物车1. 未登录时点击加入提示“请先登录”或跳转登录页权限控制2. 加入已下架商品操作失败提示“商品已下架”业务逻辑严谨性下单结算1. 库存充足生成订单库存减少购物车清空事务一致性2.高并发两个用户同时买最后一件商品仅一单成功另一单提示库存不足并发控制重点3. 购物车为空时结算提示“购物车为空”边界条件处理订单查询1. 查询自己的订单只看到自己的订单数据隔离SQL中需带user_id条件2. 尝试修改他人订单号访问应无权限或查不到数据安全性5.3 常见问题与排查技巧实录在开发过程中你几乎一定会遇到下面这些问题问题1页面显示乱码中文变成问号原因Java Web中常见的编码问题。涉及环节浏览器、Servlet、JSP、数据库。解决方案全套统一所有文件编码为UTF-8IDEA设置、JSP文件、Properties文件。配置字符编码过滤器在web.xml中配置一个Filter对所有请求和响应设置request.setCharacterEncoding(UTF-8)和response.setCharacterEncoding(UTF-8)。JSP页面头部确保有% page contentTypetext/html;charsetUTF-8 languagejava %。数据库连接字符串加上参数?useUnicodetruecharacterEncodingUTF-8。数据库和表本身创建时指定CHARSETutf8mb4。问题2点击提交按钮后页面没反应F12看到404或500错误排查步骤看浏览器控制台F12 - Console/Network确认请求是否发出URL是否正确。Servlet的WebServlet注解路径是否匹配。看Tomcat控制台日志catalina.out或IDEA运行窗口这里有最详细的错误堆栈信息。常见的NullPointerException、ClassNotFoundException、SQLException都会在这里打印。检查Servlet的doGet/doPost方法是否写对了参数名是否和前端name属性一致检查JSP页面转发路径request.getRequestDispatcher(xxx.jsp).forward(...)中的路径是相对路径以/开头则是相对于项目根目录。问题3更新了代码但刷新页面没变化原因浏览器缓存、JSP编译缓存、Tomcat热部署未生效。解决浏览器端按CtrlF5强制刷新。重启Tomcat。在IDEA中确保部署方式是“Update classes and resources”热部署。对于JSPTomcat默认会检查更新如果不行尝试清理Tomcat的work目录这是JSP编译后的缓存。问题4数据库连接失败错误信息Communications link failure或Access denied for user。检查清单MySQL服务启动了吗sudo systemctl status mysql或 查看服务列表连接字符串中的IP、端口、数据库名对吗用户名和密码对吗MySQL 8.0的密码插件可能导致旧驱动连接失败尝试在连接字符串后加useSSLfalseserverTimezoneAsia/Shanghai。数据库驱动Jar包版本与MySQL版本匹配吗推荐使用mysql-connector-java:8.0.x。问题5关于使用框架如Spring Boot的建议很多同学问能不能用Spring Boot、MyBatis-Plus、Vue来做当然可以这会让开发更高效。但对于课程设计尤其是初次接触Java Web的同学我仍然建议先用原生技术实现一遍。这就像学开车先用手动挡理解了离合、油门、档位的配合再开自动挡会游刃有余。用原生技术踩过这些坑你才能真正理解Spring等框架在背后为你做了什么它们的价值在哪里。完成基础版后你可以将其作为“二期工程”用Spring Boot重构这会是简历上一个非常棒的亮点。最后把代码写规范多写注释特别是复杂业务逻辑处。设计一个简洁清晰的目录结构写一份完整的README.md说明如何配置和运行。答辩时不仅要演示功能更要能讲清楚你的架构设计、数据库设计思路以及如何处理了像“并发下单”这样的核心难题。这个项目做下来你对Java Web开发的理解一定会远超同龄人。