赛克能源管理系统全栈开发复盘:从零完成 SpringBoot + MySQL + Redis 项目,踩遍 Web 经典坑后我总结了这些教训
文章速读本文是一篇完整的 Java Web 全栈项目实战复盘。从项目构思、技术选型、用户模块开发、登录认证MD5 JWT Redis、接口联调到最终部署在阿里云服务器我完整走了一遍企业级 Web 应用的开发流程。文中真实记录了 5 个最常见的“劝退级”错误400 参数绑定异常、数据库字段非空导致的 500、MyBatis 映射失败、context-path 引起的 404、云服务器部署端口与进程问题。每个问题都附带了错误日志 → 原因分析 → 解决方案 → 验证方法并提炼成一张【排错速查表】。如果你是正在学习 SpringBoot 的初学者这篇文章至少能帮你少踩 10 个坑。 目录一、项目背景与开发心路二、技术选型全景解读2.1 技术栈表格2.2 为什么选这套组合三、项目结构与分层设计3.1 代码目录树3.2 核心数据库表设计四、用户模块 CRUD三个“新手必炸”的坑坑 1RequestBody 用错导致 400坑 2数据库非空字段引发的 500坑 3MyBatis-Plus 驼峰映射失效五、登录认证模块MD5 JWT Redis深度实现5.1 为什么不只用 JWT5.2 完整流程图Mermaid5.3 核心代码与关键注释六、接口联调中的两个“隐藏杀手”6.1 context-path 导致的 4046.2 跨域问题CORS及 Nginx 解决方案七、阿里云部署实战从 0 到上线7.1 环境准备与端口矩阵7.2 数据库与 Redis 配置7.3 Jar 包部署与后台启动7.4 Nginx 反向代理配置7.5 部署中踩的 2 个真实坑八、面向高分的排错方法论总结8.1 排错五步法8.2 常见 Web 错误速查表九、后续优化计划含 Docker 方向十、致谢与互动一、项目背景与开发心路本学期《Web 应用项目开发》课程要求独立完成一个具有实际业务场景的全栈项目。我选择了“赛克能源管理系统”——一个面向企业内部能源数据管理的小型系统核心模块包括用户管理增删改查登录认证JWT Redis能耗数据填报与统计接口预留后续扩展整个项目耗时约20 天从最初连Autowired和Resource都分不清到最后在阿里云上稳定运行。这期间我经历了无数次 400、404、500也曾在凌晨两点盯着Field nick_name doesnt have a default value怀疑人生。但正是这些错误让我真正理解了 Web 开发的本质。本文不会只展示“最终正确代码”而是完整还原每一次踩坑、定位、解决问题的全过程。二、技术选型全景解读2.1 技术栈表格层级技术版本核心作用前端HTML5 CSS3 ES6—基础页面与交互后续可升级为 Vue后端框架SpringBoot2.7.6简化配置、内嵌 Tomcat、自动装配ORMMyBatis-Plus3.5.5单表操作零 SQL分页插件友好数据库MySQL8.0.31持久化存储用户、能耗数据缓存Redis5.0.14存储 JWT 令牌实现主动失效安全JWT MD5—无状态认证 密码不可逆加密部署阿里云轻量 宝塔 NginxCentOS 7.9生产环境部署与反向代理工具IDEA Navicat Postman Git2023 / 16开发、调试、版本控制2.2 为什么选这套组合SpringBoot 2.7稳定版本文档丰富社区活跃同时兼容我后续想整合的 Spring Security。MyBatis-Plus相比原生 MyBatis 省去大量 XML 配置且自带乐观锁、分页、代码生成器。Redis JWT纯 JWT 无法做到服务端主动失效比如踢用户下线结合 Redis 缓存 token 后登出时删除 Redis key 即可完美解决。宝塔面板对新手最友好的 Linux 运维工具文件管理、端口放行、Nginx 配置都可可视化操作。三、项目结构与分层设计3.1 代码目录树textsrc/main/java/com/saike/ems/ ├── common │ ├── R.java // 统一响应结果封装 │ └── ResultCode.java // 业务状态码枚举 ├── config │ ├── CorsConfig.java // 跨域配置 │ ├── RedisConfig.java // Redis 序列化配置 │ └── MybatisPlusConfig.java // 分页插件配置 ├── controller │ └── UserController.java ├── service │ ├── IUserService.java │ └── impl │ └── UserServiceImpl.java ├── mapper │ └── UserMapper.java ├── entity │ └── User.java ├── dto │ └── LoginDTO.java ├── interceptor │ └── JwtInterceptor.java // 全局令牌校验 └── utils ├── Md5Util.java └── JwtUtil.java3.2 核心数据库表设计sqlCREATE TABLE user ( user_id int NOT NULL AUTO_INCREMENT COMMENT 用户ID, user_name varchar(50) NOT NULL COMMENT 用户名, password char(32) NOT NULL COMMENT MD5加密后的密码, nick_name varchar(50) DEFAULT 赛克用户 COMMENT 昵称, status tinyint DEFAULT 1 COMMENT 状态1正常 0禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (user_id), UNIQUE KEY uk_user_name (user_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;设计说明password固定 32 位MD5 输出长度nick_name设置了默认值避免新增时因非空约束失败这就是坑 2 的根源用户名唯一索引防止重复注册四、用户模块 CRUD三个“新手必炸”的坑坑 1RequestBody 用错导致 400现象前端用 Postman 以x-www-form-urlencoded方式提交后端报错textorg.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing错误代码javaPostMapping(/add) public R addUser(RequestBody User user) { // ❌ 错误 return userService.save(user) ? R.ok() : R.fail(); }分析RequestBody要求请求Content-Type: application/json并且 body 中是 JSON 字符串。而表单提交的Content-Type是application/x-www-form-urlencoded参数以key1value1key2value2形式放在 body 中两者完全不兼容。正确写法javaPostMapping(/add) public R addUser(User user) { // ✅ 直接使用实体接收 return userService.save(user) ? R.ok() : R.fail(); }或者显式接收参数javapublic R addUser(RequestParam String userName, RequestParam String password) { User user new User(); user.setUserName(userName); user.setPassword(Md5Util.md5(password)); // ... }✅ 验证Postman 中选择x-www-form-urlencoded输入字段后请求返回 200。坑 2数据库非空字段引发的 500现象新增用户时控制台报错text### SQL: INSERT INTO user (user_name, password) VALUES ( ?, ? ) ### Cause: java.sql.SQLException: Field nick_name doesnt have a default value分析数据库nick_name字段定义为NOT NULL且建表时未设置DEFAULT值。而实体类中nickName默认为nullMyBatis-Plus 生成的 INSERT 语句只包含user_name和password导致数据库拒绝插入。解决方案方案一推荐修改数据库增加默认值sqlALTER TABLE user MODIFY COLUMN nick_name varchar(50) DEFAULT 默认昵称 NOT NULL;方案二代码兜底在业务层判断并设置默认值javaif (user.getNickName() null || user.getNickName().isEmpty()) { user.setNickName(赛克用户); }✅ 验证再次执行 INSERT日志显示插入成功数据库中出现新增记录。坑 3MyBatis-Plus 驼峰映射失效现象查询用户列表返回的username字段全是null但数据库中user_name有值。分析MyBatis-Plus 默认开启mapUnderscoreToCamelCase会将user_name映射为userName。而我实体类中属性名是username全小写无法匹配。错误实体类javaTableName(user) public class User { private Integer userId; private String username; // ❌ 数据库是 user_name }正确实体类javaTableName(user) public class User { TableId(user_id) private Integer userId; TableField(user_name) // ✅ 显式映射 private String username; TableField(nick_name) private String nickName; }或者统一命名风格将实体类属性改为userName利用默认驼峰转换推荐。✅ 验证查询接口返回数据中 username 正常显示。五、登录认证模块MD5 JWT Redis深度实现5.1 为什么不只用 JWT纯 JWT 的痛点无法主动失效。用户修改密码或退出登录后旧 token 在过期前仍然有效。解决方案服务端将 JWT 存入 Rediskey 为用户唯一标识如 userIdvalue 为 token。每次请求携带 token 时不仅要校验 JWT 签名还要校验 Redis 中的 token 是否存在且一致。5.2 完整流程图Mermaid5.3 核心代码与关键注释javaPostMapping(/login) public RString login(RequestBody LoginDTO loginDTO) { // 1. 查询用户已写在service中 User user userService.getByUserName(loginDTO.getUserName()); if (user null) { return R.fail(用户名不存在); } // 2. 密码校验MD5 String encrypted Md5Util.md5(loginDTO.getPassword()); if (!encrypted.equals(user.getPassword())) { return R.fail(密码错误); } // 3. 生成 JWT MapString, Object claims new HashMap(); claims.put(userId, user.getUserId()); claims.put(userName, user.getUserName()); String token JwtUtil.generate(claims); // 4. 存储到 Redis1小时过期 String redisKey token:user: user.getUserId(); stringRedisTemplate.opsForValue().set(redisKey, token, 1, TimeUnit.HOURS); return R.ok(token); }登出逻辑删除 Redis key 即可实现主动失效。javaPostMapping(/logout) public RString logout(RequestHeader(Authorization) String token) { // 解析token获取userId Integer userId JwtUtil.getUserId(token); stringRedisTemplate.delete(token:user: userId); return R.ok(已退出); }六、接口联调中的两个“隐藏杀手”6.1 context-path 导致的 404现象前端请求/user/list返回 404但后端明明写了GetMapping(/user/list)。分析application.yml中配置了server.servlet.context-path: /api导致后端实际路径为/api/user/list。前端请求没有加/api。解决统一规范前端 axios 配置baseURL: /api所有请求自动加上前缀。6.2 跨域问题CORS及 Nginx 解决方案开发阶段直接在 SpringBoot 中配置跨域过滤器javaConfiguration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOrigin(*); config.addAllowedMethod(*); config.addAllowedHeader(*); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }生产环境更推荐用 Nginx 反向代理解决见第七部分。七、阿里云部署实战从 0 到上线7.1 环境准备与端口矩阵端口服务宝塔放行阿里云安全组放行80Nginx✅✅9763SpringBoot Jar✅✅3306MySQL❌ 仅本地❌6379Redis❌ 仅本地❌8888宝塔面板✅✅7.2 数据库与 Redis 配置在宝塔中创建数据库ems导入 SQL 文件。修改application-prod.ymlyamlspring: datasource: url: jdbc:mysql://localhost:3306/ems?useSSLfalseserverTimezoneAsia/Shanghai username: root password: 你的密码 redis: host: localhost port: 63797.3 Jar 包部署与后台启动bash# 上传jar包到 /www/wwwroot/ems cd /www/wwwroot/ems # 停止旧进程如果有 pkill -f ems-0.0.1-SNAPSHOT.jar # 后台启动 nohup java -jar ems-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod logs/ems.log 21 # 查看日志 tail -f logs/ems.log7.4 Nginx 反向代理配置nginxserver { listen 80; server_name your-domain.com; # 前端静态文件 location / { root /www/wwwroot/ems-front; index index.html; } # 后端API代理解决跨域 location /api/ { proxy_pass http://127.0.0.1:9763/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }7.5 部署中踩的 2 个真实坑坑 1阿里云安全组开了 9763但宝塔防火墙没开 → 仍然无法访问 → 两边都要放行。坑 2多次部署后出现Address already in use→lsof -i:9763找到进程kill -9再重启。八、面向高分的排错方法论总结8.1 排错五步法步骤行动产出1. 复现用相同参数再次请求记录完整错误信息错误现象确定2. 看日志后端控制台 /tail -f server.log异常堆栈 错误行号3. 定位根据堆栈找到对应代码行问题代码位置4. 查证搜索引擎 官方文档 Stack Overflow2~3 种候选方案5. 验证每次只改一个变量测试通过后复盘永久解决问题8.2 常见 Web 错误速查表HTTP 状态常见原因排查方向400参数类型不匹配 /RequestBody用错检查请求头 Content-Type对比后端参数注解401未认证或 Token 失效查看 Redis 中 token 是否存在检查 JWT 过期时间404路径错误 / context-path 遗漏对比后端RequestMapping和前端的完整 URL500数据库字段缺失 / 空指针查看完整 SQL 日志检查数据库非空约束502后端服务未启动 / 端口不通ps -ef | grep java检查防火墙九、后续优化计划含 Docker 方向统一异常处理RestControllerAdvice接管所有异常返回规范格式。参数校验引入spring-boot-starter-validation使用NotNull等注解。Redis 验证优化在拦截器中实现 token 校验避免每个方法重复写。容器化部署编写Dockerfiledocker-compose.yml一键启动 MySQL、Redis、后端。dockerfile# 示例 Dockerfile FROM openjdk:11-jre COPY target/ems-0.0.1-SNAPSHOT.jar app.jar ENTRYPOINT [java, -jar, /app.jar]十、致谢与互动这门课程让我从“纸上谈兵”走向真正的工程实践。感谢老师每节课对 Web 底层原理的剖析感谢室友在我通宵调 Bug 时给予的精神支持。如果本文帮你解决了一个实际 Bug欢迎点赞 / 收藏 / 评论。你在开发中遇到过哪些“离谱”的错误评论区一起交流我会定期回复。