项目设计


项目设计

如何拦截 Token

定义拦截器

/**
 * 接口拦截器
 */
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {

    @Resource
    private RedisUtils redisUtils;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IllegalStateException{

        // 先从缓存中获取数据
        String token = SecurityUtils.getToken(request);
        String key = RedisConstants.LOGIN_TOKEN_KEY + token;
        LoginUser loginUser = (LoginUser) redisUtils.get(key);
        if (ObjectUtils.isNotEmpty(loginUser)) {
            LoginUserUtils.set(loginUser);
            return true;
        } else {
            throw new IllegalStateException("User Token Is Expires");
        }
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) {
        LoginUserUtils.remove();
    }
}

启用拦截器

/**
 * 启用拦截器
 */
@Configuration
public class LoginConfig implements WebMvcConfigurer {

    @Resource
    private LoginInterceptor loginInterceptor;

   @Override
   public void addInterceptors(InterceptorRegistry registry) {
       registry.addInterceptor(loginInterceptor).addPathPatterns("/api/**")
               .excludePathPatterns("/api/user/login",
                       "/api/user/save",
                       "/api/manage/dict_type/*");
    }

    /**
     * 显示 swagger-ui.html文档展示页,还必须注入 swagger 资源
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    }
}

这里一般用户信息会存储在 Redis 内,因此会涉及到和 Redis 相关的类。

获取 Token 的工具类

/**
 * SecurityUtils 工具类
 *
 */
public class SecurityUtils {

    /**
     * 获取请求token
     */
    public static String getToken() throws IllegalStateException
    {
        return getToken(Objects.requireNonNull(ServletUtils.getRequest()));
    }

    /**
     * 根据request获取请求token
     */
    public static String getToken(HttpServletRequest request) throws IllegalStateException{

        // 从 Hearder 中获取
        String header = request.getHeader(TokenConstants.AUTHENTICATION);

        // 从 Parameter 中获取
        if (StringUtils.isBlank(header)) {
            header = request.getParameter(TokenConstants.AUTHENTICATION);
        }

        // 从 Cookie 中获取
        if (StringUtils.isBlank(header)) {
            Cookie[] cookies = request.getCookies();
            if (ObjectUtils.isEmpty(cookies)) {
                throw new IllegalStateException("Request Token Is Empty");
            }
            for (Cookie cookie : cookies) {
                if (Objects.equals(cookie.getName(), TokenConstants.AUTHENTICATION)) {
                    header = cookie.getValue();
                }
            }
        }

        // 都为空的情况
        if (StringUtils.isBlank(header)) {
            throw new IllegalStateException("Request Token Is Empty");
        }

        if (!header.startsWith(TokenConstants.PREFIX)) {
            throw new IllegalStateException("Request Token Is Empty");
        }

        return JwtUtils.getUserKey(header.substring(TokenConstants.PREFIX.length()));
    }

    /**
     * 生成BCryptPasswordEncoder密码
     *
     * @param password 密码
     * @return 加密字符串
     */
    public static String encryptPassword(String password)
    {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        return passwordEncoder.encode(password);
    }

    /**
     * 判断密码是否相同
     *
     * @param rawPassword 真实密码
     * @param encodedPassword 加密后字符
     * @return 结果
     */
    public static boolean matchesPassword(String rawPassword, String encodedPassword)
    {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        return passwordEncoder.matches(rawPassword, encodedPassword);
    }
}

Redis 的常量类

/**
 * Redis 常量类
 */
public class RedisConstants {

    /**
     * 注册验证码 key 前缀
     */
    public static final String REGISTER_CODE_KEY = "register_codes:";

    /**
     * 短信注册验证码 key 过期时间
     */
    public static final long REGISTER_TIME = 60;

    /**
     * 邮件注册验证码 key 过期时间
     */
    public static final long REGISTER_EMAIL_TIME = 5 * 60;

    /**
     * 登录验证码 key 前缀
     */
    public static final String LOGIN_CODE_KEY = "login_codes:";

    /**
     * 注册登录验证码 key 前缀
     */
    public static final String EGISTER_LOGINR_CODE_KEY = "register_login_codes:";

    /**
     * 缓存有效期
     */
    public final static long EXPIRED_TIME = 12 * 60 * 60;

    /**
     * 权限缓存前缀
     */
    public final static String LOGIN_TOKEN_KEY = "login_tokens:";
}

放入到 Redis 内的用户信息

/**
 * User 类,提供给生成 Token 和登录拦截使用
 */
@Data
public class LoginUser {

    /**
     * 用户ID
     */
    private long id;

    /**
     * 手机号码
     */
    private String phone;

    /**
     * 邮箱
     */
    private String email;

    /**
     * 姓名
     */
    private String name;

    /**
     * Token
     */
    private String token;

