双非 Java 后端首次实习 | 个人经验分享总结

双非 Java 后端首次实习 | 个人经验分享总结

摘要:实习期间参与企业后台项目开发,熟悉企业开发流程与代码规范。

实习核心流程(结合实际经历)

由于自己进入的是一个小公司实习,当时项目刚好启动,参与了较多基础模块的开发。

一、基础准备与环境搭建阶段(入职 1-3 天)

  1. 公司基础配置:进入公司飞书、拥有个人邮箱等基础办公权限
  2. 代码拉取与环境搭建:
    • 学习并使用 git/svn 等版本管理工具 clone 项目代码(公司使用的是阿里云云效)
    • 配置项目所需配置文件,搭建后端 + 前端开发环境(后端需兼顾前端环境)
    • 解决环境依赖问题,确保项目能正常跑起来(熟悉配置文件与环境)
    • 熟悉开发工具的使用,避免因操作问题浪费时间(mentor 教了debug技巧,快捷键)

二、项目熟悉阶段(入职 1-2 周)

这个阶段任务主要是熟悉环境,熟练使用通用封装 / 工具类,自己在熟悉项目的时候,寻找少量项目bug,提交问题给 mentor 审核,并进行功能的测试,完成简单 的demo任务,熟练框架使用,代码风格,尤其是掌握 Git 与 MP 相关的使用,在代码中用的非常多。

1. 基础认知

  • 系统学习公司核心业务范围、业务流程及新人岗位能力要求,明确学习方向与目标;
  • 由同事 / 组长系统性讲解项目核心模块划分、整体技术架构、核心业务场景及上下游依赖关系,建立项目整体认知。

2. 深度熟悉

  • 梳理项目完整目录结构、模块间交互逻辑,深入理解数据库表字段设计;
  • 熟读 Common 通用包的代码,熟练掌握封装的通用工具类的调用方式;
  • 重点学习公司主流技术框架的核心,结合框架特性理解业务逻辑的实现思路;
  • 拆解业务三层架构(控制层、服务层、数据层),对照接口文档理解代码具体写法。

3. 代码实践

  • 基于公司现有框架完成简单 Demo 开发,覆盖核心业务场景的基础流程,验证对框架及通用工具的掌握程度。

三、初步实践阶段(入职 2 周后)

本人当时已较为熟悉业务,快速投入开发工作,仿照其他模块的业务代码编写风格,负责管理模块相关开发,按照接口文档完成增删改查核心操作。

同时,我也参与了部分难度较复杂任务的讨论,提出了一些建议和优化思路(实际作用有限),涉及Redisson分布式锁、第三方平台Token无感续期等相关问题。

在项目迭代过程中,我重点观察技术负责人在实践redis缓存,Redission结合自定义注解 + AOP实现方法级别的分布式锁,处理异步日志等任务的提交与执行、自定义合理线程池、使用JUC并发编程工具类等业务场景时,解决实际问题的方法。

  

熟悉企业项目

权限管理模块

@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Auth { String[] value() default ""; }

Auth:定义了一个自定义的 Java 注解,名为 Auth,通常用于标记需要权限校验的类或方法。

@Slf4j @Aspect @Component @AllArgsConstructor public class AuthAop { @Pointcut("@annotation(auth)") public void controllerAspect(Auth auth) { } @Around(value = "controllerAspect(auth)", argNames = "proceedingJoinPoint,auth") public Object aroundAuth(ProceedingJoinPoint proceedingJoinPoint, Auth auth) throws Throwable { LoginUser loginUser = LoginUserHandler.getLoginUser(); if (loginUser == null) { throw new BusinessException(ResponseCode.JWT_TOKEN_PARSING_ERROR); } String role = Optional.ofNullable(loginUser.getRole()).orElse("").toUpperCase(); if (!Arrays.asList(auth.value()).contains(role)) { throw new BusinessException(ResponseCode.INSUFFICIENT_PERMISSIONS); } Object[] args = proceedingJoinPoint.getArgs(); for (Object arg : args) { if (arg == null) { arg = loginUser; } } return proceedingJoinPoint.proceed(args); } }

AuthAspect:

@Pointcut:Spring AOP 的注解,用于声明一个切点,参数是「切点表达式」,指定拦截规则。

切点表达式 @annotation(auth)

  • @annotation() 是 AOP 内置的切点表达式,含义是拦截所有方法上标注了指定注解的方法
  • 这里的 auth 是参数名,对应后面方法的 Auth auth,表示拦截标注 @Auth 的方法,并把该注解实例传入切点方法。

public void controllerAspect(Auth auth)

  • 这是一个「切点签名方法」,本身无业务逻辑,仅用于承载 @Pointcut 注解和参数声明。
  • 参数 Auth auth:表示将拦截到的方法上的 @Auth 注解实例注入到该参数中,后续通知方法可直接使用。

@Component @WebFilter(urlPatterns = "/*") @Slf4j @AllArgsConstructor public class LoginFilter implements Filter { private static final List<String> OPEN_API = List.of(); private static final String OPEN_API_HEADER = "X-Open-Api"; private static final String OPEN_API_VERSION = "1.0.0"; private static final PathPatternParser PATH_PATTERN_PARSER = PathPatternParser.defaultInstance; private final TokenService tokenService; private final ObjectMapper objectMapper; private final SecurityIgnoreUrls authPath; private final com.daochengtech.lock.platform.openapi.auth.OpenApiAuthHandler openApiAuthHandler; // 白名单路径缓存 private List<PathPattern> ignorePatterns; //init 方法:过滤器初始化(仅启动时执行一次) @Override public void init(jakarta.servlet.FilterConfig filterConfig) { ignorePatterns = authPath.getUrls().stream() .map(PATH_PATTERN_PARSER::parse) .toList(); } /** * 登录过滤器 */ @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) servletRequest; HttpServletResponse httpResponse = (HttpServletResponse) servletResponse; httpResponse.setContentType("application/json;charset=utf-8"); String url = httpRequest.getRequestURI(); log.info("请求url:{}", url); try { // 判断是否白名单 PathContainer pathContainer = PathContainer.parsePath(url); if (ignorePatterns.stream().anyMatch(p -> p.matches(pathContainer))) { filterChain.doFilter(servletRequest, servletResponse); return; } if (handle(httpRequest, httpResponse)) { filterChain.doFilter(servletRequest, servletResponse); } } finally { // 确保清理所有上下文 try { LoginUserHandler.removeLoginUser(); } catch (Exception e) { log.debug("清理用户上下文异常", e); } try { openApiAuthHandler.clearAuthContext(); } catch (Exception e) { log.debug("清理OpenAPI上下文异常", e); } } } /** * 处理认证 * * @param httpRequest 请求 * @param httpResponse 响应 */ private boolean handle(HttpServletRequest httpRequest, HttpServletResponse httpResponse) { try { // 检查是否为OpenAPI请求 if (openApiAuthHandler.isOpenApiRequest(httpRequest)) { return handleOpenApiAuth(httpRequest, httpResponse); } // 处理普通用户认证 return handleUserAuth(httpRequest, httpResponse); } catch (Exception e) { log.error("认证处理异常", e); writeAccessDenied(httpResponse); return false; } } /** * 处理OpenAPI认证 * * @param httpRequest 请求 * @param httpResponse 响应 * @return 认证结果 */ private boolean handleOpenApiAuth(HttpServletRequest httpRequest, HttpServletResponse httpResponse) { log.debug("处理OpenAPI认证,URI: {}", httpRequest.getRequestURI()); // Token获取请求不需要认证 if (openApiAuthHandler.isTokenRequest(httpRequest)) { log.debug("Token获取请求,跳过认证"); return true; } // 执行OpenAPI认证 boolean authResult = openApiAuthHandler.authenticate(httpRequest, httpResponse); if (!authResult) { log.warn("OpenAPI认证失败,URI: {}", httpRequest.getRequestURI()); writeOpenApiAccessDenied(httpResponse); return false; } log.debug("OpenAPI认证成功,API Key: {}", openApiAuthHandler.getCurrentApiKey()); return true; } /** * 处理普通用户认证 */ private boolean handleUserAuth(HttpServletRequest httpRequest, HttpServletResponse httpResponse) { log.debug("处理普通用户认证,URI: {}", httpRequest.getRequestURI()); // 获取认证token String authenticationToken = httpRequest.getHeader(AUTHORIZATION); // 判断令牌是否存在 if (StringUtils.isBlank(authenticationToken)) { log.warn("用户认证失败:缺少认证token"); writeAccessDenied(httpResponse); return false; } // 解析token try { LoginUser loginUser = tokenService.getLoginUser(httpRequest); if (loginUser == null) { log.warn("用户认证失败:token解析失败"); writeAccessDenied(httpResponse); return false; } LoginUserHandler.setLoginUser(loginUser); log.debug("用户认证成功,用户: {}", loginUser.getUsername()); return true; } catch (Exception e) { log.error("用户token解析异常", e); writeAccessDenied(httpResponse); return false; } } /** * 拒绝访问 * * @param response 响应 */ private void writeAccessDenied(HttpServletResponse response) { writeErrorResponse(response, ResponseCode.ACCESS_DENIED); } /** * OpenAPI拒绝访问 * * @param response 响应 */ private void writeOpenApiAccessDenied(HttpServletResponse response) { writeErrorResponse(response, ResponseCode.AUTHENTICATION_FAIL); } /** * 写入错误响应 * * @param response 响应 * @param responseCode 响应码 */ private void writeErrorResponse(HttpServletResponse response, ResponseCode responseCode) { try { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write(objectMapper.writeValueAsString(CommonResult.response(responseCode))); response.getWriter().flush(); } catch (IOException e) { log.error("写入响应失败", e); } } /** * 验证OpenAPI请求 * * @param request HTTP请求 * @param response HTTP响应 * @return 认证是否成功 */ public boolean authenticate(HttpServletRequest request, HttpServletResponse response) { try { log.debug("开始OpenAPI认证,URI: {}", request.getRequestURI()); // 获取Token和版本信息 String token = request.getHeader(OPEN_API_TOKEN_HEADER); String version = request.getHeader(OPEN_API_VERSION_HEADER); // 验证必要的请求头 if (StringUtils.isBlank(token)) { log.warn("OpenAPI认证失败:缺少Token请求头"); return false; } if (StringUtils.isBlank(version)) { log.warn("OpenAPI认证失败:缺少版本请求头"); return false; } // 验证版本 if (!SUPPORTED_VERSION.equals(version)) { log.warn("OpenAPI认证失败:不支持的版本 {}", version); return false; } // 验证Token并获取上下文 OpenApiContext context = validateToken(token, version); if (context == null) { log.warn("OpenAPI认证失败:Token验证失败"); return false; } // 设置认证上下文 setAuthContext(context); log.debug("OpenAPI认证成功,API Key: {}", context.getApiKey()); return true; } catch (Exception e) { log.error("OpenAPI认证异常", e); return false; } } }

LoginFilter:登录过滤器整体执行流程

该登录过滤器作为请求进入系统的认证关卡,整体执行流程如下:

1. 初始化阶段(init方法)

过滤器初始化时,会将配置的白名单URL(通过authPath.getUrls()获取)解析为PathPattern格式并缓存到ignorePatterns列表中,为后续路径匹配做准备。


2. 核心过滤阶段(doFilter方法)

所有请求进入过滤器后,执行核心逻辑:

步骤1:请求转换与基础设置

ServletRequest/ServletResponse转换为HttpServletRequest/HttpServletResponse,并设置响应格式为application/json;charset=utf-8,记录请求URL。

步骤2:白名单路径校验

解析当前请求URL为PathContainer,匹配缓存的白名单ignorePatterns

  • 若匹配成功(属白名单),直接放行请求(filterChain.doFilter),结束当前过滤器逻辑;
  • 若不匹配,进入认证处理流程。

步骤3:认证处理

调用handle方法执行具体认证逻辑,若认证通过则放行请求,否则拦截。

步骤4:上下文清理(finally块)

无论认证成功/失败,最终都会清理用户上下文(LoginUserHandler.removeLoginUser())和OpenAPI上下文(openApiAuthHandler.clearAuthContext()),避免内存泄漏。


3. 认证处理流程(handle方法)

handle方法是认证核心,区分两种认证类型:

步骤1:判断请求类型

先通过openApiAuthHandler.isOpenApiRequest判断是否为OpenAPI请求:

