背景痛点:为什么你的毕设总是一团麻
做毕设最怕“跑起来就行”。很多同学把代码全写在 Controller 里,一个方法里既查库又算折扣还顺手发邮件,结果:
- 需求一改,牵一发动全身,调试靠“打桩+重启”;
- Service 层复制粘贴,同名变量满天飞,自己三天后都看不懂;
- 事务注解乱贴,付款失败订单却生成,老师一问就“偶发”;
- 部署到服务器,日志一屏红,敏感信息直接 console 打印,现场翻车。
归根结底:缺一套“能讲清楚、能改得动、能跑得稳”的骨架。下面这套 SpringBoot 3.x 模板,是我当年答辩 90→95 分的翻盘笔记,今天拆给你。
技术选型:为什么不是 JPA、不是 Spring Security 全家桶
- SpringBoot 3.x:内置虚拟线程、原生编译支持,答辩提到“新技术”能加分。
- MyBatis-Plus:
- 比 JPA 好写复杂 SQL,毕设里多表统计、模糊分页是常态;
- 内置代码生成器,两分钟撸完 Entity→Mapper→Service→Controller,肉眼可见节约时间。
- H2(本地)+ MySQL(生产):
- H2 文件模式,git push 即走,队友拉下来直接跑,0 搭建成本;
- 切到 MySQL 只需改一条 jdbc-url,答辩演示“秒级切换”很帅。
- Lombok:消灭 60% Get/Set/Builder,代码行数立降,老师一眼看过去“清爽”。
- 不选 Spring Security:
- 对新手太重,过滤器链讲半小时都说不清;
- JWT 自己写 80 行就够,答辩能讲清“签发→验签→续期”三步,反而体现掌握。
核心实现:一张图先看清包结构
com.example.demo ├── controller // 仅处理 http,一行业务逻辑都不写 ├── service // 事务边界,一个 public 方法一个事务 ├── mapper // MyBatis-Plus,纯 CRUD ├── domain // 仅含 POJO/实体,不依赖任何框架 ├── common │ ├── config // 全局配置(MyBatis、Redis、JWT) │ ├── exception // 统一异常+返回封装 │ └── utils // 雪花算法、JwtUtil、BCrypt DemoApplication.java1. 分层约定
- Controller 只做三件事:参数校验、调用 Service、返回 VO。
- Service 绝不出现 HttpServletRequest,保证以后可无缝迁移到 Dubbo。
- Mapper 层禁止手写 SQL 拼接,全部用 MyBatis-Plus 条件构造器,防注入。
2. RESTful 路由规范
GET /api/users 列表 POST /api/users 新增 GET /api/users/{id} 详情 PUT /api/users/{id} 全量更新 PATCH /api/users/{id}/status 局部更新 DELETE /api/users/{id} 删除统一返回体:
@Getter @AllArgsConstructor public class R<T> { private int code; private String msg; private T data; public static <T> R<T> ok(T data){ return new R<>(200,"success",data); } public static R<Void> fail(String msg){ return new R<>(500,msg,null); } }3. 全局异常拦截
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BizException.class) public R<Void> handleBiz(BizException e){ return R.fail(e.getMessage()); } @ExceptionHandler(Exception.class) public R<Void> handleAll(Exception e){ log.error("系统异常", e); return R.fail("服务器开小差~"); } }业务里想抛就抛,Controller 里看不见 try-catch,清爽。
4. JWT 基础认证(最简版)
工具类:
public class JwtUtil { private static final String KEY = "demo2025"; private static final long EXPIRE = 864_000_00; // 1d public static String create(Long userId){ return Jwts.builder() .setSubject(userId.toString()) .setExpiration(new Date(System.currentTimeMillis()+EXPIRE)) .signWith(SignatureAlgorithm.HS256, KEY) .compact(); } public static Long parse(String jwt){ try{ String sub = Jwts.parser().setSigningKey(KEY) .parseClaimsJws(jwt).getBody().getSubject(); return Long.valueOf(sub); }catch(JwtException e){ return null; } } }拦截器:
@Component public class JwtInterceptor implements HandlerInterceptor { public boolean preHandle(HttpServletRequest rq, HttpServletResponse rp, Object h) throws Exception { String token = rq.getHeader("Authorization"); if(token==null不设Bearer) throw new BizException("缺少令牌"); Long uid = JwtUtil.parse(token.replace("Bearer ","")); if(uid==null) throw new BizException("令牌无效"); rq.setAttribute("uid",uid); // 下游直接取 return true; } }注册拦截器放过登录接口即可,答辩时老师问“怎么保证安全”——答:签名密钥存服务器,过期可续期,敏感接口全拦截。
完整代码示例:User 模块 CRUD
下面代码全部可跑,注释已写好,直接粘。
1. 实体 & 枚举
@Data @TableName("t_user") public class User { @TableId(type = IdType.ASSIGN_ID) // 雪花算法 private Long id; private String username; private String password; // 已加密 private Integer status; // 0=禁用 1=启用 private LocalDateTime createTime; }2. Mapper
public interface UserMapper extends BaseMapper<User> { // 连表统计自己写,简单 CRUD 直接用 IService }3. Service 接口 & 实现
public interface IUserService extends IService<User> { Long createUser(UserDTO dto); void disable(Long id); } @Service @RequiredArgsConstructor public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements IUserService { @Override @Transactional(rollbackFor = Exception.class) public Long createUser(UserDTO dto){ // 1. 重复名校验 long c = lambdaQuery().eq(User::getUsername,dto.getUsername()).count(); if(c>0) throw new BizException("用户名已存在"); // 2. 加密 User u = new User(); u.setUsername(dto.getUsername()); u.setPassword(BCrypt.hashpw(dto.getPassword(), BCrypt.gensalt())); u.setStatus(1); u.setCreateTime(LocalDateTime.now()); // 3. 落库 save(u); return u.getId(); } @Override @Transactional public void disable(Long id){ lambdaUpdate().set(User::getStatus,0).eq(User::getId,id).update(); } }4. Controller
@RestController @RequestMapping("/api/users") @RequiredArgsConstructor public class UserController { private final IUserService userService; @PostMapping public R<Long> create(@Valid @RequestBody UserDTO dto){ return R.ok(userService.createUser(dto)); } @GetMapping("/{id}") public R<UserVO> get(@PathVariable Long id){ User u = userService.getById(id); if(u==null) throw new BizException("用户不存在"); UserVO vo = new UserVO(); BeanUtils.copyProperties(u,vo); return R.ok(vo); } @PatchMapping("/{id}/status") public R<Void> disable(@PathVariable Long id){ userService.disable(id); return R.ok(null); } }DTO/VO 用 MapStruct 转,篇幅略。至此,User 模块写完,Service 层一个 public 方法一个事务,回滚点清晰。
性能与安全:把“能跑”升级成“敢上线”
- SQL 注入:MyBatis-Plus 条件构造器内部预编译,只要不用
wrapper.apply("xxx="+var)就安全。 - 密码加密:BCrypt 加盐,明文在任何日志里不可见。
- 接口幂等:
- 新增带“username 唯一”约束,重复重试会抛业务异常,天然幂等;
- 更新用乐观锁
@Version,更新行数=0 抛“数据已变更”提示。
- 关键日志脱敏:
log.info("用户注册: username={}", username.replaceAll("(?<=\\w{2})\\w","*"));- 慢 SQL 监控:MyBatis-Plus 自带
PerformanceInterceptor,超 500ms 自动打印,答辩展示“性能保障”。
生产环境避坑 6 条
- Controller 直接调 Mapper → 事务失效,Service 层必须包一层。
- 配置分离:
application.yml只放默认,真实密码放在application-prod.yml,服务器上用环境变量覆盖,git 不留痕。 - 禁止用
System.out→ 用 Slf4j + Logback,文件按天滚动,日志目录外挂硬盘,防止打爆磁盘。 - 跨域放生产:
spring.web.cors.allowed-origins=https://www.xxx.com,不要写*。 - 文件上传别放工程目录,单独建
/data/upload,重启容器不丢。 - 服务器时间不同步会导致 JWT 验签失败,装 ntpdate 定时同步。
结语:把模板变成你自己的“高分作品”
上面这套骨架,我帮三位学弟套过外卖、二手书、实验室预约三个完全不同业务的毕设,平均答辩分数 92。它最大的价值不是代码行数,而是“能讲清”——每层职责、每个异常、每条日志都能回答老师“为什么”。
下一步,把 demo 拉下来,删掉 User,换上你的业务:课程?宠物?民宿?重构过程中你会遇到“一对多分页”“多角色鉴权”“分布式文件”等真实问题,再把解决过程写进论文,那就是货真价实的“工作量”。
祝你编码愉快,答辩时也能自信地说:
“我的系统支持水平扩展,下一版加微服务只需换注解,老师您要看哪个模块?”