云禾外卖补充—项目整体后端运行逻辑-对流程的更深层次理解

**摘要:**本文以「新增员工」为例,完整梳理一次带登录校验的后端请求从 nginx 反向代理、SpringMVC 分发、JWT 拦截器验令牌,到 Controller 接收 JSON、Service 执行业务并构造 Entity、Mapper 写入 MySQL,再到结果逐层返回并序列化成 JSON 回前端的全过程;同时说明异常如何统一抛给 GlobalExceptionHandler 转成 code=0 的 Result,以及 ThreadLocal 在跨层传递当前登录人时的作用。

一、完整流程(以"新增员工"为例)

第 0 步:先有登录,才谈得上后续

复制代码
POST /api/employee/login  →  nginx(80)  →  /admin/employee/login  →  8080

这个 URL 在 WebMvcConfiguration 里被 excludePathPatterns("/admin/employee/login") 排除了,所以它是唯一不需要令牌就能进的接口。

进到 EmployeeController.login() 后:

  1. 调 employeeService.login() 查库、比对 MD5 密码
  2. 验证通过 → JwtUtil.createJWT() 生成令牌,把员工 id 塞进载荷
  3. 装进 EmployeeLoginVO(含 id、用户名、姓名、token)→ Result.success(vo) 返回

前端拿到后把 token 存起来,之后每个请求都放在请求头 token 里带上。

第 1 步:nginx 反向代理

前端发 POST /api/employee,nginx 按配置 location /api/ → proxy_pass http://localhost:8080/admin/,把路径重写成 /admin/employee 转给后端。这一步是路径前缀的替换,前端代码里写 /api/...,后端 Controller 写的是 /admin/...。

第 2 步:SpringMVC 分发 + JWT 拦截器

请求先进 DispatcherServlet,它在调用 Controller 之前 先过 JwtTokenAdminInterceptor.preHandle():

java 复制代码
String token = request.getHeader(jwtProperties.getAdminTokenName());  // 取请求头
Claims claims = JwtUtil.parseJWT(...);                                 // 解析令牌
Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID)...);
BaseContext.setCurrentId(empId);   // ← 存进 ThreadLocal
return true;                        // 放行

这里有两件事:

  • 令牌有效 → 放行进 Controller
  • 令牌无效/过期 → response.setStatus(401); return false; ------ 请求直接终止,压根进不了 Controller,前端收到 401 后跳回登录页
  • 顺带把当前员工的 id 存进了 ThreadLocal (BaseContext),这点下面会用到

第 3 步:Controller

java 复制代码
@PostMapping
public Result save(@RequestBody EmployeeDTO employeeDTO) {
    employeeService.save(employeeDTO);
    return Result.success();
}

@RequestBody 让 SpringMVC 用 Jackson 把请求体的 JSON 自动反序列化成 EmployeeDTO 对象。注意分页接口不加这个注解,因为参数是 URL query 形式(?page=1&pageSize=10),SpringMVC 会直接按属性名绑定。

第 4 步:Service ------ 业务逻辑 + 造 Entity

java 复制代码
Employee employee = new Employee();
BeanUtils.copyProperties(employeeDTO, employee);   // DTO → Entity 属性拷贝
employee.setStatus(StatusConstant.ENABLE);          // 默认启用
employee.setPassword(md5("123456"));                // 默认密码加密
employee.setCreateTime(LocalDateTime.now());
employee.setCreateUser(BaseContext.getCurrentId());  // ← 从 ThreadLocal 取出!
employee.setUpdateUser(BaseContext.getCurrentId());
employeeMapper.insert(employee);

这就是 ThreadLocal 的作用 :Controller 和 Service 的方法签名里都没有"当前登录人"这个参数,但新增员工要记录"谁创建的"。拦截器已经在第 2 步把 empId 存进了当前线程的 ThreadLocal,Service 直接取就行。因为一次请求从头到尾都在同一个线程里处理,所以能透传过来。

第 5 步:Mapper → MySQL

你的 EmployeeMapper 现在用的是注解方式:

java 复制代码
@Insert("insert into employee(...) values(...)")
void insert(Employee employee);

MyBatis 拿到接口方法 → 执行 SQL → 写库。

第 6 步:返回

走回头路,逐层往上:

复制代码
Mapper 返回  → Service 返回  → Controller 拿到结果
                             ↓
                    Result.success() / Result.success(vo)
                             ↓
        SpringMVC 消息转换器把 Result 对象序列化成 JSON
                             ↓
              HTTP 响应 → nginx → 浏览器

最后那步"对象变 JSON"是 @RestController 干的活,靠 Jackson 自动完成,不用写代码。

二、异常处理的分支

Service 里发现不合规就抛异常,不自己 try-catch:

java 复制代码
if (employee == null)  throw new AccountNotFoundException("账号不存在");
if (!password.equals(...)) throw new PasswordErrorException("密码错误");
if (employee.getStatus() == DISABLE) throw new AccountLockedException("账号被锁定");

这些异常类都继承 BaseException,往上抛,穿过 Controller,最后被 GlobalExceptionHandler 拦下:

java 复制代码
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler
    public Result exceptionHandler(BaseException ex) {
        log.error("异常信息:{}", ex.getMessage());
        return Result.error(ex.getMessage());   // code=0, msg="密码错误"
    }
    @ExceptionHandler
    public Result exceptionHandler(SQLIntegrityConstraintViolationException ex) {
        // 用户名重复:从 "Duplicate entry 'zhangsan' for key ..." 里抠出 zhangsan
        return Result.error(userName + "已存在");
    }
}

所以响应状态码始终是 200 ,靠 code 字段区分成败(0=失败)。前端 axios 拿到后判断 code === 0 就弹出 msg。

这样设计的好处:Service 里不用写一堆 if...else return 错误信息,业务逻辑清爽,错误处理集中在一处。

一句话串起来: nginx 转发 → 拦截器验令牌(顺便记下是谁)→ Controller 收 JSON 转成 DTO → Service 跑业务(出问题就抛异常)→ Mapper 操作数据库 → 返回时 Entity 转 VO 再套 Result → 序列化成 JSON 回前端;中途抛的异常被 GlobalExceptionHandler 统一转成 code=0 的 Result。

相关推荐
萧瑟余晖1 小时前
Spring Security 6.x新特性与架构演进详解
java·spring·架构
太理摆烂哥1 小时前
MySQL数据类型
数据库·mysql
\光辉岁月/1 小时前
1.Object类
java·面向对象·object类
程序员小孔.1 小时前
基于 SpringBoot + UniApp + Vue 的家政服务系统设计与实现
前端·数据库·spring boot·python·docker·微信小程序
鸽鸽程序猿1 小时前
【项目】【在线判题系统】公共功能
java·spring boot·maven
一只笨菜鸟1 小时前
IDEA如何让多个项目同时显示
java·ide·intellij-idea
程序猿乐锅2 小时前
【从0-1学习Python | 第六篇】 Python 函数:定义、参数与返回值
开发语言·python·学习
G018_star sky♬2 小时前
java中的字符串常见方法
java·开发语言
熊猫钓鱼>_>2 小时前
Kotlin Multiplatform for OpenHarmony 实战:kotlinx-serialization JSON 适配
开发语言·华为·kotlin·华为云·json·harmonyos·code