  • 若是,执行handleOpenApiAuth处理OpenAPI认证;
  • 若否,执行handleUserAuth处理普通用户认证。

步骤2:异常兜底

若认证过程中抛出异常,记录错误日志,调用writeAccessDenied返回未授权响应,拦截请求。


4. OpenAPI认证流程(handleOpenApiAuth方法)

步骤1:特殊请求放行

若为Token获取请求(openApiAuthHandler.isTokenRequest),直接放行(无需认证)。

步骤2:执行OpenAPI认证

调用openApiAuthHandler.authenticate执行认证:

  • 认证成功:放行请求;
  • 认证失败:记录警告日志,调用writeOpenApiAccessDenied返回未授权响应,拦截请求。

5. 普通用户认证流程(handleUserAuth方法)

步骤1:Token校验

从请求头获取认证Token,若Token为空,记录警告日志,返回未授权响应,拦截请求。

步骤2:Token解析与验证

调用tokenService.getLoginUser解析Token:

  • 解析成功:将用户信息存入上下文(LoginUserHandler.setLoginUser),放行请求;
  • 解析失败(LoginUser为空)或抛出异常:记录错误日志,返回未授权响应,拦截请求。

6. 异常响应处理

认证失败时,通过writeErrorResponse统一返回标准化错误响应:

