Merge pull request #14 from hiparker/development

v 1.6.3 申请合并
pull/33/head 1.6.3
PengchengZhou 3 years ago committed by GitHub
commit af9fdfc5ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,60 @@
/**
* Copyright 2020 OPSLI https://www.opsli.com
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.opsli.api.web.system.logs;
import org.opsli.api.base.result.ResultVo;
import org.opsli.api.wrapper.system.logs.LogsModel;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import javax.servlet.http.HttpServletRequest;
/**
* API
*
* API @GetMapping @PostMapping
* Mapping Controller
*
*
*
* @author Parker
* @date 2020-09-13 17:40
*/
public interface LoginLogsApi {
/** 标题 */
String TITLE = "登录日志管理";
/** 子标题 */
String SUB_TITLE = "登录日志";
/**
*
* @param pageNo
* @param pageSize
* @param request request
* @return ResultVo
*/
@GetMapping("/findPage")
ResultVo<?> findPage(
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
HttpServletRequest request
);
}

@ -38,9 +38,9 @@ import javax.servlet.http.HttpServletRequest;
public interface LogsApi { public interface LogsApi {
/** 标题 */ /** 标题 */
String TITLE = "日志管理"; String TITLE = "操作日志管理";
/** 子标题 */ /** 子标题 */
String SUB_TITLE = "日志"; String SUB_TITLE = "操作日志";
/** /**
* *
@ -64,13 +64,6 @@ public interface LogsApi {
HttpServletRequest request HttpServletRequest request
); );
/**
*
* @return ResultVo
*/
@PostMapping("/emptyByOneMonth")
ResultVo<?> emptyByOneMonth();
/** /**
* *

@ -128,11 +128,11 @@ public interface UserApi {
/** /**
* *
* @param request request * @param userAvatarModel
* @return ResultVo * @return ResultVo
*/ */
@PostMapping("/updateAvatar") @PostMapping("/updateAvatar")
ResultVo<?> updateAvatar(MultipartHttpServletRequest request); ResultVo<?> updateAvatar(@RequestBody UserAvatarModel userAvatarModel);
/** /**

@ -0,0 +1,74 @@
/**
* Copyright 2020 OPSLI https://www.opsli.com
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.opsli.api.wrapper.system.logs;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.opsli.api.base.warpper.ApiWrapper;
/**
*
*
* @author Parker
* @date 2020-09-16 17:33
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class LoginLogsModel extends ApiWrapper {
/**
*
*/
private String tenantId;
/**
* ID xxx,xxx
*/
private String orgIds;
/**
*
*/
@ApiModelProperty(value = "用户名称")
private String username;
/**
*
*/
@ApiModelProperty(value = "真实姓名")
private String realName;
/**
*
* 1 :
* 2 :
*/
@ApiModelProperty(value = "日志类型")
private String type;
/**
* IP
*/
@ApiModelProperty(value = "操作IP地址")
private String remoteAddr;
/**
*
*/
@ApiModelProperty(value = "用户代理")
private String userAgent;
}

@ -37,6 +37,18 @@ public class LogsModel extends ApiWrapper {
public static final String TYPE_EXCEPTION = "2"; public static final String TYPE_EXCEPTION = "2";
/**
*
*/
@ApiModelProperty(value = "租户ID")
private String tenantId;
/**
* ID xxx,xxx
*/
@ApiModelProperty(value = "组织ID集合")
private String orgIds;
/** 日志类型1接入日志2错误日志 */ /** 日志类型1接入日志2错误日志 */
@ApiModelProperty(value = "日志类型") @ApiModelProperty(value = "日志类型")
private String type; private String type;

@ -0,0 +1,38 @@
/**
* Copyright 2020 OPSLI https://www.opsli.com
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.opsli.api.wrapper.system.user;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.opsli.common.annotation.validator.Validator;
import org.opsli.common.enums.ValidatorType;
/**
*
*
* @author Parker
* @date 2020-09-16 17:33
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class UserAvatarModel {
@ApiModelProperty(value = "图片地址")
@Validator({ValidatorType.IS_NOT_NULL, ValidatorType.IS_URL})
private String imgUrl;
}

@ -43,6 +43,10 @@ public enum DictType {
DATA_SCOPE_DEPT_AND_UNDER("role_data_scope","2", "本部门及以下数据"), DATA_SCOPE_DEPT_AND_UNDER("role_data_scope","2", "本部门及以下数据"),
DATA_SCOPE_ALL("role_data_scope","3", "全部数据"), DATA_SCOPE_ALL("role_data_scope","3", "全部数据"),
/** 登入登出日志类型 */
LOGIN_LOG_TYPE_LOGIN("login_log_type","1", "登入"),
LOGIN_LOG_TYPE_LOGOUT("login_log_type","2", "登出")
; ;

@ -5,7 +5,6 @@ import com.google.common.util.concurrent.*;
import lombok.Data; import lombok.Data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.opsli.common.thread.ThreadPoolFactory;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.RejectedExecutionException;

@ -1,12 +1,10 @@
package org.opsli.common.thread; package org.opsli.common.thread;
import com.alibaba.ttl.threadpool.TtlExecutors;
import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.*;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/** /**
* 线 * 线
@ -41,7 +39,7 @@ public final class ThreadPoolFactory {
* 线 * 线
* @return ThreadPoolExecutor * @return ThreadPoolExecutor
*/ */
public static ThreadPoolExecutor createDefThreadPool(){ public static ExecutorService createDefThreadPool(){
return createInitThreadPool(DEFAULT_MAX_CONCURRENT, DEFAULT_MAX_CONCURRENT * 4, DEFAULT_KEEP_ALIVE, return createInitThreadPool(DEFAULT_MAX_CONCURRENT, DEFAULT_MAX_CONCURRENT * 4, DEFAULT_KEEP_ALIVE,
TimeUnit.SECONDS, DEFAULT_SIZE, DEFAULT_THREAD_POOL_NAME, new ThreadPoolExecutor.CallerRunsPolicy()); TimeUnit.SECONDS, DEFAULT_SIZE, DEFAULT_THREAD_POOL_NAME, new ThreadPoolExecutor.CallerRunsPolicy());
} }
@ -53,7 +51,7 @@ public final class ThreadPoolFactory {
* @param poolName 线 * @param poolName 线
* @return ThreadPoolExecutor * @return ThreadPoolExecutor
*/ */
public static ThreadPoolExecutor createDefThreadPool(String poolName){ public static ExecutorService createDefThreadPool(String poolName){
return createInitThreadPool(DEFAULT_MAX_CONCURRENT, DEFAULT_MAX_CONCURRENT * 4, DEFAULT_KEEP_ALIVE, return createInitThreadPool(DEFAULT_MAX_CONCURRENT, DEFAULT_MAX_CONCURRENT * 4, DEFAULT_KEEP_ALIVE,
TimeUnit.SECONDS, DEFAULT_SIZE, poolName, new ThreadPoolExecutor.CallerRunsPolicy()); TimeUnit.SECONDS, DEFAULT_SIZE, poolName, new ThreadPoolExecutor.CallerRunsPolicy());
} }
@ -65,7 +63,7 @@ public final class ThreadPoolFactory {
* @param poolName 线 * @param poolName 线
* @return ThreadPoolExecutor * @return ThreadPoolExecutor
*/ */
public static ThreadPoolExecutor createDefThreadPool(int maxConcurrent, String poolName){ public static ExecutorService createDefThreadPool(int maxConcurrent, String poolName){
return createInitThreadPool(maxConcurrent, maxConcurrent * 4, DEFAULT_KEEP_ALIVE, return createInitThreadPool(maxConcurrent, maxConcurrent * 4, DEFAULT_KEEP_ALIVE,
TimeUnit.SECONDS, DEFAULT_SIZE, poolName, new ThreadPoolExecutor.CallerRunsPolicy()); TimeUnit.SECONDS, DEFAULT_SIZE, poolName, new ThreadPoolExecutor.CallerRunsPolicy());
} }
@ -81,7 +79,7 @@ public final class ThreadPoolFactory {
* @param handler * @param handler
* @return ThreadPoolExecutor * @return ThreadPoolExecutor
*/ */
public static ThreadPoolExecutor createInitThreadPool(final int coreConcurrent, public static ExecutorService createInitThreadPool(final int coreConcurrent,
final int maxConcurrent, final int maxConcurrent,
final long keepAlive, final long keepAlive,
final TimeUnit timeUnit, final TimeUnit timeUnit,
@ -89,11 +87,11 @@ public final class ThreadPoolFactory {
final String poolName, final String poolName,
final RejectedExecutionHandler handler final RejectedExecutionHandler handler
){ ){
return new ThreadPoolExecutor(coreConcurrent, maxConcurrent, keepAlive, timeUnit, return TtlExecutors.getTtlExecutorService(new ThreadPoolExecutor(coreConcurrent, maxConcurrent, keepAlive, timeUnit,
new LinkedBlockingDeque<>(queueSize), new LinkedBlockingDeque<>(queueSize),
new ThreadFactoryBuilder().setNameFormat(poolName).build(), new ThreadFactoryBuilder().setNameFormat(poolName).build(),
handler handler
); ));
} }
private ThreadPoolFactory(){} private ThreadPoolFactory(){}

@ -19,12 +19,14 @@ import lombok.extern.slf4j.Slf4j;
import org.opsli.common.annotation.ApiRestController; import org.opsli.common.annotation.ApiRestController;
import org.opsli.core.api.ApiRequestMappingHandlerMapping; import org.opsli.core.api.ApiRequestMappingHandlerMapping;
import org.opsli.core.autoconfigure.properties.ApiPathProperties; import org.opsli.core.autoconfigure.properties.ApiPathProperties;
import org.opsli.core.filters.interceptor.UserAuthInterceptor;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations; import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter; import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
@ -83,4 +85,10 @@ public class SpringWebMvcConfig implements WebMvcConfigurer, WebMvcRegistrations
return new CorsFilter(urlBasedCorsConfigurationSource); return new CorsFilter(urlBasedCorsConfigurationSource);
} }
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 加载特定拦截器
registry.addInterceptor(new UserAuthInterceptor());
WebMvcConfigurer.super.addInterceptors(registry);
}
} }

@ -55,6 +55,8 @@ import org.opsli.core.utils.UserUtil;
import org.opsli.plugins.excel.exception.ExcelPluginException; import org.opsli.plugins.excel.exception.ExcelPluginException;
import org.opsli.plugins.excel.listener.BatchExcelListener; import org.opsli.plugins.excel.listener.BatchExcelListener;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest; import org.springframework.web.multipart.MultipartHttpServletRequest;
@ -92,6 +94,18 @@ public abstract class BaseRestController <T extends BaseEntity, E extends ApiWra
@Autowired(required = false) @Autowired(required = false)
protected S IService; protected S IService;
/**
*
* id
*
* @param id id
* @return E
*/
@ModelAttribute
public E get(@RequestParam(required=false) String id) {
return IService.get(id);
}
/** /**
* Excel * Excel
* @param request request * @param request request

@ -228,9 +228,11 @@ public abstract class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseEnt
@Override @Override
public Page<T,E> findPage(Page<T,E> page) { public Page<T,E> findPage(Page<T,E> page) {
// 数据处理责任链
QueryWrapper<T> qWrapper = this.addHandler(entityClazz, page.getQueryWrapper());
page.pageHelperBegin(); page.pageHelperBegin();
try{ try{
List<T> list = this.findList(page.getQueryWrapper()); List<T> list = super.list(qWrapper);
PageInfo<T> pageInfo = new PageInfo<>(list); PageInfo<T> pageInfo = new PageInfo<>(list);
List<E> es = transformTs2Ms(pageInfo.getList()); List<E> es = transformTs2Ms(pageInfo.getList());
page.instance(pageInfo, es); page.instance(pageInfo, es);
@ -242,9 +244,11 @@ public abstract class CrudServiceImpl<M extends BaseMapper<T>, T extends BaseEnt
@Override @Override
public Page<T,E> findPageNotCount(Page<T,E> page) { public Page<T,E> findPageNotCount(Page<T,E> page) {
// 数据处理责任链
QueryWrapper<T> qWrapper = this.addHandler(entityClazz, page.getQueryWrapper());
page.pageHelperBegin(false); page.pageHelperBegin(false);
try{ try{
List<T> list = this.findList(page.getQueryWrapper()); List<T> list = super.list(qWrapper);
PageInfo<T> pageInfo = new PageInfo<>(list); PageInfo<T> pageInfo = new PageInfo<>(list);
List<E> es = transformTs2Ms(pageInfo.getList()); List<E> es = transformTs2Ms(pageInfo.getList());
page.instance(pageInfo, es); page.instance(pageInfo, es);

@ -18,27 +18,18 @@ package org.opsli.core.filters.aspect;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.TimeInterval; import cn.hutool.core.date.TimeInterval;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.annotation.Pointcut;
import org.opsli.core.api.TokenThreadLocal;
import org.opsli.common.exception.ServiceException;
import org.opsli.core.utils.LogUtil; import org.opsli.core.utils.LogUtil;
import org.opsli.core.utils.UserTokenUtil;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import static org.opsli.common.constants.OrderConstants.TOKEN_AOP_SORT; import static org.opsli.common.constants.OrderConstants.TOKEN_AOP_SORT;
/** /**
* *
* *
* @author parker * @author parker
* @date 2020-09-16 * @date 2020-09-16
@ -47,7 +38,7 @@ import static org.opsli.common.constants.OrderConstants.TOKEN_AOP_SORT;
@Order(TOKEN_AOP_SORT) @Order(TOKEN_AOP_SORT)
@Aspect @Aspect
@Component @Component
public class TokenAop { public class LogAop {
@Pointcut("execution(public * org.opsli..*.*Controller*.*(..))") @Pointcut("execution(public * org.opsli..*.*Controller*.*(..))")
@ -60,32 +51,6 @@ public class TokenAop {
*/ */
@Around("requestMapping()") @Around("requestMapping()")
public Object tokenAop(ProceedingJoinPoint point) throws Throwable { public Object tokenAop(ProceedingJoinPoint point) throws Throwable {
// Token
String requestToken = TokenThreadLocal.get();
// 如果 ThreadLocal为空 则去当前request中获取
if(StringUtils.isEmpty(requestToken)){
// 将 Token放入 线程缓存
try {
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
if(sra != null) {
HttpServletRequest request = sra.getRequest();
requestToken = UserTokenUtil.getRequestToken(request);
if(StringUtils.isNotEmpty(requestToken)){
// 放入当前线程缓存中
TokenThreadLocal.put(requestToken);
}
}
}catch (ServiceException e){
throw e;
}catch (Exception e){
log.error(e.getMessage(),e);
}
}
// 计时器 // 计时器
TimeInterval timer = DateUtil.timer(); TimeInterval timer = DateUtil.timer();
// 执行 // 执行
@ -103,13 +68,7 @@ public class TokenAop {
long timerCount = timer.interval(); long timerCount = timer.interval();
//保存日志 //保存日志
LogUtil.saveLog(point, exception, timerCount); LogUtil.saveLog(point, exception, timerCount);
// 线程销毁时 删除 token
if(StringUtils.isNotEmpty(requestToken)){
TokenThreadLocal.remove();
}
} }
return returnValue; return returnValue;
} }

