Merge branch 'master' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into pay_extension

 Conflicts:
	sql/ruoyi-vue-pro.sql
	yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/enums/SysDictTypeConstants.java
	yudao-admin-server/src/main/resources/application-local.yaml
	yudao-admin-server/src/main/resources/application.yaml
	yudao-admin-server/src/test/resources/sql/clean.sql
	yudao-admin-server/src/test/resources/sql/create_tables.sql
	yudao-admin-ui/src/utils/dict.js
This commit is contained in:
YunaiV
2022-01-23 04:12:34 +08:00
547 changed files with 34242 additions and 6477 deletions

View File

@@ -25,7 +25,9 @@ public interface WebFilterOrderEnum {
// Spring Security Filter 默认为 -100可见 org.springframework.boot.autoconfigure.security.SecurityProperties 配置属性类
int TENANT_SECURITY_FILTER = -99; // 需要保证在 Spring Security 过滤器后
int TENANT_SECURITY_FILTER = -99; // 需要保证在 Spring Security 过滤器后
int ACTIVITI_FILTER = -98; // 需要保证在 Spring Security 过滤后面
int DEMO_FILTER = Integer.MAX_VALUE;

View File

@@ -35,4 +35,8 @@ public final class PageResult<T> implements Serializable {
return new PageResult<>(0L);
}
public static <T> PageResult<T> empty(Long total) {
return new PageResult<>(total);
}
}

View File

