大家好,我是专注于分享后端实战经验的博主。在开发各类管理系统时,我们常常需要将复杂的业务逻辑、数据管理、权限控制和安全规范整合到一个高效、可维护的系统中。今天,我们就以“法律援助管理系统”这一具有社会价值的应用场景为例,手把手带你从零开始,基于 Spring Boot 这一主流框架,设计并实现一个功能完整、结构清晰的后端系统。无论你是正在学习 Spring Boot 的在校学生,还是需要快速搭建业务原型的开发者,这篇文章都将为你提供一套可直接复用的“脚手架”和深入骨髓的工程化思考。
1. 系统背景与核心需求分析
在深入代码之前,我们必须明确要解决什么问题。法律援助管理系统旨在为法律援助中心、律师事务所或相关公益组织提供一个数字化的管理平台,核心目标是提升案件处理效率、规范工作流程、并保障受援人的信息安全。
一个典型的系统需要涵盖以下核心模块:
- 用户与权限管理:区分系统管理员、法律援助律师、案件受理员、受援人等不同角色,实现精细化的功能与数据权限控制。
- 案件全生命周期管理:从案件申请、受理、指派律师、办理、结案到归档,实现线上化流程跟踪。
- 资源与知识库管理:管理律师信息、法律法规库、典型案例等,为案件办理提供支持。
- 数据统计与报表:为管理者提供案件数量、类型分布、律师工作量等可视化数据。
选择 Spring Boot 作为后端框架,是因为它能极大地简化 Spring 应用的初始搭建和开发过程,通过自动配置和起步依赖,让我们能快速聚焦于业务逻辑本身,而非繁琐的 XML 配置。结合 MyBatis-Plus、Spring Security、JWT 等生态组件,可以高效、稳健地构建出符合生产要求的系统。
2. 技术选型与开发环境准备
工欲善其事,必先利其器。一个清晰的技术栈和稳定的开发环境是项目成功的基石。
2.1 核心技术栈
- 后端框架:Spring Boot 2.7.x (一个长期支持版本,稳定且社区资源丰富)
- 安全框架:Spring Security + JWT (用于认证与授权)
- ORM框架:MyBatis-Plus 3.5.x (极大简化单表CRUD,同时保留MyBatis的灵活性)
- 数据库:MySQL 8.0 (关系型数据库,存储核心业务数据)
- 缓存:Redis (用于存储会话、验证码、热点数据)
- API文档:Knife4j (Swagger的增强UI,便于前后端协作与接口调试)
- 项目构建:Maven 3.6+
- JDK版本:Java 11 或 17 (LTS版本)
2.2 开发环境与工具
- 操作系统:Windows 10/11, macOS 或 Linux 均可。
- IDE:IntelliJ IDEA (社区版或旗舰版) 或 Eclipse with STS。
- 数据库工具:Navicat, DBeaver 或 MySQL Workbench。
- API测试工具:Postman 或 Insomnia。
2.3 初始化Spring Boot项目
最快捷的方式是使用 Spring Initializr 生成项目骨架。我们选择以下依赖:
- Spring Web(构建Web应用,包含Tomcat)
- Spring Security(安全框架)
- MyBatis Framework(基础MyBatis)
- MySQL Driver(数据库连接)
- Lombok(简化实体类代码)
生成并解压后,用IDE打开项目。接下来,我们需要手动在pom.xml中添加一些关键的依赖。
<!-- pom.xml 补充依赖 --> <dependencies> <!-- 之前Initializr生成的依赖... --> <!-- MyBatis-Plus 增强 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3</version> </dependency> <!-- JWT 支持 --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <!-- Redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- Knife4j API文档 --> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> <version>3.0.3</version> </dependency> <!-- 参数校验 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> </dependencies>3. 项目架构设计与核心配置
良好的目录结构是代码可维护性的第一步。我们采用常见的分层架构。
3.1 项目目录结构
src/main/java/com/example/legalaid/ ├── LegalAidApplication.java # 启动类 ├── config/ # 配置类 │ ├── MybatisPlusConfig.java │ ├── RedisConfig.java │ ├── SecurityConfig.java │ └── SwaggerConfig.java ├── controller/ # 控制层,接收请求 ├── service/ # 业务逻辑层 │ └── impl/ # 业务逻辑实现层 ├── mapper/ # 数据访问层 (MyBatis Mapper接口) ├── entity/ # 实体类,与数据库表对应 ├── dto/ # 数据传输对象,用于前后端交互 ├── vo/ # 视图对象,用于接口返回 ├── common/ # 通用组件 │ ├── constant/ # 常量类 │ ├── exception/ # 自定义异常 │ ├── result/ # 统一响应封装 │ └── utils/ # 工具类 (如JWT工具) └── filter/ # 过滤器 (如JWT认证过滤器)3.2 数据库设计与核心实体
我们设计几个核心表来支撑基本功能。这里以用户表、案件表为例。
-- 用户表 (sys_user) CREATE TABLE `sys_user` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '加密后的密码', `real_name` varchar(20) DEFAULT NULL COMMENT '真实姓名', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `email` varchar(50) DEFAULT NULL COMMENT '邮箱', `avatar` varchar(255) DEFAULT NULL COMMENT '头像', `role_code` varchar(20) NOT NULL COMMENT '角色编码 (ADMIN, LAWYER, CLERK, APPLICANT)', `status` tinyint DEFAULT '1' COMMENT '状态 (0:禁用,1:正常)', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表'; -- 案件表 (legal_case) CREATE TABLE `legal_case` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '案件ID', `case_no` varchar(50) NOT NULL COMMENT '案件编号', `title` varchar(200) NOT NULL COMMENT '案件标题', `applicant_id` bigint DEFAULT NULL COMMENT '申请人ID (关联sys_user.id)', `applicant_name` varchar(50) DEFAULT NULL COMMENT '申请人姓名', `case_type` varchar(20) DEFAULT NULL COMMENT '案件类型 (民事、刑事、行政)', `content` text COMMENT '案情描述', `status` varchar(20) DEFAULT 'PENDING' COMMENT '状态 (PENDING:待受理, ACCEPTED:已受理, ASSIGNED:已指派, PROCESSING:办理中, CLOSED:已结案)', `assigned_lawyer_id` bigint DEFAULT NULL COMMENT '指派律师ID', `assigned_lawyer_name` varchar(50) DEFAULT NULL COMMENT '指派律师姓名', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_case_no` (`case_no`), KEY `idx_applicant_id` (`applicant_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='法律援助案件表';对应的Java实体类使用Lombok简化代码:
// entity/SysUser.java package com.example.legalaid.entity; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.time.LocalDateTime; @Data @TableName("sys_user") public class SysUser { @TableId(type = IdType.AUTO) private Long id; private String username; private String password; private String realName; private String phone; private String email; private String avatar; private String roleCode; // 角色编码 private Integer status; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }3.3 关键配置文件
配置是Spring Boot的灵魂。我们需要配置数据库、Redis以及MyBatis-Plus。
# application.yml spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/legal_aid_db?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai username: root password: your_password redis: host: localhost port: 6379 database: 0 # password: 如果Redis有密码则配置 # MyBatis-Plus 配置 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台打印SQL,生产环境关闭 global-config: db-config: id-type: auto logic-delete-field: deleted # 全局逻辑删除字段名(如果表中有此字段) logic-delete-value: 1 # 逻辑已删除值 logic-not-delete-value: 0 # 逻辑未删除值 mapper-locations: classpath:mapper/*.xml # XML映射文件位置 # 自定义应用配置 legal-aid: jwt: secret: yourJwtSecretKeyHereMakeItLongAndComplex # JWT密钥,务必复杂且保密 expiration: 86400000 # token有效期,单位毫秒 (24小时)4. 核心功能模块实现
接下来,我们实现几个最核心的功能模块:统一响应封装、用户登录认证(JWT)、以及案件管理的CRUD。
4.1 统一响应结果封装
为了规范接口返回格式,我们创建一个通用的结果类。
// common/result/Result.java package com.example.legalaid.common.result; import lombok.Data; import java.io.Serializable; @Data public class Result<T> implements Serializable { private Integer code; private String message; private T data; public static <T> Result<T> success() { return success(null); } public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } // 可以定义更多便捷方法,如 error(String message) }4.2 JWT工具类与登录逻辑
首先创建JWT工具类,用于生成和解析Token。
// common/utils/JwtUtil.java package com.example.legalaid.common.utils; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.util.Date; import java.util.HashMap; import java.util.Map; @Component public class JwtUtil { @Value("${legal-aid.jwt.secret}") private String secret; @Value("${legal-aid.jwt.expiration}") private Long expiration; // 生成Token public String generateToken(String username, String roleCode) { Map<String, Object> claims = new HashMap<>(); claims.put("username", username); claims.put("role", roleCode); return Jwts.builder() .setClaims(claims) .setSubject(username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + expiration)) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } // 从Token中解析用户名 public String getUsernameFromToken(String token) { return getClaimsFromToken(token).getSubject(); } // 从Token中解析角色 public String getRoleFromToken(String token) { return (String) getClaimsFromToken(token).get("role"); } // 验证Token是否过期 public Boolean isTokenExpired(String token) { Date expiration = getClaimsFromToken(token).getExpiration(); return expiration.before(new Date()); } private Claims getClaimsFromToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } }然后,实现用户登录的Service和Controller。
// dto/LoginDTO.java package com.example.legalaid.dto; import lombok.Data; import javax.validation.constraints.NotBlank; @Data public class LoginDTO { @NotBlank(message = "用户名不能为空") private String username; @NotBlank(message = "密码不能为空") private String password; }// service/impl/AuthServiceImpl.java package com.example.legalaid.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.example.legalaid.common.utils.JwtUtil; import com.example.legalaid.dto.LoginDTO; import com.example.legalaid.entity.SysUser; import com.example.legalaid.mapper.SysUserMapper; import com.example.legalaid.service.AuthService; import com.example.legalaid.vo.LoginVO; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.Map; @Service @RequiredArgsConstructor // Lombok注解,自动注入final字段 public class AuthServiceImpl implements AuthService { private final SysUserMapper userMapper; private final JwtUtil jwtUtil; private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); @Override public LoginVO login(LoginDTO loginDTO) { // 1. 查询用户 SysUser user = userMapper.selectOne(new LambdaQueryWrapper<SysUser>() .eq(SysUser::getUsername, loginDTO.getUsername())); if (user == null) { throw new RuntimeException("用户名或密码错误"); } // 2. 校验密码 (存储时应为加密后的密码) if (!passwordEncoder.matches(loginDTO.getPassword(), user.getPassword())) { throw new RuntimeException("用户名或密码错误"); } // 3. 校验状态 if (user.getStatus() != 1) { throw new RuntimeException("账号已被禁用,请联系管理员"); } // 4. 生成JWT Token String token = jwtUtil.generateToken(user.getUsername(), user.getRoleCode()); // 5. 构造返回结果 LoginVO loginVO = new LoginVO(); loginVO.setUserId(user.getId()); loginVO.setUsername(user.getUsername()); loginVO.setRealName(user.getRealName()); loginVO.setRoleCode(user.getRoleCode()); loginVO.setToken(token); return loginVO; } }// controller/AuthController.java package com.example.legalaid.controller; import com.example.legalaid.common.result.Result; import com.example.legalaid.dto.LoginDTO; import com.example.legalaid.service.AuthService; import com.example.legalaid.vo.LoginVO; import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/auth") @RequiredArgsConstructor public class AuthController { private final AuthService authService; @PostMapping("/login") public Result<LoginVO> login(@Validated @RequestBody LoginDTO loginDTO) { LoginVO loginVO = authService.login(loginDTO); return Result.success(loginVO); } }4.3 案件管理CRUD与分页查询
使用MyBatis-Plus可以快速实现基础数据操作。首先创建Mapper接口。
// mapper/LegalCaseMapper.java package com.example.legalaid.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.example.legalaid.entity.LegalCase; import org.apache.ibatis.annotations.Param; public interface LegalCaseMapper extends BaseMapper<LegalCase> { // 自定义复杂分页查询示例:根据状态和标题关键词查询 IPage<LegalCase> selectCasePage(Page<LegalCase> page, @Param("status") String status, @Param("keyword") String keyword); }对应的XML映射文件(如果使用XML方式):
<!-- resources/mapper/LegalCaseMapper.xml --> <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.legalaid.mapper.LegalCaseMapper"> <select id="selectCasePage" resultType="com.example.legalaid.entity.LegalCase"> SELECT * FROM legal_case <where> <if test="status != null and status != ''"> AND status = #{status} </if> <if test="keyword != null and keyword != ''"> AND (title LIKE CONCAT('%', #{keyword}, '%') OR case_no LIKE CONCAT('%', #{keyword}, '%')) </if> </where> ORDER BY create_time DESC </select> </mapper>接着实现Service和Controller。
// service/LegalCaseService.java package com.example.legalaid.service; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.service.IService; import com.example.legalaid.entity.LegalCase; import com.example.legalaid.dto.CaseQueryDTO; public interface LegalCaseService extends IService<LegalCase> { IPage<LegalCase> queryPage(CaseQueryDTO queryDTO); }// service/impl/LegalCaseServiceImpl.java package com.example.legalaid.service.impl; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.example.legalaid.dto.CaseQueryDTO; import com.example.legalaid.entity.LegalCase; import com.example.legalaid.mapper.LegalCaseMapper; import com.example.legalaid.service.LegalCaseService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor public class LegalCaseServiceImpl extends ServiceImpl<LegalCaseMapper, LegalCase> implements LegalCaseService { private final LegalCaseMapper legalCaseMapper; @Override public IPage<LegalCase> queryPage(CaseQueryDTO queryDTO) { Page<LegalCase> page = new Page<>(queryDTO.getPageNum(), queryDTO.getPageSize()); return legalCaseMapper.selectCasePage(page, queryDTO.getStatus(), queryDTO.getKeyword()); } }// controller/LegalCaseController.java package com.example.legalaid.controller; import com.baomidou.mybatisplus.core.metadata.IPage; import com.example.legalaid.common.result.Result; import com.example.legalaid.dto.CaseQueryDTO; import com.example.legalaid.entity.LegalCase; import com.example.legalaid.service.LegalCaseService; import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/case") @RequiredArgsConstructor public class LegalCaseController { private final LegalCaseService legalCaseService; @PostMapping("/page") public Result<IPage<LegalCase>> queryByPage(@Validated @RequestBody CaseQueryDTO queryDTO) { IPage<LegalCase> page = legalCaseService.queryPage(queryDTO); return Result.success(page); } @GetMapping("/{id}") public Result<LegalCase> getDetail(@PathVariable Long id) { LegalCase legalCase = legalCaseService.getById(id); return Result.success(legalCase); } @PostMapping public Result<Void> create(@Validated @RequestBody LegalCase legalCase) { // 业务逻辑:生成案件编号、设置初始状态等 legalCase.setCaseNo("LA" + System.currentTimeMillis()); // 简单示例 legalCase.setStatus("PENDING"); legalCaseService.save(legalCase); return Result.success(); } @PutMapping public Result<Void> update(@Validated @RequestBody LegalCase legalCase) { legalCaseService.updateById(legalCase); return Result.success(); } @DeleteMapping("/{id}") public Result<Void> delete(@PathVariable Long id) { legalCaseService.removeById(id); return Result.success(); } }5. 集成Spring Security与JWT认证过滤器
现在,我们需要保护我们的API,确保只有携带有效Token的请求才能访问受保护的资源。
5.1 配置Spring Security
创建一个Security配置类,放行登录接口,其他接口需要认证。
// config/SecurityConfig.java package com.example.legalaid.config; import com.example.legalaid.filter.JwtAuthenticationFilter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) // 启用方法级安全注解 @RequiredArgsConstructor public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // 禁用CSRF,因为使用JWT无状态认证 .csrf().disable() // 基于Token,不需要Session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 公开接口,允许匿名访问 .antMatchers("/api/auth/login", "/doc.html", "/webjars/**", "/swagger-resources/**", "/v2/api-docs").permitAll() // 其他所有请求都需要认证 .anyRequest().authenticated() .and() // 添加JWT过滤器 .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }5.2 实现JWT认证过滤器
该过滤器会拦截请求,从Header中提取Token并进行验证。
// filter/JwtAuthenticationFilter.java package com.example.legalaid.filter; import com.example.legalaid.common.utils.JwtUtil; import lombok.RequiredArgsConstructor; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; private final UserDetailsService userDetailsService; // 需要实现这个Service来加载用户权限 @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String authHeader = request.getHeader("Authorization"); String username = null; String jwt = null; if (authHeader != null && authHeader.startsWith("Bearer ")) { jwt = authHeader.substring(7); try { username = jwtUtil.getUsernameFromToken(jwt); } catch (Exception e) { // Token解析失败,可能是过期或伪造,直接放行,后续Security会处理为未认证 logger.error("JWT Token解析失败", e); } } if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { // 从数据库或缓存加载用户信息(这里简化为从数据库查) UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); // 验证Token有效性 if (jwtUtil.isTokenExpired(jwt)) { // Token已过期,可以在这里抛出异常或返回特定响应 logger.warn("JWT Token已过期"); } else { // Token有效,设置认证信息到Security上下文 UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } filterChain.doFilter(request, response); } }你需要实现一个UserDetailsService来根据用户名加载用户信息和权限(角色)。
6. 集成Knife4j生成API文档
清晰的API文档是前后端协作的桥梁。Knife4j的集成非常简单。
// config/SwaggerConfig.java package com.example.legalaid.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.service.Contact; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc; @Configuration @EnableSwagger2WebMvc public class SwaggerConfig { @Bean public Docket createRestApi() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() // 指定扫描的包路径 .apis(RequestHandlerSelectors.basePackage("com.example.legalaid.controller")) .paths(PathSelectors.any()) .build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("法律援助管理系统 API 文档") .description("基于Spring Boot + MyBatis-Plus + Spring Security的后端API") .contact(new Contact("开发者", "", "developer@example.com")) .version("1.0") .build(); } }启动应用后,访问http://localhost:8080/doc.html即可看到美观的API文档界面。
7. 常见问题与排查思路
在开发过程中,你可能会遇到以下典型问题:
| 问题现象 | 可能原因 | 排查思路与解决方案 |
|---|---|---|
应用启动失败,报DataSource相关错误 | 1. 数据库连接URL、用户名、密码错误。 2. MySQL服务未启动。 3. 数据库驱动版本不匹配。 | 1. 检查application.yml中的数据库配置。2. 使用客户端工具(如Navicat)测试连接。 3. 确认MySQL版本与 mysql-connector-java依赖版本兼容。 |
调用接口返回403 Forbidden或401 Unauthorized | 1. 请求未携带Token或Token已过期。 2. Token格式错误(未以 Bearer开头)。3. 接口路径未被Security放行,但用户权限不足。 | 1. 检查请求头Authorization: Bearer <your_token>。2. 使用登录接口获取新Token。 3. 检查 SecurityConfig中的antMatchers配置和控制器上的@PreAuthorize注解。 |
MyBatis-Plus 插入/更新时,自动填充字段(如create_time)不生效 | 1. 实体类字段未加@TableField(fill = ...)注解。2. 未配置元对象处理器(MetaObjectHandler)。 | 1. 确认实体类字段注解正确。 2. 创建配置类实现 MetaObjectHandler,重写insertFill和updateFill方法。 |
| 分页查询失效,返回所有数据 | 未添加MyBatis-Plus的分页插件配置。 | 在MybatisPlusConfig配置类中注册PaginationInterceptor(3.4.x) 或MybatisPlusInterceptor(3.5.x+) 分页插件。 |
| Knife4j 页面能打开,但接口列表为空 | 1.@EnableSwagger2WebMvc注解缺失或位置不对。2. DocketBean中扫描的包路径不正确。 | 1. 确保配置类有@EnableSwagger2WebMvc。2. 检查 RequestHandlerSelectors.basePackage是否指向了你的Controller包。 |
JWT Token 解析失败,报SignatureException | 1. 生成Token和解析Token使用的密钥(secret)不一致。2. Token被篡改。 | 1. 确保application.yml中的legal-aid.jwt.secret在应用启动后没有变化。2. 密钥需要足够复杂且保密。 |
8. 工程最佳实践与扩展建议
完成基础功能后,一个健壮的生产级系统还需要考虑更多。
- 密码安全:永远不要明文存储密码。使用
BCryptPasswordEncoder进行哈希加密。在用户注册或修改密码时,调用passwordEncoder.encode(rawPassword)进行加密后再存入数据库。 - 接口幂等性:对于创建、支付等关键接口,需考虑幂等设计,防止重复提交。可以使用Token机制或数据库唯一约束。
- 数据权限控制:不同角色的用户能看到的数据范围不同。例如,律师只能看到指派给自己的案件。可以在Service层通过查询条件动态添加
WHERE子句来实现。 - 全局异常处理:使用
@ControllerAdvice和@ExceptionHandler定义全局异常处理器,将各种异常(如业务异常、参数校验异常、认证异常)转换为统一的Result对象返回,避免暴露服务器内部错误信息。 - 日志记录:使用SLF4J + Logback记录详细的业务日志和操作日志,便于问题追踪和审计。尤其要记录敏感操作(如案件状态变更、用户权限修改)。
- 配置分离:将
application.yml拆分为application-dev.yml(开发)、application-test.yml(测试)、application-prod.yml(生产),通过spring.profiles.active指定激活的环境。 - API版本管理:在URL路径(如
/api/v1/case)或请求头中引入版本号,为后续接口不兼容升级留有余地。 - 前端分离部署:本系统为纯后端API,前端可使用Vue、React等框架独立开发,通过Nginx进行反向代理和部署,解决跨域问题。
- 数据库优化:为频繁查询的字段(如
status,create_time,assigned_lawyer_id)建立索引。对于大数据量表,考虑历史数据归档或分库分表。 - 部署与监控:使用Docker容器化部署,配合Jenkins或GitLab CI/CD实现自动化构建和部署。集成Spring Boot Actuator和Prometheus、Grafana进行应用监控。
从零到一构建一个完整的Spring Boot后端系统,关键在于理解各组件(Spring MVC, Security, MyBatis-Plus)如何协同工作,并遵循分层和模块化的设计思想。本文提供的代码和思路是一个坚实的起点,你可以在此基础上,根据具体的法律援助业务需求,扩展如文件上传(证据材料)、消息通知(短信/邮件)、工作流引擎(审批流程)等更复杂的模块。动手将代码跑起来,在调试和扩展中,你会对Spring Boot生态有更深刻的理解。