@ -0,0 +1,61 @@
/**
* Copyright 2020 OPSLI https://www.opsli.com
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.opsli.core.filters.interceptor;
import lombok.extern.slf4j.Slf4j;
import org.opsli.core.holder.UserContextHolder;
import org.opsli.core.utils.UserTokenUtil;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* , jwt token
*
* @author
* @date 2021122216:35:20
*/
@Slf4j
public class UserAuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(
HttpServletRequest request, HttpServletResponse response, Object handler){
if (handler instanceof ResourceHttpRequestHandler) {
return true;
}
try {
String requestToken = UserTokenUtil.getRequestToken(request);
UserContextHolder.setToken(requestToken);
}catch (Exception e){
log.error(e.getMessage(), e);
}
return true;
}
@Override
public void afterCompletion(
HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex){
// 上下文属性值清除,防止内存泄漏
UserContextHolder.clear();
}
}

@ -72,13 +72,24 @@ public class StartPrint {
* *
*/ */
public void errorPrint(){ public void errorPrint(){
this.errorPrint(null);
}
/**
*
*
*/
public void errorPrint(String errorMsg){
// 睡一秒打印 // 睡一秒打印
ThreadUtil.sleep(1, TimeUnit.SECONDS); ThreadUtil.sleep(1, TimeUnit.SECONDS);
String printStr = String printStr =
"\n----------------------------------------------------------\n" + "\n----------------------------------------------------------\n" +
systemName + " 框架启动失败! 请检查相关配置!\n" + systemName + " 框架启动失败! 请检查相关配置!\n" +
"----------------------------------------------------------\n"; "----------------------------------------------------------\n";
Console.log(printStr); Console.error(printStr);
if(StringUtils.isNotEmpty(errorMsg)){
Console.error(errorMsg);
}
} }

@ -13,9 +13,9 @@
* License for the specific language governing permissions and limitations under * License for the specific language governing permissions and limitations under
* the License. * the License.
*/ */
package org.opsli.core.api; package org.opsli.core.holder;
import com.alibaba.ttl.TransmittableThreadLocal;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.opsli.core.utils.UserTokenUtil; import org.opsli.core.utils.UserTokenUtil;
import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestAttributes;
@ -23,27 +23,32 @@ import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.util.Optional;
/** /**
* 线 Token *
* *
* @author parker * @author
* @date 2020-09-15 * @date 2021122216:22:59
*/ */
public class TokenThreadLocal { public final class UserContextHolder {
/** 临时线程存储 token 容器 */
private static final ThreadLocal<String> TOKEN_DATA = new ThreadLocal<>();
public static void put(String token) { /**
if (TOKEN_DATA.get() == null) { *
TOKEN_DATA.set(token); * 线
} */
} public static final ThreadLocal<String> THREAD_LOCAL = new TransmittableThreadLocal<>();
public static String get() { /**
*
* ,:
* LoginUserInfo loginUserInfo = UserContextHolder.get().orElseThrow(() -> new BusinessException("提示未登录即可"));
*
* @return Optional<LoginUserInfo>
*/
public static Optional<String> getToken() {
String token = TOKEN_DATA.get(); String token = THREAD_LOCAL.get();
// 2021-03-10 // 2021-03-10
// 这里纠正 Token 在被多聚合项目 aop切面 remove后 无法获得Token bug // 这里纠正 Token 在被多聚合项目 aop切面 remove后 无法获得Token bug
@ -59,12 +64,24 @@ public class TokenThreadLocal {
}catch (Exception ignored){} }catch (Exception ignored){}
} }
return token; return Optional.ofNullable(token);
} }
public static void remove() {
try { public static void setToken(String token) {
TOKEN_DATA.remove(); if (THREAD_LOCAL.get() == null) {
}catch (Exception ignored){} THREAD_LOCAL.set(token);
}
}
public static void clear() {
THREAD_LOCAL.remove();
} }
/**
*
*/
private UserContextHolder(){}
} }