    /**
     * 最后登录时间,这里保存的是毫秒
     */
    private Long lastLoginTime;

    /**
     * Token 过期时间,这里保存的是毫秒
     */
    private Long tokenTime;

}

用户工具类,使用 ThreadLocal 进行存储

/**
 * LoginUserUtils 工具类
 */
public class LoginUserUtils {

    private LoginUserUtils() {}

    private static final ThreadLocal<LoginUser> CURRENT_USER = new ThreadLocal<>();

    public static void set(LoginUser loginUser) {
        CURRENT_USER.set(loginUser);
    }

    public static LoginUser get() {
        return CURRENT_USER.get();
    }

    public static void remove() {
        CURRENT_USER.remove();
    }
}

如何定义全局错误类型

封装基础类的错误信息

/**
 * 基础错误信息封装
 */
@Slf4j
@RestControllerAdvice
@RestController
public class ExceptionsHandler implements ErrorController {

    /**
     * RuntimeException
     * @param illegalStateException
     * @return
     */
    @ExceptionHandler(IllegalStateException.class)
    public R<?> illegalStateException(IllegalStateException illegalStateException) {
        log.error(illegalStateException.getMessage());
        return R.fail(illegalStateException.getMessage());
    }

    /**
     * MethodArgumentNotValidException
     * @param methodArgumentNotValidException
     * @return
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public R<?> methodArgumentNotValidException(MethodArgumentNotValidException methodArgumentNotValidException) {
        String string = methodArgumentNotValidException.getBindingResult().getFieldErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.toList()).toString();
        log.error(string.substring(1, string.length()-1));
        return R.fail(string.substring(1, string.length()-1));
    }

    /**
     * 处理不配 Controller 层拦截的错误
     * @param response
     * @return
     */
    @RequestMapping("/error")
    public R<?> error(HttpServletResponse response) {
        int status = response.getStatus();
        if (status == HttpServletResponse.SC_NOT_FOUND) {
            return R.fail("接口地址错误,请检查并核对接口正确的请求地址!");
        } if (status == HttpServletResponse.SC_METHOD_NOT_ALLOWED) {
            return R.fail("接口请求错误,请检查并核对接口正确的请求方式!");
        }
        return R.fail("接口方法错误,请查看对应日志!");
    }

    /**
     * RhxManageException 自定义异常
     * @param rhxManageException
     * @return
     */
    @ExceptionHandler(RhxManageException.class)
    public R<?> rhxManageException(RhxManageException rhxManageException) {
        log.error(rhxManageException.getMessage());
        return R.fail(rhxManageException.getMessage());
    }
}

自定义异常

/**
 * 自定义异常
 */
public class RhxManageException extends RuntimeException{

    public RhxManageException() {}

    public RhxManageException(String message) {
        super(message);
    }
}

使用

public TSysDictType getLevelCode(String code, Long parentId) {
    TSysDictType tSysDictType = sysDictTypeService.getOne(new LambdaQueryWrapper<TSysDictType>().eq(TSysDictType::getCode, code));
    Optional.ofNullable(tSysDictType).orElseThrow(() -> new RhxManageException("数据字典编码不存在"));
}

如何在一个接口查询主表和子表数据

所有的查询都在一个接口内,其中主表一张,子表两张,但查询并不是每次都查询这三张表的数据,而是分为以下两种情况:

  1. 主表 + 子表1。
  2. 主表 + 子表1 + 子表2。

思路 一

在 SQL 里,通过标签 / 去做条件判断。

这种思路,如果涉及到的条件比较到,SQL 里要写一堆的判断条件。

思路 二

写两个 SQL 查询,在代码里控制使用那个查询条件。

这种思路,SQL 会变的简单,易于修改和维护,但会出现 SQL 代码部分重复的现象。

解决方式

采用思路二的方式进行处理,代码如下:

public R<?> getPage(DeciSubPageDTO pageDTO) {
    PageHelper.startPage(pageDTO.getPageNo(),pageDTO.getPageSize());
    List<DeciSubPageVO> pageList;
    // 查询子表数据的条件
    if (ObjectUtils.isNotEmpty(pageDTO.getAddStatus()) || ObjectUtils.isNotEmpty(pageDTO.getDline()) || ObjectUtils.isNotEmpty(pageDTO.getType()) || ObjectUtils.isNotEmpty(pageDTO.getFully())) {
        pageList = decisionService.getPageRecord(pageDTO);
    } else {
        pageList = decisionService.getPage(pageDTO);
    }
    return R.ok(new PageInfo<>(pageList));
}

扩展

默认排序是根据更新时间排序,如何在这个基础上在根据 co_id 这个条件排序,数据库使用的是:MySQL5.7。

