简介:以饿了么为原型的Java Web前后端分离项目,基于Vue.js+Servlet+AJAX技术栈实现,适合Java Web学习者、前端转后端开发者以及需要课程设计参考的高校学生。项目覆盖用户登录、商品查询、订单处理等典型业务场景,前端通过Vue组件组织页面与交互,后端由Servlet接收AJAX请求并返回JSON数据,清晰展现从浏览器请求到服务端响应的完整链路,同时涉及RESTful接口组织、数据库读写与安全设计思路。压缩包共131个文件、约4.15MB,其中45张PNG图片直观呈现页面效果与运行结果,LESS/SCSS/CSS样式文件与HTML页面共同构成前端展示层,Java源码及class文件对应Servlet业务逻辑,JAR包提供依赖支持,properties与XML等配置文件用于环境设置,目录按前端资源和后端代码分层,便于逐模块对照学习。已有1435人学习下载,作为外卖类Web应用开发范例,既能帮助初学者快速建立前后端分离项目整体认知,也可用于毕业设计或进阶实战时参考其中的接口设计与代码组织。
1. 这个饿了么风格的外卖后端项目,是理解前后端分离最好的起点
每次拿到一套带完整前端页面的 Java Web 项目,我第一件事不是看代码,而是先确认它到底在教什么。这个饿了么后端项目是典型的「Vue + Servlet + AJAX」前后端分离架构:前端用 Vue 管理页面交互,后端用原生 Servlet 暴露接口,两者通过 AJAX 交换 JSON 数据。没有 Spring Boot 那种自动装配的黑匣子,所有请求怎么进、参数怎么解析、数据怎么返回,每一层都看得清清楚楚。适合两类人:一是正在做 Java Web 课程设计、需要一套能跑通全流程的参考实现;二是写惯了 Spring Boot、想回头补 Servlet 底层功底的开发者。项目不大,但登录、商家列表、购物车、下单、订单管理这些外卖核心链路一个不少,非常适合二次开发。
2. 技术路线与整体架构:一个请求从 Vue 到 Servlet 的完整链路
2.1 技术选型:为什么还在用 Servlet,而不是直接上 Spring Boot
先说结论:这项目不是技术落后,而是刻意保留 Servlet 层来暴露 Web 应用的本质。Spring Boot 的@RestController写起来爽,但请求怎么被容器接收、web.xml怎么映射 URL、HttpServletRequest里到底装了什么,这些底层细节全被框架吞掉了。Servlet 方案下,一个 HTTP 请求从进入到返回,每一步都是你自己管理的代码,调试起来反而更直接。
常见做法是这么分工的:
| 层 | 技术 | 职责 |
|---|---|---|
| 前端展示 | Vue 2 + Element UI + Axios | 页面渲染、表单校验、调用后端接口 |
| 接口层 | Java Servlet 3.0+ | 接收 AJAX 请求、解析参数、返回 JSON |
| 业务层 | Service 类 | 订单计算、登录校验、状态流转 |
| 数据层 | DAO + JDBC 封装 | 操作 MySQL 表,返回实体对象 |
| 数据库 | MySQL 5.7 / 8.0 | 用户、商家、菜品、订单、购物车数据存储 |
前端工程通过 Vue CLI 创建,和后端是两个独立项目。前端开发服务器跑在8080或8081,后端 Tomcat 跑在8080(会调整),两者之间通过 Axios 发起 HTTP 请求通信。生产环境下前端npm run build打包成静态资源,丢到 Tomcat 的webapps下由同一个容器托管,本地开发则用代理解决跨域。
2.2 请求如何从 Vue 页面走到 MySQL:一张链路图讲透
我拆这种项目时,习惯先把「一个请求的完整生命周期」捋清楚。拿「用户登录」举例:
- 用户在登录页输入账号密码,点击提交。
- Vue 组件里的
submitForm方法被触发,调用api/login(loginForm)。 api模块通过 Axios 发起POST /api/user/login,请求体是{ "username": "admin", "password": "123456" }。- Tomcat 根据映射规则找到
UserLoginServlet,容器把 HTTP 请求封装成HttpServletRequest对象。 - Servlet 的
doPost方法被调用,从请求体里取出 JSON 字符串,转成 Java 对象。 - Servlet 调用
UserService.login(username, password),Service 层做参数校验,比如检查是否为空。 - Service 调用
UserDAO.findByUsername(username),DAO 层用 JDBC 执行SELECT语句。 - 结果集封装成
User实体对象返回,Service 判断密码是否匹配。 - Servlet 把结果拼成
{ "code": 200, "message": "登录成功", "data": { "userId": 1 } },写入响应。 - Vue 的 Axios
then回调拿到 JSON,根据code决定跳转或提示错误。
// 前端的 api/login 方法,Vue 组件里调用的唯一入口 login(loginForm) { return request({ url: '/api/user/login', // 后端 Servlet 映射的地址 method: 'post', data: loginForm // { username: 'admin', password: '123456' } }) }这段代码的逻辑核心在于:前端不关心后端是 Servlet 还是 Spring Boot,只要接口约定好 URL、请求方法和参数结构,就能联调。request是提前封装好的 Axios 实例,统一处理了请求头里的Content-Type和响应拦截器,所有接口都走它,这样后期要加 token 校验或统一错误提示,只需改一个文件。
Servlet 端的写法是另一个关键点:
@WebServlet("/api/user/login") // 注解方式声明 URL 映射,省去 web.xml 配置 public class UserLoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 设置请求和响应的编码,防止中文乱码 request.setCharacterEncoding("UTF-8"); response.setContentType("application/json;charset=UTF-8"); // 2. 读取前端传来的 JSON 请求体 BufferedReader reader = request.getReader(); StringBuilder sb = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { sb.append(line); } JSONObject params = JSONObject.parseObject(sb.toString()); String username = params.getString("username"); String password = params.getString("password"); // 3. 调业务层做登录校验 UserService userService = new UserService(); User user = userService.login(username, password); // 4. 组装统一返回格式 JSONObject result = new JSONObject(); if (user != null) { result.put("code", 200); result.put("message", "登录成功"); result.put("data", user.toJson()); } else { result.put("code", 500); result.put("message", "用户名或密码错误"); } response.getWriter().write(result.toJSONString()); } }这里有个实际开发中容易忽略的细节:request.getReader()只对 POST 请求体有效,如果前端用 GET 传参,得改用request.getParameter("username")。这个项目的所有写操作都走 POST,读操作走 GET,所以两类入参方式都能在代码里看到。response.setContentType("application/json;charset=UTF-8")这行决定了浏览器解析返回内容时用什么编码和格式,漏掉它前端拿到的就是乱码或下载文件而非 JSON。
2.3 前端开发服务器的跨域处理
前后端分离后,第一个拦路虎是跨域。前端跑在http://localhost:8081,后端跑在http://localhost:8080,浏览器默认拒绝跨端口 AJAX 请求。常见做法有两种,这个项目里用的是 Vue CLI 的 devServer 代理。
// vue.config.js 里的代理配置 module.exports = { devServer: { port: 8081, // 前端开发服务器端口 proxy: { '/api': { // 以 /api 开头的请求全部转发到后端 target: 'http://localhost:8080', // 后端 Tomcat 地址 changeOrigin: true } } } }这种配置妙在:浏览器看到的请求地址始终是同源的http://localhost:8081/api/xxx,代理在 Node 层替你把请求转发到后端,避开了浏览器跨域限制。changeOrigin: true会把请求头里的Host字段改成目标地址,后端拿到的就像是浏览器直接请求它一样,调试时不至于被防盗链或域名校验卡住。生产环境直接把打包产物扔到 Tomcat 的webapps下,同源部署,代理配置就不再生效,代码里的baseURL需要配合调整。
3. 源码结构与核心模块拆解:从登录到下单的代码长什么样
3.1 前后端目录结构
拿到源码后先看目录,能快速建立项目的整体认知。这类 Servlet + Vue 项目通常分两个根目录,前端叫frontend或直接叫vue-web,后端叫backend或server。后端是标准的 Maven 工程结构:
backend/ ├── pom.xml # Maven 依赖管理 ├── src/main/java │ └── com/edu/order │ ├── common/ # 公共工具类 │ │ ├── DBUtil.java # JDBC 连接池封装 │ │ ├── Result.java # 统一响应结果类 │ │ └── JsonUtil.java # JSON 序列化工具 │ ├── entity/ # 实体类 │ │ ├── User.java │ │ ├── Merchant.java │ │ ├── Dish.java │ │ ├── CartItem.java │ │ └── Order.java │ ├── dao/ # 数据库操作层 │ │ ├── UserDAO.java │ │ ├── MerchantDAO.java │ │ ├── DishDAO.java │ │ ├── CartDAO.java │ │ └── OrderDAO.java │ ├── service/ # 业务逻辑层 │ │ ├── UserService.java │ │ ├── CartService.java │ │ └── OrderService.java │ └── servlet/ # 控制器层,一个 Servlet 管一组接口 │ ├── UserLoginServlet.java │ ├── MerchantListServlet.java │ ├── DishListServlet.java │ ├── CartServlet.java │ └── OrderServlet.java ├── src/main/resources │ └── db.properties # 数据库连接配置 └── src/main/webapp └── WEB-INF/web.xml # 项目部署描述符实体类对应数据库表,DAO 层只有 SQL 操作没有业务判断,Service 层不碰 JDBC,Servlet 层不写 SQL。这条依赖链是单向的,改任何一层都不影响另外两层。前端vue-web目录典型结构是:
vue-web/ ├── src/ │ ├── api/ # 所有后端接口的封装,按模块拆文件 │ │ ├── request.js # Axios 统一实例 │ │ ├── user.js # 用户相关接口 │ │ ├── merchant.js # 商家和菜品接口 │ │ └── order.js # 购物车和订单接口 │ ├── router/ # 前端路由配置 │ ├── store/ # Vuex 状态管理 │ ├── views/ # 页面组件 │ │ ├── Login.vue │ │ ├── Home.vue │ │ ├── Cart.vue │ │ └── OrderList.vue │ └── App.vue前后端严格通过api目录下的接口函数交互,页面组件不直接写 Axios。这是前后端分离项目最该养成的习惯:接口变更时只需改api层,业务代码不用动。
3.2 登录模块:从表单到数据库校验的完整逻辑
登录是最能体现分层思想的功能。先看 DAO 层的实现,这层只做一件事:查用户是否存在。
public class UserDAO { // 根据用户名查询用户 public User findByUsername(String username) { String sql = "SELECT id, username, password, nickname, avatar FROM t_user WHERE username = ?"; // JDBC 查询代码省略,使用 DBUtil 工具类获取连接 try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ResultSet rs = ps.executeQuery(); if (rs.next()) { User user = new User(); user.setId(rs.getInt("id")); user.setUsername(rs.getString("username")); user.setPassword(rs.getString("password")); user.setNickname(rs.getString("nickname")); // 注意:返回的 User 对象带了 password 字段 return user; } } catch (SQLException e) { e.printStackTrace(); } return null; } }使用PreparedStatement而不是Statement,核心目的是防 SQL 注入。?占位符由 JDBC 驱动做转义,即使用户输入' or 1=1这类攻击性字符串,也只是被当作普通字符参与比较。这段代码的另一个细节是结果集处理:rs.next()判断是否有下一行,没有就返回 null,调用方根据 null 判断登录失败,这是 JDBC 最朴素的写法,不需要任何 ORM。
Service 层的职责是补上 DAO 层不做的业务判断:
public class UserService { private UserDAO userDAO = new UserDAO(); public User login(String username, String password) { if (username == null || password == null || username.trim().isEmpty() || password.isEmpty()) { return null; // 参数为空直接返回 null,省一次数据库查询 } User user = userDAO.findByUsername(username); if (user != null && user.getPassword().equals(password)) { return user; // 密码匹配才算登录成功 } return null; } }这段代码先做非空校验再走数据库,是英明的——把无效请求提前拦截掉,否则空参查询会成为 SQL 层面的隐患。密码比对用的是 String 的equals方法,这个项目是教学用途所以明文存储,真实生产环境必须用 BCrypt 之类的算法做哈希存储。拆这种项目时,我一般会在 Service 层留这个注释,提醒二次开发的同学注意安全改造。
3.3 数据访问层:DBUtil 连接池的设计思路
整个项目的数据层都跑在 DBUtil 之上。它解决的问题是「每次数据操作都获取一个数据库连接」。如果每次手动DriverManager.getConnection,高并发下数据库连接会被很快耗尽,所以通常用连接池技术,这个项目的常见写法是封装一个简易连接池或直接使用阿里巴巴的 Druid。
public class DBUtil { private static DruidDataSource dataSource; static { try { Properties props = new Properties(); // 从类路径下的 db.properties 读取配置 props.load(DBUtil.class.getClassLoader().getResourceAsStream("db.properties")); dataSource = new DruidDataSource(); dataSource.setUrl(props.getProperty("jdbc.url")); dataSource.setUsername(props.getProperty("jdbc.username")); dataSource.setPassword(props.getProperty("jdbc.password")); dataSource.setInitialSize(Integer.parseInt(props.getProperty("jdbc.initialSize"))); } catch (Exception e) { throw new ExceptionInInitializerError("数据库初始化失败"); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }连接池的核心配置有三个:initialSize是启动时预先创建的连接数,maxActive是最大连接数,maxWait是拿不到连接时的等待时间。开发机配置initialSize=5足够,生产环境根据并发量调大。DBUtil 用了静态块初始化,类加载时只执行一次,避免重复创建数据源。
3.4 下单模块:事务和状态更新的核心
购物车和下单是外卖系统的命门。这个项目里有一个很值得学习的点:下单不是简单 insert 一条订单记录,而是同时操作多个表。
public class OrderService { private OrderDAO orderDAO = new OrderDAO(); private CartDAO cartDAO = new CartDAO(); public boolean createOrder(int userId, int merchantId, List<CartItem> items) { Connection conn = null; try { conn = DBUtil.getConnection(); // 开启手动事务,保证多表操作要么全部成功要么全部回滚 conn.setAutoCommit(false); // 1. 插入订单主表,拿到生成的订单 ID double totalPrice = 0; for (CartItem item : items) { totalPrice += item.getPrice() * item.getQuantity(); } int orderId = orderDAO.insertOrder(conn, userId, merchantId, totalPrice); // 2. 插入订单明细表 for (CartItem item : items) { orderDAO.insertOrderDetail(conn, orderId, item); } // 3. 清空该用户的购物车 cartDAO.clearCart(conn, userId); conn.commit(); // 全部成功,提交事务 return true; } catch (Exception e) { if (conn != null) { try { conn.rollback(); // 任何一步失败,全部回滚 } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } } }setAutoCommit(false)是事务开始的分界线。insertOrder返回的orderId是数据库自增主键,insertOrderDetail需要拿它做外键。整个过程把订单主表、订单明细表、购物车三块数据变成要么全部生效要么完全不生效的原子操作,这就是事务的意义。拆项目时多留意这类多表联写的地方,通常就是业务的核心复杂度所在。
购物车部分常见做法是建一张t_cart表,字段包含user_id、dish_id、quantity,用户每次加菜就是 update 这条记录的 quantity。登录后首次进入购物车页面,前端调用 CartServlet 的 GET 接口拿当前用户购物车列表,数据通过联表查询拼装菜品名称、价格和图片地址。
4. 环境准备与运行部署:从 JDK 到 Vue 的完整启动步骤
4.1 环境清单与版本匹配
跑这个项目,最先要搞定的是一套兼容的开发环境。Servlet 项目对版本敏感,版本配不对,启动就直接翻车。我通常建议按下面这套来装:
| 组件 | 推荐版本 | 注意事项 |
|---|---|---|
| JDK | 1.8 或 11 | Servlet 3.0+ 和 Tomcat 9 都需要 JDK 8 起步 |
| Tomcat | 8.5 或 9.0 | 8.5 以下不支持 Servlet 3.1 注解 |
| MySQL | 5.7 或 8.0 | 8.0 需要对应版本的 JDBC 驱动 |
| Maven | 3.6+ | 项目依赖统一由 Maven 管理 |
| Node.js | 14 及以上 | Vue CLI 5 要求 Node 12 起步 |
| IDEA | Ultimate 或社区版 | 社区版需要手动配置 Tomcat |
需要注意:MySQL 8.0 的认证插件是caching_sha2_password,老版本驱动连不上;Tomcat 10 开始包名从javax.servlet变成jakarta.servlet,现有代码如果按javax写的,直接扔到 Tomcat 10 会报ClassNotFoundException。这个项目用的是javax命名空间,所以选 Tomcat 9 最稳。
4.2 数据库初始化
源码包里一般附带sql/order_db.sql初始化脚本。建表脚本按这个思路:
-- 用户表 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, nickname VARCHAR(50), avatar VARCHAR(255), role TINYINT DEFAULT 0 -- 0 普通用户, 1 商家 ); -- 商家表 CREATE TABLE t_merchant ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, image VARCHAR(255), delivery_fee DECIMAL(5,2) DEFAULT 0, rating FLOAT DEFAULT 5.0 ); -- 菜品表 CREATE TABLE t_dish ( id INT PRIMARY KEY AUTO_INCREMENT, merchant_id INT NOT NULL, name VARCHAR(100) NOT NULL, price DECIMAL(8,2) NOT NULL, image VARCHAR(255), description VARCHAR(500) ); -- 购物车表 CREATE TABLE t_cart ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, dish_id INT NOT NULL, quantity INT DEFAULT 1, UNIQUE KEY uk_user_dish (user_id, dish_id) -- 同一个用户同一道菜只有一条记录 ); -- 订单主表 CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, merchant_id INT NOT NULL, total_price DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0, -- 0 待支付, 1 已支付, 2 配送中, 3 已完成 create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 订单明细表 CREATE TABLE t_order_detail ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, dish_id INT NOT NULL, dish_name VARCHAR(100) NOT NULL, -- 冗余菜品名,防止菜品改名影响历史订单 price DECIMAL(8,2) NOT NULL, -- 冗余下单时的价格 quantity INT NOT NULL );执行时用source命令或 Navicat 直接运行脚本。注意t_cart表的唯一键uk_user_dish,它的作用是防止同一用户重复添加同一道菜时产生两条记录,而是直接更新数量——这是购物车表设计的一个细节。
4.3 后端启动步骤
后端是 Maven 工程,导入方式很简单:
- IDEA 里选择
File -> New -> Project from Existing Sources,选中backend目录下的pom.xml。 - 等待 Maven 下载依赖(首次会比较慢,网络不好时可能卡在
druid或fastjson的下载)。 - 修改
src/main/resources/db.properties里的连接信息:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/order_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=你的密码 jdbc.initialSize=5 jdbc.maxActive=20- 配置 Tomcat:IDEA 里
Run -> Edit Configurations,点加号选Tomcat Server -> Local,把 Tomcat 9 的目录指进去,然后在Deployment页签点击+选Artifact,把项目部署上去。 - 启动 Tomcat,控制台出现
Server startup in xxx ms就成功了。
serverTimezone=Asia/Shanghai这个参数很关键,MySQL 8.0 默认时区是 UTC,不加它 JDBC 驱动会报时区错误。连接失败时先看这个参数。
4.4 前端启动步骤
前端部分,Vue CLI 项目:
# 进入前端项目目录 cd vue-web # 安装依赖 npm install # 启动开发服务器 npm run servenpm install是前端项目最容易出问题的环节,网络不好时推荐用国内镜像源:
npm config set registry https://registry.npmmirror.com启动成功后浏览器访问http://localhost:8081,如果第 2 章里的代理配置没问题,前端页面能正常拿到后端数据。如果网络没问题但npm install卡住,看看是不是没配镜像源——我遇到过不少同学在这一步卡一下午,换源后一分钟装完。
4.5 首次运行自测清单
启动完成不等于项目能跑,建议按这个顺序自测链路:
- 浏览器打开
http://localhost:8081,进入登录页,输入源码自带的admin / 123456,能跳转到首页说明登录接口通。 - 打开浏览器 DevTools 的 Network 面板,点击菜单栏的「商家列表」,看
/api/merchant/list请求返回 200 且 JSON 里有商家数据,说明 DAO 查询正常。 - 点进一家店,随便加一个菜进购物车,查看
/api/cart/add是否有响应。若 500,多半是数据库表字段对不上。 - 提交订单,确认订单列表页出现新订单。如果这一步通了,整个前后端链路就完全打通了。
5. 常见问题与避坑:JDBC、Tomcat、编码与跨域踩坑记录
5.1 接口返回中文变问号
现象:前端页面能拿到数据,但所有中文显示成???,英文和数字正常。
原因:Tomcat 8 以上 GET 请求的 URI 默认按 UTF-8 解码,但 POST 请求体的编码取决于request.setCharacterEncoding有没有在读取参数前调用。如果 Servlet 里漏了request.setCharacterEncoding("UTF-8"),容器会用默认的 ISO-8859-1 解码请求体,中文就乱码了。同理,响应侧漏掉response.setContentType("application/json;charset=UTF-8"),浏览器就会用页面编码来解析,输出中文变问号。
解决:在 Servlet 的doPost方法第一行加request.setCharacterEncoding("UTF-8"),并确保响应头显式声明charset=UTF-8。检查 web.xml 里有没有配置CharacterEncodingFilter,这个全局过滤器可以一次性解决所有 Servlet 的编码问题。
5.2 浏览器报 CORS 跨域错误
现象:前端请求发出后,Network 面板显示请求状态(failed),Console 里报No 'Access-Control-Allow-Origin' header is present on the requested resource。
原因:前端localhost:8081直接请求后端localhost:8080,浏览器判定为跨域,拦截了响应。这类项目通常用 devServer 代理,如果你的请求地址写死了完整后端 URL,代理就不生效了。常见于 Axios 的baseURL配成了http://localhost:8080,跳过了 Vue CLI 的/api前缀。
解决:确认vue.config.js里配置了 proxy,且请求路径以/api开头,路径才能被代理捕获。如果必须用完整地址,给每个 Servlet 的doOptions方法加上 CORS 响应头:
response.setHeader("Access-Control-Allow-Origin", "http://localhost:8081"); response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); response.setHeader("Access-Control-Allow-Headers", "Content-Type");5.3 访问 Servlet 返回 404
现象:前端点击某个功能,Network 面板显示 404,后端控制台没有对应日志。
原因:404 有三种情况。一是 URL 写错,前端请求的路径和@WebServlet注解的值不一致;二是 Servlet 类没有被 Tomcat 扫描到;三是 Tomcat 版本太老,不支持注解方式映射,必须用 web.xml。
解决:先核对 URL。再看@WebServlet注解的类是否放在com/edu/order/servlet包下,Tomcat 默认扫描规则是启动时扫描部署目录下的WEB-INF/classes。最后检查 Tomcat 版本,8.5 及以上支持注解,8.0 以下需要手动写 web.xml。
5.4 前端拿到的 data 是字符串而不是 JSON
现象:接口返回 200,但 Vue 组件里res.data不是对象,而是"[object Object]"或一串字符串,渲染不出数据。
原因:Servlet 输出 JSON 时可能调了两次response.getWriter().write(),第一次写了个普通文本,第二次才写 JSON;或者返回的字符串本身就是被转义过的 JSON,比如把 JSON 字符串当成普通字符串置于双引号内。
解决:检查 Servlet 里是否只调用了一次write。如果用了JSONObject.toJSONString后直接write,一般没问题。另一个常见做法是看 Fastjson 的JSONObject.parseObject和toJSONString是否配对使用,避免把 JSON 对象直接toString输出。前端层面,请求函数里加一层JSON.parse(res.data)只能临时解决,根治还得修后端。
5.5 MySQL 8.0 连接报错 SSL 和驱动问题
现象:后端启动时报Unable to load authentication plugin 'caching_sha2_password'或Public Key Retrieval is not allowed。
原因:MySQL 8.0 默认认证插件是caching_sha2_password,MySQL 5.x 时代的驱动类com.mysql.jdbc.Driver不支持。另外 8.0 默认开启 SSL 但 JDBC 连接串没配 SSL 参数,驱动就抛异常。
解决:驱动用mysql-connector-java8.0.x 以上版本,pom.xml 里确认依赖版本;JDBC URL 加allowPublicKeyRetrieval=true&useSSL=false两个参数。完整的连接串可以参考第 4.3 节,注意顺序不能乱。
6. 二次开发:把「订单状态流转」从数据库到 Vue 完整实现一遍
拿到源码后,光跑通不够,要上手改才算吃透。我建议你从「订单状态流转」这个小功能入手——这个改动牵涉数据库、后端接口、前端页面三个层面,完整走一遍就能彻底理解整个项目的数据流。
第一步先看需求:当前订单表只有status字段,注释是0 待支付, 1 已支付, 2 配送中, 3 已完成。我给商家加一个「接单」操作,把订单从「已支付」推到「配送中」。先改数据库,加一个字段记录操作时间:
ALTER TABLE t_order ADD COLUMN accept_time DATETIME DEFAULT NULL COMMENT '商家接单时间';后端在 OrderServlet 里加一个方法,接收orderId参数,更新状态和时间:
@WebServlet("/api/order/accept") public class OrderAcceptServlet extends HttpServlet { private OrderDAO orderDAO = new OrderDAO(); @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); response.setContentType("application/json;charset=UTF-8"); JSONObject result = new JSONObject(); // 从 JSON 请求体里取订单 ID JSONObject params = JsonUtil.parse(request); int orderId = params.getIntValue("orderId"); boolean success = orderDAO.updateStatus(orderId, 2, new Date()); result.put("code", success ? 200 : 500); result.put("message", success ? "接单成功" : "取消失败"); response.getWriter().write(result.toJSONString()); } }DAO 层写对应的更新 SQL,注意要加WHERE status = 1条件做状态校验,防止重复接单:
public boolean updateStatus(int orderId, int targetStatus, Date time) { String sql = "UPDATE t_order SET status = ?, accept_time = ? WHERE id = ? AND status = 1"; // 执行更新,返回受影响行数 > 0 说明更新成功 }前端在第 3.4 节的订单列表页加一个按钮,用第 2.3 节的request调用新接口:
// api/order.js 里新增方法 acceptOrder(orderId) { return request({ url: '/api/order/accept', method: 'post', data: { orderId: orderId } }) } // OrderList.vue 模板里,对状态为 1 的订单显示接单按钮 <el-button v-if="order.status === 1" type="success" @click="handleAccept(order.id)">接单</el-button> handleAccept(orderId) { this.acceptOrder(orderId).then(res => { if (res.code === 200) { this.$message.success('接单成功') this.fetchOrderList() // 重新拉取订单列表,刷新状态 } }) }这一步完整覆盖了「前端组件 → api 层 → Servlet → DAO → 数据库 → 响应回前端」的全链路。从数据库加字段到现在,一个功能改下来基本不用动其他模块,这就是分层设计的价值。
拆过这么多项目,我养成一个习惯:拿到源码先梳理请求链路,再改代码,改完一定打开 Network 面板看每个请求的响应。每次改完接口我都强制走一遍「看 Network → 看响应体 → 看前端是否按预期渲染」三步,用这个流程查出的问题比我翻代码猜原因快得多。希望帮到你。
本文还有配套的精品资源,点击获取