@ -18,7 +18,7 @@ public class ApplicationFailedEventListener implements ApplicationListener<Appli
@Override @Override
public void onApplicationEvent(ApplicationFailedEvent event) { public void onApplicationEvent(ApplicationFailedEvent event) {
StartPrint.getInstance().errorPrint(); StartPrint.getInstance().errorPrint(event.getException().getMessage());
} }
} }

@ -145,9 +145,12 @@ public class QueryDataPermsHandler implements QueryBuilderChain{
// 如果组织为空 则默认权限为查自己的数据 // 如果组织为空 则默认权限为查自己的数据
if(!ConditionType.SELF.equals(conditionType) && if(!ConditionType.SELF.equals(conditionType) &&
CollUtil.isEmpty(orgIdGroupList)){ CollUtil.isEmpty(orgIdGroupList)){
// 如果不是 查看全部数据 需要默认角色权限为只查自己
if(!ConditionType.ALL.equals(conditionType)){
conditionType = ConditionType.SELF; conditionType = ConditionType.SELF;
} }
} }
}
// 常量 // 常量
final ConditionType finalConditionType = conditionType; final ConditionType finalConditionType = conditionType;

@ -3,7 +3,10 @@ package org.opsli.core.security.shiro.realm;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.*; import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.realm.AuthorizingRealm;
@ -11,8 +14,8 @@ import org.apache.shiro.subject.PrincipalCollection;
import org.opsli.api.wrapper.system.tenant.TenantModel; import org.opsli.api.wrapper.system.tenant.TenantModel;
import org.opsli.api.wrapper.system.user.UserModel; import org.opsli.api.wrapper.system.user.UserModel;
import org.opsli.common.enums.DictType; import org.opsli.common.enums.DictType;
import org.opsli.core.api.TokenThreadLocal;
import org.opsli.common.exception.TokenException; import org.opsli.common.exception.TokenException;
import org.opsli.core.holder.UserContextHolder;
import org.opsli.core.msg.TokenMsg; import org.opsli.core.msg.TokenMsg;
import org.opsli.core.security.shiro.token.JwtToken; import org.opsli.core.security.shiro.token.JwtToken;
import org.opsli.core.utils.TenantUtil; import org.opsli.core.utils.TenantUtil;
@ -119,7 +122,8 @@ public class JwtRealm extends AuthorizingRealm implements FlagRealm {
public static void authToken() public static void authToken()
throws TokenException { throws TokenException {
String accessToken = TokenThreadLocal.get(); String accessToken = UserContextHolder.getToken().orElseThrow(() -> new TokenException(
TokenMsg.EXCEPTION_TOKEN_LOSE_EFFICACY));
// 1. 校验 token 是否有效 // 1. 校验 token 是否有效
boolean verify = UserTokenUtil.verify(accessToken); boolean verify = UserTokenUtil.verify(accessToken);
@ -151,7 +155,8 @@ public class JwtRealm extends AuthorizingRealm implements FlagRealm {
return; return;
} }
String accessToken = TokenThreadLocal.get(); String accessToken = UserContextHolder.getToken().orElseThrow(() -> new TokenException(
TokenMsg.EXCEPTION_TOKEN_LOSE_EFFICACY));
// 查询 用户信息 // 查询 用户信息
String userId = UserTokenUtil.getUserIdByToken(accessToken); String userId = UserTokenUtil.getUserIdByToken(accessToken);

@ -27,6 +27,7 @@ import org.aspectj.lang.reflect.MethodSignature;
import org.opsli.api.wrapper.system.logs.LogsModel; import org.opsli.api.wrapper.system.logs.LogsModel;
import org.opsli.api.wrapper.system.menu.MenuModel; import org.opsli.api.wrapper.system.menu.MenuModel;
import org.opsli.api.wrapper.system.user.UserModel; import org.opsli.api.wrapper.system.user.UserModel;
import org.opsli.api.wrapper.system.user.UserOrgRefModel;
import org.opsli.common.annotation.EnableLog; import org.opsli.common.annotation.EnableLog;
import org.opsli.common.annotation.RequiresPermissionsCus; import org.opsli.common.annotation.RequiresPermissionsCus;
import org.opsli.common.utils.IPUtil; import org.opsli.common.utils.IPUtil;
@ -118,6 +119,15 @@ public final class LogUtil {
logsModel.setUpdateBy(user.getId()); logsModel.setUpdateBy(user.getId());
logsModel.setIzManual(true); logsModel.setIzManual(true);
// 如果组织IDs 为空则进行默认赋值
UserOrgRefModel userOrgRefModel = UserUtil.getUserDefOrgByUserId(user.getId());
if(null != userOrgRefModel){
logsModel.setOrgIds(userOrgRefModel.getOrgIds());
}
// 赋值 租户ID
logsModel.setTenantId(user.getTenantId());
// 保存日志 // 保存日志
LogsThreadPool.process(logsModel); LogsThreadPool.process(logsModel);
} catch (Exception ex){ } catch (Exception ex){

@ -34,9 +34,9 @@ import org.opsli.common.constants.TokenConstants;
import org.opsli.common.constants.TokenTypeConstants; import org.opsli.common.constants.TokenTypeConstants;
import org.opsli.common.enums.LoginLimitRefuse; import org.opsli.common.enums.LoginLimitRefuse;
import org.opsli.common.exception.TokenException; import org.opsli.common.exception.TokenException;
import org.opsli.core.api.TokenThreadLocal;
import org.opsli.core.autoconfigure.properties.GlobalProperties; import org.opsli.core.autoconfigure.properties.GlobalProperties;
import org.opsli.core.cache.CacheUtil; import org.opsli.core.cache.CacheUtil;
import org.opsli.core.holder.UserContextHolder;
import org.opsli.core.msg.CoreMsg; import org.opsli.core.msg.CoreMsg;
import org.opsli.core.msg.TokenMsg; import org.opsli.core.msg.TokenMsg;
import org.opsli.plugins.redis.RedisPlugin; import org.opsli.plugins.redis.RedisPlugin;
@ -162,7 +162,10 @@ public class UserTokenUtil {
* @return String * @return String
*/ */
public static String getUserIdByToken() { public static String getUserIdByToken() {
return getUserIdByToken(TokenThreadLocal.get()); String token = UserContextHolder.getToken().orElseThrow(() -> new TokenException(
TokenMsg.EXCEPTION_TOKEN_LOSE_EFFICACY));
return getUserIdByToken(token);
} }
/** /**
* Token ID * Token ID
@ -186,7 +189,9 @@ public class UserTokenUtil {
* @return String * @return String
*/ */
public static String getUserNameByToken() { public static String getUserNameByToken() {
return getUserNameByToken(TokenThreadLocal.get()); String token = UserContextHolder.getToken().orElseThrow(() -> new TokenException(
TokenMsg.EXCEPTION_TOKEN_LOSE_EFFICACY));
return getUserNameByToken(token);
} }
/** /**
* Token * Token
@ -209,7 +214,9 @@ public class UserTokenUtil {
* @return String * @return String
*/ */
public static String getTenantIdByToken() { public static String getTenantIdByToken() {
return getTenantIdByToken(TokenThreadLocal.get()); String token = UserContextHolder.getToken().orElseThrow(() -> new TokenException(
TokenMsg.EXCEPTION_TOKEN_LOSE_EFFICACY));
return getTenantIdByToken(token);
} }
/** /**
@ -330,8 +337,8 @@ public class UserTokenUtil {
// 判断账号是否临时锁定 // 判断账号是否临时锁定
Long loseTimeMillis = (Long) redisPlugin.get( Long loseTimeMillis = Convert.toLong(redisPlugin.get(
CacheUtil.formatKey(RedisConstants.PREFIX_ACCOUNT_SLIP_LOCK + username)); CacheUtil.formatKey(RedisConstants.PREFIX_ACCOUNT_SLIP_LOCK + username)));
if(loseTimeMillis != null){ if(loseTimeMillis != null){
Date currDate = DateUtil.date(); Date currDate = DateUtil.date();
DateTime loseDate = DateUtil.date(loseTimeMillis); DateTime loseDate = DateUtil.date(loseTimeMillis);

@ -31,10 +31,10 @@ import org.opsli.api.wrapper.system.user.UserModel;
import org.opsli.api.wrapper.system.user.UserOrgRefModel; import org.opsli.api.wrapper.system.user.UserOrgRefModel;
import org.opsli.common.constants.RedisConstants; import org.opsli.common.constants.RedisConstants;
import org.opsli.common.exception.TokenException; import org.opsli.common.exception.TokenException;
import org.opsli.core.api.TokenThreadLocal;
import org.opsli.core.autoconfigure.properties.GlobalProperties; import org.opsli.core.autoconfigure.properties.GlobalProperties;
import org.opsli.core.cache.CacheUtil; import org.opsli.core.cache.CacheUtil;
import org.opsli.core.cache.SecurityCache; import org.opsli.core.cache.SecurityCache;
import org.opsli.core.holder.UserContextHolder;
import org.opsli.core.msg.CoreMsg; import org.opsli.core.msg.CoreMsg;
import org.opsli.core.msg.TokenMsg; import org.opsli.core.msg.TokenMsg;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -60,7 +60,7 @@ import static org.opsli.common.constants.OrderConstants.UTIL_ORDER;
public class UserUtil { public class UserUtil {
/** 修改租户权限 */ /** 修改租户权限 */
private static final String PERMS_TENANT = "system_user_tenant"; private static final String PERMS_TENANT = "system_set_tenant_admin";
/** 用户Service */ /** 用户Service */
private static UserApi userApi; private static UserApi userApi;
@ -88,7 +88,8 @@ public class UserUtil {
ThrowExceptionUtil.isThrowException(!IS_INIT, ThrowExceptionUtil.isThrowException(!IS_INIT,
CoreMsg.OTHER_EXCEPTION_UTILS_INIT); CoreMsg.OTHER_EXCEPTION_UTILS_INIT);
String token = TokenThreadLocal.get(); String token = UserContextHolder.getToken().orElseThrow(() -> new TokenException(
TokenMsg.EXCEPTION_TOKEN_LOSE_EFFICACY));
// 如果还是没获取到token 则抛出异常 // 如果还是没获取到token 则抛出异常
if(StringUtils.isEmpty(token)){ if(StringUtils.isEmpty(token)){
@ -115,7 +116,8 @@ public class UserUtil {
ThrowExceptionUtil.isThrowException(!IS_INIT, ThrowExceptionUtil.isThrowException(!IS_INIT,
CoreMsg.OTHER_EXCEPTION_UTILS_INIT); CoreMsg.OTHER_EXCEPTION_UTILS_INIT);
String token = TokenThreadLocal.get(); String token = UserContextHolder.getToken().orElseThrow(() -> new TokenException(
TokenMsg.EXCEPTION_TOKEN_LOSE_EFFICACY));
// 如果还是没获取到token 则抛出异常 // 如果还是没获取到token 则抛出异常
if(StringUtils.isEmpty(token)){ if(StringUtils.isEmpty(token)){

@ -77,10 +77,7 @@ public class GenTableRestController extends BaseRestController<GenTable, GenTabl
// 判断代码生成器 是否启用 // 判断代码生成器 是否启用
GeneratorHandleUtil.judgeGeneratorEnable(super.globalProperties); GeneratorHandleUtil.judgeGeneratorEnable(super.globalProperties);
// 如果系统内部调用 则直接查数据库
if(model != null && model.getIzApi() != null && model.getIzApi()){
model = IService.get(model); model = IService.get(model);
}
GenTableAndColumnModel currModel = WrapperUtil.transformInstance( GenTableAndColumnModel currModel = WrapperUtil.transformInstance(
model, GenTableAndColumnModel.class model, GenTableAndColumnModel.class

@ -71,10 +71,7 @@ public class GenTemplateDetailRestController extends BaseRestController<GenTempl
// 判断代码生成器 是否启用 // 判断代码生成器 是否启用
GeneratorHandleUtil.judgeGeneratorEnable(super.globalProperties); GeneratorHandleUtil.judgeGeneratorEnable(super.globalProperties);
// 如果系统内部调用 则直接查数据库
if(model != null && model.getIzApi() != null && model.getIzApi()){
model = IService.get(model); model = IService.get(model);
}
return ResultVo.success(model); return ResultVo.success(model);
} }

@ -70,10 +70,7 @@ public class GenTemplateRestController extends BaseRestController<GenTemplate, G
// 判断代码生成器 是否启用 // 判断代码生成器 是否启用
GeneratorHandleUtil.judgeGeneratorEnable(super.globalProperties); GeneratorHandleUtil.judgeGeneratorEnable(super.globalProperties);
// 如果系统内部调用 则直接查数据库
if(model != null && model.getIzApi() != null && model.getIzApi()){
model = IService.get(model); model = IService.get(model);
}
return ResultVo.success(model); return ResultVo.success(model);
} }

@ -87,10 +87,7 @@ public class SysAreaRestController extends BaseRestController<SysArea, SysAreaMo
@RequiresPermissions("system_area_select") @RequiresPermissions("system_area_select")
@Override @Override
public ResultVo<SysAreaModel> get(SysAreaModel model) { public ResultVo<SysAreaModel> get(SysAreaModel model) {
// 如果系统内部调用 则直接查数据库
if(model != null && model.getIzApi() != null && model.getIzApi()){
model = IService.get(model); model = IService.get(model);
}
return ResultVo.success(model); return ResultVo.success(model);
} }

@ -72,10 +72,7 @@ public class DictDetailRestController extends BaseRestController<SysDictDetail,
@ApiOperation(value = "获得单条字典明细数据", notes = "获得单条字典明细数据 - ID") @ApiOperation(value = "获得单条字典明细数据", notes = "获得单条字典明细数据 - ID")
@Override @Override
public ResultVo<DictDetailModel> get(DictDetailModel model) { public ResultVo<DictDetailModel> get(DictDetailModel model) {
// 如果系统内部调用 则直接查数据库
if(model != null && model.getIzApi() != null && model.getIzApi()){
model = IService.get(model); model = IService.get(model);
}
return ResultVo.success(model); return ResultVo.success(model);
} }

@ -74,10 +74,7 @@ public class DictRestController extends BaseRestController<SysDict, DictModel, I
@ApiOperation(value = "获得单条字典数据", notes = "获得单条字典数据 - ID") @ApiOperation(value = "获得单条字典数据", notes = "获得单条字典数据 - ID")
@Override @Override
public ResultVo<DictModel> get(DictModel model) { public ResultVo<DictModel> get(DictModel model) {
// 如果系统内部调用 则直接查数据库
if(model != null && model.getIzApi() != null && model.getIzApi()){
model = IService.get(model); model = IService.get(model);
}
return ResultVo.success(model); return ResultVo.success(model);
} }

@ -22,24 +22,26 @@ import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.opsli.api.base.result.ResultVo; import org.opsli.api.base.result.ResultVo;
import org.opsli.api.wrapper.system.logs.LoginLogsModel;
import org.opsli.api.wrapper.system.menu.MenuModel; import org.opsli.api.wrapper.system.menu.MenuModel;
import org.opsli.api.wrapper.system.options.OptionsModel; import org.opsli.api.wrapper.system.options.OptionsModel;
import org.opsli.api.wrapper.system.tenant.TenantModel; import org.opsli.api.wrapper.system.tenant.TenantModel;
import org.opsli.api.wrapper.system.user.UserModel; import org.opsli.api.wrapper.system.user.UserModel;
import org.opsli.common.annotation.LoginCrypto;
import org.opsli.common.annotation.Limiter; import org.opsli.common.annotation.Limiter;
import org.opsli.common.enums.DictType; import org.opsli.common.annotation.LoginCrypto;
import org.opsli.common.thread.AsyncProcessExecutor;
import org.opsli.common.thread.AsyncProcessExecutorFactory;
import org.opsli.core.utils.ValidatorUtil;
import org.opsli.core.api.TokenThreadLocal;
import org.opsli.common.enums.AlertType; import org.opsli.common.enums.AlertType;
import org.opsli.common.enums.DictType;
import org.opsli.common.enums.OptionsType; import org.opsli.common.enums.OptionsType;
import org.opsli.common.exception.TokenException; import org.opsli.common.exception.TokenException;
import org.opsli.common.thread.AsyncProcessExecutor;
import org.opsli.common.thread.AsyncProcessExecutorFactory;
import org.opsli.common.utils.IPUtil; import org.opsli.common.utils.IPUtil;
import org.opsli.core.holder.UserContextHolder;
import org.opsli.core.msg.TokenMsg; import org.opsli.core.msg.TokenMsg;
import org.opsli.core.utils.*; import org.opsli.core.utils.*;
import org.opsli.modulars.system.login.entity.LoginForm; import org.opsli.modulars.system.login.entity.LoginForm;
import org.opsli.modulars.system.logs.factory.UserLoginLogFactory;
import org.opsli.modulars.system.logs.service.ILoginLogsService;
import org.opsli.modulars.system.user.service.IUserService; import org.opsli.modulars.system.user.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@ -53,6 +55,7 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
/** /**
* / / * / /
@ -68,6 +71,8 @@ public class LoginRestController {
@Autowired @Autowired
private IUserService iUserService; private IUserService iUserService;
@Autowired
private ILoginLogsService iLoginLogsService;
/** /**
* *
@ -111,10 +116,17 @@ public class LoginRestController {
// 如果验证成功, 则清除锁定信息 // 如果验证成功, 则清除锁定信息
UserTokenUtil.clearLockAccount(form.getUsername()); UserTokenUtil.clearLockAccount(form.getUsername());
// 如果不是超级管理员 并且不是系统系统用户 则进行验证 // 如果不是超级管理员 则要进行安全验证
if(!StringUtils.equals(UserUtil.SUPER_ADMIN, user.getUsername()) && if(!StringUtils.equals(UserUtil.SUPER_ADMIN, user.getUsername())){
!TenantUtil.SUPER_ADMIN_TENANT_ID.equals(user.getTenantId())
){ // 如果不是 系统用户, 也就是租户用户 需要验证租户启用情况
if(!TenantUtil.SUPER_ADMIN_TENANT_ID.equals(user.getTenantId())){
// 验证租户是否生效
TenantModel tenant = TenantUtil.getTenant(user.getTenantId());
if(tenant == null){
throw new TokenException(TokenMsg.EXCEPTION_LOGIN_TENANT_NOT_USABLE);
}
}
// 账号锁定验证 // 账号锁定验证
if(StringUtils.isEmpty(user.getEnable()) || if(StringUtils.isEmpty(user.getEnable()) ||
@ -123,12 +135,6 @@ public class LoginRestController {
throw new TokenException(TokenMsg.EXCEPTION_LOGIN_ACCOUNT_LOCKED); throw new TokenException(TokenMsg.EXCEPTION_LOGIN_ACCOUNT_LOCKED);
} }
// 验证租户是否生效
TenantModel tenant = TenantUtil.getTenant(user.getTenantId());
if(tenant == null){
throw new TokenException(TokenMsg.EXCEPTION_LOGIN_TENANT_NOT_USABLE);
}
// 检测用户是否有角色 // 检测用户是否有角色
List<String> roleModelList = UserUtil.getUserRolesByUserId(user.getId()); List<String> roleModelList = UserUtil.getUserRolesByUserId(user.getId());
if(CollUtil.isEmpty(roleModelList)){ if(CollUtil.isEmpty(roleModelList)){
@ -160,6 +166,9 @@ public class LoginRestController {
//生成token并保存到Redis //生成token并保存到Redis
ResultVo<UserTokenUtil.TokenRet> resultVo = UserTokenUtil.createToken(user); ResultVo<UserTokenUtil.TokenRet> resultVo = UserTokenUtil.createToken(user);
if(resultVo.isSuccess()){ if(resultVo.isSuccess()){
// 保存Token 到当前线程缓存
UserContextHolder.setToken(resultVo.getData().getToken());
AsyncProcessExecutor normalExecutor = AsyncProcessExecutorFactory.createNormalExecutor(); AsyncProcessExecutor normalExecutor = AsyncProcessExecutorFactory.createNormalExecutor();
// 异步保存IP // 异步保存IP
normalExecutor.put(()->{ normalExecutor.put(()->{
@ -167,6 +176,11 @@ public class LoginRestController {
String clientIpAddress = IPUtil.getClientIdBySingle(request); String clientIpAddress = IPUtil.getClientIdBySingle(request);
user.setLoginIp(clientIpAddress); user.setLoginIp(clientIpAddress);
iUserService.updateLoginIp(user); iUserService.updateLoginIp(user);
// 记录用户登录日志 如果系统较大 可考虑 Elastic 的 filebeat
// 小系统 直接存在 mysql就好
LoginLogsModel userLoginModel = UserLoginLogFactory.getUserLoginModel(request, user, true);
iLoginLogsService.insert(userLoginModel);
}); });
normalExecutor.execute(); normalExecutor.execute();
} }
@ -180,12 +194,26 @@ public class LoginRestController {
@Limiter @Limiter
@ApiOperation(value = "登出", notes = "登出") @ApiOperation(value = "登出", notes = "登出")
@PostMapping("/system/logout") @PostMapping("/system/logout")
public ResultVo<?> logout() { public ResultVo<?> logout(HttpServletRequest request) {
String token = TokenThreadLocal.get(); String token = UserContextHolder.getToken().orElseThrow(() -> new TokenException(
TokenMsg.EXCEPTION_TOKEN_LOSE_EFFICACY));
// 登出失败没有授权Token // 登出失败没有授权Token
if(StringUtils.isEmpty(token)){ if(StringUtils.isEmpty(token)){
return ResultVo.error(TokenMsg.EXCEPTION_LOGOUT_ERROR.getMessage()); return ResultVo.error(TokenMsg.EXCEPTION_LOGOUT_ERROR.getMessage());
} }
// 异步记录信息
AsyncProcessExecutor normalExecutor = AsyncProcessExecutorFactory.createNormalExecutor();
UserModel user = UserUtil.getUser();
normalExecutor.put(()->{
// 记录用户登录日志 如果系统较大 可考虑 Elastic 的 filebeat
// 小系统 直接存在 mysql就好
LoginLogsModel userLoginModel = UserLoginLogFactory.getUserLoginModel(request, user, false);
iLoginLogsService.insert(userLoginModel);
});
normalExecutor.execute();
UserTokenUtil.logout(token); UserTokenUtil.logout(token);
return ResultVo.success(TokenMsg.EXCEPTION_LOGOUT_SUCCESS.getMessage()); return ResultVo.success(TokenMsg.EXCEPTION_LOGOUT_SUCCESS.getMessage());
} }

@ -0,0 +1,67 @@
/**
* Copyright 2020 OPSLI https://www.opsli.com
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.opsli.modulars.system.logs.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.opsli.core.base.entity.BaseEntity;
/**
*
*
* @author
* @date 202231817:45:18
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class SysLoginLogs extends BaseEntity {
/**
*
*/
private String tenantId;
/**
* ID xxx,xxx
*/
private String orgIds;
/**
*
*/
private String username;
/**
*
*/
private String realName;
/**
*
* 1 :
* 2 :
*/
private String type;
/**
* IP
*/
private String remoteAddr;
/**
*
*/
private String userAgent;
}

@ -39,6 +39,14 @@ public class SysLogs extends BaseEntity {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/**
*
*/
private String tenantId;
/**
* ID xxx,xxx
*/
private String orgIds;
/** 日志类型1接入日志2错误日志 */ /** 日志类型1接入日志2错误日志 */
private String type; private String type;

@ -0,0 +1,49 @@
package org.opsli.modulars.system.logs.factory;
import cn.hutool.core.util.StrUtil;
import org.opsli.api.wrapper.system.logs.LoginLogsModel;
import org.opsli.api.wrapper.system.user.UserModel;
import org.opsli.api.wrapper.system.user.UserOrgRefModel;
import org.opsli.common.enums.DictType;
import org.opsli.core.utils.UserUtil;
import org.springframework.util.ObjectUtils;
import javax.servlet.http.HttpServletRequest;
/**
* @author
* @date 2022/3/18 14:34
*/
public final class UserLoginLogFactory {
/**
*
* @param user
* @return
*/
public static LoginLogsModel getUserLoginModel(HttpServletRequest request, UserModel user, boolean isLogin){
//记录登入登出日志
LoginLogsModel loginInfo = new LoginLogsModel();
loginInfo.setUsername(user.getUsername());
loginInfo.setRealName(user.getRealName());
// *** 需要确保 user对象的ip信息是有值的
loginInfo.setRemoteAddr(user.getLoginIp());
String header = request.getHeader("User-Agent");
loginInfo.setUserAgent(header);
loginInfo.setIzManual(true);
loginInfo.setCreateBy(user.getId());
loginInfo.setUpdateBy(user.getId());
UserOrgRefModel orgByUserId = UserUtil.getUserDefOrgByUserId(user.getId());
loginInfo.setOrgIds(ObjectUtils.isEmpty(orgByUserId)? "0" : orgByUserId.getOrgIds() );
loginInfo.setTenantId(StrUtil.blankToDefault(user.getTenantId(),null));
if(isLogin){
loginInfo.setType(DictType.LOGIN_LOG_TYPE_LOGIN.getValue());
}else {
loginInfo.setType(DictType.LOGIN_LOG_TYPE_LOGOUT.getValue());
}
return loginInfo;
}
}

@ -0,0 +1,31 @@
/**
* Copyright 2020 OPSLI https://www.opsli.com
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.opsli.modulars.system.logs.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.opsli.modulars.system.logs.entity.SysLoginLogs;
/**
* Mapper
*
* @author Parker
* @date 2020-11-28 18:59:59
*/
@Mapper
public interface SysLoginLogsMapper extends BaseMapper<SysLoginLogs> {
}

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.opsli.modulars.system.logs.mapper.SysLoginLogsMapper">
</mapper>

@ -0,0 +1,32 @@
/**
* Copyright 2020 OPSLI https://www.opsli.com
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.opsli.modulars.system.logs.service;
import org.opsli.api.wrapper.system.logs.LoginLogsModel;
import org.opsli.core.base.service.interfaces.CrudServiceInterface;
import org.opsli.modulars.system.logs.entity.SysLoginLogs;
/**
* Service
*
* @author Parker
* @date 2020-11-28 18:59:59
*/
public interface ILoginLogsService extends CrudServiceInterface<SysLoginLogs, LoginLogsModel> {
}

@ -0,0 +1,42 @@
/**
* Copyright 2020 OPSLI https://www.opsli.com
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.opsli.modulars.system.logs.service.impl;
import org.opsli.api.wrapper.system.logs.LoginLogsModel;
import org.opsli.core.base.service.impl.CrudServiceImpl;
import org.opsli.modulars.system.logs.entity.SysLoginLogs;
import org.opsli.modulars.system.logs.mapper.SysLoginLogsMapper;
import org.opsli.modulars.system.logs.service.ILoginLogsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* Service Impl
*
* @author Parker
* @date 2020-11-28 18:59:59
*/
@Service
public class LoginLogsServiceImpl extends CrudServiceImpl<SysLoginLogsMapper, SysLoginLogs, LoginLogsModel>
implements ILoginLogsService {
@Autowired
private SysLoginLogsMapper mapper;
}

@ -0,0 +1,73 @@
/**
* Copyright 2020 OPSLI https://www.opsli.com
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.opsli.modulars.system.logs.web;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.opsli.api.base.result.ResultVo;
import org.opsli.api.web.system.logs.LoginLogsApi;
import org.opsli.api.web.system.logs.LogsApi;
import org.opsli.api.wrapper.system.logs.LoginLogsModel;
import org.opsli.api.wrapper.system.logs.LogsModel;
import org.opsli.common.annotation.ApiRestController;
import org.opsli.common.annotation.EnableLog;
import org.opsli.core.base.controller.BaseRestController;
import org.opsli.core.persistence.Page;
import org.opsli.core.persistence.querybuilder.QueryBuilder;
import org.opsli.core.persistence.querybuilder.WebQueryBuilder;
import org.opsli.modulars.system.logs.entity.SysLoginLogs;
import org.opsli.modulars.system.logs.entity.SysLogs;
import org.opsli.modulars.system.logs.service.ILoginLogsService;
import org.opsli.modulars.system.logs.service.ILogsService;
import javax.servlet.http.HttpServletRequest;
/**
* Controller
*
* @author Parker
* @date 2020-11-28 18:59:59
*/
@Api(tags = LoginLogsApi.TITLE)
@Slf4j
@ApiRestController("/{ver}/system/login-logs")
public class LoginLogsRestController extends BaseRestController<SysLoginLogs, LoginLogsModel, ILoginLogsService>
implements LoginLogsApi {
/**
*
* @param pageNo
* @param pageSize
* @param request request
* @return ResultVo
*/
@ApiOperation(value = "获得分页数据", notes = "获得分页数据 - 查询构造器")
@RequiresPermissions("devops_login_logs_select")
@Override
public ResultVo<?> findPage(Integer pageNo, Integer pageSize, HttpServletRequest request) {
QueryBuilder<SysLoginLogs> queryBuilder = new WebQueryBuilder<>(entityClazz, request.getParameterMap());
Page<SysLoginLogs, LoginLogsModel> page = new Page<>(pageNo, pageSize);
page.setQueryWrapper(queryBuilder.build());
page = IService.findPage(page);
return ResultVo.success(page.getPageData());
}
}

@ -56,10 +56,7 @@ public class LogsRestController extends BaseRestController<SysLogs, LogsModel, I
@RequiresPermissions("devops_logs_select") @RequiresPermissions("devops_logs_select")
@Override @Override
public ResultVo<LogsModel> get(LogsModel model) { public ResultVo<LogsModel> get(LogsModel model) {
// 如果系统内部调用 则直接查数据库
if(model != null && model.getIzApi() != null && model.getIzApi()){
model = IService.get(model); model = IService.get(model);
}
return ResultVo.success(model); return ResultVo.success(model);
} }
@ -84,20 +81,6 @@ public class LogsRestController extends BaseRestController<SysLogs, LogsModel, I
} }
/**
*
* @return ResultVo
*/
@ApiOperation(value = "清空一个月前的日志", notes = "清空一个月前的日志")
@RequiresPermissions("devops_logs_delete")
@EnableLog
@Override
public ResultVo<?> emptyByOneMonth(){
IService.emptyByOneMonth();
return ResultVo.success("清空日志成功");
}
@Override @Override
public ResultVo<?> insert(LogsModel model) { public ResultVo<?> insert(LogsModel model) {
IService.insert(model); IService.insert(model);

@ -284,12 +284,9 @@ public class MenuRestController extends BaseRestController<SysMenu, MenuModel, I
// 生成根节点菜单 // 生成根节点菜单
model = getGenMenuModel(); model = getGenMenuModel();
}else{ }else{
// 如果系统内部调用 则直接查数据库
if (model.getIzApi() != null && model.getIzApi()){
model = IService.get(model); model = IService.get(model);
} }
} }
}
return ResultVo.success(model); return ResultVo.success(model);
} }

@ -74,10 +74,7 @@ public class SysOptionsRestController extends BaseRestController<SysOptions, Opt
@RequiresPermissions("system_options_select") @RequiresPermissions("system_options_select")
@Override @Override
public ResultVo<OptionsModel> get(OptionsModel model) { public ResultVo<OptionsModel> get(OptionsModel model) {
// 如果系统内部调用 则直接查数据库
if(model != null && model.getIzApi() != null && model.getIzApi()){
model = IService.get(model); model = IService.get(model);
}
return ResultVo.success(model); return ResultVo.success(model);
} }

@ -243,10 +243,13 @@ public class SysOrgRestController extends BaseRestController<SysOrg, SysOrgModel
} }
QueryWrapper<SysOrg> wrapperByEmpty = queryBuilder.build(); QueryWrapper<SysOrg> wrapperByEmpty = queryBuilder.build();
wrapperByEmpty.in(FieldUtil.humpToUnderline(MyBatisConstants.FIELD_ID), genOrgIdSet); if(CollUtil.isNotEmpty(genOrgIdSet)){
// 如果传入ID 则不包含自身 wrapperByEmpty.in(
FieldUtil.humpToUnderline(MyBatisConstants.FIELD_ID), genOrgIdSet);
} // 如果传入ID 则不包含自身
if(StringUtils.isNotEmpty(id)){ if(StringUtils.isNotEmpty(id)){
wrapperByEmpty.notIn(FieldUtil.humpToUnderline(MyBatisConstants.FIELD_ID), id); wrapperByEmpty.notIn(
FieldUtil.humpToUnderline(MyBatisConstants.FIELD_ID), id);
} }
// 获得父节点组织 // 获得父节点组织
@ -268,7 +271,11 @@ public class SysOrgRestController extends BaseRestController<SysOrg, SysOrgModel
} }
// 排除父节点ID // 排除父节点ID
wrapper.notIn(FieldUtil.humpToUnderline(MyBatisConstants.FIELD_ID), genOrgIdSet); if(CollUtil.isNotEmpty(genOrgIdSet)){
wrapperByEmpty.in(
FieldUtil.humpToUnderline(MyBatisConstants.FIELD_ID), genOrgIdSet);
}
// 获得子节点组织 // 获得子节点组织
List<SysOrg> childList = IService.findList(wrapper); List<SysOrg> childList = IService.findList(wrapper);
if(CollUtil.isNotEmpty(childList)){ if(CollUtil.isNotEmpty(childList)){
@ -304,12 +311,9 @@ public class SysOrgRestController extends BaseRestController<SysOrg, SysOrgModel
// 生成根节点组织 // 生成根节点组织
model = getGenOrgModel(); model = getGenOrgModel();
}else{ }else{
// 如果系统内部调用 则直接查数据库
if (model.getIzApi() != null && model.getIzApi()){
model = IService.get(model); model = IService.get(model);
} }
} }
}
return ResultVo.success(model); return ResultVo.success(model);
} }