  • 原SQL
SELECT a.id, a.sub_id, b.en_name, b.cn_name, b.cas, b.ec, a.reg_status, a.co_name, a.co_id, a.co_status, a.busi_name, a.tech_name, a.end_time 
FROM 
    t_decision a 
    LEFT JOIN t_base_substance b ON a.sub_id = b.id 
WHERE 
    a.deleted = 0 
    AND b.deleted = 0
ORDER BY
    CASE
    WHEN #{pageDTO.sort} = 1 THEN a.dline_time
    WHEN #{pageDTO.sort} = 2 THEN a.create_time
    ELSE a.update_time
END DESC
  • 优化后的SQL
SELECT a.id, a.sub_id, b.en_name, b.cn_name, b.cas, b.ec, a.reg_status, a.co_name, a.co_id, a.co_status, a.busi_name, a.tech_name, a.end_time 
FROM 
    t_decision a 
    LEFT JOIN t_base_substance b ON a.sub_id = b.id

<if test="pageDTO.groupByCoId != null and pageDTO.groupByCoId == 1">
    LEFT JOIN ( SELECT t.co_id, MAX(t.update_time) AS max_update_time FROM t_decision t LEFT JOIN t_base_substance s ON t.sub_id = s.id  WHERE t.deleted = 0 AND s.deleted = 0 GROUP BY t.co_id ) g ON a.co_id = g.co_id
</if>

WHERE 
    a.deleted = 0
    AND b.deleted = 0
ORDER BY

<if test="pageDTO.groupByCoId != null and pageDTO.groupByCoId == 1">
    g.max_update_time DESC,
    FIELD(a.update_time, g.max_update_time) DESC,
    a.co_id,
    CASE
        WHEN #{pageDTO.sort} = 1 THEN a.dline_time
        WHEN #{pageDTO.sort} = 2 THEN a.create_time
        ELSE a.update_time
    END DESC
</if>

<if test="pageDTO.groupByCoId == null or pageDTO.groupByCoId == 2">
    CASE
        WHEN #{pageDTO.sort} = 1 THEN a.dline_time
        WHEN #{pageDTO.sort} = 2 THEN a.create_time
        ELSE a.update_time
    END DESC
</if>

注意上述 SQL 代码里的标签,就是增加的条件。

如何调取其它项目的接口

情况 一

当前项目为 JAVA 项目,调取第三方的接口,如果第三方提供的是完整接口地址,那么只需要使用类 RestTemplate 进行调取即可。

  • 代码如下
public String getCnName(String oaId) {
    String cnName = null;
    Map<String, String> map = new HashMap<>();
    map.put("oaid", oaId);
    log.info("调取XX接口传参:" + map);
    try {
        ResponseEntity<CompanyVO> responseEntity = restTemplate.getForEntity(url, CompanyVO.class, map);
        log.info("调XX查询接口结果:" + responseEntity);
        if (responseEntity.getStatusCode().is2xxSuccessful()) {
            CompanyVO companyVO = responseEntity.getBody();
            if (ObjectUtils.isNotEmpty(companyVO)) cnName = companyVO.getData().get(0).getCompName();
        } else {
            log.error("服务异常-调取XX接口失败:" + responseEntity);
            throw new RhxSiefException("服务异常-调取XX接口失败!");
        }
    } catch (Exception e) {
        log.error("服务异常-调取XX接口失败:" + e);
        throw new RhxSiefException("服务异常-调取XX接口失败!");
    }
    return cnName;
}

其中 url 为接口地址,如下:http://192.168.xxxx:81/api/oa/company/searchByOaId?oaid={oaid}。CompanyVO 为接口返回的实体类,根据实际接口返回参数来设计。

情况 二

当前项目为 JAVA 项目,调取第三方的接口,但第三方接口不是一个完整的接口,第三方接口是 Python 脚本。

  • 代码如下
@Service
public class PythonServiceImpl implements IPythonService {

    @Override
    public String calculateEnthalpy(String pressure, String temperature) {
        StringBuilder result = new StringBuilder();
        try {
            // 构建命令, "python3" 表示使用python3命令; "/usr/py/calcul.py" 为脚本在服务器中的位置。
            String[] command = new String[]{"python3", "/usr/py/calcul.py", pressure, temperature};

            // 创建进程
            ProcessBuilder processBuilder = new ProcessBuilder(command);
            processBuilder.redirectErrorStream(true);
            Process process = processBuilder.start();

            // 读取输出
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line;
            while ((line = reader.readLine()) != null) {
                result.append(line);
            }

            // 等待进程结束
            int exitCode = process.waitFor();
            if (exitCode != 0) {
                throw new RuntimeException("Python script execution failed with exit code " + exitCode);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return formatNumber(result.toString());
    }
}

其中 “python3” 表示使用 python3 命令。”/usr/py/calcul.py” 为脚本 calcul.py 在服务器中的位置。


文章作者: L Q
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 L Q !
  目录