diff --git a/common/src/main/java/com/canvas/web/annotation/rest/AnonymousDeleteMapping.java b/common/src/main/java/com/canvas/web/annotation/rest/AnonymousDeleteMapping.java new file mode 100644 index 0000000..c9c37ef --- /dev/null +++ b/common/src/main/java/com/canvas/web/annotation/rest/AnonymousDeleteMapping.java @@ -0,0 +1,59 @@ +package com.canvas.web.annotation.rest; + + +import com.canvas.web.annotation.AnonymousAccess; +import org.springframework.core.annotation.AliasFor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +import java.lang.annotation.*; + +@AnonymousAccess +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@RequestMapping(method = RequestMethod.DELETE) +public @interface AnonymousDeleteMapping { + + /** + * Alias for {@link RequestMapping#name}. + */ + @AliasFor(annotation = RequestMapping.class) + String name() default ""; + + /** + * Alias for {@link RequestMapping#value}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] value() default {}; + + /** + * Alias for {@link RequestMapping#path}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] path() default {}; + + /** + * Alias for {@link RequestMapping#params}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] params() default {}; + + /** + * Alias for {@link RequestMapping#headers}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] headers() default {}; + + /** + * Alias for {@link RequestMapping#consumes}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] consumes() default {}; + + /** + * Alias for {@link RequestMapping#produces}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] produces() default {}; +} diff --git a/common/src/main/java/com/canvas/web/config/AuditorConfig.java b/common/src/main/java/com/canvas/web/config/AuditorConfig.java new file mode 100644 index 0000000..0a24b64 --- /dev/null +++ b/common/src/main/java/com/canvas/web/config/AuditorConfig.java @@ -0,0 +1,26 @@ +package com.canvas.web.config; + +import com.canvas.web.utils.SecurityUtils; +import org.springframework.data.domain.AuditorAware; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@Component("auditorAware") +public class AuditorConfig implements AuditorAware { + /** + * 返回操作员标志信息 + * + * @return / + */ + @Override + public Optional getCurrentAuditor() { + try { + // 这里应根据实际业务情况获取具体信息 + return Optional.of(SecurityUtils.getCurrentUsername()); + }catch (Exception ignored){} + // 用户定时任务,或者无Token调用的情况 + return Optional.of("System"); + } + +} diff --git a/common/src/main/java/com/canvas/web/config/CommonConfig.java b/common/src/main/java/com/canvas/web/config/CommonConfig.java deleted file mode 100644 index cf5d64b..0000000 --- a/common/src/main/java/com/canvas/web/config/CommonConfig.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.canvas.web.config; - - -import lombok.Data; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Configuration; - -@Configuration -@Data -public class CommonConfig { - - /** - * 图片域名 - */ - public static String imageURL; - - /** - * 是否演示环境:true是,false否 - */ - public static boolean appDebug; - - /** - * 图片域名赋值 - * - * @param url 域名地址 - */ - @Value("${javaweb.image-url}") - public void setImageURL(String url) { - imageURL = url; - } - - /** - * 是否演示模式 - * - * @param debug - */ - @Value("${javaweb.app-debug}") - public void setAppDebug(boolean debug) { - appDebug = debug; - } -} diff --git a/common/src/main/java/com/canvas/web/config/ElPermissionConfig.java b/common/src/main/java/com/canvas/web/config/ElPermissionConfig.java new file mode 100644 index 0000000..f99485a --- /dev/null +++ b/common/src/main/java/com/canvas/web/config/ElPermissionConfig.java @@ -0,0 +1,21 @@ +package com.canvas.web.config; + + +import com.canvas.web.utils.SecurityUtils; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Service; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@Service(value = "el") +public class ElPermissionConfig { + + public Boolean check(String... permissions) { + // 获取当前用户的所有权限 + List elPermissions = SecurityUtils.getCurrentUser().getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()); + // 判断当前用户的所有权限是否包含接口上定义的权限 + return elPermissions.contains("admin") || Arrays.stream(permissions).anyMatch(elPermissions::contains); + } +} diff --git a/common/src/main/java/com/canvas/web/config/RedisConfig.java b/common/src/main/java/com/canvas/web/config/RedisConfig.java new file mode 100644 index 0000000..eadc2c8 --- /dev/null +++ b/common/src/main/java/com/canvas/web/config/RedisConfig.java @@ -0,0 +1,197 @@ +package com.canvas.web.config; + +import cn.hutool.core.lang.Assert; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.parser.ParserConfig; +import com.alibaba.fastjson.serializer.SerializerFeature; +import com.canvas.web.utils.StringUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.codec.digest.DigestUtils; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cache.Cache; +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.interceptor.CacheErrorHandler; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.RedisSerializer; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Configuration +@EnableCaching +@ConditionalOnClass(RedisOperations.class) +@EnableConfigurationProperties(RedisProperties.class) +public class RedisConfig extends CachingConfigurerSupport { + + /** + * 设置 redis 数据默认过期时间,默认2小时 + * 设置@cacheable 序列化方式 + */ + @Bean + public RedisCacheConfiguration redisCacheConfiguration(){ + FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class); + RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig(); + configuration = configuration.serializeValuesWith(RedisSerializationContext. + SerializationPair.fromSerializer(fastJsonRedisSerializer)).entryTtl(Duration.ofHours(6)); + return configuration; + } + + @SuppressWarnings("all") + @Bean(name = "redisTemplate") + @ConditionalOnMissingBean(name = "redisTemplate") + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + //序列化 + FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class); + // value值的序列化采用fastJsonRedisSerializer + template.setValueSerializer(fastJsonRedisSerializer); + template.setHashValueSerializer(fastJsonRedisSerializer); + // 全局开启AutoType,这里方便开发,使用全局的方式 + ParserConfig.getGlobalInstance().setAutoTypeSupport(true); + // 建议使用这种方式,小范围指定白名单 + // ParserConfig.getGlobalInstance().addAccept("me.zhengjie.domain"); + // key的序列化采用StringRedisSerializer + template.setKeySerializer(new StringRedisSerializer()); + template.setHashKeySerializer(new StringRedisSerializer()); + template.setConnectionFactory(redisConnectionFactory); + return template; + } + + /** + * 自定义缓存key生成策略,默认将使用该策略 + */ + @Bean + @Override + public KeyGenerator keyGenerator() { + return (target, method, params) -> { + Map container = new HashMap<>(3); + Class targetClassClass = target.getClass(); + // 类地址 + container.put("class",targetClassClass.toGenericString()); + // 方法名称 + container.put("methodName",method.getName()); + // 包名称 + container.put("package",targetClassClass.getPackage()); + // 参数列表 + for (int i = 0; i < params.length; i++) { + container.put(String.valueOf(i),params[i]); + } + // 转为JSON字符串 + String jsonString = JSON.toJSONString(container); + // 做SHA256 Hash计算,得到一个SHA256摘要作为Key + return DigestUtils.sha256Hex(jsonString); + }; + } + + @Bean + @Override + public CacheErrorHandler errorHandler() { + // 异常处理,当Redis发生异常时,打印日志,但是程序正常走 + log.info("初始化 -> [{}]", "Redis CacheErrorHandler"); + return new CacheErrorHandler() { + @Override + public void handleCacheGetError(RuntimeException e, Cache cache, Object key) { + log.error("Redis occur handleCacheGetError:key -> [{}]", key, e); + } + + @Override + public void handleCachePutError(RuntimeException e, Cache cache, Object key, Object value) { + log.error("Redis occur handleCachePutError:key -> [{}];value -> [{}]", key, value, e); + } + + @Override + public void handleCacheEvictError(RuntimeException e, Cache cache, Object key) { + log.error("Redis occur handleCacheEvictError:key -> [{}]", key, e); + } + + @Override + public void handleCacheClearError(RuntimeException e, Cache cache) { + log.error("Redis occur handleCacheClearError:", e); + } + }; + } + +} + +/** + * Value 序列化 + * + * @author / + * @param + */ +class FastJsonRedisSerializer implements RedisSerializer { + + private final Class clazz; + + FastJsonRedisSerializer(Class clazz) { + super(); + this.clazz = clazz; + } + + @Override + public byte[] serialize(T t) { + if (t == null) { + return new byte[0]; + } + return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(StandardCharsets.UTF_8); + } + + @Override + public T deserialize(byte[] bytes) { + if (bytes == null || bytes.length <= 0) { + return null; + } + String str = new String(bytes, StandardCharsets.UTF_8); + return JSON.parseObject(str, clazz); + } + +} + +/** + * 重写序列化器 + * + * @author / + */ +class StringRedisSerializer implements RedisSerializer { + + private final Charset charset; + + StringRedisSerializer() { + this(StandardCharsets.UTF_8); + } + + private StringRedisSerializer(Charset charset) { + Assert.notNull(charset, "Charset must not be null!"); + this.charset = charset; + } + + @Override + public String deserialize(byte[] bytes) { + return (bytes == null ? null : new String(bytes, charset)); + } + + @Override + public byte[] serialize(Object object) { + String string = JSON.toJSONString(object); + if (StringUtils.isBlank(string)) { + return null; + } + string = string.replace("\"", ""); + return string.getBytes(charset); + } +} diff --git a/common/src/main/java/com/canvas/web/config/UploadFileConfig.java b/common/src/main/java/com/canvas/web/config/UploadFileConfig.java deleted file mode 100644 index 2d4a7b5..0000000 --- a/common/src/main/java/com/canvas/web/config/UploadFileConfig.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.canvas.web.config; - - -import lombok.Data; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Configuration; - -@Configuration -@Data -public class UploadFileConfig { - /** - * 上传目录 - */ - public static String uploadFolder; - /** - * 访问路径 - */ - public static String staticAccessPath; - /** - * 上传服务器的映射文件夹 - */ - public static String accessPath; - - @Value("${file.uploadFolder}") - public void setUploadFolder(String path) { - uploadFolder = path; - } - - @Value("${file.staticAccessPath}") - public void setStaticAccessPath(String path) { - staticAccessPath = path; - } - - @Value("${file.accessPath}") - public void setAccessPath(String path) { - accessPath = path; - } -} diff --git a/common/src/main/java/com/canvas/web/enums/DataScopeEnum.java b/common/src/main/java/com/canvas/web/enums/DataScopeEnum.java new file mode 100644 index 0000000..27e4863 --- /dev/null +++ b/common/src/main/java/com/canvas/web/enums/DataScopeEnum.java @@ -0,0 +1,30 @@ +package com.canvas.web.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum DataScopeEnum { + + /* 全部的数据权限 */ + ALL("全部", "全部的数据权限"), + + /* 自己部门的数据权限 */ + THIS_LEVEL("本级", "自己部门的数据权限"), + + /* 自定义的数据权限 */ + CUSTOMIZE("自定义", "自定义的数据权限"); + + private final String value; + private final String description; + + public static DataScopeEnum find(String val) { + for (DataScopeEnum dataScopeEnum : DataScopeEnum.values()) { + if (val.equals(dataScopeEnum.getValue())) { + return dataScopeEnum; + } + } + return null; + } +} diff --git a/common/src/main/java/com/canvas/web/utils/CommonUtils.java b/common/src/main/java/com/canvas/web/utils/CommonUtils.java deleted file mode 100644 index 24593e2..0000000 --- a/common/src/main/java/com/canvas/web/utils/CommonUtils.java +++ /dev/null @@ -1,294 +0,0 @@ -package com.canvas.web.utils; - -import com.alibaba.fastjson.JSONObject; -import com.canvas.web.config.CommonConfig; -import org.springframework.util.StringUtils; - -import java.lang.reflect.Field; -import java.security.MessageDigest; -import java.text.NumberFormat; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class CommonUtils { - /** - * 分转元 - * - * @param amount - * @return - */ - public static String fenToYuan(String amount) { - NumberFormat format = NumberFormat.getInstance(); - try { - Number number = format.parse(amount); - double temp = number.doubleValue() / 100.0; - format.setGroupingUsed(false); - // 设置返回的小数部分所允许的最大位数 - format.setMaximumFractionDigits(2); - amount = format.format(temp); - } catch (ParseException e) { - e.printStackTrace(); - } - return amount; - } - - /** - * 元转分 - * - * @param amount - * @return - */ - public static String yuanToFen(String amount) { - NumberFormat format = NumberFormat.getInstance(); - try { - Number number = format.parse(amount); - double temp = number.doubleValue() * 100.0; - format.setGroupingUsed(false); - // 设置返回数的小数部分所允许的最大位数 - format.setMaximumFractionDigits(0); - amount = format.format(temp); - } catch (ParseException e) { - e.printStackTrace(); - } - return amount; - } - - /** - * 获取当前时间时间戳 - * - * @return - */ - public static Integer timeStamp() { - long currentTime = System.currentTimeMillis(); - String time = String.valueOf(currentTime / 1000); - return Integer.valueOf(time); - } - - /** - * 时间转为日期格式 - * - * @param time - * @param format - * @return - */ - public static String formatTime(Integer time, String format) { - if (StringUtils.isEmpty(time)) { - return ""; - } - SimpleDateFormat dateFormat = new SimpleDateFormat(format); - long timeLong = time.longValue() * 1000; - Date date = new Date(timeLong); - return dateFormat.format(date); - } - - /** - * 获取到图片域名的地址 - * - * @param imageUrl - * @return - */ - public static String getImageURL(String imageUrl) { - return CommonConfig.imageURL + imageUrl; - } - - /** - * 验证邮箱是否正确 - * - * @param email - * @return - */ - public static boolean isEmail(String email) { - boolean flag = false; - try { - String check = "^([a-z0-9A-Z]+[-|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$"; - Pattern regex = Pattern.compile(check); - Matcher matcher = regex.matcher(email); - flag = matcher.matches(); - } catch (Exception e) { - flag = false; - } - return flag; - } - - /** - * 验证手机号是否正确 - * - * @param mobile - * @return - */ - public static boolean isMobile(String mobile) { - boolean flag = false; - try { - Pattern p = Pattern.compile("^((13[0-9])|(15[^4,\\D])|(18[0,5-9]))\\d{8}$"); - Matcher m = p.matcher(mobile); - flag = m.matches(); - } catch (Exception e) { - flag = false; - } - return flag; - } - - /** - * 生成指定位数的随机字符串 - * - * @param isNum 是否是纯数字 - * @param length 长度 - * @return - */ - public static String getRandomStr(boolean isNum, int length) { - String resultStr = ""; - String str = isNum ? "1234567890" : "1234567890abcdefghijkmnpqrstuvwxyz"; - int len = str.length(); - boolean isStop = true; - do { - resultStr = ""; - int count = 0; - for (int i = 0; i < length; i++) { - double dblR = Math.random() * len; - int intR = (int) Math.floor(dblR); - char c = str.charAt(intR); - if (('0' <= c) && (c <= '9')) { - count++; - } - resultStr += str.charAt(intR); - } - if (count >= 2) { - isStop = false; - } - } while (isStop); - return resultStr; - } - - /** - * 判断是否在数组中 - * - * @param s - * @param array - * @return - */ - public static boolean inArray(final String s, final String[] array) { - for (String item : array) { - if (item != null && item.equals(s)) { - return true; - } - } - return false; - } - - /** - * 从html中提取纯文本 - * - * @param strHtml - * @return - */ - public static String stripHtml(String strHtml) { - String content = strHtml.replaceAll("]+>", ""); //剔出的标签 - content = content.replaceAll("\\s*|\t|\r|\n", "");//去除字符串中的空格,回车,换行符,制表符 - return content; - } - - /** - * 去除字符串中的空格、回车、换行符、制表符等 - * - * @param str 原始字符串 - * @return - */ - public static String replaceSpecialStr(String str) { - String repl = ""; - if (str != null) { - Pattern p = Pattern.compile("\\s*|\t|\r|\n"); - Matcher m = p.matcher(str); - repl = m.replaceAll(""); - } - return repl; - } - - /** - * 判断某个元素是否在数组中 - * - * @param key 元素 - * @param map 数组 - * @return - */ - public static boolean inArray(String key, Map map) { - boolean flag = false; - for (String k : map.keySet()) { - if (k.equals(key)) { - flag = true; - } - } - return flag; - } - - /** - * 对象转Map - * - * @param obj 对象 - * @return - * @throws IllegalAccessException - */ - public static Map objectToMap(Object obj) throws IllegalAccessException { - Map map = new HashMap<>(); - Class clazz = obj.getClass(); - for (Field field : clazz.getDeclaredFields()) { - field.setAccessible(true); - String fieldName = field.getName(); - Object value = field.get(obj); - map.put(fieldName, value); - } - return map; - } - - /** - * 判断是否是JSON格式 - * - * @param str JSON字符串 - * @return - */ - private boolean isJson(String str) { - try { - JSONObject jsonStr = JSONObject.parseObject(str); - return true; - } catch (Exception e) { - return false; - } - } - - /** - * MD5方法 - * - * @param source - * @return - */ - public static String md5(byte[] source) { - try { - MessageDigest md = MessageDigest.getInstance("MD5"); - md.update(source); - StringBuffer buf = new StringBuffer(); - for (byte b : md.digest()) { - buf.append(String.format("%02x", b & 0xff)); - } - return buf.toString(); - } catch (Exception e) { - e.printStackTrace(); - return null; - } - } - - /** - * 密码加密 - * - * @param password 密码 - * @return - */ - public static String password(String password) { - String md51 = md5(password.getBytes()); - String pwd = md5((md51 + "IgtUdEQJyVevaCxQnY").getBytes()); - return pwd; - } -} diff --git a/common/src/main/java/com/canvas/web/utils/SecurityUtils.java b/common/src/main/java/com/canvas/web/utils/SecurityUtils.java new file mode 100644 index 0000000..c027d00 --- /dev/null +++ b/common/src/main/java/com/canvas/web/utils/SecurityUtils.java @@ -0,0 +1,77 @@ +package com.canvas.web.utils; + +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.canvas.web.enums.DataScopeEnum; +import com.canvas.web.exception.BaseException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; + +import java.util.List; + +@Slf4j +public class SecurityUtils { + + /** + * 获取当前登录的用户 + * */ + public static UserDetails getCurrentUser(){ + UserDetailsService userDetailsService=SpringContextHolder.getBean(UserDetailsService.class); + return userDetailsService.loadUserByUsername(getCurrentUsername()); + } + + + + /** + * 获取系统用户名称 + * + * @return 系统用户名称 + */ + public static String getCurrentUsername() { + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null) { + throw new BaseException( "当前登录状态过期");//HttpStatus.UNAUTHORIZED, + } + if (authentication.getPrincipal() instanceof UserDetails) { + UserDetails userDetails = (UserDetails) authentication.getPrincipal(); + return userDetails.getUsername(); + } + throw new BaseException( "找不到当前登录的信息");//HttpStatus.UNAUTHORIZED + } + + /** + * 获取系统用户ID + * @return 系统用户ID + */ + public static Long getCurrentUserId() { + UserDetails userDetails = getCurrentUser(); + return new JSONObject(new JSONObject(userDetails).get("user")).get("id", Long.class); + } + + + /** + * 获取当前用户的数据权限 + * @return / + */ + public static List getCurrentUserDataScope(){ + UserDetails userDetails = getCurrentUser(); + JSONArray array = JSONUtil.parseArray(new JSONObject(userDetails).get("dataScopes")); + return JSONUtil.toList(array,Long.class); + } + + /** + * 获取数据权限级别 + * @return 级别 + */ + public static String getDataScopeType() { + List dataScopes = getCurrentUserDataScope(); + if(dataScopes.size() != 0){ + return ""; + } + return DataScopeEnum.ALL.getValue(); + } +} diff --git a/system/src/main/java/com/canvas/web/config/thread/ThreadPoolExecutorUtil.java b/system/src/main/java/com/canvas/web/config/thread/ThreadPoolExecutorUtil.java new file mode 100644 index 0000000..e31128d --- /dev/null +++ b/system/src/main/java/com/canvas/web/config/thread/ThreadPoolExecutorUtil.java @@ -0,0 +1,21 @@ +package com.canvas.web.config.thread; + +import com.canvas.web.utils.SpringContextHolder; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class ThreadPoolExecutorUtil { + public static ThreadPoolExecutor getPoll(){ + AsyncTaskProperties properties = SpringContextHolder.getBean(AsyncTaskProperties.class); + return new ThreadPoolExecutor( + properties.getCorePoolSize(), + properties.getMaxPoolSize(), + properties.getKeepAliveSeconds(), + TimeUnit.SECONDS, + new ArrayBlockingQueue<>(properties.getQueueCapacity()), + new TheadFactoryName() + ); + } +} diff --git a/system/src/main/java/com/canvas/web/modules/controller/OnlineController.java b/system/src/main/java/com/canvas/web/modules/controller/OnlineController.java deleted file mode 100644 index 2c72604..0000000 --- a/system/src/main/java/com/canvas/web/modules/controller/OnlineController.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.canvas.web.modules.controller; - - -import com.canvas.web.modules.security.service.OnlineUserService; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequiredArgsConstructor -@RequestMapping("/auth/online") -public class OnlineController { - - private final OnlineUserService onlineUserService; -} diff --git a/system/src/main/java/com/canvas/web/modules/security/config/SpringSecurityConfig.java b/system/src/main/java/com/canvas/web/modules/security/config/SpringSecurityConfig.java index bf532c3..b9539a1 100644 --- a/system/src/main/java/com/canvas/web/modules/security/config/SpringSecurityConfig.java +++ b/system/src/main/java/com/canvas/web/modules/security/config/SpringSecurityConfig.java @@ -123,7 +123,7 @@ public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { // 所有类型的接口都放行 .antMatchers(anonymousUrls.get(RequestMethodEnum.ALL.getType()).toArray(new String[0])).permitAll() // 所有请求都需要认证 测试需求注释 - //.anyRequest().authenticated() + .anyRequest().authenticated() .and().apply(securityConfigurerAdapter()); } diff --git a/system/src/main/java/com/canvas/web/modules/controller/AuthorizationController.java b/system/src/main/java/com/canvas/web/modules/security/controller/AuthorizationController.java similarity index 66% rename from system/src/main/java/com/canvas/web/modules/controller/AuthorizationController.java rename to system/src/main/java/com/canvas/web/modules/security/controller/AuthorizationController.java index dfaabdb..0518c78 100644 --- a/system/src/main/java/com/canvas/web/modules/controller/AuthorizationController.java +++ b/system/src/main/java/com/canvas/web/modules/security/controller/AuthorizationController.java @@ -1,28 +1,35 @@ -package com.canvas.web.modules.controller; - +package com.canvas.web.modules.security.controller; +import cn.hutool.core.util.IdUtil; +import com.canvas.web.annotation.rest.AnonymousDeleteMapping; +import com.canvas.web.annotation.rest.AnonymousGetMapping; import com.canvas.web.annotation.rest.AnonymousPostMapping; import com.canvas.web.config.RsaProperties; import com.canvas.web.exception.BaseException; +import com.canvas.web.modules.security.config.bean.LoginCodeEnum; +import com.canvas.web.modules.security.config.bean.LoginProperties; import com.canvas.web.modules.security.config.bean.SecurityProperties; import com.canvas.web.modules.security.security.TokenProvider; -import com.canvas.web.modules.security.config.bean.LoginProperties; import com.canvas.web.modules.security.service.OnlineUserService; import com.canvas.web.modules.security.service.dto.AuthUserDto; +import com.canvas.web.modules.security.service.dto.JwtUserDto; import com.canvas.web.utils.RedisUtils; import com.canvas.web.utils.RsaUtils; +import com.canvas.web.utils.SecurityUtils; import com.canvas.web.utils.StringUtils; +import com.wf.captcha.base.Captcha; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import com.canvas.web.modules.security.service.dto.JwtUserDto; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -31,6 +38,7 @@ import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.TimeUnit; @Slf4j @RestController @@ -38,7 +46,6 @@ import java.util.Map; @RequiredArgsConstructor @Api(tags = "系统:系统授权接口") public class AuthorizationController { - private final SecurityProperties properties; private final RedisUtils redisUtils; private final OnlineUserService onlineUserService; @@ -48,7 +55,6 @@ public class AuthorizationController { @Resource private LoginProperties loginProperties; - @ApiOperation("登录授权") @AnonymousPostMapping(value = "/login") public ResponseEntity login(@Validated @RequestBody AuthUserDto authUser, HttpServletRequest request) throws Exception { @@ -83,4 +89,38 @@ public class AuthorizationController { } return ResponseEntity.ok(authInfo); } + + @ApiOperation("获取用户信息") + @GetMapping(value = "/info") + public ResponseEntity getUserInfo() { + return ResponseEntity.ok(SecurityUtils.getCurrentUser()); + } + + @ApiOperation("获取验证码") + @AnonymousGetMapping(value = "/code") + public ResponseEntity getCode() { + // 获取运算的结果 + Captcha captcha = loginProperties.getCaptcha(); + String uuid = properties.getCodeKey() + IdUtil.simpleUUID(); + //当验证码类型为 arithmetic时且长度 >= 2 时,captcha.text()的结果有几率为浮点型 + String captchaValue = captcha.text(); + if (captcha.getCharType() - 1 == LoginCodeEnum.arithmetic.ordinal() && captchaValue.contains(".")) { + captchaValue = captchaValue.split("\\.")[0]; + } + // 保存 + redisUtils.set(uuid, captchaValue, loginProperties.getLoginCode().getExpiration(), TimeUnit.MINUTES); + // 验证码信息 + Map imgResult = new HashMap(2) {{ + put("img", captcha.toBase64()); + put("uuid", uuid); + }}; + return ResponseEntity.ok(imgResult); + } + + @ApiOperation("退出登录") + @AnonymousDeleteMapping(value = "/logout") + public ResponseEntity logout(HttpServletRequest request) { + onlineUserService.logout(tokenProvider.getToken(request)); + return new ResponseEntity<>(HttpStatus.OK); + } } diff --git a/system/src/main/java/com/canvas/web/modules/system/domain/Menu.java b/system/src/main/java/com/canvas/web/modules/system/domain/Menu.java index 83fb1c0..dd6ddc7 100644 --- a/system/src/main/java/com/canvas/web/modules/system/domain/Menu.java +++ b/system/src/main/java/com/canvas/web/modules/system/domain/Menu.java @@ -3,6 +3,8 @@ package com.canvas.web.modules.system.domain; import com.canvas.web.base.BaseEntity; import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.annotations.ApiModelProperty; +import lombok.Getter; +import lombok.Setter; import javax.persistence.*; import javax.validation.constraints.NotNull; @@ -10,10 +12,14 @@ import java.io.Serializable; import java.util.Objects; import java.util.Set; +@Entity +@Getter +@Setter +@Table(name = "sys_menu") public class Menu extends BaseEntity implements Serializable { @Id - @Column(name = "menu_id") + @Column(name = "id") @NotNull(groups = {Update.class}) @ApiModelProperty(value = "ID", hidden = true) @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/system/src/main/java/com/canvas/web/modules/system/domain/Org.java b/system/src/main/java/com/canvas/web/modules/system/domain/Org.java index cbf8d4b..caa4dda 100644 --- a/system/src/main/java/com/canvas/web/modules/system/domain/Org.java +++ b/system/src/main/java/com/canvas/web/modules/system/domain/Org.java @@ -1,8 +1,46 @@ package com.canvas.web.modules.system.domain; import com.canvas.web.base.BaseEntity; +import io.swagger.annotations.ApiModelProperty; +import lombok.Getter; +import lombok.Setter; +import javax.persistence.*; +import javax.validation.constraints.NotNull; import java.io.Serializable; +import java.util.Objects; +@Entity +@Getter +@Setter +@Table(name="organization") public class Org extends BaseEntity implements Serializable { + @Id + @Column(name = "id") + @NotNull(groups = Update.class) + @GeneratedValue(strategy = GenerationType.IDENTITY) + @ApiModelProperty(value = "ID", hidden = true) + private Long id; + + private String name; + + private String version; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Org org = (Org) o; + return Objects.equals(id, org.id) && + Objects.equals(name, org.name); + } + + @Override + public int hashCode() { + return Objects.hash(id, name); + } } diff --git a/system/src/main/java/com/canvas/web/modules/system/domain/Role.java b/system/src/main/java/com/canvas/web/modules/system/domain/Role.java index d059d39..99438b1 100644 --- a/system/src/main/java/com/canvas/web/modules/system/domain/Role.java +++ b/system/src/main/java/com/canvas/web/modules/system/domain/Role.java @@ -1,6 +1,7 @@ package com.canvas.web.modules.system.domain; import com.canvas.web.base.BaseEntity; +import com.canvas.web.enums.DataScopeEnum; import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.annotations.ApiModelProperty; import lombok.Getter; @@ -21,7 +22,7 @@ import java.util.Set; public class Role extends BaseEntity implements Serializable { @Id - @Column(name = "role_id") + @Column(name = "id") @NotNull(groups = {Update.class}) @GeneratedValue(strategy = GenerationType.IDENTITY) @ApiModelProperty(value = "ID", hidden = true) @@ -34,8 +35,8 @@ public class Role extends BaseEntity implements Serializable { @ManyToMany @JoinTable(name = "sys_roles_menus", - joinColumns = {@JoinColumn(name = "role_id",referencedColumnName = "role_id")}, - inverseJoinColumns = {@JoinColumn(name = "menu_id",referencedColumnName = "menu_id")}) + joinColumns = {@JoinColumn(name = "role_id",referencedColumnName = "id")}, + inverseJoinColumns = {@JoinColumn(name = "menu_id",referencedColumnName = "id")}) @ApiModelProperty(value = "菜单", hidden = true) private Set menus; @@ -50,8 +51,8 @@ public class Role extends BaseEntity implements Serializable { @ApiModelProperty(value = "名称", hidden = true) private String name; -// @ApiModelProperty(value = "数据权限,全部 、 本级 、 自定义") -// private String dataScope = DataScopeEnum.THIS_LEVEL.getValue(); + @ApiModelProperty(value = "数据权限,全部 、 本级 、 自定义") + private String dataScope = DataScopeEnum.THIS_LEVEL.getValue(); @Column(name = "level") @ApiModelProperty(value = "级别,数值越小,级别越大") diff --git a/system/src/main/java/com/canvas/web/modules/system/domain/User.java b/system/src/main/java/com/canvas/web/modules/system/domain/User.java index 9c89af0..ab72224 100644 --- a/system/src/main/java/com/canvas/web/modules/system/domain/User.java +++ b/system/src/main/java/com/canvas/web/modules/system/domain/User.java @@ -22,7 +22,7 @@ import java.util.Set; public class User extends BaseEntity implements Serializable { @Id - @Column(name = "user_id") + @Column(name = "id") @NotNull(groups = Update.class) @GeneratedValue(strategy = GenerationType.IDENTITY) @ApiModelProperty(value = "ID", hidden = true) @@ -32,8 +32,8 @@ public class User extends BaseEntity implements Serializable { @ManyToMany @ApiModelProperty(value = "用户角色") @JoinTable(name = "sys_users_roles", - joinColumns = {@JoinColumn(name = "user_id",referencedColumnName = "user_id")}, - inverseJoinColumns = {@JoinColumn(name = "role_id",referencedColumnName = "role_id")}) + joinColumns = {@JoinColumn(name = "user_id",referencedColumnName = "id")}, + inverseJoinColumns = {@JoinColumn(name = "role_id",referencedColumnName = "id")}) private Set roles; diff --git a/system/src/main/java/com/canvas/web/modules/system/repository/MenuRepository.java b/system/src/main/java/com/canvas/web/modules/system/repository/MenuRepository.java index ace0116..39cce3e 100644 --- a/system/src/main/java/com/canvas/web/modules/system/repository/MenuRepository.java +++ b/system/src/main/java/com/canvas/web/modules/system/repository/MenuRepository.java @@ -1,4 +1,8 @@ package com.canvas.web.modules.system.repository; -public interface MenuRepository { +import com.canvas.web.modules.system.domain.Menu; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface MenuRepository extends JpaRepository, JpaSpecificationExecutor { } diff --git a/system/src/main/java/com/canvas/web/modules/system/repository/OrgRepository.java b/system/src/main/java/com/canvas/web/modules/system/repository/OrgRepository.java index f81dcda..964be33 100644 --- a/system/src/main/java/com/canvas/web/modules/system/repository/OrgRepository.java +++ b/system/src/main/java/com/canvas/web/modules/system/repository/OrgRepository.java @@ -1,4 +1,8 @@ package com.canvas.web.modules.system.repository; -public interface OrgRepository { +import com.canvas.web.modules.system.domain.Org; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface OrgRepository extends JpaRepository, JpaSpecificationExecutor { } diff --git a/system/src/main/java/com/canvas/web/modules/system/repository/RoleRepository.java b/system/src/main/java/com/canvas/web/modules/system/repository/RoleRepository.java index 76173c0..cc4f24a 100644 --- a/system/src/main/java/com/canvas/web/modules/system/repository/RoleRepository.java +++ b/system/src/main/java/com/canvas/web/modules/system/repository/RoleRepository.java @@ -1,4 +1,8 @@ package com.canvas.web.modules.system.repository; -public interface RoleRepository { +import com.canvas.web.modules.system.domain.Role; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface RoleRepository extends JpaRepository, JpaSpecificationExecutor { } diff --git a/system/src/main/java/com/canvas/web/modules/system/repository/UserRepository.java b/system/src/main/java/com/canvas/web/modules/system/repository/UserRepository.java index fa5d6b7..5fd2c03 100644 --- a/system/src/main/java/com/canvas/web/modules/system/repository/UserRepository.java +++ b/system/src/main/java/com/canvas/web/modules/system/repository/UserRepository.java @@ -1,4 +1,8 @@ package com.canvas.web.modules.system.repository; -public interface UserRepository { +import com.canvas.web.modules.system.domain.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface UserRepository extends JpaRepository, JpaSpecificationExecutor { } diff --git a/system/src/main/java/com/canvas/web/modules/system/service/impl/DataServiceImpl.java b/system/src/main/java/com/canvas/web/modules/system/service/impl/DataServiceImpl.java new file mode 100644 index 0000000..3e24501 --- /dev/null +++ b/system/src/main/java/com/canvas/web/modules/system/service/impl/DataServiceImpl.java @@ -0,0 +1,20 @@ +package com.canvas.web.modules.system.service.impl; + +import com.canvas.web.modules.system.service.DataService; +import com.canvas.web.modules.system.service.dto.UserDto; +import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.stereotype.Service; + +import java.util.List; + + +@Service +@RequiredArgsConstructor +@CacheConfig(cacheNames = "data") +public class DataServiceImpl implements DataService { + @Override + public List getDeptIds(UserDto user) { + return null; + } +} diff --git a/system/src/main/java/com/canvas/web/modules/system/service/impl/RoleServiceImpl.java b/system/src/main/java/com/canvas/web/modules/system/service/impl/RoleServiceImpl.java new file mode 100644 index 0000000..52c9e2b --- /dev/null +++ b/system/src/main/java/com/canvas/web/modules/system/service/impl/RoleServiceImpl.java @@ -0,0 +1,20 @@ +package com.canvas.web.modules.system.service.impl; + +import com.canvas.web.modules.system.service.RoleService; +import com.canvas.web.modules.system.service.dto.UserDto; +import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@CacheConfig(cacheNames = "role") +public class RoleServiceImpl implements RoleService { + @Override + public List mapToGrantedAuthorities(UserDto user) { + return null; + } +} diff --git a/system/src/main/java/com/canvas/web/modules/system/service/impl/UserServiceImpl.java b/system/src/main/java/com/canvas/web/modules/system/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..408ecf4 --- /dev/null +++ b/system/src/main/java/com/canvas/web/modules/system/service/impl/UserServiceImpl.java @@ -0,0 +1,66 @@ +package com.canvas.web.modules.system.service.impl; + +import com.canvas.web.modules.system.service.UserService; +import com.canvas.web.modules.system.service.dto.UserDto; +import com.canvas.web.modules.system.service.dto.UserQueryCriteria; +import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Service +@RequiredArgsConstructor +@CacheConfig(cacheNames = "user") +public class UserServiceImpl implements UserService{ + @Override + public UserDto findById(long id) { + return null; + } + + @Override + public void delete(Set ids) { + + } + + @Override + public UserDto findByName(String userName) { + return null; + } + + @Override + public void updatePass(String username, String encryptPassword) { + + } + + @Override + public Map updateAvatar(MultipartFile file) { + return null; + } + + @Override + public void updateEmail(String username, String email) { + + } + + @Override + public Object queryAll(UserQueryCriteria criteria, Pageable pageable) { + return null; + } + + @Override + public List queryAll(UserQueryCriteria criteria) { + return null; + } + + @Override + public void download(List queryAll, HttpServletResponse response) throws IOException { + + } +} diff --git a/system/src/main/resources/config/application-prod.yml b/system/src/main/resources/config/application-prod.yml index e69de29..282d253 100644 --- a/system/src/main/resources/config/application-prod.yml +++ b/system/src/main/resources/config/application-prod.yml @@ -0,0 +1,122 @@ +#配置数据源 +spring: + datasource: + druid: + db-type: com.alibaba.druid.pool.DruidDataSource + driverClassName: net.sf.log4jdbc.sql.jdbcapi.DriverSpy + url: jdbc:log4jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:yxk_canvasscreen}?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false + username: ${DB_USER:root} + password: ${DB_PWD:ftzn83560792} + # 初始连接数 + initial-size: 5 + # 最小连接数 + min-idle: 10 + # 最大连接数 + max-active: 20 + # 获取连接超时时间 + max-wait: 5000 + # 连接有效性检测时间 + time-between-eviction-runs-millis: 60000 + # 连接在池中最小生存的时间 + min-evictable-idle-time-millis: 300000 + # 连接在池中最大生存的时间 + max-evictable-idle-time-millis: 900000 + test-while-idle: true + test-on-borrow: false + test-on-return: false + # 检测连接是否有效 + validation-query: select 1 + # 配置监控统计 + webStatFilter: + enabled: true + stat-view-servlet: + enabled: true + url-pattern: /druid/* + reset-enable: false + login-username: admin + login-password: 123456 + filter: + stat: + enabled: true + # 记录慢SQL + log-slow-sql: true + slow-sql-millis: 1000 + merge-sql: true + wall: + config: + multi-statement-allow: true + +# 登录相关配置 +login: + # 登录缓存 + cache-enable: true + # 是否限制单用户登录 + single-login: false + # 验证码 + login-code: + # 验证码类型配置 查看 LoginProperties 类 + code-type: arithmetic + # 登录图形验证码有效时间/分钟 + expiration: 2 + # 验证码高度 + width: 111 + # 验证码宽度 + heigth: 36 + # 内容长度 + length: 2 + # 字体名称,为空则使用默认字体,如遇到线上乱码,设置其他字体即可 + font-name: + # 字体大小 + font-size: 25 + +#jwt +jwt: + header: Authorization + # 令牌前缀 + token-start-with: Bearer + # 必须使用最少88位的Base64对该令牌进行编码 + base64-secret: ZmQ0ZGI5NjQ0MDQwY2I4MjMxY2Y3ZmI3MjdhN2ZmMjNhODViOTg1ZGE0NTBjMGM4NDA5NzYxMjdjOWMwYWRmZTBlZjlhNGY3ZTg4Y2U3YTE1ODVkZDU5Y2Y3OGYwZWE1NzUzNWQ2YjFjZDc0NGMxZWU2MmQ3MjY1NzJmNTE0MzI= + # 令牌过期时间 此处单位/毫秒 ,默认2小时,可在此网站生成 https://www.convertworld.com/zh-hans/time/milliseconds.html + token-validity-in-seconds: 7200000 + # 在线用户key + online-key: online-token- + # 验证码 + code-key: code-key- + # token 续期检查时间范围(默认30分钟,单位默认毫秒),在token即将过期的一段时间内用户操作了,则给用户的token续期 + detect: 1800000 + # 续期时间范围,默认 1小时,这里单位毫秒 + renew: 3600000 + +# IP 本地解析 +ip: + local-parsing: false + +#是否允许生成代码,生产环境设置为false +generator: + enabled: false + +#如果生产环境要开启swagger,需要配置请求地址 +#springfox: +# documentation: +# swagger: +# v2: +# host: # 接口域名或外网ip + +#是否开启 swagger-ui +swagger: + enabled: true + +# 文件存储路径 +file: + mac: + path: ~/file/ + avatar: ~/avatar/ + linux: + path: /home/www/yxkadmin/file/ + avatar: /home/www/yxkadmin/avatar/ + windows: + path: D:\yxkdmin\file\ + avatar: D:\yxkadmin\avatar\ + # 文件大小 /M + maxSize: 100 + avatarMaxSize: 5 \ No newline at end of file diff --git a/system/src/main/resources/generator.properties b/system/src/main/resources/generator.properties new file mode 100644 index 0000000..2ed9370 --- /dev/null +++ b/system/src/main/resources/generator.properties @@ -0,0 +1,27 @@ +#数据库类型转Java类型 +tinyint=Integer +smallint=Integer +mediumint=Integer +int=Integer +integer=Integer + +bigint=Long + +float=Float + +double=Double + +decimal=BigDecimal + +bit=Boolean + +char=String +varchar=String +tinytext=String +text=String +mediumtext=String +longtext=String + +date=Timestamp +datetime=Timestamp +timestamp=Timestamp \ No newline at end of file diff --git a/system/src/main/resources/ip2region/ip2region.db b/system/src/main/resources/ip2region/ip2region.db new file mode 100644 index 0000000..43e1daf Binary files /dev/null and b/system/src/main/resources/ip2region/ip2region.db differ diff --git a/system/src/main/resources/log4jdbc.log4j2.properties b/system/src/main/resources/log4jdbc.log4j2.properties new file mode 100644 index 0000000..302525f --- /dev/null +++ b/system/src/main/resources/log4jdbc.log4j2.properties @@ -0,0 +1,4 @@ +# If you use SLF4J. First, you need to tell log4jdbc-log4j2 that you want to use the SLF4J logger +log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator +log4jdbc.auto.load.popular.drivers=false +log4jdbc.drivers=com.mysql.cj.jdbc.Driver \ No newline at end of file diff --git a/system/src/main/resources/logback.xml b/system/src/main/resources/logback.xml new file mode 100644 index 0000000..37dc8d9 --- /dev/null +++ b/system/src/main/resources/logback.xml @@ -0,0 +1,45 @@ + + + yxkAdmin + + + + + + + ${log.pattern} + ${log.charset} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file