JavaWeb课程设计实战:手写摩托车商城,掌握Servlet+JSP+JDBC核心链路

发布时间:2026/10/11 15:05:51
JavaWeb课程设计实战:手写摩托车商城,掌握Servlet+JSP+JDBC核心链路 简介这是一份面向JavaWeb初学者与课程设计学习者的摩托车商城实战项目源码适合已掌握Servlet、JSP基础、希望通过完整项目串联知识点的人群。项目涵盖后台管理、评论、购物车、登录注册、商品与订单管理、支付及购买记录等模块采用MVC模式与分页逻辑实现是理解SSM、SpringBoot底层原理的良好过渡素材。压缩包共559个文件约32.29MB包含61个java源文件、56个jsp页面、126张jpg与62张webp图片资源、63个jar依赖、12个css与14个js脚本以及数据库sql文件、xml配置等结构完整导入后修改数据库连接即可运行。目前已有895人学习下载。读者可从中获得一套可直接运行的商城系统参考实现学习Servlet与DAO分层、分页封装、订单流程等核心代码逻辑为后续框架学习打下扎实基础。1. 从零到一为什么我建议你亲手写一个摩托车商城而不是抄现成代码如果你正在为课程设计发愁想找一个既能体现 JavaWeb 完整技术栈、又不会在答辩时被导师问穿的题目那“摩托车商城”这个方向值得认真考虑。它比图书管理、学生成绩系统多了真实的业务复杂度——商品有品牌、排量、车型分类下单涉及库存扣减支付要考虑并发后台还要管订单状态流转。这些恰好是 JavaWeb 课程里 Servlet、JSP、JDBC、Session、Filter 的核心练兵场。我带过几届学生的课程设计发现一个规律凡是直接下载现成项目改个名字交上去的答辩时被问到“库存超卖怎么处理”“Session 和 Cookie 在这里怎么配合”基本就卡住了。而自己从零搭一遍的人哪怕功能少两个也能把请求响应链路讲清楚。这篇笔记就按“能跑起来、能讲清楚、能扛住追问”的标准把摩托车商城的落地路径拆开讲。适合已经学过 Servlet 和 JDBC、但还没完整做过一个 Web 项目的人。2. 技术选型与数据库设计别急着写 Servlet先把表结构定死2.1 为什么用 JSP Servlet JDBC 而不是直接上 Spring Boot课程设计场景下我一般建议用原生 JSP Servlet JDBC。原因很实际导师要看的是你对 Web 底层流程的理解Spring Boot 把很多东西封装了答辩时反而不好展示。而且原生方案依赖少Tomcat 一配就能跑不会出现 Maven 拉包失败导致演示翻车的情况。技术栈定下来是这样的层次技术选择说明前端展示JSP JSTL 原生 CSS不用前端框架降低联调成本控制层Servlet 3.1手写 doGet/doPost体现请求处理业务层普通 Java 类Service手动组织事务数据层JDBC Druid 连接池比裸 DriverManager 更接近生产数据库MySQL 8.0课程设计通用选择会话管理HttpSession Filter登录态和权限拦截连接池用 Druid 是常见做法配置一个 druid.properties 放在 src 根目录即可。如果学校机房环境受限退回 DriverManager 也能跑只是每次请求都建连接会慢演示时数据量小感知不明显。2.2 摩托车商城的表结构六张表撑起核心链路数据库设计是后面所有代码的地基。摩托车这个品类有几个特殊字段必须提前想好排量cc、车型街车/仿赛/巡航/拉力、品牌。这些直接影响分类查询和筛选功能。-- 用户表区分普通用户和管理员 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, -- 存 MD5 加盐后的值 phone VARCHAR(20), role TINYINT DEFAULT 0, -- 0 普通用户1 管理员 create_time DATETIME DEFAULT NOW() ); -- 商品表摩托车核心字段 CREATE TABLE t_product ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, brand VARCHAR(50), category VARCHAR(20), -- 街车/仿赛/巡航/拉力 displacement INT, -- 排量单位 cc price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, image_url VARCHAR(255), status TINYINT DEFAULT 1 -- 1 上架0 下架 ); -- 订单表订单主表 CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, -- 业务订单号 user_id INT NOT NULL, total_amount DECIMAL(10,2), status TINYINT DEFAULT 0, -- 0 待付款 1 已付款 2 已发货 3 已完成 create_time DATETIME DEFAULT NOW() ); -- 订单明细表一个订单对应多件商品 CREATE TABLE t_order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, product_id INT NOT NULL, product_name VARCHAR(100), -- 冗余存名称防止商品被删后订单看不清 price DECIMAL(10,2), quantity INT ); -- 购物车表 CREATE TABLE t_cart ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, product_id INT NOT NULL, quantity INT DEFAULT 1, UNIQUE KEY uk_user_product (user_id, product_id) );这里有两个设计决策值得在答辩时主动讲一是订单明细表冗余存了 product_name 和 price因为商品价格会变、商品可能下架订单必须保留成交时的快照二是购物车表加了 user_id product_id 的唯一索引这样同一用户重复加购同一商品时用ON DUPLICATE KEY UPDATE quantity quantity 1就能实现数量累加不用先查再判断。2.3 项目包结构按职责分层别全堆在 src 根目录包结构乱是课程设计被扣分的重灾区。我一般按这个结构组织com.moto.shop ├── dao // 数据访问每个实体一个 DAO 类 ├── service // 业务逻辑处理事务和校验 ├── servlet // 控制层接收请求转发 ├── entity // 实体类和表一一对应 ├── util // 工具类DBUtil、MD5Util、OrderNoUtil └── filter // 编码过滤器、登录拦截过滤器分层的好处是答辩时导师问“你的业务逻辑写在哪”你能明确指向 service 包。如果全写在 Servlet 里代码超过 200 行就很难维护改一个库存扣减逻辑要翻半天。3. 核心功能实现从商品列表到下单的完整链路3.1 商品列表与分类筛选一个 Servlet 搞定多条件查询商品列表是用户进来看的第一个页面要支持按车型分类、按品牌筛选、按价格排序。核心思路是动态拼 SQL但必须用 PreparedStatement 防注入。// ProductServlet.java 中的列表查询方法 protected void list(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String category req.getParameter(category); String brand req.getParameter(brand); String sort req.getParameter(sort); StringBuilder sql new StringBuilder( SELECT * FROM t_product WHERE status 1); ListObject params new ArrayList(); // 动态拼接条件值用占位符 if (category ! null !category.isEmpty()) { sql.append( AND category ?); params.add(category); } if (brand ! null !brand.isEmpty()) { sql.append( AND brand ?); params.add(brand); } // 排序字段必须白名单校验不能直接拼用户输入 if (price_asc.equals(sort)) { sql.append( ORDER BY price ASC); } else if (price_desc.equals(sort)) { sql.append( ORDER BY price DESC); } else { sql.append( ORDER BY id DESC); } ListProduct products productDao.query(sql.toString(), params.toArray()); req.setAttribute(products, products); req.getRequestDispatcher(/product_list.jsp).forward(req, resp); }这段代码的关键点在于条件值全部走?占位符只有排序方向是拼接的而且用白名单限定死了三个可选值。如果直接把sort参数拼进 SQL就是典型的注入漏洞答辩时被问到会很难看。参数说明上category 对应车型brand 对应品牌两个都可以为空表示不筛选。3.2 购物车与库存扣减下单时最容易出并发问题的地方购物车本身逻辑简单难的是下单时扣库存。课程设计演示通常只有你一个人操作但导师很可能问“如果两个人同时买最后一台车会怎样”。这里必须用数据库行锁或者乐观锁。// OrderService.java 下单核心逻辑 public String createOrder(int userId, ListCartItem items) throws Exception { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 BigDecimal total BigDecimal.ZERO; for (CartItem item : items) { // 关键带条件更新库存stock quantity 才扣 String sql UPDATE t_product SET stock stock - ? WHERE id ? AND stock ?; PreparedStatement ps conn.prepareStatement(sql); ps.setInt(1, item.getQuantity()); ps.setInt(2, item.getProductId()); ps.setInt(3, item.getQuantity()); int rows ps.executeUpdate(); if (rows 0) { // 影响行数为 0 说明库存不足回滚 conn.rollback(); throw new RuntimeException(商品库存不足 item.getProductName()); } total total.add(item.getPrice() .multiply(new BigDecimal(item.getQuantity()))); } // 插入订单主表和明细表 String orderNo OrderNoUtil.generate(userId); insertOrder(conn, orderNo, userId, total); insertOrderItems(conn, orderNo, items); conn.commit(); return orderNo; } catch (Exception e) { if (conn ! null) conn.rollback(); throw e; } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } }逻辑说明整个下单过程在一个事务里完成。扣库存用的是UPDATE ... WHERE stock ?这种带条件的更新靠数据库的行锁保证原子性——如果两个线程同时执行数据库会串行化第二个线程发现 stock 已经不够影响行数为 0直接回滚。这比“先 SELECT 查库存再 UPDATE”可靠得多后者在并发下必然超卖。参数上quantity 是购买数量productId 是商品 ID。注意事务里所有操作必须用同一个 Connection不能每个 DAO 方法各自去连接池拿连接否则事务不生效。这是新手最容易踩的坑。3.3 登录拦截与权限控制一个 Filter 挡住未登录请求后台管理页面不能让普通用户直接输 URL 就进去。用 Filter 统一拦截比在每个 Servlet 里写判断优雅得多。// AuthFilter.java WebFilter(urlPatterns {/admin/*, /order/*}) public class AuthFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; HttpSession session request.getSession(false); User user (session null) ? null : (User) session.getAttribute(loginUser); if (user null) { // 未登录跳转登录页 response.sendRedirect(request.getContextPath() /login.jsp); return; } // 后台路径额外校验管理员角色 String uri request.getRequestURI(); if (uri.contains(/admin/) user.getRole() ! 1) { response.sendError(403, 无权限访问); return; } chain.doFilter(req, resp); // 放行 } }这里getSession(false)表示不自动创建新 Session避免为未登录用户生成无用会话。拦截路径用/admin/*和/order/*通配登录页和商品列表页不拦。角色判断放在登录判断之后普通用户访问后台返回 403 而不是跳登录页因为已经登录了只是权限不够。4. 避坑与排查课程设计里最容易翻车的五个点4.1 中文乱码POST 和 GET 要分开处理现象表单提交的中文商品名在数据库里变成问号或者页面显示乱码。原因Tomcat 8 以后 GET 请求默认 URI 编码是 UTF-8但 POST 请求体默认还是 ISO-8859-1。很多人只设了request.setCharacterEncoding(UTF-8)这只对 POST 生效。解决统一用一个 EncodingFilter 处理并且只在 POST 时设置请求编码响应编码统一设 UTF-8。WebFilter(/*) public class EncodingFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; // POST 请求体编码 if (POST.equalsIgnoreCase(request.getMethod())) { request.setCharacterEncoding(UTF-8); } response.setContentType(text/html;charsetUTF-8); chain.doFilter(req, resp); } }另外数据库连接 URL 要加useUnicodetruecharacterEncodingutf8建库时用utf8mb4字符集。三处都对了才不会乱码。4.2 数据库连接泄漏连接池耗尽后整个应用卡死现象演示到一半页面一直转圈最后报Wait millis 60000, active 20之类的连接池超时。原因DAO 方法里拿了 Connection 但异常路径没关闭或者 ResultSet、PreparedStatement 没关。连接池最大连接数被占满后新请求拿不到连接就阻塞。解决所有 JDBC 资源在 finally 里按 ResultSet → PreparedStatement → Connection 的顺序关闭。更稳妥的做法是用 try-with-resourcespublic Product findById(int id) { String sql SELECT * FROM t_product WHERE id ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, id); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { return mapRow(rs); } } } catch (SQLException e) { e.printStackTrace(); } return null; }try-with-resources 会自动关闭资源即使抛异常也不漏。注意 Connection 关闭时是归还给连接池而不是真正断开所以放心关。4.3 订单号重复用时间戳加随机数不够可靠现象偶尔插入订单时报Duplicate entry for key order_no。原因用System.currentTimeMillis()生成订单号高并发下同一毫秒会生成相同值。解决订单号用“时间戳 用户ID后四位 随机数”组合或者直接用 UUID 去掉横线。更简单的是用数据库自增 ID 加日期前缀但要在插入后回查。我一般用这个工具方法public class OrderNoUtil { private static final Random RANDOM new Random(); public static String generate(int userId) { String time new SimpleDateFormat(yyyyMMddHHmmss) .format(new Date()); int rand RANDOM.nextInt(9000) 1000; // 4 位随机 return time userId % 10000 rand; } }这样重复概率极低而且订单号里带了时间和用户信息排查问题时一眼能看出大概。4.4 JSP 页面取不到 Session 数据作用域搞混了现象登录后在 JSP 里用${loginUser.username}显示不出来但 Servlet 里明明 setAttribute 了。原因把数据存到了 request 作用域但登录后是重定向到首页request 作用域在重定向后就失效了。登录态必须存 Session。解决登录成功用session.setAttribute(loginUser, user)然后response.sendRedirect()。如果用的是request.getRequestDispatcher().forward()request 作用域才有效。记住重定向是两次请求转发是一次请求。登录、购物车这类跨页面数据一律放 Session。4.5 后台删除商品导致订单查不到外键约束和冗余字段的取舍现象管理员删了一个下架商品结果历史订单里那件商品显示为空。原因订单明细表用 product_id 关联商品表商品被物理删除后 JOIN 查不到数据。解决两个方案。一是商品不做物理删除只改 status 为下架订单查询时用 LEFT JOIN 且不限制商品状态。二是在订单明细里冗余存 product_name 和 price查询订单时直接读冗余字段不依赖商品表。我一般两个都做商品逻辑删除订单明细冗余快照。这样即使商品真被删了订单信息依然完整。5. 进阶技巧让课程设计在答辩时多拿十分的两个细节5.1 用分页插件手写一个通用分页别每页都写 LIMIT商品列表和订单列表都需要分页。如果每个查询都手写LIMIT ?, ?代码重复且容易算错偏移量。我一般抽一个 PageResult 泛型类配合一个分页查询方法。public class PageResultT { private ListT list; // 当前页数据 private int pageNum; // 当前页码从 1 开始 private int pageSize; // 每页条数 private int total; // 总记录数 private int pages; // 总页数 public PageResult(ListT list, int pageNum, int pageSize, int total) { this.list list; this.pageNum pageNum; this.pageSize pageSize; this.total total; this.pages (total pageSize - 1) / pageSize; // 向上取整 } // getter 省略 }DAO 里分页查询分两步先SELECT COUNT(*)拿总数再用LIMIT (pageNum-1)*pageSize, pageSize拿当前页。JSP 里根据 pages 和 pageNum 渲染上一页/下一页链接。这个类在答辩时是个加分项因为它体现了你对通用性的思考而不是每个功能各写各的。5.2 用 Filter 记录请求耗时排查慢查询课程设计数据量小一般不会慢。但导师可能问“你怎么知道哪个页面慢”。加一个简单的耗时 Filter在控制台打印每个请求的处理时间既显得专业也方便自己调试。WebFilter(/*) public class TimeCostFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { long start System.currentTimeMillis(); chain.doFilter(req, resp); long cost System.currentTimeMillis() - start; HttpServletRequest request (HttpServletRequest) req; // 超过 500ms 的请求重点标记 String flag cost 500 ? [SLOW] : ; System.out.println(request.getRequestURI() 耗时 cost ms flag); } }这个 Filter 放在编码过滤器之后执行。演示时打开控制台能看到每个请求的耗时如果某个页面明显偏慢就去检查对应的 SQL 有没有走索引。比如商品列表按 category 筛选如果 category 字段没建索引数据量上来就会慢。建索引的语句很简单ALTER TABLE t_product ADD INDEX idx_category (category);。最后说个我自己的习惯每次改完 DAO 层的 SQL我都会在 Navicat 里先单独跑一遍确认结果对了再写进 Java 代码。因为 Java 里调试 SQL 要重启 Tomcat效率太低。这个习惯帮我省了大量重启时间也避免了很多“代码没问题但数据不对”的玄学问题。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询