简介:这份资源是面向计算机专业学生与Java初学者的一份毕业设计配套文档,主题为基于JavaEE的网上商城管理系统,适合作为课程设计、毕业设计选题参考或Java Web入门练手项目。系统采用Java语言与B/S架构,以IDEA为开发工具,数据库选用开源MySQL,整体分为前台与后台两大模块:前台涵盖登录注册、首页、我的订单、我的消息、我的地址、我的信息、售后维权与通知公告等;后台管理员端则包含个人中心、首页轮播管理、通知公告管理、客户管理、商品管理、售后维权管理及统计中心等功能,结构完整、模块划分清晰。资源包内共1个doc文档,约2.31MB,内容围绕选题背景、需求分析、系统架构与数据库设计、编码实现及测试验证展开,可帮助读者理解商城类系统的整体设计思路与功能组织方式。目前已有1674人学习下载,适合需要参考完整设计文档、梳理开发流程或进行功能扩展的读者。
1. 从零搭一套 JavaEE 网上商城:为什么我劝你先跑通一条订单链路
很多人搜「基于javaEE网上商城管理系统」,脑子里想的是把商品、订单、用户、后台全做齐,结果一上手就卡在环境上——Tomcat 起不来、JDBC 连不上、JSP 页面 404。我带过几届实习生,十个里有八个是这么翻车的。这套系统的本质,是用 Servlet + JSP + JDBC 把「浏览商品 → 加购物车 → 下单 → 后台改库存」这条链路跑通,它解决的是中小型电商后台从零到一的问题,适合课程设计、毕设,也适合想补 JavaWeb 全链路手感的后端新人。别贪多,先把一条订单链路跑穿,剩下的模块都是复制粘贴加改字段。这一章先讲清楚它由哪些层组成、为什么这么分层,后面几章再落到建表、写 Servlet、配环境、排错。
2. 分层架构与建表:先想清楚数据怎么流,再动手写代码
2.1 为什么是 Servlet + JSP + JDBC 这套组合
JavaEE 网上商城管理系统最常见的分层是四层:表现层用 JSP 渲染页面,控制层用 Servlet 接请求,业务层用 Service 类做校验和事务,持久层用 JDBC 或轻量封装访问数据库。有人会问为什么不上 Spring Boot,答案很现实——课程设计和老项目里,Servlet 是绕不开的基本功,理解了 request/response 的生命周期,后面学任何框架都是降维打击。
数据流向是这样的:浏览器发请求 → web.xml 或注解映射到某个 Servlet → Servlet 调 Service → Service 调 DAO → DAO 用 JDBC 查 MySQL → 结果逐层返回 → Servlet 转发到 JSP 渲染。这条链路里,Servlet 只负责收参数和跳转,业务判断放 Service,SQL 只出现在 DAO。我见过太多人把 SQL 写在 JSP 里,后期改一个字段要翻十几个文件,血泪经验。
选型上,JDBC 用原生还是用 DBUtils?如果只是课程设计,原生 JDBC 加一个简单的 BaseDao 封装就够了,依赖少、好调试。DBUtils 能省掉结果集映射的重复代码,但多一个 jar 包。我的建议是先用原生 JDBC 把增删改查写一遍,理解 Connection、PreparedStatement、ResultSet 的关闭顺序,再考虑封装。
2.2 商城核心表结构与建表 SQL
一个能跑通订单链路的商城,最少需要五张表:用户表、商品表、分类表、订单表、订单明细表。购物车可以放 Session 里,也可以单独建表,课程设计阶段放 Session 更省事。
-- 用户表:存登录信息和角色 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, -- 登录名,唯一 password VARCHAR(64) NOT NULL, -- 存 MD5 后的密码,别存明文 role TINYINT DEFAULT 0, -- 0 普通用户,1 管理员 create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 商品表:核心字段是价格和库存 CREATE TABLE t_product ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, -- 用 DECIMAL 不用 FLOAT,避免精度丢失 stock INT NOT NULL DEFAULT 0, category_id INT, img_path VARCHAR(200), -- 图片相对路径 status TINYINT DEFAULT 1 -- 1 上架,0 下架 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单表:订单号用时间戳+随机数生成 CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, user_id INT NOT NULL, total_price DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0, -- 0 待付款,1 已付款,2 已发货 create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单明细:一个订单对应多条商品记录 CREATE TABLE t_order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL, price DECIMAL(10,2) NOT NULL -- 下单时的价格快照,别关联查现价 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;建表有三个参数必须说清楚。第一,价格字段一律用 DECIMAL(10,2),FLOAT 在累加时会飘,订单金额对不上就是玄学 bug。第二,订单明细里的 price 是下单那一刻的价格快照,不能靠 product_id 去关联查现价,否则商品调价后历史订单金额全变。第三,字符集统一 utf8mb4,不然商品名里的特殊符号会变问号。外键约束在课程设计里可以不加,靠 Service 层保证一致性,加了反而在删除时容易报错。
2.3 用 Maven 组织项目目录
目录结构决定了你后期找文件的速度。标准做法是按层分包,而不是按功能分包。
src/main/java/com/shop/ ├── dao/ # 数据访问,每个表一个 Dao 类 │ ├── UserDao.java │ └── ProductDao.java ├── service/ # 业务逻辑,事务控制在这一层 │ └── OrderService.java ├── servlet/ # 控制层,按模块分 │ ├── ProductServlet.java │ └── OrderServlet.java ├── entity/ # 实体类,和表字段一一对应 │ └── Product.java └── util/ # 工具类 └── DBUtil.java src/main/webapp/ ├── WEB-INF/web.xml ├── css/ js/ images/ └── index.jsppom.xml 里只需要三个核心依赖:servlet-api(scope 设 provided,因为 Tomcat 自带)、mysql-connector-java、jstl。别一股脑把 Spring 全家桶塞进去,课程设计用不上,还会拖慢启动。
3. 从商品列表到下单:Servlet 与 JDBC 的最小可跑实现
3.1 数据库连接工具类 DBUtil
连接管理是 JDBC 最容易翻车的地方。每次 new Connection 不关,跑几十次请求数据库连接池就满了。正确做法是写一个工具类统一获取和关闭。
public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/shop?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USER = "root"; private static final String PWD = "your_password"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); // 新版驱动类名带 cj } catch (ClassNotFoundException e) { throw new RuntimeException("驱动加载失败", e); } } public static Connection getConn() throws SQLException { return DriverManager.getConnection(URL, USER, PWD); } // 关闭顺序:ResultSet -> Statement -> Connection,反了会报错 public static void close(Connection c, Statement s, ResultSet r) { try { if (r != null) r.close(); } catch (SQLException ignored) {} try { if (s != null) s.close(); } catch (SQLException ignored) {} try { if (c != null) c.close(); } catch (SQLException ignored) {} } }URL 里三个参数必须带:useSSL=false 关掉 SSL 警告,serverTimezone 设成 Asia/Shanghai 否则时间差 8 小时,characterEncoding=utf8 防止中文乱码。驱动类名 com.mysql.cj.jdbc.Driver 是 MySQL 8.x 的写法,5.x 是 com.mysql.jdbc.Driver,写错直接 ClassNotFoundException。关闭顺序不能反,先关 ResultSet 再关 Statement 最后关 Connection,这是 JDBC 的硬规矩。
3.2 商品列表查询与分页
商品列表是用户进商城看到的第一屏,必须支持分页,否则商品一多页面直接卡死。
public List<Product> findByPage(int page, int size) { List<Product> list = new ArrayList<>(); String sql = "SELECT id,name,price,stock,img_path FROM t_product " + "WHERE status=1 ORDER BY id DESC LIMIT ?,?"; try (Connection c = DBUtil.getConn(); PreparedStatement ps = c.prepareStatement(sql)) { ps.setInt(1, (page - 1) * size); // 起始偏移量 ps.setInt(2, size); // 每页条数 try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Product p = new Product(); p.setId(rs.getInt("id")); p.setName(rs.getString("name")); p.setPrice(rs.getBigDecimal("price")); // 金额用 BigDecimal p.setStock(rs.getInt("stock")); p.setImgPath(rs.getString("img_path")); list.add(p); } } } catch (SQLException e) { e.printStackTrace(); } return list; }分页公式是 (page-1)*size,page 从 1 开始。用 LIMIT 而不是在 Java 里 subList,数据量大时性能差几十倍。金额字段用 getBigDecimal 而不是 getDouble,和建表时的 DECIMAL 对应。try-with-resources 能自动关资源,比手动 close 更省心,但要注意它只关括号里声明的对象。
3.3 下单 Servlet:事务与库存扣减
下单是整个系统最容易出问题的地方,核心是「扣库存」和「写订单」必须在一个事务里,要么都成功要么都回滚。
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session = req.getSession(); User user = (User) session.getAttribute("loginUser"); if (user == null) { resp.sendRedirect("login.jsp"); return; } Map<Integer,Integer> cart = (Map<Integer,Integer>) session.getAttribute("cart"); Connection conn = null; try { conn = DBUtil.getConn(); conn.setAutoCommit(false); // 开启事务,关掉自动提交 BigDecimal total = BigDecimal.ZERO; for (Map.Entry<Integer,Integer> e : cart.entrySet()) { int pid = e.getKey(), qty = e.getValue(); // 扣库存:带 stock>=? 条件,防止超卖 String sql = "UPDATE t_product SET stock=stock-? WHERE id=? AND stock>=?"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, qty); ps.setInt(2, pid); ps.setInt(3, qty); if (ps.executeUpdate() == 0) { throw new RuntimeException("库存不足,商品ID=" + pid); } } // 查价格累加总额(略) } // 写订单主表和明细表(略) conn.commit(); // 全部成功才提交 session.removeAttribute("cart"); resp.sendRedirect("orderList.jsp"); } catch (Exception ex) { try { if (conn != null) conn.rollback(); } catch (SQLException ignored) {} req.setAttribute("msg", "下单失败:" + ex.getMessage()); req.getRequestDispatcher("cart.jsp").forward(req, resp); } finally { try { if (conn != null) conn.close(); } catch (SQLException ignored) {} } }关键点有三个。第一,conn.setAutoCommit(false) 开启事务,所有操作共用同一个 Connection,不能每个 DAO 各拿各的连接,那样事务根本不生效。第二,扣库存的 SQL 带 stock>=? 条件,靠数据库的行锁保证不超卖,比先查再改安全得多。第三,任何一步抛异常都要 rollback,否则会出现库存扣了订单没写的情况。这套写法在并发不高时够用,高并发下要换成乐观锁或消息队列,但课程设计阶段没必要。
4. 环境配置与联调:VSCode 跑 JavaEE 的踩坑清单
4.1 VSCode 配置 JavaEE 语言环境
现在很多人不想装笨重的 Eclipse,用 VSCode 配 JavaEE 环境。核心是装三个扩展:Extension Pack for Java、Community Server Connectors、Maven for Java。装完后按 Ctrl+Shift+P 输入 Java: Configure Java Runtime,确认 JDK 路径指向 1.8 或 11。
Tomcat 的配置在 Community Server Connectors 里,点 SERVERS 面板的加号,选 Tomcat 安装目录,再把项目 add 进去。这里有个坑:VSCode 默认不会把 src/main/webapp 识别为 web 根目录,需要在项目根目录建 .vscode/settings.json:
{ "java.configuration.updateBuildConfiguration": "automatic", "java.compile.nullAnalysis.mode": "automatic", "maven.executable.path": "mvn", "files.exclude": { "**/target": true } }如果启动后访问 404,先看 Tomcat 的 context path 是不是项目名,再看 web.xml 里 servlet 的 url-pattern 有没有写错。VSCode 的 Java 语言服务器偶尔会抽风,改完代码不生效就 Ctrl+Shift+P 执行 Java: Clean Java Language Server Workspace 重启一次。
4.2 中文乱码的三处源头
乱码是 JavaWeb 的经典问题,源头有三个,逐个排查。第一处是 JSP 页面顶部没写<%@ page contentType="text/html;charset=UTF-8" %>。第二处是 Servlet 里 req.setCharacterEncoding("UTF-8") 没在 getParameter 之前调用,POST 请求会乱码。第三处是数据库连接 URL 没带 characterEncoding=utf8,存进去就是问号。三处都对了还乱码,检查 MySQL 的库和表字符集是不是 utf8mb4。
GET 请求的中文乱码更麻烦,因为参数在 URL 里,setCharacterEncoding 管不到。Tomcat 8 以后默认 URIEncoding 就是 UTF-8,如果还是乱码,去 server.xml 的 Connector 标签加 URIEncoding="UTF-8"。
4.3 用 Postman 单独测 Servlet 接口
前后端联调时,JSP 页面报错很难定位是 Servlet 的问题还是页面的问题。我的习惯是先用 Postman 直接打 Servlet 的 URL,看返回的 JSON 或状态码。比如测商品列表接口,POST 到 http://localhost:8080/shop/product?action=list,带上参数,看返回内容。这样能把控制层和表现层的问题分开,省一半调试时间。
5. 避坑与排查:五个让我加班到凌晨的坑
5.1 现象:Tomcat 启动报 ClassNotFoundException: com.mysql.cj.jdbc.Driver
原因:mysql-connector 的 jar 没放进 WEB-INF/lib,或者 Maven 依赖 scope 写成了 provided。Tomcat 只认 WEB-INF/lib 下的 jar,Maven 的 provided 依赖不会打进去。
解决:确认 pom.xml 里 mysql-connector-java 的 scope 是默认的 compile,然后 mvn clean package 重新打包,或者手动把 jar 复制到 WEB-INF/lib。
5.2 现象:下单后库存扣了,订单表却没数据
原因:Service 层每个 DAO 方法各自 getConn(),事务没生效。JDBC 事务要求所有操作共用同一个 Connection。
解决:把 Connection 作为参数在 DAO 方法间传递,或者用 ThreadLocal 绑定当前线程的连接。下单逻辑里统一用外层传入的 conn。
5.3 现象:商品价格显示 19.900000000000002
原因:数据库字段用了 FLOAT 或 DOUBLE,Java 里用了 double 接收。浮点数在二进制里无法精确表示小数。
解决:建表用 DECIMAL(10,2),Java 用 BigDecimal,运算用 add、multiply 方法,别用 + 和 *。
5.4 现象:JSP 页面报「无法在 web.xml 或标注中找到 servlet 映射」
原因:Servlet 类上的 @WebServlet("/product") 注解没生效,或者 web.xml 的 metadata-complete="true" 导致注解被忽略。
解决:检查 web.xml 的 version 是不是 3.0 以上,metadata-complete 是不是 false。用注解就别在 web.xml 里重复配 servlet,两者选一个。
5.5 现象:VSCode 里改完代码,浏览器刷新还是旧页面
原因:Tomcat 没重新部署,或者浏览器缓存了 JSP 编译结果。
解决:在 SERVERS 面板右键 Tomcat 选 Restart,浏览器按 Ctrl+F5 强制刷新。JSP 改动一般会自动重编译,Java 类改动必须重启。
6. 进阶技巧:用过滤器统一登录校验和字符编码
系统做到后期,你会发现每个 Servlet 开头都在写「判断登录」和「设置编码」,重复代码一大堆。这时候该上 Filter 了。Filter 能在请求到达 Servlet 之前统一处理,登录校验和字符编码是最典型的两个场景。
@WebFilter("/*") // 拦截所有请求 public class AuthFilter implements Filter { // 白名单:登录页、登录接口、静态资源不拦截 private static final String[] WHITE = {"/login.jsp", "/login", "/css/", "/js/", "/images/"}; public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; request.setCharacterEncoding("UTF-8"); // 统一编码,省掉每个 Servlet 写一遍 response.setContentType("text/html;charset=UTF-8"); String uri = request.getRequestURI(); for (String w : WHITE) { if (uri.contains(w)) { chain.doFilter(req, resp); return; } } // 非白名单请求校验登录 if (request.getSession().getAttribute("loginUser") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(req, resp); // 放行 } }这个 Filter 有两个作用。第一,setCharacterEncoding 只写一次,所有 POST 请求的中文都不乱码了,比在每个 Servlet 里重复写靠谱。第二,登录校验集中在一处,新增页面不用再复制判断逻辑。白名单用 contains 匹配,注意 /login 会匹配到 /login.jsp,顺序上把更具体的放前面,或者用 equals 精确匹配。
Filter 的执行顺序由 web.xml 里的 filter-mapping 顺序决定,用注解的话顺序不确定。如果同时有编码过滤器和登录过滤器,建议编码过滤器用注解,登录过滤器在 web.xml 里配,保证编码先执行。
最后说个验证方法:写完 Filter 后,故意在未登录状态访问 /orderList,看是否跳转到登录页;再用中文商品名搜索,看是否乱码。两个都通过,说明 Filter 生效了。我一般会在 Filter 里加一行日志打印拦截的 URI,排查时一眼就能看出哪些请求被放行、哪些被拦了。这套东西不难,但能省下大量重复代码,值得花半小时加上。希望帮到你。
本文还有配套的精品资源,点击获取