@ -62,10 +62,7 @@ public class RoleRestController extends BaseRestController<SysRole, RoleModel, I
@RequiresPermissions("system_role_select") @RequiresPermissions("system_role_select")
@Override @Override
public ResultVo<RoleModel> get(RoleModel model) { public ResultVo<RoleModel> get(RoleModel model) {
// 如果系统内部调用 则直接查数据库
if(model != null && model.getIzApi() != null && model.getIzApi()){
model = IService.get(model); model = IService.get(model);
}
return ResultVo.success(model); return ResultVo.success(model);
} }

@ -86,10 +86,7 @@ public class TenantRestController extends BaseRestController<SysTenant, TenantMo
@RequiresPermissions("system_tenant_select") @RequiresPermissions("system_tenant_select")
@Override @Override
public ResultVo<TenantModel> get(TenantModel model) { public ResultVo<TenantModel> get(TenantModel model) {
// 如果系统内部调用 则直接查数据库
if(model != null && model.getIzApi() != null && model.getIzApi()){
model = IService.get(model); model = IService.get(model);
}
return ResultVo.success(model); return ResultVo.success(model);
} }

@ -63,6 +63,7 @@ import org.opsli.plugins.oss.service.BaseOssStorageService;
import org.opsli.plugins.oss.service.OssStorageService; import org.opsli.plugins.oss.service.OssStorageService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest; import org.springframework.web.multipart.MultipartHttpServletRequest;
@ -201,44 +202,20 @@ public class UserRestController extends BaseRestController<SysUser, UserModel, I
/** /**
* *
* @param request request * @param userAvatarModel
* @return ResultVo * @return ResultVo
*/ */
@ApiOperation(value = "上传头像", notes = "上传头像") @ApiOperation(value = "上传头像", notes = "上传头像")
@Override @Override
public ResultVo<?> updateAvatar(MultipartHttpServletRequest request) { public ResultVo<?> updateAvatar(UserAvatarModel userAvatarModel) {
Iterator<String> itr = request.getFileNames();
String uploadedFile = itr.next();
List<MultipartFile> files = request.getFiles(uploadedFile);
if (CollectionUtils.isEmpty(files)) {
// 请选择文件
return ResultVo.error(SystemMsg.EXCEPTION_USER_FILE_NULL.getCode(),
SystemMsg.EXCEPTION_USER_FILE_NULL.getMessage());
}
try {
MultipartFile multipartFile = files.get(0);
Resource resource = multipartFile.getResource();
String filename = resource.getFilename();
// 调用OSS 服务保存头像
OssStorageService ossStorageService = OssStorageFactory.INSTANCE.getHandle();
BaseOssStorageService.FileAttr fileAttr = ossStorageService.upload(
multipartFile.getInputStream(), FileUtil.extName(filename));
UserModel user = UserUtil.getUserBySource(); UserModel user = UserUtil.getUserBySource();
// 更新头像至数据库 // 更新头像至数据库
UserModel userModel = new UserModel(); UserModel userModel = new UserModel();
userModel.setId(user.getId()); userModel.setId(user.getId());
userModel.setAvatar(fileAttr.getFileStoragePath()); userModel.setAvatar(userAvatarModel.getImgUrl());
IService.updateAvatar(userModel); IService.updateAvatar(userModel);
// 刷新用户信息 // 刷新用户信息
UserUtil.refreshUser(user); UserUtil.refreshUser(user);
}catch (IOException e){
log.error(e.getMessage(), e);
return ResultVo.error("更新头像失败,请稍后再试");
}
return ResultVo.success(); return ResultVo.success();
} }
@ -325,10 +302,7 @@ public class UserRestController extends BaseRestController<SysUser, UserModel, I
//@RequiresPermissions("system_user_select") //@RequiresPermissions("system_user_select")
@Override @Override
public ResultVo<UserModel> get(UserModel model) { public ResultVo<UserModel> get(UserModel model) {
// 如果系统内部调用 则直接查数据库
if(model != null && model.getIzApi() != null && model.getIzApi()){
model = IService.get(model); model = IService.get(model);
}
return ResultVo.success(model); return ResultVo.success(model);
} }

