项目设计
如何拦截 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。
思路 一
在 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 在服务器中的位置。