  • 设置响应状态码为401(SC_UNAUTHORIZED);
  • CommonResult.response(responseCode)序列化为JSON写入响应体;
  • 区分普通用户认证失败和OpenAPI认证失败的响应码。

SecurityIgnoreUrls(配置绑定类)与 secure.ignored.urls(白名单 URL 配置)

@Getter @Setter @Configuration @ConfigurationProperties(prefix = "secure.ignored") public class SecurityIgnoreUrls { private List<String> urls = new ArrayList<>(); }
secure.ignored.urls[0]=/swagger-ui.html secure.ignored.urls[1]=/swagger-resources/** secure.ignored.urls[2]=/v3/** secure.ignored.urls[3]=/swagger-ui/** secure.ignored.urls[4]=/swagger-ui/index.html secure.ignored.urls[5]=/web/sys/login secure.ignored.urls[6]=/common/captcha secure.ignored.urls[7]=/test/callback/test secure.ignored.urls[8]=/api/socket/**

通过 Spring Boot 的@ConfigurationProperties注解,将配置文件中secure.ignored前缀的配置绑定到这个类的urls属性上,让代码可以通过注入该类直接获取白名单 URL 列表(无需手动解析配置)。

Token管理模块

@Slf4j @Component public class TokenService { public static final String TOKEN_PREFIX = "Bearer "; public static final String LOGIN_USER = "LOGIN_USER:"; public static final String TOKEN = "token"; public static final String USERNAME = "username"; public static final String FORMAT = "%s%s:%s"; private final String header; private final byte[] secretKey; private final Long expirationHours; private final RedisService redisService; @Autowired public TokenService( @Value("${auth.jwt.header}") String header, @Value("${auth.jwt.secret}") String secret, @Value("${auth.jwt.expiration}") Long expiration, RedisService redisService) { this.header = header; this.secretKey = secret.getBytes(StandardCharsets.UTF_8); this.expirationHours = expiration; this.redisService = redisService; } public LoginUser getLoginUser(HttpServletRequest request) { try { Claims claims = parseToken(getToken(request)); String tokenId = claims.get(TOKEN, String.class); String username = claims.get(USERNAME, String.class); String key = String.format(FORMAT, LOGIN_USER, tokenId, username); return redisService.get(key, LoginUser.class); } catch (Exception e) { log.error("获取用户信息异常: {}", e.getMessage()); throw new BusinessException(ResponseCode.AUTHENTICATION_FAIL); } } public Long getExpireTime(HttpServletRequest request) { try { Claims claims = parseToken(getToken(request)); String tokenId = claims.get(TOKEN, String.class); String username = claims.get(USERNAME, String.class); String key = String.format(FORMAT, LOGIN_USER, tokenId, username); return redisService.getExpire(key); } catch (Exception e) { log.error("获取用户过期时间异常: {}", e.getMessage()); return null; } } public String createToken(LoginUser loginUser) { String uuid = UUID.randomUUID().toString(); Map<String, Object> claims = new HashMap<>(); claims.put(TOKEN, uuid); claims.put(USERNAME, loginUser.getUsername()); String token = Jwts.builder() .claims(claims) .expiration(new Date(System.currentTimeMillis() + expirationHours * 3600 * 1000)) .signWith(Keys.hmacShaKeyFor(secretKey), Jwts.SIG.HS512) .compact(); loginUser.setToken(token); refreshToken(loginUser, uuid); return token; } public void refreshToken(LoginUser loginUser, String uuid) { try { // 1. 重新生成JWT Token(更新过期时间) Map<String, Object> claims = new HashMap<>(); claims.put(TOKEN, uuid); claims.put(USERNAME, loginUser.getUsername()); String newToken = Jwts.builder() .claims(claims) .expiration(new Date(System.currentTimeMillis() + expirationHours * 3600 * 1000)) .signWith(Keys.hmacShaKeyFor(secretKey), Jwts.SIG.HS512) .compact(); loginUser.setToken(newToken); // 3. 刷新Redis缓存(续期+更新LoginUser) String key = String.format(FORMAT, LOGIN_USER, uuid, loginUser.getUsername()); redisService.set(key, loginUser, Duration.ofHours(expirationHours)); log.debug("Token刷新成功,用户名:{},新Token过期时间:{}小时", loginUser.getUsername(), expirationHours); } catch (Exception e) { log.error("Token刷新异常,用户名:{}", loginUser.getUsername(), e); throw new BusinessException("Token续期失败!"); } } public String getUsernameFromToken(String token) { return parseToken(token).get(USERNAME, String.class); } public String getToken(HttpServletRequest request) { String token = request.getHeader(header); if (StringUtils.isNotBlank(token) && token.startsWith(TOKEN_PREFIX)) { return token.substring(TOKEN_PREFIX.length()); } throw new BusinessException(ResponseCode.JWT_TOKEN_PARSING_ERROR); } public void logout(String username) { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); if (requestAttributes == null) { return; } HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference( RequestAttributes.REFERENCE_REQUEST); if (request == null) { return; } try { Claims claims = parseToken(getToken(request)); String tokenId = claims.get(TOKEN, String.class); String key = String.format(FORMAT, LOGIN_USER, tokenId, username); redisService.del(key); } catch (Exception e) { log.error("登出时删除Token异常: {}", e.getMessage()); } } }

1. Token 创建流程(createToken 方法)

  • 步骤1:生成 UUID 作为 Token 的唯一标识(tokenId);
  • 步骤2:构建 JWT 载荷(claims),存入 tokenId 和用户名;
  • 步骤3:使用secretKey 进行 HS512 签名,设置 Token 过期时间,生成并返回 JWT Token;
  • 步骤4:将 Token 绑定到 LoginUser 对象,调用 refreshToken 方法将 LoginUser 信息存入 Redis,并设置与 Token 相同的过期时间。

2. Token 解析验证流程(getLoginUser 方法)

  • 步骤1:从请求头中提取 Token(去掉前缀 "Bearer ");
  • 步骤2:用 secretKey 验证并解析 Token,获取载荷中的 tokenId 和用户名;
  • 步骤3:拼接 Redis Key,从 Redis 中获取 LoginUser 对象;
  • 步骤4:解析/获取失败则抛出认证失败异常,成功则返回 LoginUser。

3. Token 刷新流程(refreshToken 方法)

  • 步骤1:重新生成JWT-Token令牌;
  • 步骤2:将 LoginUser 重新存入 Redis,重置过期时间为配置的 expirationHours 小时。

4. Token 注销流程(logout 方法)

  • 步骤1:从请求上下文获取 HttpServletRequest,提取并解析 Token,获取 tokenId;
  • 步骤2:拼接 Redis Key(LOGIN_USER:{tokenId}:{username});
  • 步骤3:删除 Redis 中该 Key 对应的 LoginUser 数据,完成 Token 失效。

5. 过期时间查询流程(getExpireTime 方法)

  • 步骤1:解析 Token 获取 tokenId 和用户名,拼接 Redis Key;
  • 步骤2:查询 Redis 中该 Key 的剩余过期时间并返回;
  • 步骤3:异常则返回 null,不抛出异常(仅日志记录)。

Token 创建:生成 JWT Token + 存储用户信息到 Redis(带过期时间);

Token 验证:解析 JWT 并从 Redis 校验/获取用户信息;

Token 管理:支持刷新(重置 Redis 过期时间)、注销(删除 Redis 数据)、查询过期时间;

核心依赖:JWT 签名保证 Token 不被篡改,Redis 存储保证用户信息可追溯、可失效。

参与企业项目

企业管理模块

 cacheAll
public List<Company> cacheAll() { final String cacheKey = "company:all"; List<Company> cachedList = redisService.get(cacheKey, List.class); if (cachedList != null) { return cachedList; } synchronized (cacheLock) { cachedList = redisService.get(cacheKey, List.class); if (cachedList != null) { return cachedList; } List<Company> list = list(); redisService.set(cacheKey, list, 3600); // 设置1小时过期 return list; } }

这段代码是企业级系统中 "全量数据缓存加载" 类功能的典型通用逻辑框架,核心遵循 "缓存查询→双重校验加锁→数据库查询→缓存写入→结果返回" 的标准化流程,具体可拆解为 4 个核心步骤:

  1. 一级缓存查询:定义固定缓存键(cacheKey = "company:all"),先从 Redis 中查询缓存数据,若缓存存在则直接返回(缓存功能的通用前置步骤,优先使用缓存提升查询性能);
  2. 双重校验 + 同步锁:缓存不存在时,通过 synchronized (cacheLock) 加本地同步锁,锁内再次查询缓存,若仍不存在再执行数据库查询(缓存加载的通用并发防护,避免缓存击穿);
  3. 数据库查询 + 缓存写入:锁内执行全量数据库查询,将查询结果写 Redis 并设置过期时间。
  4. 结果统一返回:无论从缓存还是数据库获取数据,最终统一返回全量企业列表。

getCompanyFromCache
public Optional<Company> getCompanyFromCache(Long companyId) { return cacheAll().stream() .filter(c -> c.getId().equals(companyId)) .findFirst(); }

这段代码是企业级系统中 "基于全量缓存的单条实体精准查询" 类功能的典型通用逻辑框架,核心遵循 "全量缓存加载→精准过滤→空值安全返回" 的标准化流程,具体可拆解为 3 个核心步骤:

  1. 全量缓存加载(复用缓存数据):调用 cacheAll () 方法加载全量企业缓存数据。
  2. 精准数据过滤(定位目标实体):通过 stream ().filter (c -> c.getId ().equals (companyId)) 过滤出匹配企业 ID 的实体,结合 findFirst () 获取单条结果。
  3. 空值安全返回:返回 Optional<Company>类型结果,未匹配到返回空 Optional 而非 null。

getCompanyList

@Validated 是 Spring 框架提供的参数校验注解,核心作用是自动校验接口入参的合法性,避免我们手动写大量 if-else 判断参数是否为空、格式是否正确。

@PostMapping("/list") public CommonResult<SimplePage<CompanyListVO>> getCompanyList(@RequestBody @Validated CompanyListDTO dto){ return CommonResult.success(companyService.getCompanyList(dto,U.get())); }
@Data @EqualsAndHashCode(callSuper = true) @Schema(description = "企业列表查询") public class CompanyListDTO extends PageableQuery { @Schema(description = "企业ID") private Long companyId; @Size(min = 3, max = 20, message = "查询最少三个字") @Schema(description = "公司名称--模糊查询最少三个字") private String name; }

@Validated 加在 Controller 接口的 @RequestBody CompanyListDTO dto 参数上,核心是触发 CompanyListDTO 类中所有 JSR 380 校验注解的执行:

  • CompanyListDTO 有生效的校验注解(@Size(min = 3, max = 20, message = "最少三个字")),前端调用 /list 接口时,会自动校验 name 字段的长度是否符合 3-20 字符的规则;
  • 若校验失败,会直接抛出 MethodArgumentNotValidException 异常,拦截非法参数,不会进入 companyService.getCompanyList() 业务逻辑层。
public SimplePage<CompanyListVO> getCompanyList(CompanyListDTO dto, LoginUser loginUser) { return PageUtil.getPage(new SimplePage<>( companyRepository.getCompanyList(Page.of(dto.getPageNum(), dto.getPageSize()), dto, loginUser)), r -> CompanyListVO.builder() .id(r.getId()) .name(r.getName()) .shortName(r.getShortName()) .contact(r.getContact()) .phone(r.getPhone()) .address(r.getAddress()) .build()); }
public class PageUtil { public static <T> SimplePage<T> parse(SimplePage<?> src, List<T> list) { SimplePage<T> ret = new SimplePage<>(); ret.setPages(src.getPages()); ret.setPageNum(src.getPageNum()); ret.setPageSize(src.getPageSize()); ret.setTotal(src.getTotal()); ret.setList(list); return ret; } public static <R, T> SimplePage<R> getPage(SimplePage<T> src, Function<T, R> mapper) { SimplePage<R> ret = new SimplePage<>(); ret.setPages(src.getPages()); ret.setPageNum(src.getPageNum()); ret.setPageSize(src.getPageSize()); ret.setTotal(src.getTotal()); ret.setList(src.getList().stream().map(mapper).filter(Objects::nonNull).toList()); return ret; } }
public Page<Company> getCompanyList(Page<Company> page, CompanyListDTO dto, LoginUser loginUser) { return page(page, Wrappers.<Company>lambdaQuery() .eq(dto.getCompanyId() != null, Company::getId, dto.getCompanyId()) .like(StringUtils.isNotBlank(dto.getName()), Company::getName, dto.getName()) .eq(loginUser.isEnterprise(), Company::getId, loginUser.getAuthCompanyId()) .orderByDesc(Company::getCreateTime)); }

这段代码是 "分页列表查询 + 数据转换" 类功能的典型实现,具体可拆解为 4 个核心步骤:

  1. 分页参数封装 + 仓储层查询:通过 Page.of () 封装统一的分页参数,调用 getCompanyList () 执行数据库分页查询,返回包含 "分页元数据 + 原始实体列表" 的 SimplePage 对象;
  2. 通用工具类转换(DO→VO):调用 PageUtil.getPage () 工具方法,结合 Function 函数式接口传入 VO 构建逻辑(r -> CompanyListVO.builder ()),将 Entity 转换为前端展示 VO;
  3. 分页元数据透传 + 空值过滤:工具类完整保留原分页对象的总页数、总条数、当前页 / 页大小等元数据,同时通过 stream ().filter (Objects::nonNull) 过滤转换后的空值;
  4. 统一结果返回:返回转换后的 SimplePage<CompanyListVO>对象,包含前端所需的展示列表和完整分页元数据。

getCompanyDetailById
@GetMapping("/detail/{companyId}") @Operation(summary = "企业详情") public CommonResult<Company> getCompanyDetailById( @PathVariable(required = false) @Parameter(description = "企业ID") @Validated @NotNull(message = "MERCHANT_ID_NOT_NULL") Long companyId) { return CommonResult.success(companyService.getCompanyDetailById(companyId, U.get())); }
  1. @PathVariable负责接收路径参数required = false控制参数是否可选;
  2. @Parameter仅用于接口文档说明,不影响参数逻辑;
  3. @Validated + @NotNull组合:前者激活校验,后者执行 "参数非 null" 的校验规则。

 getCompanyDropDownList
@GetMapping("/drop/down/list") @Operation(summary = "企业下拉列表", description = "企业下拉列表") public CommonResult<List<CompanyDropDown>> getCompanyDropDownList(@RequestParam(value = "companyId", required = false) @Parameter(description = "企业ID") Long companyId) { return CommonResult.success(companyService.getCompanyDropDownList(companyId, U.get())); }
public List<CompanyDropDown> getCompanyDropDownList(Long companyId, LoginUser loginUser) { return companyRepository.getCompanyDropDownList(companyId, loginUser); }
public List<CompanyDropDown> getCompanyDropDownList(Long companyId, LoginUser loginUser) { return cacheAll().stream() .filter(c -> companyId == null || c.getId().equals(companyId)) .filter(c -> !loginUser.isEnterprise() || c.getId().equals(loginUser.getAuthCompanyId())) .filter(c -> c.getId() != 1L) .sorted(Comparator.comparing(Company::getName)) .map(CompanyDropDown::from) .collect(Collectors.toList()); }

这段代码是「下拉列表数据查询 + 全量缓存优化」类功能的典型实现,具体如下:

  1. 缓存全量加载:优先调用 cacheAll() 加载全量企业缓存数据,替代直接查询数据库,提升高频下拉列表查询的响应效率;
  2. 多维度条件过滤:通过流式过滤实现三层规则校验 —— 匹配指定企业 ID、校验用户数据权限、排除系统默认企业;
  3. 排序与数据转换:按企业名称升序排序,再将企业实体映射为下拉列表专用 VO;
  4. 结构化结果返回:将过滤、排序、转换后的结果封装为列表,满足前端下拉框的业务需求。

public Company getCompanyDetailById(Long companyId, LoginUser loginUser) { // 步骤1:从缓存中查询 Optional<Company> cacheCompany = getCompanyFromCache(companyId); if (cacheCompany.isPresent()) { Company company = cacheCompany.get(); // 步骤2:权限校验 if (loginUser.isEnterprise() && !company.getId().equals(loginUser.getAuthCompanyId())) { throw new BusinessException(ResponseCode.MERCHANT_NOT_EXISTS); } return company; } // 步骤3:缓存未命中,查库兜底 Company company = companyRepository.getByIdWithAuth(companyId, loginUser); if (company == null) { throw new BusinessException(ResponseCode.MERCHANT_NOT_EXISTS); } // 步骤4:更新缓存 companyRepository.removeCache(); companyRepository.cacheAll(); return company; }
public Company getByIdWithAuth(Long companyId, LoginUser loginUser) { return getOne(Wrappers.<Company>lambdaQuery() .eq(Company::getId, companyId) .eq(loginUser.isEnterprise(), Company::getId, loginUser.getAuthCompanyId()) .last("limit 1")); }

这段代码是「单实体详情查询 + 缓存优化」类功能的典型实现,具体拆解如下:

  1. 缓存优先精准查询:优先调用 getCompanyFromCache(companyId) 从全量企业缓存中过滤匹配目标企业 ID 的数据,替代直接查库,利用缓存提升高频详情查询的响应效率;
  2. 权限二次校验:即使缓存命中目标企业数据,仍需校验登录用户数据权限;
  3. 数据库兜底查询:若缓存未命中目标数据,则通过 Wrappers.lambdaQuery() 构建查询条件,并通过 last("limit 1") 限定单条结果返回,同时查询后更新缓存保证后续命中率;
  4. 结果有效性校验:对缓存 / 数据库查询结果做非空校验,若返回 null 则抛出标准化业务异常,作为详情查询的通用兜底逻辑,避免空值引发前端渲染异常或下游业务空指针;
  5. 合规结果返回:经缓存 / 数据库查询、权限校验、有效性校验后,返回符合条件的企业实体对象,既保证查询性能,又满足数据权限管控和业务完整性要求。

 getCompanyDropDownList
@GetMapping("/drop/down/list") @Operation(summary = "企业下拉列表", description = "企业下拉列表") public CommonResult<List<CompanyDropDown>> getCompanyDropDownList(@RequestParam(value = "companyId", required = false) @Parameter(description = "企业ID") Long companyId) { return CommonResult.success(companyService.getCompanyDropDownList(companyId, U.get())); }
public List<CompanyDropDown> getCompanyDropDownList(Long companyId, LoginUser loginUser) { return companyRepository.getCompanyDropDownList(companyId, loginUser); }
public List<CompanyDropDown> getCompanyDropDownList(Long companyId, LoginUser loginUser) { return cacheAll().stream() .filter(c -> companyId == null || c.getId().equals(companyId)) .filter(c -> !loginUser.isEnterprise() || c.getId().equals(loginUser.getAuthCompanyId())) .filter(c -> c.getId() != 1L) .sorted(Comparator.comparing(Company::getName)) .map(CompanyDropDown::from) .collect(Collectors.toList()); }

这段代码是「下拉列表数据查询 + 全量缓存优化」类功能的典型实现,具体如下:

  1. 缓存全量加载:优先调用 cacheAll() 加载全量企业缓存数据,替代直接查询数据库,提升高频下拉列表查询的响应效率;
  2. 多维度条件过滤:通过流式过滤实现三层规则校验 —— 匹配指定企业 ID、校验用户数据权限、排除系统默认企业;
  3. 排序与数据转换:按企业名称升序排序,再将企业实体映射为下拉列表专用 VO;
  4. 结构化结果返回:将过滤、排序、转换后的结果封装为列表,满足前端下拉框的业务需求。

addCompany
@Log(title = "新增企业") @Auth(value = {STRAUTH.ADMIN, STRAUTH.MANAGER}) @PutMapping @Operation(summary = "新增企业") public CommonResult<Long> addOperatorCompanyUser(@RequestBody @Validated AddCompanyDTO dto) { return CommonResult.success(companyService.addCompany(dto, U.get())); }
@Lock4j(lockType = "addCompany", key = "#dto.name") @Transactional(rollbackFor = Exception.class) public Long addCompany(AddCompanyDTO dto, LoginUser loginUser) { long count = companyRepository.countByName(dto.getName()); if (count > 0) { throw new BusinessException(ResponseCode.MERCHANT_EXISTS); } Company company = Company.builder() .name(dto.getName()) .shortName(dto.getShortName()) .contact(dto.getContact()) .phone(dto.getPhone()) .address(dto.getAddress()) .build(); companyRepository.save(company); log.info("用户:{} 添加企业:{}", loginUser.getUsername(), dto.getName()); companyRepository.removeCache(); if (Objects.equals(dto.getCreateMainAccount(), true)) { AddSysUserDTO userDTO = new AddSysUserDTO(); userDTO.setRoles(Collections.singletonList(STRAUTH.MANAGER)); userDTO.setCompanyId(company.getId()); userDTO.setUsername(dto.getName()); userDTO.setPassword("Aa123456"); userDTO.setNickname(dto.getName()); userDTO.setStatus(true); SpringUtil.getBean(CompanyUserService.class).addSysUser(userDTO); log.info("用户:{} 创建企业主账号:{}", loginUser.getUsername(), dto.getName()); } return company.getId(); }

这段代码是 "新增实体" 类功能的典型实现,具体如下:

  1. 唯一性校验:通过companyRepository.countByName(dto.getName())校验企业名称是否已存在,避免重复创建,若重复则抛出业务异常;
  2. 参数转换:将入参AddCompanyDTO转换为Company实体对象,仅保留业务所需字段;
  3. 数据持久化:将实体保存数据库,结合@Transactional注解,确保保存失败时事务回滚;
  4. 缓存清理:调用removeCache()清理缓存,避免新增数据与缓存数据不一致;
  5. 扩展业务处理:根据createMainAccount参数判断是否创建企业主账号;
  6. 日志记录 + 结果返回:记录关键操作日志,最终返回新增实体的主键 ID;
  7. 并发控制(额外保障):通过@Lock4j()基于企业名称加锁,解决高并发下的重复创建问题。

updateCompany
@Log(title = "修改企业") @PostMapping @Operation(summary = "修改企业") public CommonResult<Boolean> updateCompany(@RequestBody @Validated UpdateCompanyDTO dto) { return CommonResult.success(companyService.updateCompany(dto, U.get())); }
@Lock4j(lockType = "updateCompany", key = "#dto.companyId") @Transactional(rollbackFor = Exception.class) public Boolean updateCompany(UpdateCompanyDTO dto, LoginUser loginUser) { Company company = companyRepository.getById(dto.getCompanyId()); if (company == null) { throw new BusinessException(ResponseCode.MERCHANT_NOT_EXISTS); } if (Boolean.TRUE.equals(loginUser.isAdmin()) || Boolean.TRUE.equals(loginUser.isManager()) || company.getId().equals(loginUser.getCompanyId())) { companyRepository.updateCompany(dto); log.info("用户:{} 更新企业:{}", loginUser.getUsername(), dto.getName()); return true; } throw new BusinessException(ResponseCode.MERCHANT_NOT_EXISTS); }
public void updateCompany(UpdateCompanyDTO dto) { update(Wrappers.<Company>lambdaUpdate() .set(StringUtils.isNotBlank(dto.getName()), Company::getName, dto.getName()) .set(StringUtils.isNotBlank(dto.getShortName()), Company::getShortName, dto.getShortName()) .set(StringUtils.isNotBlank(dto.getContact()), Company::getContact, dto.getContact()) .set(StringUtils.isNotBlank(dto.getPhone()), Company::getPhone, dto.getPhone()) .set(StringUtils.isNotBlank(dto.getAddress()), Company::getAddress, dto.getAddress()) .eq(Company::getId, dto.getCompanyId())); removeCache(); }

这段代码是 "实体更新" 类功能的典型实现,具体如下:

  1. 存在性校验:通过 companyRepository.getById () 查询目标企业是否存在,否则抛出异常;
  2. 权限校验:校验当前登录用户是否为管理员,或是否归属企业,仅满足权限条件才更新;
  3. 数据更新:调用 updateCompany (dto) 执行企业信息更新,结合 @Transactional 注解,确保更新失败时事务回滚,保证数据一致性;
  4. 并发控制:通过 @Lock4j () 基于企业 ID 加分布式锁,解决高并发下的重复更新问题;
  5. 日志记录 + 结果返回:记录用户更新企业的关键操作日志,权限校验通过则返回 true,未通过则抛出权限相关业务异常。

delCompany
@Log(title = "删除企业") @Auth(value = {STRAUTH.ADMIN, STRAUTH.MANAGER}) @DeleteMapping("/{companyId}") @Operation(summary = "删除企业") public CommonResult<Boolean> delCompany( @PathVariable(required = false) @Validated @NotNull(message = "企业id不能为空") Long companyId) { return CommonResult.success(companyService.delCompany(companyId, U.get())); }
@Lock4j(lockType = "delCompany", key = "#companyId") @Transactional(rollbackFor = Exception.class) public Boolean delCompany(Long companyId, LoginUser loginUser) { Company company = companyRepository.getById(companyId); if (company == null) { throw new BusinessException(ResponseCode.MERCHANT_NOT_EXISTS); } if (company.getId() == 1L) { throw new BusinessException(ResponseCode.MERCHANT_DISABLE_DELETED); } // 检查企业下是否有设备,检查企业是否存在用户子账号 if (deviceRepository.countByCompanyId(companyId) > 0) { throw new BusinessException(ResponseCode.MERCHANT_DEVICE_EXISTS); } if (userRepository.countByCompanyId(companyId) > 0) { throw new BusinessException(ResponseCode.MERCHANT_USER_EXISTS); } companyRepository.removeById(companyId); log.info("用户:{} 删除企业:{}", loginUser.getUsername(), company.getName()); companyRepository.removeCache(); return true; }
public void removeCache() { redisService.del("company:all"); }

这段代码是 "实体删除" 类功能的典型实现,具体如下:

  1. 存在性校验:通过 getById (companyId) 查询企业是否存在,若不存在则抛出业务异常;
  2. 特殊规则校验:校验目标企业是否为系统核心企业(ID=1L),若是则抛出业务异常;
  3. 关联数据校验:依次检查企业下是否关联设备、是否存在用户子账号,否则抛出业务异常;
  4. 数据删除:调用removeById () 执行删除操作,删除失败时事务回滚,保证数据一致性;
  5. 并发控制 + 缓存清理:通过 @Lock4j () 基于企业 ID 加分布式锁,解决高并发下的重复删除问题;删除后调用 removeCache () 清理缓存,避免删除数据与缓存数据不一致。
  6. 日志记录 + 结果返回:记录用户删除企业的关键操作日志,所有校验通过后返回 true。

企业用户账号管理模块

sysUserList
@PostMapping("/list") @Operation(summary = "系统用户列表") public CommonResult<SimplePage<SysUserListVO>> sysUserList(@RequestBody SysUserListDTO dto) { return CommonResult.success(userService.userList(dto)); }
public SimplePage<SysUserListVO> userList(SysUserListDTO dto) { return new SimplePage<>(userRepository.getUserList(Page.of(dto.getPageNum(), dto.getPageSize()), dto, U.get())); }
public Page<SysUserListVO> getUserList(Page page, SysUserListDTO dto, LoginUser loginUser) { Page<SysUserListVO> list = baseMapper.getUserList(page, dto, loginUser); for (SysUserListVO vo : list.getRecords()) { vo.setRoleList(Arrays.asList(vo.getRoleString().split(","))); } return list; }
Page<SysUserListVO> getUserList(@Param("page") Page page, @Param("dto") SysUserListDTO dto, @Param("loginUser") LoginUser loginUser);
<select resultType="com.daochengtech.lock.platform.core.model.vo.SysUserListVO"> select u.id as userId, u.u_username as username, company_id AS companyId, u_role AS roleString, u_nickname AS nickname, u_avatar AS avatar, u_enable AS status, c.c_name as companyName, u.create_time AS createTime, u.modify_time AS modifyTime from (select * from dc_user where deleted = 0 <if test="dto.companyId != null"> company_id = #{dto.companyId} </if> <if test="dto.username != null"> and u_username like concat('%',#{dto.username},'%') </if> <if test="loginUser !=null and loginUser.isEnterprise() == true"> and company_id = #{loginUser.companyId} </if> <if test="dto.userId != null"> and id = #{dto.userId} </if> ) u left join dc_company c on u.company_id = c.id </select>

这段代码是 "系统用户分页列表查询(多条件 + 权限过滤 + 结果格式化)" 典型实现,具体如下:

  1. 接口层接收请求:通过 @PostMapping ("/list") 定义用户列表接口;
  2. 服务层分页封装:调用Page.of ()封装分页参数,传入查询方法,返回SimplePage对象 ;
  3. 仓储层动态条件查询:Mapper 层通过 XML 构建动态 SQL,支持企业 ID、用户名、用户 ID 等业务条件过滤,同时追加登录用户权限过滤,并关联企业表查询企业名称;
  4. 结果格式化:遍历分页列表将角色字符串(String)拆为角色列表(List),补充到 VO 中 ;
  5. 分层数据返回:Mapper 层返回 Page<SysUserListVO>,服务层封装为 SimplePage对象。 

userDetail
@PostMapping("/detail") @Operation(summary = "用户详情") public CommonResult<UserDetailVO> userDetail(@RequestParam(value = "userId", required = false) @Validated @NotNull(message = "ID_NOT_NULL") Long userId) { return CommonResult.success(userService.userDetail(userId)); }
public UserDetailVO userDetail(Long userId) { UserDetailVO userDetail = userRepository.getUserDetail(userId, U.get()); userDetail.setRoleList(Arrays.stream(userDetail.getRoleString().split(",")).toList()); return userDetail; }
public UserDetailVO getUserDetail(Long userId, LoginUser loginUser) { return baseMapper.getUserDetail(userId, loginUser); }
UserDetailVO getUserDetail(@Param("userId") Long userId, @Param("loginUser") LoginUser loginUser);
<select resultType="com.daochengtech.lock.platform.core.model.vo.UserDetailVO"> select u.id as userId, u.u_username as username, c.id AS companyId, c.c_name as companyName, u_role AS roleString, u.u_username AS username, u_nickname AS nickname, u_avatar AS avatar, u_enable AS status, u.create_time AS createTime, u.modify_time AS modifyTime from dc_user u left join dc_company c on u.company_id = c.id where u.deleted = 0 and u.id = #{userId} </select>

这段代码是 "用户详情查询(参数校验 + 结果格式化)" 典型实现,具体如下:

  1. 接口层参数校验:通过 @RequestParam 接收用户 ID 参数,配合 @Validated + @NotNull 强制校验参数非空,若为空则抛出校验异常;
  2. 服务层查询调用:调用 getUserDetail () 方法,获取原始详情数据;
  3. 仓储层精准查询:Mapper 层通过 XML 构建精准 SQL,根据用户 ID 查询用户基础信息,并关联企业表查询所属企业名称,同时过滤已删除数据(deleted = 0);
  4. 结果格式化处理:将返回的角色字符串 roleString 拆分为角色列表 roleList ,补充 VO。

systemUserResetPwd
@Log(title = "重设密码") @PostMapping("/reset/pwd") @Operation(summary = "重设密码") public CommonResult<Boolean> systemUserResetPwd(@RequestBody @Validated SysUserRestPwdDTO dto) { return CommonResult.success(userService.sysUserRestPwd(dto, U.get())); }
@Transactional(rollbackFor = Exception.class) @Lock4j(lockType = "SYS_USER_RESET_PWD", key = "#dto.getUserId()") public Boolean sysUserRestPwd(SysUserRestPwdDTO dto, LoginUser loginUser) { User user = userRepository.getById(dto.getUserId()); if (user == null) { throw new BusinessException(ResponseCode.GET_USER_ERROR); } if (loginUser.isEnterprise() && !user.getId().equals(loginUser.getId())) { throw new BusinessException(ResponseCode.USER_NOT_EXIST); } userRepository.sysUserRestPwd(dto); tokenService.logout(user.getUsername()); return true; }
public Boolean sysUserRestPwd(SysUserRestPwdDTO dto) { return this.update(Wrappers.<User>lambdaUpdate() .set(User::getPassword, passwordEncoder.encode(dto.getNewPassword())) .eq(User::getId, dto.getUserId())); }
public void logout(String username) { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); if (requestAttributes == null) { return; } HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference( RequestAttributes.REFERENCE_REQUEST); if (request == null) { return; } try { Claims claims = parseToken(getToken(request)); String tokenId = claims.get(TOKEN, String.class); String key = String.format(FORMAT, LOGIN_USER, tokenId, username); redisService.del(key); } catch (Exception e) { log.error("登出时删除Token异常: {}", e.getMessage()); } }

这段代码是 "用户密码重设(含权限校验 + 安全登出)" 类功能的典型实现,具体如下:

  1. 接口层参数校验:通过@Validated 触发 DTO 的参数校验,确保重置密码所需参数合法;
  2. 存在性校验:通过 getById () 查询目标用户是否存在,若不存在则抛出业务异常;
  3. 权限校验:校验当前登录用户是否为企业用户且非目标用户本人,若是则抛出业务异常;
  4. 密码更新:调用sysUserRestPwd (dto) 更新用户密码,密码通过 encode () 加密后存储;
  5. 并发控制:通过 @Lock4j () 基于用户 ID 加分布式锁,解决高并发下的重复重置密码问题;
  6. 安全登出处理:调用 tokenService.logout () 方法,解析用户当前登录 Token 并删除 Redis 中对应的 Token 缓存,强制用户重新登录。

addSysUser
@Log(title = "添加系统用户") @Auth(value = {STRAUTH.ADMIN, STRAUTH.MANAGER, STRAUTH.ENTERPRISE}) @PostMapping("/add/account") @Operation(summary = "添加系统用户") public CommonResult<Boolean> addSysUser(@RequestBody @Validated AddSysUserDTO dto) { return CommonResult.success(userService.addSysUser(dto)); }
@Transactional(rollbackFor = Exception.class) @Lock4j(lockType = "SYS_USER_ADD", key = "#dto.getUsername()") public Boolean addSysUser(AddSysUserDTO dto) { if (userRepository.checkUsername(dto.getUsername())) { return userRepository.addSysUser(dto); } throw new BusinessException(ResponseCode.USERNAME_EXIST); }
public boolean checkUsername(String username) { return this.count(Wrappers.<User>lambdaQuery() .eq(User::getUsername, username)) <= 0; }
public Boolean addSysUser(AddSysUserDTO dto) { Company company = companyRepository.getById(dto.getCompanyId()); if (company == null) { throw new BusinessException(ResponseCode.MERCHANT_NOT_EXISTS); } User user = User.builder() .username(dto.getUsername()) .password(passwordEncoder.encode(dto.getPassword())) .companyId(company.getId()) .avatar(dto.getAvatar()) .role(String.join(",", dto.getRoles()).replace(" ", "")) .nickname(Optional.ofNullable(dto.getNickname()).orElse(dto.getUsername())) .enable(dto.getStatus()) .build(); return this.save(user); }

这段代码是 "添加系统用户"类功能的典型实现,具体如下:

  1. 接口层参数校验:通过 @Validated 触发 DTO 的参数校验,确保新增用户所需参数合法;
  2. 用户名唯一性校验:调用 checkUsername () 方法,统计用户是否已存在,否则抛出异常;
  3. 企业存在性校验:通过 getById () 查询关联企业是否存在,若不存在则抛出业务异常;
  4. 用户实体构建:将 AddSysUserDTO 转换为 User 数据库实体,密码通过 encode () 加密存储,角色列表拼接为字符串,昵称为空时兜底为用户名;
  5. 数据保存:调用 save (user) 保存用户实体,结合 @Transactional 确保失败时事务回滚。

第三方系统访问模块

getToken
/** * 获取访问Token */ @Operation(summary = "获取访问Token", description = "使用API密钥和秘钥获取访问Token,用于后续API调用") @PostMapping("/auth/token") public CommonResult<TokenResponse> getToken(@Valid @RequestBody TokenRequest request) { log.info("OpenAPI Token获取请求,API Key: {}", request.getApiKey()); try { TokenResponse tokenResponse = openApiService.generateTokenResponse(request.getApiKey(), request.getSecret()); log.info("OpenAPI Token生成成功,API Key: {}", request.getApiKey()); return CommonResult.success(tokenResponse); } catch (Exception e) { log.error("OpenAPI Token生成失败,API Key: {}", request.getApiKey(), e); throw e; } }
public TokenResponse generateTokenResponse(String apiKey, String secret) { log.info("生成Token响应,apiKey: {}", apiKey); TokenInfo tokenInfo = generateToken(apiKey, secret); // 计算过期时间(秒) long expiresIn = java.time.Duration.between(LocalDateTime.now(), tokenInfo.getExpiresAt()).getSeconds(); return TokenResponse.builder() .token(tokenInfo.getToken()) .tokenType("Bearer") .expiresIn(expiresIn) .scope("device:control") .build(); }
public TokenInfo generateToken(String apiKey, String secret) { log.info("生成Token,apiKey: {}", apiKey); // 验证API密钥和秘钥 ApiKey entity = apiKeyRepository.findByApiKey(apiKey); if (entity == null) { throw new BusinessException(ResponseCode.FAIL, "API密钥不存在"); } if (!entity.getEnabled()) { throw new BusinessException(ResponseCode.FAIL, "API密钥已禁用"); } if (!SecretEncoder.matches(secret, entity.getSecret())) { throw new BusinessException(ResponseCode.FAIL, "秘钥错误"); } // 撤销现有Token(一个API密钥同时只能有一个有效Token) tokenRepository.revokeTokensByApiKey(apiKey); // 生成新Token String token = ApiKeyGenerator.generateToken(); LocalDateTime now = LocalDateTime.now(); LocalDateTime expiresAt = now.plusHours(DEFAULT_TOKEN_EXPIRE_HOURS); TokenInfo tokenInfo = new TokenInfo(); tokenInfo.setToken(token); tokenInfo.setApiKey(apiKey); tokenInfo.setCreatedAt(now); tokenInfo.setExpiresAt(expiresAt); // 保存到Redis tokenRepository.saveToken(tokenInfo, DEFAULT_TOKEN_EXPIRE_HOURS); log.info("Token生成成功,apiKey: {}, token: {}", apiKey, token); return tokenInfo; }

这段代码是开放平台(OpenAPI)中访问 Token 生成与管控的核心业务逻辑,面向第三方系统 / 客户端提供 API 调用的身份认证能力,核心解决 "API 调用的合法性校验、Token 唯一性管控、过期自动失效" 三大问题,具体如下:

一、核心业务定位

该功能是 OpenAPI 的 "身份网关",第三方系统需先通过接口提交API Key(应用标识)Secret(应用秘钥) 获取 Token,后续所有 API 调用都需在请求头中携带该 Token,系统通过校验 Token 的有效性完成身份认证,避免 API 被非法调用。


二、完整业务流程

  1. 请求接收与日志记录:前端 / 第三方系统调用/auth/token接口,传入apiKeysecret,接口先记录请求日志(含apiKey),便于后续问题排查;
  2. API Key 合法性校验:根据apiKey查询数据库中的ApiKey实体,校验三大规则:
    • apiKey是否存在(不存在则抛 "API 密钥不存在" 异常);
    • apiKey是否启用(禁用则抛 "API 密钥已禁用" 异常);
    • secret是否匹配(通过matches()校验加密后的秘钥,错误则抛 "秘钥错误" 异常);
  3. 旧 Token 强制撤销:为保证 "一个 API Key 同时仅存在一个有效 Token",先撤销该apiKey关联的所有已存在 Token;
  4. 新 Token 生成:通过ApiKeyGenerator.generateToken()生成随机、唯一的 Token 字符串;
  5. Token 有效期设置:设定 Token 过期时间,计算当前时间到过期时间的秒数(expiresIn),用于告知第三方 Token 的有效时长;
  6. Token 持久化存储:将 Token、apiKey、创建时间、过期时间封装为TokenInfo,保存到 Redis,利用 Redis 的过期机制实现 Token 自动失效,减轻数据库查询压力;
  7. 响应数据封装:构建TokenResponse返回给调用方,包含核心字段:
    • token:实际用于 API 调用的令牌字符串;
    • tokenType:固定为Bearer
    • expiresIn:Token 过期剩余秒数;
    • scope:Token 权限范围;
  8. 异常兜底与日志:全程捕获异常,异常直接抛出由全局异常处理器返回标准化错误。

refreshToken
@PostMapping("/refreshToken") @Operation(summary = "刷新token") public CommonResult<String> refreshToken(HttpServletRequest request) { return CommonResult.success(systemService.refreshToken(request)); }
/** public String refreshToken(HttpServletRequest request) { //秒 Long expireTime = tokenService.getExpireTime(request); // 如果剩30分钟就刷新token if (expireTime < 30 * 60) { return tokenService.createToken(U.get()); } return tokenService.getToken(request); } */ public String refreshToken(HttpServletRequest request) { //秒 Long expireTime = tokenService.getExpireTime(request); // 如果剩30分钟就刷新token if (expireTime < 30 * 60) { String oldToken = tokenService.getToken(request); LoginUser loginUser = tokenService.getLoginUser(request); if (loginUser == null) { throw new BusinessException(ResponseCode.AUTHENTICATION_FAIL, "旧Token无效,无法刷新"); } String newToken = tokenService.createToken(loginUser); tokenService.invalidateOldToken(oldToken); return newToken; } return tokenService.getToken(request); }

这段代码是Token 刷新(refreshToken) 核心业务逻辑,属于用户认证会话管理的关键能力,实现 "Token 无感刷新" 的问题,同时兼顾 Token 安全性管控。

代码规范

判空工具类

核心推荐:ObjectUtil(Hutool 的ObjectUtil)能覆盖业务中 99% 以上的常用类型判空场景

1.字符串判空

  • 仅想判断 "字符串是不是 null":直接用str == null
  • 想判断 "字符串是 null,或者是空字符串("")":优先用StringUtils.isEmpty(str),替代繁琐的str == null || str =="";
  • 想判断 "字符串无任何有效字符"(包括 null、""、全空格):必须用StringUtils.isBlank(str),比如用户输入的空格、换行符都能被识别;
  • 想判断 "字符串非空且有有效字符":用StringUtils.isNotBlank(str),是业务中最常用非空判断。

2.基本数据类型判空

基本数据类型(int、long、boolean 等)没有 null 的概念,因为它们是值类型,不是对象:

  • 不存在 "判 null" 的说法,只能判断 "是否为默认值":比如 int 默认值是 0,就用num == 0;long 默认值是 0L,就用num == 0L;boolean 默认值是 false,就用flag == false
  • 绝对不能写int num == null,会直接编译报错。

3.包装类判空

包装类是对象,既有 "引用是否为空",也有 "值是否有效":

  • 仅判断 "引用是不是 null":直接用num == null(如Integer num == null);
  • 既判 null,又判值是否为默认值:先判num == null,再判num == 0,或用 Hutool 的ObjectUtil.isEmpty(num) || num == 0
  • 非空且值有效:如判断 Integer 非空且大于 0,用ObjectUtil.isNotEmpty(num) && num > 0

4.数组判空

数组是对象,判空要兼顾 "引用为空" 和 "数组长度为 0":

  • 仅判断 "数组引用是不是 null":用arr == null
  • 既判 null,又判空数组:优先用 Hutool 的ObjectUtil.isEmpty(arr)
  • 多维数组(比如 String [][]):需要逐层判空,先判外层数组非空,再判内层数组非空。

5.普通对象判空

  • 自定义对象:仅判断 "引用是不是 null",直接用obj == null(比如User user == null);
  • 集合(List、Map):判空要兼顾 "引用为空" 和 "集合为空",用ObjectUtil.isEmpty(list)
  • 通用非空判断:不管是自定义对象、包装类还是数组,都能用ObjectUtil.isNotEmpty(obj)

判空注解

1. @NotNull

@NotNull 是通用性最强的非空注解,可作用于所有数据类型(包括基本类型包装类、字符串、集合、自定义对象等)。它的核心校验规则仅判定目标值是否为 null,不关注值的内容:即便目标是字符串(如 "")、空集合(如 new ArrayList<>()),只要不是 null,该注解的校验就会通过。


2. @NotBlank

@NotBlank 是专门针对 String 类型 的强非空注解,校验规则比 @NotNull 更严格:它不仅要求目标字符串不能为 null,还要求去除首尾空格后字符串的长度必须大于 0。也就是说,null、空字符串 ""、仅包含空格的字符串 " " 都会被该注解判定为校验失败。


3. @NotEmpty

@NotEmpty 适用于 String 类型、集合(List/Set/Map)、数组,核心规则是:目标值不能为 null,且对应的 "长度 / 大小" 必须大于 0。对字符串而言,它仅判定是否为 null 或空字符串 "";对集合 / 数组而言,它判定是否为 null 或空容器,只要容器中有至少一个元素,即便元素本身是 null,也能通过校验。

日志框架

<?xml version="1.0" encoding="UTF-8"?> <configuration scan="true" scanPeriod="60 seconds" debug="false"> <!-- 引入spirng boot默认的logback配置文件 --> <include resource="org/springframework/boot/logging/logback/defaults.xml"/> <springProperty scope="context" name="appName" source="spring.application.name"/> <!-- Simple file output --> <appender name="FILE"> <!-- <File>/log/common-service.log</File> --> <rollingPolicy> <!-- daily rollover --> <FileNamePattern>/var/log/${appName}/%d{yyyy-MM-dd,aux}/%d{yyyy-MM-dd HH}.log</FileNamePattern> <!-- keep 30 days' worth of history --> <maxHistory>744</maxHistory> </rollingPolicy> <encoder> <Pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{35} - %msg %n</Pattern> </encoder> </appender> <!-- Console output --> <appender name="STDOUT"> <!-- 采用Spring boot中默认的控制台彩色日志输出模板 --> <encoder> <pattern>${CONSOLE_LOG_PATTERN}</pattern> </encoder> <!-- Only log level WARN and above --> <filter> <level>DEBUG</level> </filter> </appender> <!-- 异步输出 --> <!--出现了丢失日志的问题--> <appender name="ASYNC_FILE"> <!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 --> <discardingThreshold>0</discardingThreshold> <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 --> <queueSize>512</queueSize> <!-- 添加附加的appender,最多只能添加一个 --> <appender-ref ref="FILE"/> </appender> <appender name="ASYNC_STDOUT"> <!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 --> <discardingThreshold>0</discardingThreshold> <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 --> <queueSize>512</queueSize> <!-- 添加附加的appender,最多只能添加一个 --> <appender-ref ref="STDOUT"/> </appender> <!-- Enable FILE and STDOUT appenders for all log messages. By default, only log at level INFO and above. --> <root level="INFO"> <appender-ref ref="ASYNC_FILE"/> <appender-ref ref="ASYNC_STDOUT"/> </root> </configuration>

模块1:基础配置(全局开关)

<configuration scan="true" scanPeriod="60 seconds" debug="false"> <include resource="org/springframework/boot/logging/logback/defaults.xml"/> <springProperty scope="context" name="appName" source="spring.application.name"/>

核心作用

  • 是整个日志配置的 "基础底座",定义全局规则(自动刷新配置、关闭框架调试日志);
  • 复用 Spring Boot 官方的默认日志规则(不用自己写基础格式);
  • 读取项目名称,后续日志文件路径会用这名称,让日志文件和项目绑定。

模块2:文件输出模块(FILE Appender)

<appender name="FILE"> <rollingPolicy> <FileNamePattern>/var/log/${appName}/%d{yyyy-MM-dd,aux}/%d{yyyy-MM-dd HH}.log</FileNamePattern> <maxHistory>744</maxHistory> </rollingPolicy> <encoder> <Pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{35} - %msg %n</Pattern> </encoder> </appender>

核心作用

  • 定义 "日志写进文件" 的规则,是最核心的文件日志输出器;
  • 按 "小时" 切分日志文件(比如每小时生成新文件),按 "日" 建文件夹,避免单个文件过大;
  • 日志只保留30天(744小时),自动清理旧日志,防止占满服务器磁盘;
  • 定义日志内容的格式(包含时间、线程、级别、类名、日志内容)。

模块3:控制台输出模块(STDOUT Appender)

<appender name="STDOUT"> <encoder> <pattern>${CONSOLE_LOG_PATTERN}</pattern> </encoder> <filter> <level>DEBUG</level> </filter> </appender>

核心作用

  • 定义"日志输出到控制台"的规则,方便开发时实时看日志;
  • 用 Spring Boot 自带的彩色日志格式(ERROR 红、INFO 绿),视觉更清晰;
  • 过滤掉 TRACE 级别的日志(只输出 DEBUG 及以上),减少控制台无用日志。

模块4:异步输出优化模块(ASYNC_* Appender)

<appender name="ASYNC_FILE"> <discardingThreshold>0</discardingThreshold> <queueSize>512</queueSize> <appender-ref ref="FILE"/> </appender> <appender name="ASYNC_STDOUT"> <discardingThreshold>0</discardingThreshold> <queueSize>512</queueSize> <appender-ref ref="STDOUT"/> </appender>

核心作用

  • 给"文件输出"和 "控制台输出"加"异步缓冲",是性能优化模块;
  • 主线程打日志时不用等日志写完,直接继续执行代码,避免日志写入拖慢程序;
  • 配置"不丢日志"(队列满了也不丢弃),同时调整队列大小(512)平衡性能和内存。

模块5:全局日志开关模块(root)

<root level="INFO"> <appender-ref ref="ASYNC_FILE"/> <appender-ref ref="ASYNC_STDOUT"/> </root>

核心作用

  • 整个项目的日志"总控制",定义全局日志级别为 INFO(只输出 INFO、WARN、ERROR);
  • 绑定前面异步输出器,最终实现所有符合级别的日志,既异步写文件,又异步输出到控制台。

Optional 判空

传统判空写法

String message; if (e == null || e.getMessage() == null) {; } else { message = e.getMessage(); }

Optional 简化写法

String message = Optional.ofNullable(e.getMessage()).orElse("");

切面优先级

@Aspect @Slf4j @Order(-10) @Component public class LockAop {} @Transactional(rollbackFor = Exception.class) @Lock4j(lockType = "addCompany", key = "#dto.name") public Long addCompany(AddCompanyDTO dto, LoginUser loginUser) { //业务逻辑... }

debug调试:

@Transactional 对应的 Spring 事务切面默认优先级数值是 Integer.MAX_VALUE@Order 数值越小优先级越高,因此 -10 > 2147483647,LockAop 先执行前置逻辑、后执行后置逻辑。

同一方法上多个切面(含 Spring 内置事务切面)的执行逻辑:标注 @Order 的切面按数值越小优先级越高,无 @Order 的内置切面(如 @Transactional)默认优先级最低;高优先级切面的环绕通知会先执行前置逻辑,再触发低优先级切面 / 目标方法,最后执行高优先级切面的后置逻辑。

通用能力封装

RedisService 封装类

1. Redis 统一配置:序列化 + 缓存时效 + 自定义服务注入

@EnableCaching @Configuration @Slf4j public class BaseRedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisSerializer<Object> serializer = redisSerializer(); RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(serializer); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(serializer); redisTemplate.afterPropertiesSet(); return redisTemplate; } @Bean public RedisSerializer<Object> redisSerializer() { //创建JSON序列化器 ObjectMapper objectMapper = new ObjectMapper(); //支持LocalDate objectMapper.registerModule(new JavaTimeModule()); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); //必须设置,否则无法将JSON转化为对象,会转化成Map类型 objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL); return new Jackson2JsonRedisSerializer<>(objectMapper, Object.class); } @Bean public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) { RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory); //设置Redis缓存有效期为1天 RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer())).entryTtl(Duration.ofDays(1)); return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration); } @Bean public RedisService redisService(RedisTemplate<String, Object> redisTemplate) { return new RedisServiceImpl(redisTemplate); } }

2. 定义 Redis 通用操作接口,封装 Redis 各类数据结构操作

public interface RedisService { /** * 保存属性 秒 */ void set(String key, Object value, long time); /** * 保存属性 指定时间 Duration */ void set(String key, Object value, Duration duration); /** * 保存属性 */ void set(String key, Object value); Boolean setIfAbsent(String key, Object value, Duration duration); Boolean setIfAbsent(String key, Object value); /** * 获取属性 */ Object get(String key); <T> T get(String key, Class<T> T); /** * 删除属性 */ Boolean del(String key); /** * 批量删除属性 */ Long del(List<String> keys); /** * 设置过期时间 */ Boolean expire(String key, long time); /** * 获取过期时间 */ Long getExpire(String key); /** * 判断是否有该属性 */ Boolean hasKey(String key); /** * 按delta递增 */ Long incr(String key, long delta); /** * 按delta递减 */ Long decr(String key, long delta); /** * 获取Hash结构中的属性 */ Object hGet(String key, String hashKey); /** * 向Hash结构中放入一个属性 * 单位:秒 */ Boolean hSet(String key, String hashKey, Object value, long time); void hSetIfAbsent(String key, String hashKey, Object value); /** * 向Hash结构中放入一个属性 */ void hSet(String key, String hashKey, Object value); /** * 直接获取整个Hash结构 */ Map<Object, Object> hGetAll(String key); /** * 直接设置整个Hash结构 */ Boolean hSetAll(String key, Map<String, Object> map, long time); /** * 直接设置整个Hash结构 */ void hSetAll(String key, Map<String, ?> map); /** * 删除Hash结构中的属性 */ void hDel(String key, Object... hashKey); /** * 判断Hash结构中是否有该属性 */ Boolean hHasKey(String key, String hashKey); /** * Hash结构中属性递增 */ Long hIncr(String key, String hashKey, Long delta); /** * Hash结构中属性递减 */ Long hDecr(String key, String hashKey, Long delta); /** * 获取Set结构 */ Set<Object> sMembers(String key); /** * 向Set结构中添加属性 */ Long sAdd(String key, Object... values); /** * 向Set结构中添加属性 */ Long sAdd(String key, long time, Object... values); /** * 是否为Set中的属性 */ Boolean sIsMember(String key, Object value); /** * 获取Set结构的长度 */ Long sSize(String key); /** * 删除Set结构中的属性 */ Long sRemove(String key, Object... values); /** * 获取List结构中的属性 */ List<Object> lRange(String key, long start, long end); /** * 获取List结构的长度 */ Long lSize(String key); /** * 根据索引获取List中的属性 */ Object lIndex(String key, long index); /** * 向List结构中添加属性 */ Long lPush(String key, Object value); /** * 向List结构中添加属性 */ Long lPush(String key, Object value, long time); /** * 向List结构中批量添加属性 */ Long lPushAll(String key, Object... values); /** * 向List结构中批量添加属性 */ Long lPushAll(String key, Long time, Object... values); /** * 从List结构中移除属性 */ Long lRemove(String key, long count, Object value); void setExpire(String key); Set<String> keys(String key); /** * 模糊匹配删除 */ void delByPattern(String pattern); }

3. 实现 RedisService 接口,封装多结构 Redis 操作逻辑

@Slf4j @AllArgsConstructor public class RedisServiceImpl implements RedisService { private RedisTemplate<String, Object> redisTemplate; @Override public void set(String key, Object value, long time) { redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); } @Override public void set(String key, Object value, Duration duration) { redisTemplate.opsForValue().set(key, value, duration); } @Override public void set(String key, Object value) { redisTemplate.opsForValue().set(key, value); } @Override public Boolean setIfAbsent(String key, Object value, Duration duration) { return redisTemplate.opsForValue().setIfAbsent(key, value, duration); } @Override public Boolean setIfAbsent(String key, Object value) { return redisTemplate.opsForValue().setIfAbsent(key, value); } @Override public Object get(String key) { return redisTemplate.opsForValue().get(key); } @Override @SuppressWarnings("unchecked") public <T> T get(String key, Class<T> clazz) { Object entity = redisTemplate.opsForValue().get(key); try { if (entity != null) { return (T) entity; } } catch (Exception e) { throw new RuntimeException("redis get key is error"); } return null; } @Override public Boolean del(String key) { return redisTemplate.delete(key); } @Override public Long del(List<String> keys) { return redisTemplate.delete(keys); } @Override public Boolean expire(String key, long time) { return redisTemplate.expire(key, time, TimeUnit.SECONDS); } @Override public Long getExpire(String key) { return redisTemplate.getExpire(key, TimeUnit.SECONDS); } @Override public Boolean hasKey(String key) { return redisTemplate.hasKey(key); } @Override public Long incr(String key, long delta) { return redisTemplate.opsForValue().increment(key, delta); } @Override public Long decr(String key, long delta) { return redisTemplate.opsForValue().increment(key, -delta); } @Override public Object hGet(String key, String hashKey) { return redisTemplate.opsForHash().get(key, hashKey); } @Override public Boolean hSet(String key, String hashKey, Object value, long time) { redisTemplate.opsForHash().put(key, hashKey, value); return expire(key, time); } @Override public void hSetIfAbsent(String key, String hashKey, Object value) { redisTemplate.opsForHash().putIfAbsent(key, hashKey, value); } @Override public void hSet(String key, String hashKey, Object value) { redisTemplate.opsForHash().put(key, hashKey, value); } @Override public Map<Object, Object> hGetAll(String key) { return redisTemplate.opsForHash().entries(key); } @Override public Boolean hSetAll(String key, Map<String, Object> map, long time) { redisTemplate.opsForHash().putAll(key, map); return expire(key, time); } @Override public void hSetAll(String key, Map<String, ?> map) { redisTemplate.opsForHash().putAll(key, map); } @Override public void hDel(String key, Object... hashKey) { redisTemplate.opsForHash().delete(key, hashKey); } @Override public Boolean hHasKey(String key, String hashKey) { return redisTemplate.opsForHash().hasKey(key, hashKey); } @Override public Long hIncr(String key, String hashKey, Long delta) { return redisTemplate.opsForHash().increment(key, hashKey, delta); } @Override public Long hDecr(String key, String hashKey, Long delta) { return redisTemplate.opsForHash().increment(key, hashKey, -delta); } @Override public Set<Object> sMembers(String key) { return redisTemplate.opsForSet().members(key); } @Override public Long sAdd(String key, Object... values) { return redisTemplate.opsForSet().add(key, values); } @Override public Long sAdd(String key, long time, Object... values) { Long count = redisTemplate.opsForSet().add(key, values); expire(key, time); return count; } @Override public Boolean sIsMember(String key, Object value) { return redisTemplate.opsForSet().isMember(key, value); } @Override public Long sSize(String key) { return redisTemplate.opsForSet().size(key); } @Override public Long sRemove(String key, Object... values) { return redisTemplate.opsForSet().remove(key, values); } @Override public List<Object> lRange(String key, long start, long end) { return redisTemplate.opsForList().range(key, start, end); } @Override public Long lSize(String key) { return redisTemplate.opsForList().size(key); } @Override public Object lIndex(String key, long index) { return redisTemplate.opsForList().index(key, index); } @Override public Long lPush(String key, Object value) { return redisTemplate.opsForList().rightPush(key, value); } @Override public Long lPush(String key, Object value, long time) { Long index = redisTemplate.opsForList().rightPush(key, value); expire(key, time); return index; } @Override public Long lPushAll(String key, Object... values) { return redisTemplate.opsForList().rightPushAll(key, values); } @Override public Long lPushAll(String key, Long time, Object... values) { Long count = redisTemplate.opsForList().rightPushAll(key, values); expire(key, time); return count; } @Override public Long lRemove(String key, long count, Object value) { return redisTemplate.opsForList().remove(key, count, value); } @Override public void setExpire(String key) { try { redisTemplate.opsForValue().getAndExpire(key, Duration.ofDays(1)); } catch (Exception ignored) { } } @Override public Set<String> keys(String key) { return redisTemplate.keys(key + "**"); } /** * 模糊匹配删除 * * @param pattern */ @Override public void delByPattern(String pattern) { //使用scan进行删除 redisTemplate.execute((RedisCallback<Void>) connection -> { try (connection; Cursor<byte[]> cursor = connection.keyCommands().scan(ScanOptions.scanOptions().match(pattern).count(1000).build())) { while (cursor.hasNext()) { byte[] key = cursor.next(); redisTemplate.delete(new String(key, StandardCharsets.UTF_8)); } } return null; }); } }

Redission 实现分布式锁

1. 定义 Lock4j 注解,配置分布式锁核心参数

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Lock4j { /** * 锁的前缀 指定是什么类型 */ String lockType() default ""; /** * 支持spEL表达式 锁的key */ String key() default ""; /** * 锁超时时间,默认30000毫秒(可在配置文件全局设置) */ long lockWatchdogTimeout() default 0; /** * 等待加锁超时时间,默认10000毫秒 -1 则表示一直等待(可在配置文件全局设置) */ long attemptTimeout() default 0; }

2. 基于 AOP 实现 Lock4j 注解,落地 Redisson 分布式锁

@Aspect @Slf4j @Order(-10) @Component public class LockAop { public static final String SEPARATOR = ":"; /** * 锁超时时间 */ private final long lockWatchdog; /** * 等待加锁超时时间,默认2000毫秒 -1 则表示一直等待(可在配置文件全局设置) */ private final long attempt; private final RedissonClient redissonClient; public LockAop(@Value("${redisson.lockWatchdog}") long lockWatchdog, @Value("${redisson.attempt}") long attempt, RedissonClient redissonClient) { this.lockWatchdog = lockWatchdog; this.attempt = attempt; this.redissonClient = redissonClient; } @Pointcut("@annotation(lock4j)") public void controllerAspect(Lock4j lock4j) { } @Around(value = "controllerAspect(lock4j)", argNames = "proceedingJoinPoint,lock4j") public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint, Lock4j lock4j) throws Throwable { String keys = lock4j.key(); if (!StringUtils.hasText(keys)) { throw new BusinessException(ResponseCode.KEYS_EMPTY); } String key = getRedissonKey(proceedingJoinPoint, lock4j); long attemptTimeout = lock4j.attemptTimeout() == 0 ? attempt : lock4j.attemptTimeout(); long lockWatchdogTimeout = lock4j.lockWatchdogTimeout() == 0 ? lockWatchdog : lock4j.lockWatchdogTimeout(); boolean res = false; //创建了一个分布式锁的对象实例 RLock rLock = redissonClient.getLock(lock4j.lockType() + SEPARATOR + key); //执行aop if (rLock != null) { try { if (attemptTimeout == -1) { res = true; //一直等待加锁 rLock.lock(lockWatchdogTimeout, TimeUnit.MILLISECONDS); } else { // waitTime – 获取锁的最长时间 leaseTime – 租赁时间 unit – 时间单位 res = rLock.tryLock(attemptTimeout, lockWatchdogTimeout, TimeUnit.MILLISECONDS); } log.info("Lock:{},interrupted:{},hold:{},threadId:{} ", rLock.getName(), Thread.currentThread().isInterrupted(), rLock.isHeldByCurrentThread(), Thread.currentThread().threadId()); if (res) { return proceedingJoinPoint.proceed(); } else { throw new BusinessException(ResponseCode.GET_LOCK_ERROR); } } finally { //获取到锁且在锁定状态 if (res && rLock.isLocked()) { rLock.unlock(); } } } throw new BusinessException(ResponseCode.REPEATED_SUBMIT); } /** * 解析spEL 表达式获取实际的key * * @param proceedingJoinPoint 切点 * @param lock4j 注解 * @return 实际的key */ private String getRedissonKey(ProceedingJoinPoint proceedingJoinPoint, Lock4j lock4j) { //spEL解析器 ExpressionParser parser = new SpelExpressionParser(); //spEL上下文 EvaluationContext context = new StandardEvaluationContext(); //拿到参数 MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature(); Method method = signature.getMethod(); String[] parameterNames = new StandardReflectionParameterNameDiscoverer().getParameterNames(method); Object[] parameterValues = proceedingJoinPoint.getArgs(); //组装出对象 for (int i = 0; i < Objects.requireNonNull(parameterNames).length; i++) { context.setVariable(parameterNames[i], parameterValues[i]); } Expression expression = parser.parseExpression(lock4j.key()); //EL表达式里的实际值 Object realValue = expression.getValue(context); return lock4j.lockType() + realValue; } }

LockAop 核心流程

  1. 初始化配置:注入全局锁超时时间、等待加锁超时时间、Redisson ,定义锁 Key 分隔符;
  2. 切点定义:匹配所有标注@Lock4j注解的方法,作为分布式锁的增强目标;
  3. 参数校验:校验注解中锁 Key 配置是否为空,为空则抛异常;
  4. 动态 Key 解析:解析注解中的 SpEL 表达式,结合方法参数生成实际业务锁 Key;
  5. 参数适配:优先使用注解配置的超时时间,无配置则用全局默认值;
  6. 锁对象创建:根据锁类型 + 分隔符 + 业务 Key,创建 Redisson 分布式锁对象;
  7. 加锁逻辑
    1. 等待超时为 - 1:调用lock()阻塞等待,直到获取锁;
    2. 有等待超时:调用tryLock()尝试加锁,超时未获取则失败;
  8. 业务执行:加锁成功则执行目标方法,失败则抛 "获取锁失败" 异常;
  9. 锁释放:最终在 finally 块中,仅当成功获取锁且锁未释放时,执行解锁操作;
  10. 异常兜底:锁对象创建失败时,抛 "重复提交" 异常。

CompletableUtil 工具包

封装 CompletableUtil 工具类,支持线程 / 虚拟线程异步执行

public class CompletableUtil { public static <U> CompletableFuture<U> supply(Supplier<U> supplier) { return CompletableFuture.supplyAsync(supplier, CustomThreadPool.getEXECUTOR()); } public static CompletableFuture<Void> run(Runnable runnable) { return CompletableFuture.runAsync(runnable, CustomThreadPool.getEXECUTOR()); } /** * 使用虚拟线程执行任务(适用于I/O密集型操作) */ public static <U> CompletableFuture<U> supplyVirtual(Supplier<U> supplier) { return CompletableFuture.supplyAsync(supplier, CustomThreadPool.getVIRTUAL_EXECUTOR()); } /** * 使用虚拟线程执行任务(适用于I/O密集型操作) */ public static CompletableFuture<Void> runVirtual(Runnable runnable) { return CompletableFuture.runAsync(runnable, CustomThreadPool.getVIRTUAL_EXECUTOR()); } }

private void saveLogAsync(OperationLog operationLog) { CompletableUtil.run(() -> { try { SpringUtil.getBean(OperationLogMapper.class).insert(operationLog); } catch (Exception exception) { log.error(LOG_EXCEPTION_MESSAGE, exception); } }); }

CompletableFuture 是 Java 8 引入的异步编程工具,基于 Future 增强,支持异步任务执行、结果回调、多任务组合等能力,能大幅简化异步编程逻辑(避免手动创建线程+回调地狱)。

核心优势

对比传统 Thread/Runnable/Future,它的核心优势:

  1. 异步执行+结果回调:任务执行完自动触发回调,无需手动轮询 Future.get()
  2. 多任务组合:支持串行、并行、任意一个完成、全部完成等组合方式;
  3. 异常处理:内置异常捕获机制,避免异步任务异常导致线程挂掉;
  4. 线程池适配:可指定自定义线程池,控制异步任务的线程资源。

基础用法

1. 核心API分类

类型

核心方法

作用

异步执行无返回值

runAsync(Runnable runnable)<br>runAsync(Runnable runnable, Executor executor)

异步执行任务,无返回结果

异步执行有返回值

supplyAsync(Supplier<U> supplier)<br>supplyAsync(Supplier<U> supplier, Executor executor)

异步执行任务,有返回结果

结果回调

thenApply(Function<T, R> fn)<br>thenAccept(Consumer<T> consumer)<br>thenRun(Runnable action)

任务完成后处理结果

异常处理

exceptionally(Function<Throwable, T> fn)<br>handle(BiFunction<T, Throwable, R> fn)

捕获任务异常并返回默认值/同时处理正常结果和异常

2. 基础示例

1)异步执行无返回值(比如异步记录日志)

// 自定义线程池(推荐,避免用默认的ForkJoinPool) private static final ExecutorService asyncPool = new ThreadPoolExecutor( 5, 10, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100), new ThreadFactory() { private int count = 0; @Override public Thread newThread(Runnable r) { return new Thread(r, "async-task-" + (++count)); } }, new ThreadPoolExecutor.CallerRunsPolicy() ); // 异步记录企业创建日志(无返回值) public void asyncLogCompanyCreate(CompanyDTO dto) { CompletableFuture.runAsync(() -> { // 模拟日志记录逻辑 log.info("异步记录企业创建日志:{}", dto.getName()); try { Thread.sleep(1000); // 模拟耗时操作 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }, asyncPool); // 指定自定义线程池 }

2)异步执行有返回值(比如异步查询企业详情)

// 异步查询企业详情(有返回值) public CompletableFuture<CompanyDTO> asyncGetCompanyById(Long id) { return CompletableFuture.supplyAsync(() -> { CompanyDO companyDO = companyMapper.selectById(id); return convertToDTO(companyDO); }, asyncPool) .exceptionally(e -> { log.error("异步查询企业失败:id={}", id, e); return new CompanyDTO(); }); } public void testAsyncGetCompany() { CompletableFuture<CompanyDTO> future = asyncGetCompanyById(1L); // 同步获取结果:get()会阻塞,join()不会抛检查异常 CompanyDTO dto = future.join(); log.info("企业名称:{}", dto.getName()); }

3)结果回调(任务完成后处理结果)

// 异步查询企业后,自动回调处理结果(无需阻塞) public void testThenApply() { asyncGetCompanyById(1L) // thenApply:处理结果并返回新值(串行执行) .thenApply(dto -> { dto.setShortName(dto.getName() + "-"); return dto; }) // thenAccept:消费结果(无返回值) .thenAccept(dto -> log.info("处理:{}", dto.getShortName())) // thenRun:结果处理完后执行无参数操作 .thenRun(() -> log.info("处理完成")); }

三、进阶用法

实际业务中常需要 "并行执行多个异步任务,再合并结果",CompletableFuture 提供丰富的API:

组合场景

核心方法

串行执行

thenCompose(Function<T, CompletableFuture<R>> fn)(嵌套异步任务)

并行执行-全部完成

allOf(CompletableFuture<?>... cfs)(所有任务完成后执行)

并行执行-任意一个完成

anyOf(CompletableFuture<?>... cfs)(任意一个任务完成后执行)

结果合并

thenCombine(CompletableFuture<U> other, BiFunction<T, U, R> fn)(两个任务结果合并)

1. 串行异步任务(比如先查企业,再查该企业的用户)

场景:异步查企业 ,再异步查该企业的用户

public CompletableFuture<List<UserDTO>> asyncGetCompanyUser(Long companyId) { return asyncGetCompanyById(companyId) // thenCompose:嵌套异步任务 .thenCompose(dto -> CompletableFuture.supplyAsync(() -> { List<UserDO> userDOList = userMapper.selectByCompanyId(companyId); return userDOList.stream().map(this::convertToUserDTO).toList(); }, asyncPool)); }

2. 并行执行多个任务(全部完成后合并结果)

场景:创建企业时,并行异步初始化 "企业配置" + "企业权限",全部完成后返回结果。

// 并行执行多个异步任务,全部完成后合并结果 public CompletableFuture<Boolean> asyncInitCompany(Long companyId) { //异步初始化企业配置 CompletableFuture<Boolean> configFuture = CompletableFuture.supplyAsync(() -> { companyConfigMapper.init(companyId); return true; }, asyncPool); //异步初始化企业权限 CompletableFuture<Boolean> permissionFuture = CompletableFuture.supplyAsync(() -> { companyPermissionMapper.init(companyId); return true; }, asyncPool); // 等待所有任务完成,再判断结果 return CompletableFuture.allOf(configFuture, permissionFuture) .thenApply(v -> { // 获取每个任务的结果 boolean configOk = configFuture.join(); boolean permissionOk = permissionFuture.join(); return configOk && permissionOk; }) .exceptionally(e -> { log.error("初始化企业失败:{}", companyId, e); return false; }); }

3. 并行执行多个任务(任意一个完成就返回)

场景:查询企业信息,同时从缓存和数据库查,哪个快用哪个。

// 并行查缓存+数据库,任意一个完成就返回 public CompletableFuture<CompanyDTO> asyncGetCompanyFast(Long companyId) { //查缓存 CompletableFuture<CompanyDTO> cacheFuture = CompletableFuture.supplyAsync(() -> { log.info("从缓存查企业:{}", companyId); try { Thread.sleep(200); // 模拟缓存查询耗时 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return cacheService.getCompany(companyId); }, asyncPool); //查数据库 CompletableFuture<CompanyDTO> dbFuture = CompletableFuture.supplyAsync(() -> { log.info("从数据库查企业:{}", companyId); try { Thread.sleep(500); // 模拟数据库查询耗时 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } CompanyDO do = companyMapper.selectById(companyId); return convertToDTO(do); }, asyncPool); // 任意一个任务完成就返回结果 return CompletableFuture.anyOf(cacheFuture, dbFuture) .thenApply(result -> { // 结果是Object类型,需要强转 CompanyDTO dto = (CompanyDTO) result; if (dto == null) { // 如果缓存没查到,返回数据库结果(兜底) return dbFuture.join(); } return dto; }); }

定时任务线程池

@Configuration public class SchedulerConfig implements SchedulingConfigurer { private static final int POOL_SIZE = 20; @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setPoolSize(POOL_SIZE); scheduler.setThreadNamePrefix("Tplus-Scheduler-"); scheduler.setWaitForTasksToCompleteOnShutdown(true); scheduler.setAwaitTerminationSeconds(60); taskRegistrar.setTaskScheduler(scheduler); scheduler.initialize(); } }

该配置类是Spring 定时任务的线程池定制化配置,替代 Spring 默认的单线程调度器,解决定时任务串行执行、阻塞、宕机丢失任务等问题,核心价值是提升定时任务的并发能力和稳定性。

  1. 自定义线程池:创建核心线程数为 20 的 ThreadPoolTaskScheduler,替代默认单线程;
  2. 线程命名:设置线程名前缀Tplus-Scheduler-,便于日志排查线程归属;
  3. 优雅停机setWaitForTasksToCompleteOnShutdown(true):应用关闭时等待所有定时任务执行完成;setAwaitTerminationSeconds(60):最多等待 60 秒,超时则强制终止;
  4. 绑定调度器:将自定义线程池绑定到 ScheduledTaskRegistrar,让所有 @Scheduled 注解的定时任务使用该线程池执行。

自定义线程池实现

public class CustomThreadPool { private static final int CORE_POOL_SIZE = Math.max(4, Runtime.getRuntime().availableProcessors() * 2); private static final int MAX_POOL_SIZE = 200; private static final int QUEUE_CAPACITY = 1000; // 60 秒 private static final long KEEP_ALIVE_TIME = 60L; @Getter public static final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor( CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS, new LinkedBlockingQueue<>(QUEUE_CAPACITY), new CustomThreadFactory("event-handler-thread-"), new ThreadPoolExecutor.CallerRunsPolicy() ); }
public class CustomThreadFactory implements ThreadFactory { private final AtomicInteger threadNumber = new AtomicInteger(1); private final String namePrefix; public CustomThreadFactory(String namePrefix) { this.namePrefix = namePrefix; } @Override public Thread newThread(Runnable r) { Thread t = new Thread(r, namePrefix + threadNumber.getAndIncrement()); t.setDaemon(false); t.setPriority(Thread.NORM_PRIORITY); return t; } }

自定义线程池,根据 CPU 核心动态设置核心线程数,使用有界队列、自定义命名线程工厂与安全拒绝策略,避免资源耗尽与 OOM,保障异步任务稳定可控执行。

AOP实现日志记录

@Slf4j @Aspect @Component public class LogAspect { /** * 排除敏感属性字段 */ private static final Set<String> EXCLUDE_PROPERTIES = new HashSet<>(); private static final String LOG_EXCEPTION_MESSAGE = "操作日志异常信息: "; static { EXCLUDE_PROPERTIES.add("password"); EXCLUDE_PROPERTIES.add("oldPassword"); EXCLUDE_PROPERTIES.add("newPassword"); EXCLUDE_PROPERTIES.add("confirmPassword"); } /** * 处理完请求后执行 * * @param joinPoint 切点 */ @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "result") public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object result) { handleLog(joinPoint, controllerLog, null, result); } /** * 拦截异常操作 * * @param joinPoint 切点 * @param e 异常 */ @AfterThrowing(value = "@annotation(controllerLog)", throwing = "e") public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) { handleLog(joinPoint, controllerLog, e, null); } protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object ret) { try { HttpServletRequest request = ServletUtils.getRequest(); if (request != null) { OperationLog operationLog = buildOperationLog(joinPoint, request, controllerLog, ret, e); saveLogAsync(operationLog); } } catch (Exception exp) { log.error(LOG_EXCEPTION_MESSAGE, exp); } } private Map<String, String[]> getParamsWithoutSensitiveInfo(HttpServletRequest request, Log controllerLog) { Map<String, String[]> params = new HashMap<>(ServletUtils.getParams(request)); // 使用Set优化敏感信息过滤 EXCLUDE_PROPERTIES.forEach(params::remove); if (controllerLog.exclude() != null) { Arrays.stream(controllerLog.exclude()).forEach(params::remove); } return params; } private OperationLog buildOperationLog(JoinPoint joinPoint, HttpServletRequest request, Log controllerLog, Object ret, Exception e) { UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent")); LoginUser currentUser; try { currentUser = U.get(); } catch (Exception ex) { currentUser = new LoginUser(); } String className = joinPoint.getTarget().getClass().getName(); String methodName = joinPoint.getSignature().getName(); Map<String, String[]> params = getParamsWithoutSensitiveInfo(request, controllerLog); OperationLog operationLog = new OperationLog(); operationLog.setOperationModule(controllerLog.title()); operationLog.setOperationMethod(className + "." + methodName + "()"); operationLog.setOperationParams(JSON.toJSONString(params)); operationLog.setOperationMethodType(request.getMethod()); operationLog.setOperationUrl(request.getRequestURI()); if (e != null) { System.out.println(e.getMessage()); operationLog.setOperationResult(e != null ? StringUtils.substring(e.getMessage(), 0, 20) : String.valueOf(ret)); } operationLog.setOperationIp(IPUtil.getClientIP(request)); operationLog.setOperationBrowser(getUserAgentString(userAgent)); operationLog.setOperationOs(getOperatingSystemString(userAgent)); operationLog.setOperationUser(currentUser.getUsername()); operationLog.setOperationUserId(currentUser.getId()); operationLog.setOperationTime(LocalDateTime.now(ZoneId.systemDefault())); operationLog.setCompanyId(currentUser.getAuthCompanyId()); List<Object> args = Arrays.stream(joinPoint.getArgs()) .filter(CommonDTO.class::isInstance).toList(); operationLog.setOperationBody(JSON.toJSONString(args)); return operationLog; } private void saveLogAsync(OperationLog operationLog) { CompletableUtil.run(() -> { try { SpringUtil.getBean(OperationLogMapper.class).insert(operationLog); } catch (Exception exception) { log.error(LOG_EXCEPTION_MESSAGE, exception); } }); } private String getUserAgentString(UserAgent userAgent) { return userAgent.getBrowser().toString(); } private String getOperatingSystemString(UserAgent userAgent) { return userAgent.getOperatingSystem().toString(); } } 

Bug处理

缓存失效问题

@Lock4j(lockType = "delCompany", key = "#companyId") @Transactional(rollbackFor = Exception.class) public Boolean delCompany(Long companyId, LoginUser loginUser) { Company company = companyRepository.getById(companyId); if (company == null) { throw new BusinessException(ResponseCode.MERCHANT_NOT_EXISTS); } if (company.getId() == 1L) { throw new BusinessException(ResponseCode.MERCHANT_DISABLE_DELETED); } // 检查企业下是否有设备,检查企业是否存在用户子账号 if (deviceRepository.countByCompanyId(companyId) > 0) { throw new BusinessException(ResponseCode.MERCHANT_DEVICE_EXISTS); } if (userRepository.countByCompanyId(companyId) > 0) { throw new BusinessException(ResponseCode.MERCHANT_USER_EXISTS); } companyRepository.removeById(companyId); log.info("用户:{} 删除企业:{}", loginUser.getUsername(), company.getName()); //companyRepository.removeCache(); return true; }

缓存机制未接入实际业务流程,所有对外核心业务方法均直接操作数据库,既不读取也不写入缓存;而 removeCache () 仅删除 "company:all" 缓存键,该缓存键仅由内部方法设置且未被外部业务调用,即便增删改方法执行了 removeCache (),也因目标缓存从未被实际使用而无法产生效果,最终导致缓存逻辑形同虚设,removeCache () 方法丧失实际作用。

续期后旧 Token 未失效

@PostMapping("/refreshToken") @Operation(summary = "刷新token") public CommonResult<String> refreshToken(HttpServletRequest request) { return CommonResult.success(systemService.refreshToken(request)); }
public String refreshToken(HttpServletRequest request) { //秒 Long expireTime = tokenService.getExpireTime(request); // 如果剩30分钟就刷新token if (expireTime < 30 * 60) { return tokenService.createToken(loginUser); } return tokenService.getToken(request); }
public void syncToken2Redis(LoginUser loginUser, String uuid) { String key = String.format(FORMAT, LOGIN_USER, uuid, loginUser.getUsername()); redisService.set(key, loginUser, Duration.ofHours(expirationHours)); }

在 Token 续期逻辑中,当检测到 Token 剩余有效期不足 30 分钟时,仅生成并返回新 Token,同时将新 Token 信息同步至 Redis,但未对 Redis 中存储的旧 Token 执行删除 / 过期等失效操作,导致旧 Token 未被禁用,仍能正常校验通过,存在同一账号多 Token 有效、权限管控失效的风险。

解决办法:在 Token 续期时,当检测到 Token 剩余有效期不足 30 分钟生成新 Token 并同步至 Redis 后,新增调用 invalidateOldToken 方法对旧 Token 执行失效处理:该方法通过登录用户唯一标识检索 Redis 中存储的旧 Token 缓存 Key,执行删除操作,彻底禁用旧 Token。

public String refreshToken(HttpServletRequest request) { //秒 Long expireTime = tokenService.getExpireTime(request); // 如果剩30分钟就刷新token if (expireTime < 30 * 60) { String oldToken = tokenService.getToken(request); LoginUser loginUser = tokenService.getLoginUser(request); if (loginUser == null) { throw new BusinessException(ResponseCode.AUTHENTICATION_FAIL, "旧Token无效,无法刷新"); } String newToken = tokenService.createToken(loginUser); tokenService.invalidateOldToken(oldToken); return newToken; } return tokenService.getToken(request); }
public void invalidateOldToken(String oldToken) { if (StringUtils.isBlank(oldToken)) { log.warn("失效旧Token失败:Token为空"); return; } try { Claims claims = parseToken(oldToken); String oldTokenId = claims.get(TOKEN, String.class); String username = claims.get(USERNAME, String.class); String oldKey = String.format(FORMAT, LOGIN_USER, oldTokenId, username); if (redisService.del(oldKey)) { log.info("失效旧Token成功,username:{},tokenId:{}", username, oldTokenId); } else { log.warn("失效旧Token失败:Redis中未找到该Token缓存,username:{},tokenId:{}", username, oldTokenId); } } catch (Exception e) { log.error("失效旧Token异常:{}", e.getMessage(), e); throw new BusinessException(ResponseCode.AUTHENTICATION_FAIL, "失效旧Token失败"); } }

感谢你的阅读!🌹

Read more

【超详细】Python FastAPI 入门:写给新手的“保姆级”教程

【超详细】Python FastAPI 入门:写给新手的“保姆级”教程

前言  作为一名大学生,最近在做 Python Web 开发时发现了一个“宝藏”框架——FastAPI。 以前学 Django 光配置就头大,学 Flask 又不知道怎么写规范。直到遇到了 FastAPI,我才体会到什么叫“写代码像呼吸一样自然”。 这篇文章不讲复杂的原理,只讲最基础、最实用的操作,带你从 0 到 1 跑通第一个 API 接口! 一、FastAPI 是什么 在 Python 的世界里,做网站后台(Web 开发)主要有三巨头: 1. Django:老大哥,功能全但笨重,像一辆重型卡车。 2. Flask:二哥,轻便灵活但插件多,像一辆自行组装的赛车。 3.

By Ne0inhk
Python——Windows11环境安装配置Python 3.12.5

Python——Windows11环境安装配置Python 3.12.5

目录 * 一、下载Python * 二、下载Python步骤 * 三、安装Python * 四、验证Python * 4.1、验证Python环境 * 4.2、验证pip * 4.3、更新pip * 4.4、pip镜像源切换(永久切换,全局生效,清华镜像源和阿里云镜像源二选一即可) * 4.4.1、清华镜像源 * 4.4.2、阿里云镜像源 * 4.5、安装依赖包(检验是否成功) * 五、配置环境变量(可选) 一、下载Python 或者百度网盘下载 链接: https://pan.baidu.com/s/1Rc8g1mZrfDtOexev2JK7NA?pwd=

By Ne0inhk

2026最新保姆级教程:Windows 下使用 uv 从零配置 Python (OpenCV) 环境指南

Windows 下使用 uv 从零配置 Python (OpenCV) 环境指南 本文档适用于在一台全新的 Windows 电脑上,使用 uv 快速配置vscode + Python 3.10 开发环境,并安装 OpenCV 库。同时包含关于 uv 的进阶说明。 B站配套视频 2026最新:使用uv管理python&opencv 🟢 第一步:安装 uv 包管理器 既然电脑上什么都没有,我们需要先安装这个核心工具。 1. 按下 Win + R 键,输入 powershell,按回车打开终端。 2. 复制并粘贴以下命令,按回车运行(三选一): * 或者进如 uv 下载链接 找到

By Ne0inhk