@ -1,8 +1,27 @@
package org.opsli.modulars.tools.oss.web; package org.opsli.modulars.tools.oss.web;
import cn.hutool.core.io.FileUtil;
import com.alibaba.excel.util.CollectionUtils;
import com.baomidou.mybatisplus.extension.service.IService;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.opsli.api.base.result.ResultVo;
import org.opsli.api.wrapper.system.user.UserModel;
import org.opsli.common.annotation.ApiRestController; import org.opsli.common.annotation.ApiRestController;
import org.opsli.core.utils.UserUtil;
import org.opsli.modulars.system.SystemMsg;
import org.opsli.plugins.oss.OssStorageFactory;
import org.opsli.plugins.oss.service.BaseOssStorageService;
import org.opsli.plugins.oss.service.OssStorageService;
import org.springframework.core.io.Resource;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
/** /**
@ -16,6 +35,39 @@ import org.opsli.common.annotation.ApiRestController;
@ApiRestController("/{ver}/tools/oss") @ApiRestController("/{ver}/tools/oss")
public class OssRestController { public class OssRestController {
/**
*
* @param request request
* @return ResultVo
*/
@ApiOperation(value = "文件上传", notes = "文件上传")
@PostMapping("/upload")
public ResultVo<?> upload(MultipartHttpServletRequest request) {
Iterator<String> itr = request.getFileNames();
String uploadedFile = itr.next();
List<MultipartFile> files = request.getFiles(uploadedFile);
if (CollectionUtils.isEmpty(files)) {
// 请选择文件
return ResultVo.error(SystemMsg.EXCEPTION_USER_FILE_NULL.getCode(),
SystemMsg.EXCEPTION_USER_FILE_NULL.getMessage());
}
try {
MultipartFile multipartFile = files.get(0);
Resource resource = multipartFile.getResource();
String filename = resource.getFilename();
// 调用OSS 服务保存文件
OssStorageService ossStorageService = OssStorageFactory.INSTANCE.getHandle();
BaseOssStorageService.FileAttr fileAttr = ossStorageService.upload(
multipartFile.getInputStream(), FileUtil.extName(filename));
return ResultVo.success(fileAttr);
}catch (IOException e){
log.error(e.getMessage(), e);
}
return ResultVo.error();
}
} }