@@ -50,14 +50,14 @@ public class CollectionUtils {
if (CollUtil.isEmpty(from)) {
return new ArrayList<>();
}
return from.stream().map(func).collect(Collectors.toList());
return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toList());
}
public static <T, U> Set<U> convertSet(Collection<T> from, Function<T, U> func) {
if (CollUtil.isEmpty(from)) {
return new HashSet<>();
}
return from.stream().map(func).collect(Collectors.toSet());
return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toSet());
}
public static <T, K> Map<K, T> convertMap(Collection<T> from, Function<T, K> keyFunc) {

View File

@@ -0,0 +1,29 @@
package cn.iocoder.yudao.framework.common.util.io;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.StrUtil;
import java.io.InputStream;
/**
* IO 工具类,用于 {@link cn.hutool.core.io.IoUtil} 缺失的方法
*
* @author 芋道源码
*/
public class IoUtils {
/**
* 从流中读取 UTF8 编码的内容
*
* @param in 输入流
* @param isClose 是否关闭
* @return 内容
* @throws IORuntimeException IO 异常
*/
public static String readUtf8(InputStream in, boolean isClose) throws IORuntimeException {
return StrUtil.utf8Str(IoUtil.read(in, isClose));
}
}

View File

@@ -4,6 +4,7 @@ import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
@@ -43,6 +44,14 @@ public class JsonUtils {
}
}
public static byte[] toJsonByte(Object object) {
try {
return objectMapper.writeValueAsBytes(object);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
public static <T> T parseObject(String text, Class<T> clazz) {
if (StrUtil.isEmpty(text)) {
return null;
@@ -84,4 +93,21 @@ public class JsonUtils {
}
}
// TODO @Li和上面的风格保持一致哈。parseTree
public static JsonNode readTree(String text) {
try {
return objectMapper.readTree(text);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static JsonNode readTree(byte[] text) {
try {
return objectMapper.readTree(text);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,16 @@
package cn.iocoder.yudao.framework.common.util.number;
import cn.hutool.core.util.StrUtil;
/**
* 数字的工具类,补全 {@link cn.hutool.core.util.NumberUtil} 的功能
*
* @author 芋道源码
*/
public class NumberUtils {
public static Long parseLong(String str) {
return StrUtil.isNotEmpty(str) ? Long.valueOf(str) : null;
}
}

View File

@@ -1,9 +1,12 @@
package cn.iocoder.yudao.framework.common.util.object;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReflectUtil;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Objects;
import java.util.function.Consumer;
/**
@@ -53,4 +56,8 @@ public class ObjectUtils {
return null;
}
public static <T> boolean equalsAny(T obj, T... array) {
return Arrays.asList(array).contains(obj);
}
}

View File

@@ -0,0 +1,16 @@
package cn.iocoder.yudao.framework.common.util.object;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
/**
* {@link cn.iocoder.yudao.framework.common.pojo.PageParam} 工具类
*
* @author 芋道源码
*/
public class PageUtils {
public static int getStart(PageParam pageParam) {
return (pageParam.getPageNo() - 1) * pageParam.getPageSize();
}
}

View File

@@ -12,7 +12,9 @@ import java.util.regex.Pattern;
*/
public class ValidationUtils {
private static Pattern PATTERN_URL = Pattern.compile("^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]");
private static final Pattern PATTERN_URL = Pattern.compile("^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]");
private static final Pattern PATTERN_XML_NCNAME = Pattern.compile("[a-zA-Z_][\\-_.0-9_a-zA-Z$]*");
public static boolean isMobile(String mobile) {
if (StrUtil.length(mobile) != 11) {
@@ -27,4 +29,9 @@ public class ValidationUtils {
&& PATTERN_URL.matcher(url).matches();
}
public static boolean isXmlNCName(String str) {
return StringUtils.hasText(str)
&& PATTERN_XML_NCNAME.matcher(str).matches();
}
}

View File

@@ -19,6 +19,7 @@
<properties>
<activiti.version>7.1.0.M6</activiti.version>
</properties>
<!-- TODO @jason后续弄到 yudao-dependencies 里 -->
<dependencyManagement>
<dependencies>
<dependency>
@@ -36,6 +37,14 @@
<artifactId>yudao-common</artifactId>
</dependency>
<!-- Web 相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
@@ -66,6 +75,12 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-image-generator</artifactId>
<version>${activiti.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -1,32 +1,30 @@
package cn.iocoder.yudao.framework.activiti.config;
import org.activiti.api.runtime.shared.identity.UserGroupManager;
import org.activiti.spring.SpringProcessEngineConfiguration;
import org.activiti.spring.boot.ProcessEngineConfigurationConfigurer;
import org.apache.ibatis.session.SqlSessionFactory;
import cn.iocoder.yudao.framework.activiti.core.web.ActivitiWebFilter;
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
import org.activiti.image.ProcessDiagramGenerator;
import org.activiti.image.impl.DefaultProcessDiagramGenerator;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
@Configuration
public class YudaoActivitiConfiguration {
@Component
public static class SqlSessionFactoryProcessEngineConfigurationConfigurer
implements ProcessEngineConfigurationConfigurer {
private final SqlSessionFactory sqlSessionFactory;
public SqlSessionFactoryProcessEngineConfigurationConfigurer(SqlSessionFactory sessionFactory) {
this.sqlSessionFactory = sessionFactory;
}
@Override
public void configure(SpringProcessEngineConfiguration springProcessEngineConfiguration) {
springProcessEngineConfiguration.setSqlSessionFactory(sqlSessionFactory);
}
/**
* Activiti 流程图的生成器。目前管理后台的流程图 svg通过它绘制生成。
*/
@Bean
public ProcessDiagramGenerator processDiagramGenerator() {
return new DefaultProcessDiagramGenerator();
}
@Bean
public FilterRegistrationBean<ActivitiWebFilter> activitiWebFilter() {
FilterRegistrationBean<ActivitiWebFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new ActivitiWebFilter());
registrationBean.setOrder(WebFilterOrderEnum.ACTIVITI_FILTER);
return registrationBean;
}
}

View File

@@ -0,0 +1,110 @@
package cn.iocoder.yudao.framework.activiti.core.util;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
import com.alibaba.ttl.TransmittableThreadLocal;
import org.activiti.bpmn.converter.BpmnXMLConverter;
import org.activiti.bpmn.model.BpmnModel;
import org.activiti.bpmn.model.FlowElement;
import org.activiti.bpmn.model.Process;
import org.activiti.engine.impl.identity.Authentication;
import org.activiti.engine.impl.util.io.BytesStreamSource;
import javax.xml.bind.Element;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
/**
* Activiti 工具类
*
* @author 芋道源码
*/
public class ActivitiUtils {
static {
setAuthenticationThreadLocal();
}
// ========== Authentication 相关 ==========
/**
* 反射修改 Authentication 的 authenticatedUserIdThreadLocal 静态变量,使用 TTL 线程变量
* 目的:保证 @Async 等异步执行时,变量丢失的问题
*/
private static void setAuthenticationThreadLocal() {
ReflectUtil.setFieldValue(Authentication.class, "authenticatedUserIdThreadLocal",
new TransmittableThreadLocal<String>());
}
public static void setAuthenticatedUserId(Long userId) {
Authentication.setAuthenticatedUserId(String.valueOf(userId));
}
public static void clearAuthenticatedUserId() {
Authentication.setAuthenticatedUserId(null);
}
public static boolean equals(String userIdStr, Long userId) {
return Objects.equals(userId, NumberUtils.parseLong(userIdStr));
}
// ========== BPMN XML 相关 ==========
/**
* 构建对应的 BPMN Model
*
* @param bpmnBytes 原始的 BPMN XML 字节数组
* @return BPMN Model
*/
public static BpmnModel buildBpmnModel(byte[] bpmnBytes) {
// 转换成 BpmnModel 对象
BpmnXMLConverter converter = new BpmnXMLConverter();
return converter.convertToBpmnModel(new BytesStreamSource(bpmnBytes), true, true);
}
/**
* 获得 BPMN 流程中,指定的元素们
*
* @param model
* @param clazz 指定元素。例如说,{@link org.activiti.bpmn.model.UserTask}、{@link org.activiti.bpmn.model.Gateway} 等等
* @return 元素们
*/
public static <T extends FlowElement> List<T> getBpmnModelElements(BpmnModel model, Class<T> clazz) {
List<T> result = new ArrayList<>();
model.getProcesses().forEach(process -> {
process.getFlowElements().forEach(flowElement -> {
if (flowElement.getClass().isAssignableFrom(clazz)) {
result.add((T) flowElement);
}
});
});
return result;
}
public static String getBpmnXml(BpmnModel model) {
if (model == null) {
return null;
}
return StrUtil.utf8Str(getBpmnBytes(model));
}
public static byte[] getBpmnBytes(BpmnModel model) {
if (model == null) {
return new byte[0];
}
BpmnXMLConverter converter = new BpmnXMLConverter();
return converter.convertToXML(model);
}
public static boolean equals(BpmnModel oldModel, BpmnModel newModel) {
// 由于 BpmnModel 未提供 equals 方法,所以只能转成字节数组,进行比较
return Arrays.equals(getBpmnBytes(oldModel), getBpmnBytes(newModel));
}
}

View File

@@ -0,0 +1,37 @@
package cn.iocoder.yudao.framework.activiti.core.web;
import cn.iocoder.yudao.framework.activiti.core.util.ActivitiUtils;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Activiti Web 过滤器,将 userId 设置到 {@link org.activiti.engine.impl.identity.Authentication} 中
*
* @author 芋道源码
*/
public class ActivitiWebFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
try {
// 设置工作流的用户
Long userId = SecurityFrameworkUtils.getLoginUserId();
if (userId != null) {
ActivitiUtils.setAuthenticatedUserId(userId);
}
// 过滤
chain.doFilter(request, response);
} finally {
// 清理
ActivitiUtils.clearAuthenticatedUserId();
}
}
}

View File

@@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.iocoder.yudao.framework.activiti.config.YudaoActivitiConfiguration

View File

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.framework.mybatis.core.dataobject;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableLogic;
import lombok.Builder;
import lombok.Data;
import java.io.Serializable;

View File

@@ -4,11 +4,14 @@ import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import org.apache.ibatis.annotations.Param;
import java.util.Collection;
import java.util.List;
/**
@@ -28,14 +31,26 @@ public interface BaseMapperX<T> extends BaseMapper<T> {
return selectOne(new QueryWrapper<T>().eq(field, value));
}
default T selectOne(SFunction<T, ?> field, Object value) {
return selectOne(new LambdaQueryWrapper<T>().eq(field, value));
}
default T selectOne(String field1, Object value1, String field2, Object value2) {
return selectOne(new QueryWrapper<T>().eq(field1, value1).eq(field2, value2));
}
default T selectOne(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2) {
return selectOne(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2));
}
default Integer selectCount(String field, Object value) {
return selectCount(new QueryWrapper<T>().eq(field, value)).intValue();
}
default Integer selectCount(SFunction<T, ?> field, Object value) {
return selectCount(new LambdaQueryWrapper<T>().eq(field, value)).intValue();
}
default List<T> selectList() {
return selectList(new QueryWrapper<>());
}
@@ -44,4 +59,21 @@ public interface BaseMapperX<T> extends BaseMapper<T> {
return selectList(new QueryWrapper<T>().eq(field, value));
}
default List<T> selectList(SFunction<T, ?> field, Object value) {
return selectList(new LambdaQueryWrapper<T>().eq(field, value));
}
default List<T> selectList(String field, Collection<?> values) {
return selectList(new QueryWrapper<T>().in(field, values));
}
default List<T> selectList(SFunction<T, ?> field, Collection<?> values) {
return selectList(new LambdaQueryWrapper<T>().in(field, values));
}
default void insertBatch(Collection<T> entities) {
// TODO 芋艿:修改成支持批量的
entities.forEach(this::insert);
}
}

View File

@@ -0,0 +1,129 @@
package cn.iocoder.yudao.framework.mybatis.core.query;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.ArrayUtils;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import org.springframework.util.StringUtils;
import java.util.Collection;
/**
* 拓展 MyBatis Plus QueryWrapper 类,主要增加如下功能:
*
* 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。
*
* @param <T> 数据类型
*/
public class LambdaQueryWrapperX<T> extends LambdaQueryWrapper<T> {
public LambdaQueryWrapperX<T> likeIfPresent(SFunction<T, ?> column, String val) {
if (StringUtils.hasText(val)) {
return (LambdaQueryWrapperX<T>) super.like(column, val);
}
return this;
}
public LambdaQueryWrapperX<T> inIfPresent(SFunction<T, ?> column, Collection<?> values) {
if (!CollectionUtils.isEmpty(values)) {
return (LambdaQueryWrapperX<T>) super.in(column, values);
}
return this;
}
public LambdaQueryWrapperX<T> inIfPresent(SFunction<T, ?> column, Object... values) {
if (!ArrayUtils.isEmpty(values)) {
return (LambdaQueryWrapperX<T>) super.in(column, values);
}
return this;
}
public LambdaQueryWrapperX<T> eqIfPresent(SFunction<T, ?> column, Object val) {
if (val != null) {
return (LambdaQueryWrapperX<T>) super.eq(column, val);
}
return this;
}
public LambdaQueryWrapperX<T> neIfPresent(SFunction<T, ?> column, Object val) {
if (val != null) {
return (LambdaQueryWrapperX<T>) super.ne(column, val);
}
return this;
}
public LambdaQueryWrapperX<T> gtIfPresent(SFunction<T, ?> column, Object val) {
if (val != null) {
return (LambdaQueryWrapperX<T>) super.gt(column, val);
}
return this;
}
public LambdaQueryWrapperX<T> geIfPresent(SFunction<T, ?> column, Object val) {
if (val != null) {
return (LambdaQueryWrapperX<T>) super.ge(column, val);
}
return this;
}
public LambdaQueryWrapperX<T> ltIfPresent(SFunction<T, ?> column, Object val) {
if (val != null) {
return (LambdaQueryWrapperX<T>) super.lt(column, val);
}
return this;
}
public LambdaQueryWrapperX<T> leIfPresent(SFunction<T, ?> column, Object val) {
if (val != null) {
return (LambdaQueryWrapperX<T>) super.le(column, val);
}
return this;
}
public LambdaQueryWrapperX<T> betweenIfPresent(SFunction<T, ?> column, Object val1, Object val2) {
if (val1 != null && val2 != null) {
return (LambdaQueryWrapperX<T>) super.between(column, val1, val2);
}
if (val1 != null) {
return (LambdaQueryWrapperX<T>) ge(column, val1);
}
if (val2 != null) {
return (LambdaQueryWrapperX<T>) le(column, val2);
}
return this;
}
// ========== 重写父类方法,方便链式调用 ==========
@Override
public LambdaQueryWrapperX<T> eq(boolean condition, SFunction<T, ?> column, Object val) {
super.eq(condition, column, val);
return this;
}
@Override
public LambdaQueryWrapperX<T> eq(SFunction<T, ?> column, Object val) {
super.eq(column, val);
return this;
}
@Override
public LambdaQueryWrapperX<T> orderByDesc(SFunction<T, ?> column) {
super.orderByDesc(true, column);
return this;
}
@Override
public LambdaQueryWrapperX<T> last(String lastSql) {
super.last(lastSql);
return this;
}
@Override
public LambdaQueryWrapperX<T> in(SFunction<T, ?> column, Collection<?> coll) {
super.in(column, coll);
return this;
}
}

View File

@@ -65,11 +65,6 @@ public class LoginUser implements UserDetails {
* 所属岗位
*/
private Set<Long> postIds;
/**
* group 目前指岗位代替
*/
// TODO jason这个字段改成 postCodes 明确更好哈
private List<String> groups;
// ========== 上下文 ==========
/**
@@ -100,10 +95,7 @@ public class LoginUser implements UserDetails {
@Override
@JsonIgnore// 避免序列化
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> list = new ArrayList<>(1);
// TODO @芋艿:看看有没更优化的方案
list.add(new SimpleGrantedAuthority("ROLE_ACTIVITI_USER"));
return list;
return new HashSet<>();
}
@Override

View File

@@ -1,6 +1,5 @@
package cn.iocoder.yudao.framework.security.core.util;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import org.springframework.lang.Nullable;
@@ -12,7 +11,6 @@ import org.springframework.security.web.authentication.WebAuthenticationDetailsS
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.Objects;
import java.util.Set;
/**
@@ -100,12 +98,6 @@ public class SecurityFrameworkUtils {
// 原因是Spring Security 的 Filter 在 ApiAccessLogFilter 后面,在它记录访问日志时,线上上下文已经没有用户编号等信息
WebFrameworkUtils.setLoginUserId(request, loginUser.getId());
WebFrameworkUtils.setLoginUserType(request, loginUser.getUserType());
// TODO @jason使用 userId 会不会更合适哈?
// TODO @芋艿activiti 需要使用 ttl 上下文
// TODO @jason清理问题
if (Objects.equals(UserTypeEnum.ADMIN.getValue(), loginUser.getUserType())) {
org.activiti.engine.impl.identity.Authentication.setAuthenticatedUserId(loginUser.getUsername());
}
}
}

View File

@@ -31,6 +31,14 @@ public class RandomUtils {
// 字符串
PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(String.class,
(dataProviderStrategy, attributeMetadata, map) -> randomString());
// Integer
PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(Integer.class, (dataProviderStrategy, attributeMetadata, map) -> {
// 如果是 status 的字段,返回 0 或 1
if (attributeMetadata.getAttributeName().equals("status")) {
return RandomUtil.randomEle(CommonStatusEnum.values()).getStatus();
}
return RandomUtil.randomInt();
});
// Boolean
PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(Boolean.class, (dataProviderStrategy, attributeMetadata, map) -> {
// 如果是 deleted 的字段,返回非删除