简介:这是一套基于JSP与Servlet技术栈实现的JavaWeb蛋糕店售卖网站完整源码,面向正在学习JavaWeb开发、需要课程设计或毕业设计参考的初学者与进阶开发者。项目采用IDEA 2017.3.5开发,运行于Tomcat 8.5.35,数据库为MySQL 5.7,并引入c3p0连接池与DBUtil工具类,技术选型贴近企业常见实践。前台涵盖商品推荐与分类展示、详情查看、购物车增删改、用户注册登录、个人信息与收货地址维护、下单付款、订单查询及关键字搜索;后台支持订单状态流转、用户管理、商品类目与商品信息的增删改查,功能链路完整。资源包共290个文件,包含58个jsp页面、55个java源码、20个xml配置、14个js脚本及css、图片等静态资源,另附1个sql建库脚本,整体约19.11MB,目录结构清晰,便于按模块阅读与二次开发。目前已有127人学习,适合作为JavaWeb综合实战与功能扩展的参考模板。
1. 蛋糕店售卖网站源码拆解:从 JSP+Servlet 到 MySQL 的完整落地路径
很多同学做 JavaWeb 课设或毕业设计时,一看到“蛋糕店售卖网站”这种题目就头大——前台要展示商品、加购物车、下单,后台还要管订单、管用户、管类目,功能一多就不知道从哪下手。这份基于 JSP 和 Servlet 的 JavaWeb 源码包,恰好把这一整套电商核心链路跑通了。它用 Bootstrap 做前端布局,MySQL 5.7 存数据,c3p0 连接池加 DBUtils 操作数据库,开发环境是 IDEA 2017.3.5 配 Tomcat 8.5.35。适合正在找 JavaWeb 完整案例 MySQL 实战项目的同学,也适合想拿它改造成自己毕设的开发者。下面我按“能跑起来、能看懂、能改”的顺序,把这份资源拆开讲。
2. 环境搭建与数据库初始化:IDEA 2017.3.5 + Tomcat 8.5.35 + MySQL 5.7
2.1 开发环境版本对齐与 JDK 配置
这份源码对版本比较敏感,不是随便换个 Tomcat 9 或 MySQL 8 就能直接跑的。先说你最容易翻车的地方:JDK 版本。源码用的是 JDK 8,如果你本机装了多个 JDK,IDEA 里一定要把 Project SDK 和 Module SDK 都指到 1.8。常见做法是在File → Project Structure → Project里设置 Language level 为 8,然后在Modules → Sources里确认同样为 8。Tomcat 8.5.35 在 IDEA 里的配置路径是Run → Edit Configurations → + → Tomcat Server → Local,在 Deployment 标签页添加 Artifact,Application context 建议设为/cake,这样访问路径就是http://localhost:8080/cake/。
MySQL 5.7 的安装注意字符集选utf8mb4,排序规则用utf8mb4_general_ci。如果你用 MySQL 8,驱动类要从com.mysql.jdbc.Driver改成com.mysql.cj.jdbc.Driver,连接串还要加serverTimezone=Asia/Shanghai,否则会报时区错误。这是第一个血泪经验:版本不对,后面全是玄学报错。
2.2 数据库建表与 c3p0 连接池配置
源码里数据库操作依赖 c3p0 和 DBUtils,所以c3p0-config.xml必须放在src根目录下,不能放到WEB-INF里。一个典型的配置长这样:
<c3p0-config> <default-config> <property name="driverClass">com.mysql.jdbc.Driver</property> <property name="jdbcUrl">jdbc:mysql://localhost:3306/cake_shop?useUnicode=true&characterEncoding=utf8</property> <property name="user">root</property> <property name="password">你的密码</property> <property name="initialPoolSize">5</property> <property name="maxPoolSize">20</property> <property name="checkoutTimeout">3000</property> </default-config> </c3p0-config>initialPoolSize是初始连接数,本地开发 5 就够;maxPoolSize是最大连接数,课设演示 20 足够,设太大反而拖慢启动。checkoutTimeout是获取连接的超时时间,单位毫秒,3000 表示 3 秒拿不到连接就抛异常。数据库名要和jdbcUrl里保持一致,建库语句用CREATE DATABASE cake_shop DEFAULT CHARACTER SET utf8mb4;。建表 SQL 一般在源码的sql文件夹里,导入时先选库再执行,否则会报“No database selected”。
2.3 项目导入与 Tomcat 部署验证
导入项目后,先检查WEB-INF/lib下有没有 c3p0、DBUtils、mysql-connector 这三个 jar。如果没有,需要手动加到 Artifact 的WEB-INF/lib里,光在 IDEA 的 Libraries 里加是不够的,部署时会丢。启动 Tomcat 后,控制台如果出现 c3p0 的初始化日志,说明连接池加载成功。访问首页能看到蛋糕商品列表,就说明 JSP 编译和 Servlet 映射都正常。如果首页 404,先看web.xml里welcome-file是不是index.jsp,再看 Artifact 的 Output Layout 里有没有把 JSP 文件包含进去。
3. 前台核心链路:商品展示、购物车与订单提交
3.1 商品多维度展示的 Servlet 转发逻辑
前台首页的商品展示分三类:条幅推荐、热销推荐、新品推荐,再加上按类型展示。这些数据不是写死在 JSP 里的,而是通过 Servlet 查库后setAttribute再转发到 JSP。典型流程是:IndexServlet调用 Service 层,Service 调用 DAO 层,DAO 用 DBUtils 的QueryRunner执行 SQL,把结果集映射成Product实体列表。JSP 页面用 JSTL 的<c:forEach>遍历输出。
// IndexServlet.java 核心片段 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { ProductService productService = new ProductService(); // 条幅推荐:按推荐类型查 List<Product> bannerList = productService.getProductsByType("banner"); // 热销推荐:按销量倒序取前 8 条 List<Product> hotList = productService.getHotProducts(8); // 新品推荐:按上架时间倒序取前 8 条 List<Product> newList = productService.getNewProducts(8); request.setAttribute("bannerList", bannerList); request.setAttribute("hotList", hotList); request.setAttribute("newList", newList); request.getRequestDispatcher("/index.jsp").forward(request, response); }这里的关键参数是getHotProducts(8)里的 8,表示取前 8 条,你可以改成 4 或 12 来适配页面布局。getProductsByType("banner")里的"banner"是数据库里的推荐类型字段值,改的时候要同步改数据库。注意转发用forward而不是redirect,因为 request 域里的数据要带到 JSP,重定向会丢数据。
3.2 购物车数据结构与数量修改实现
购物车是这份源码里比较有代表性的部分。它没有用 Redis 或 Session 存整个购物车对象,而是把购物车项存成Map<Integer, CartItem>,key 是商品 ID,value 是包含商品信息和数量的CartItem。这样修改数量时直接根据商品 ID 定位,效率比 List 遍历高。加入购物车的 Servlet 逻辑是:先判断 Session 里有没有 cart,没有就 new 一个;然后判断商品是否已在 cart 里,在就数量加一,不在就新增一项。
// CartServlet.java 加入购物车 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int productId = Integer.parseInt(request.getParameter("pid")); HttpSession session = request.getSession(); Map<Integer, CartItem> cart = (Map<Integer, CartItem>) session.getAttribute("cart"); if (cart == null) { cart = new HashMap<>(); session.setAttribute("cart", cart); } if (cart.containsKey(productId)) { CartItem item = cart.get(productId); item.setCount(item.getCount() + 1); } else { Product product = productService.getProductById(productId); CartItem item = new CartItem(); item.setProduct(product); item.setCount(1); cart.put(productId, item); } response.sendRedirect(request.getContextPath() + "/cart.jsp"); }pid是前端传过来的商品 ID,用Integer.parseInt转换时如果参数为空会抛NumberFormatException,所以前端链接里必须带pid。修改数量的逻辑类似,只是把setCount改成前端传过来的目标数量。这里有个坑:Session 里的购物车在用户关闭浏览器后默认 30 分钟失效,如果你希望持久化,得在用户登录后把购物车写回数据库。
3.3 订单提交与用户订单查询
付款流程是:购物车页面点击“去结算”,先校验用户是否登录,没登录就跳登录页;登录后进入订单确认页,填写收货信息,提交后生成订单主表和订单明细表。订单主表存订单号、用户 ID、总金额、收货人、电话、地址、状态;订单明细表存订单号、商品 ID、数量、单价。生成订单时要用事务,保证主表和明细表同时成功或同时失败。
// OrderService.java 生成订单 public void createOrder(Order order, List<OrderItem> itemList) { Connection conn = null; try { conn = C3P0Utils.getConnection(); conn.setAutoCommit(false); orderDao.insertOrder(conn, order); for (OrderItem item : itemList) { orderDao.insertOrderItem(conn, item); } conn.commit(); } catch (SQLException e) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } throw new RuntimeException("下单失败"); } finally { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }setAutoCommit(false)是关闭自动提交,手动控制事务。conn.commit()提交,conn.rollback()回滚。注意 DBUtils 的QueryRunner默认是自动提交,所以这里要自己拿 Connection 来管事务。用户订单查询就是按用户 ID 查订单主表,再根据订单号查明细表,在 JSP 里嵌套遍历展示。
4. 后台管理模块:订单、用户、类目与商品操作
4.1 管理员权限拦截与后台入口
后台功能不是所有登录用户都能进的。源码的做法是:用户表里有个role字段,0 表示普通用户,1 表示管理员。登录成功后把role存到 Session,JSP 页面用<c:if test="${sessionScope.user.role == 1}">判断是否显示后台管理按钮。但光隐藏按钮不够,直接访问后台 URL 还是能进,所以需要一个权限过滤器。
// AdminFilter.java public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; User user = (User) request.getSession().getAttribute("user"); if (user == null || user.getRole() != 1) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(req, resp); }过滤器在web.xml里配置拦截/admin/*路径。user.getRole() != 1这个判断是核心,少了它普通用户就能越权操作。常见错误是过滤器配了但没生效,检查web.xml里<url-pattern>是不是写成了/admin而不是/admin/*。
4.2 订单状态流转与用户管理
后台订单操作包括按状态查询、发货、完成、删除。订单状态一般用数字表示:1 待付款、2 已付款待发货、3 已发货、4 已完成。发货操作就是把状态从 2 改成 3,完成就是从 3 改成 4。删除订单要同时删主表和明细表,否则会留下孤儿数据。
-- 按状态查询订单 SELECT * FROM orders WHERE status = ? ORDER BY create_time DESC; -- 修改订单状态 UPDATE orders SET status = ? WHERE order_id = ?; -- 删除订单(先删明细再删主表) DELETE FROM order_item WHERE order_id = ?; DELETE FROM orders WHERE order_id = ?;用户管理包括查询所有用户、新增用户、修改密码、修改信息和删除。新增用户时密码要做 MD5 加密再存,源码里如果没加密,建议你加上,这是基本的安全习惯。删除用户前要先检查该用户有没有未完成订单,有的话不能直接删,否则订单表的外键会出问题。
4.3 商品类目与商品 CRUD 的关联约束
商品类目和商品是一对多关系,一个类目下可以有多个商品。删除类目时如果该类目下还有商品,直接删会报外键约束错误。正确做法是先查该类目下有没有商品,有就提示“请先移除该类目下的商品”,没有才执行删除。
// CategoryService.java 删除类目 public boolean deleteCategory(int categoryId) { int productCount = productDao.countByCategoryId(categoryId); if (productCount > 0) { return false; // 有商品,不允许删除 } return categoryDao.deleteById(categoryId) > 0; }商品新增时要选类目,修改时可以改类目。商品图片上传是另一个容易出问题的地方,源码里如果用的是相对路径,部署后图片可能找不到。建议把上传目录配成绝对路径,或者在 Tomcat 里配虚拟路径。商品删除同样要先检查有没有被订单引用,有的话只能下架不能删除。
5. 避坑与排查:版本、编码、连接池与路径问题
5.1 中文乱码:从请求到响应的三层排查
现象:表单提交中文变成问号或乱码。原因通常有三层:请求编码、响应编码、数据库编码。解决顺序是先在 Servlet 里加request.setCharacterEncoding("UTF-8"),再设response.setContentType("text/html;charset=UTF-8"),然后检查数据库连接串有没有characterEncoding=utf8,最后确认表和字段的字符集是utf8mb4。如果是 GET 请求乱码,还要在 Tomcat 的server.xml里给 Connector 加URIEncoding="UTF-8"。
5.2 c3p0 连接池报错:驱动类找不到与连接泄漏
现象:启动时报ClassNotFoundException: com.mysql.jdbc.Driver。原因是 mysql-connector jar 没放到WEB-INF/lib下。解决方法是把 jar 复制到该目录,并在 IDEA 的 Artifact 里确认已包含。另一个常见现象是跑一段时间后报“Too many connections”,原因是 DAO 里拿了 Connection 没关。用 DBUtils 的QueryRunner一般不用手动关,但自己写事务的地方一定要在 finally 里 close。
5.3 JSP 页面路径错误:相对路径与绝对路径混用
现象:CSS 和 JS 加载不出来,页面样式全乱。原因是 JSP 里用了相对路径,而 Servlet 转发后浏览器地址栏还是原地址,相对路径就变了。解决办法是统一用${pageContext.request.contextPath}拼绝对路径,比如<link rel="stylesheet" href="${pageContext.request.contextPath}/css/bootstrap.css">。这样不管怎么转发,路径都不会错。
5.4 订单重复提交:刷新页面导致重复下单
现象:用户提交订单后按 F5 刷新,又生成一笔订单。原因是表单提交后直接转发到了成功页,刷新时浏览器重复提交了上一次的 POST 请求。解决办法是提交后用response.sendRedirect重定向到订单列表页,而不是forward。重定向会让浏览器发起新的 GET 请求,刷新就不会重复提交。
5.5 Tomcat 启动失败:端口占用与 JDK 版本不匹配
现象:启动 Tomcat 报Address already in use: JVM_Bind。原因是 8080 端口被占用了。解决办法是改 Tomcat 的conf/server.xml里的 Connector 端口,或者在 IDEA 的 Run Configuration 里改 HTTP port。另一个现象是报Unsupported major.minor version,这是 JDK 版本不匹配,编译用的 JDK 比运行的高,统一改成 JDK 8 即可。
6. 二次开发与进阶技巧:从课设到可演示项目
如果你想让这个项目在答辩时更出彩,有几个方向可以改。第一是加一个简单的分页,商品列表和订单列表都加上,用LIMIT和OFFSET实现,前端用 Bootstrap 的分页组件。第二是把密码存储从明文改成 MD5 加盐,登录时先查盐再校验,这是安全加分项。第三是给订单状态加一个时间轴展示,用户能直观看到“已下单→已发货→已完成”的流转。
// 分页查询示例 public List<Product> getProductsByPage(int page, int pageSize) { int offset = (page - 1) * pageSize; String sql = "SELECT * FROM product LIMIT ? OFFSET ?"; try { return queryRunner.query(sql, new BeanListHandler<>(Product.class), pageSize, offset); } catch (SQLException e) { throw new RuntimeException(e); } }page是当前页码,从 1 开始;pageSize是每页条数,建议 8 或 12;offset是偏移量,公式是(page - 1) * pageSize。这个分页逻辑可以直接复用到订单列表和用户列表。
还有一个技巧是给项目加一个全局异常处理页。在web.xml里配<error-page>,把 404 和 500 都指向一个友好的错误页,答辩时如果现场演示出 bug,至少不会看到一堆堆栈信息。我一般还会在web.xml里配一个session-timeout,默认 30 分钟,演示时改成 60 分钟,避免讲到一半 Session 过期。
最后说一个我自己的习惯:每次改完 DAO 层的 SQL,我都会先在 MySQL 客户端里把 SQL 跑一遍,确认结果对了再写进 Java 代码。因为 Java 里报的 SQL 异常往往只给一个SQLException,具体哪错了还得靠数据库客户端来定位。从那以后我每次调 SQL 都强制走一遍“客户端验证→代码集成→页面联调”的流程,省了很多来回折腾的时间。希望帮到你。
本文还有配套的精品资源,点击获取