@ -61,10 +61,7 @@ public class TestCarRestController extends BaseRestController<TestCar, TestCarMo
@RequiresPermissions("gentest_carinfo_select") @RequiresPermissions("gentest_carinfo_select")
@Override @Override
public ResultVo<TestCarModel> get(TestCarModel model) { public ResultVo<TestCarModel> get(TestCarModel model) {
// 如果系统内部调用 则直接查数据库
if(model != null && model.getIzApi() != null && model.getIzApi()){
model = IService.get(model); model = IService.get(model);
}
return ResultVo.success(model); return ResultVo.success(model);
} }

@ -62,10 +62,7 @@ public class TestUserRestController extends BaseRestController<TestUser, TestUse
@RequiresPermissions("gentest_user_select") @RequiresPermissions("gentest_user_select")
@Override @Override
public ResultVo<TestUserModel> get(TestUserModel model) { public ResultVo<TestUserModel> get(TestUserModel model) {
// 如果系统内部调用 则直接查数据库
if(model != null && model.getIzApi() != null && model.getIzApi()){
model = IService.get(model); model = IService.get(model);
}
return ResultVo.success(model); return ResultVo.success(model);
} }

@ -45,10 +45,7 @@ public class TestRestController extends BaseRestController<TestEntity, TestModel
@RequiresPermissions("gentest_test_select") @RequiresPermissions("gentest_test_select")
@Override @Override
public ResultVo<TestModel> get(TestModel model) { public ResultVo<TestModel> get(TestModel model) {
// 如果系统内部调用 则直接查数据库
if(model != null && model.getIzApi() != null && model.getIzApi()){
model = IService.get(model); model = IService.get(model);
}
return ResultVo.success(model); return ResultVo.success(model);
} }

@ -44,7 +44,7 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
* *
* *
* @author Parker * @author Parker
* @date 202151810:53:27 * @date 202151810:53:27

@ -301,6 +301,12 @@
<version>${fastjson.version}</version> <version>${fastjson.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.12.5</version>
</dependency>
<!-- 糊涂工具包 --> <!-- 糊涂工具包 -->
<dependency> <dependency>
<groupId>cn.hutool</groupId> <groupId>cn.hutool</groupId>

Loading…
Cancel
Save