diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml index 3c1a591f..6a726227 100644 --- a/ruoyi-admin/pom.xml +++ b/ruoyi-admin/pom.xml @@ -53,6 +53,12 @@ com.ruoyi ruoyi-system + + + forest-core + com.dtflys.forest + + diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/service/impl/SysPushServiceImpl.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/service/impl/SysPushServiceImpl.java index cabadc9b..4daf28bf 100644 --- a/ruoyi-cai/src/main/java/com/ruoyi/cai/service/impl/SysPushServiceImpl.java +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/service/impl/SysPushServiceImpl.java @@ -294,7 +294,7 @@ public class SysPushServiceImpl extends ServiceImpl impl if(imResp != null && imResp.isSuccess()){ sysPushLog.setStatus(SystemPushLogStatusEnum.SEND_SUCCESS.getCode()); }else{ - sysPushLog.setStatus(SystemPushLogStatusEnum.SEND_SUCCESS.getCode()); + sysPushLog.setStatus(SystemPushLogStatusEnum.SEND_FAIL.getCode()); } sysPushLog.setResult(JSON.toJSONString(imResp)); }catch (Exception e){ diff --git a/ruoyi-sms/pom.xml b/ruoyi-sms/pom.xml index 0592d421..2d024a24 100644 --- a/ruoyi-sms/pom.xml +++ b/ruoyi-sms/pom.xml @@ -23,17 +23,17 @@ ruoyi-common - + + <!– 排除京东短信内存在的fastjson等待作者后续修复 –> com.alibaba fastjson - + --> diff --git a/ruoyi-yunxin/src/main/java/com/ruoyi/yunxin/YunExecutor.java b/ruoyi-yunxin/src/main/java/com/ruoyi/yunxin/YunExecutor.java index 501cb9c7..eb697f90 100644 --- a/ruoyi-yunxin/src/main/java/com/ruoyi/yunxin/YunExecutor.java +++ b/ruoyi-yunxin/src/main/java/com/ruoyi/yunxin/YunExecutor.java @@ -17,15 +17,14 @@ public class YunExecutor { public static Executor YUN_EXECUTOR; static { - ThreadPoolExecutor roomExecutor = new ThreadPoolExecutor(CPU_NUM, + YUN_EXECUTOR = new ThreadPoolExecutor(CPU_NUM, CPU_NUM << 2, 5, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), init("yunxinThreadPoll-%d"), new ThreadPoolExecutor.CallerRunsPolicy()); - YUN_EXECUTOR = TtlExecutors.getTtlExecutor(roomExecutor); - +// YUN_EXECUTOR = TtlExecutors.getTtlExecutor(roomExecutor); } private static ThreadFactory init(String nameFormat){ diff --git a/ruoyi-yunxin/src/main/java/com/ruoyi/yunxin/client/ImMessageClient.java b/ruoyi-yunxin/src/main/java/com/ruoyi/yunxin/client/ImMessageClient.java index 9c8794c3..7ae3700f 100644 --- a/ruoyi-yunxin/src/main/java/com/ruoyi/yunxin/client/ImMessageClient.java +++ b/ruoyi-yunxin/src/main/java/com/ruoyi/yunxin/client/ImMessageClient.java @@ -1,8 +1,7 @@ package com.ruoyi.yunxin.client; -import com.dtflys.forest.annotation.BaseRequest; -import com.dtflys.forest.annotation.Body; -import com.dtflys.forest.annotation.Post; +import com.dtflys.forest.annotation.*; +import com.ruoyi.yunxin.convertor.CustomFormConvertor; import com.ruoyi.yunxin.interceptor.GlodonTokenInterceptor; import com.ruoyi.yunxin.req.SendAttachMsgReq; import com.ruoyi.yunxin.req.SendBatchAttachMsgReq; @@ -21,6 +20,7 @@ public interface ImMessageClient { * @return */ @Post(url = "/nimserver/msg/sendMsg.action") + @BodyType(type = "form", encoder = CustomFormConvertor.class) YxDataR sendMsg(@Body SendMsgReq req); @Post(url = "/nimserver/msg/sendBatchMsg.action") diff --git a/ruoyi-yunxin/src/main/java/com/ruoyi/yunxin/convertor/CustomFormConvertor.java b/ruoyi-yunxin/src/main/java/com/ruoyi/yunxin/convertor/CustomFormConvertor.java new file mode 100644 index 00000000..a4bda868 --- /dev/null +++ b/ruoyi-yunxin/src/main/java/com/ruoyi/yunxin/convertor/CustomFormConvertor.java @@ -0,0 +1,254 @@ +package com.ruoyi.yunxin.convertor; + +import com.dtflys.forest.config.ForestConfiguration; +import com.dtflys.forest.converter.ConvertOptions; +import com.dtflys.forest.converter.ForestConverter; +import com.dtflys.forest.converter.ForestEncoder; +import com.dtflys.forest.converter.json.ForestJsonConverter; +import com.dtflys.forest.http.ForestBody; +import com.dtflys.forest.http.ForestRequest; +import com.dtflys.forest.http.ForestRequestBody; +import com.dtflys.forest.http.Lazy; +import com.dtflys.forest.http.body.SupportFormUrlEncoded; +import com.dtflys.forest.mapping.MappingParameter; +import com.dtflys.forest.mapping.MappingTemplate; +import com.dtflys.forest.utils.ForestDataType; +import com.dtflys.forest.utils.ReflectUtils; +import com.dtflys.forest.utils.RequestNameValue; +import com.dtflys.forest.utils.StringUtils; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Array; +import java.lang.reflect.Type; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.*; + +@Component +public class CustomFormConvertor implements ForestConverter, ForestEncoder { + + private final ForestConfiguration configuration; + + public CustomFormConvertor(ForestConfiguration configuration) { + this.configuration = configuration; + } + + + @Override + public T convertToJavaObject(String source, Type targetType) { + return null; + } + + @Override + public T convertToJavaObject(byte[] source, Class targetType, Charset charset) { + return null; + } + + @Override + public T convertToJavaObject(byte[] source, Type targetType, Charset charset) { + return null; + } + + @Override + public ForestDataType getDataType() { + return ForestDataType.FORM; + } + + @Override + public String encodeToString(Object obj) { + final ForestJsonConverter jsonConverter = configuration.getJsonConverter(); + final Map map = jsonConverter.convertObjectToMap(obj); + final List nameValueList = new LinkedList<>(); + for (Map.Entry entry : map.entrySet()) { + final RequestNameValue nameValue = new RequestNameValue(entry.getKey(), MappingParameter.TARGET_BODY); + nameValue.setValue(entry.getValue()); + nameValueList.add(nameValue); + } + final List newNameValueList = processFromNameValueList( + null, nameValueList, configuration, ConvertOptions.defaultOptions()); + return formUrlEncodedString(newNameValueList, StandardCharsets.UTF_8); + } + + /** + * 处理Form表单中的集合项 + * @param newNameValueList 键值对列表 + * @param configuration Forest配置 + * @param name 表单项目名 + * @param collection 集合对象 + * @param target 请求目标位置 + */ + protected void processFormCollectionItem(List newNameValueList, ForestConfiguration configuration, String name, Collection collection, int target) { + int index = 0; + for (Iterator iterator = collection.iterator(); iterator.hasNext(); ) { + final Object item = iterator.next(); + final String subName = name + "[" + index + "]"; + processFormItem(newNameValueList, configuration, subName, item, target); + index++; + } + } + + /** + * 处理Form表单中的数组项 + * @param newNameValueList 键值对列表 + * @param configuration Forest配置 + * @param name 表单项目名 + * @param array 数组 + * @param target 请求目标位置 + */ + protected void processFormArrayItem(List newNameValueList, ForestConfiguration configuration, String name, Object array, int target) { + final int len = Array.getLength(array); + for (int i = 0; i < len; i++) { + final Object item = Array.get(array, i); + final String subName = name + "[" + i + "]"; + processFormItem(newNameValueList, configuration, subName, item, target); + } + } + + /** + * 处理Form表单中的Map项 + * @param newNameValueList 键值对列表 + * @param configuration Forest配置 + * @param name 表单项目名 + * @param map Map对象 + * @param target 请求目标位置 + */ + protected void processFormMapItem(List newNameValueList, ForestConfiguration configuration, String name, Map map, int target) { + for (Iterator iterator = map.entrySet().iterator(); iterator.hasNext(); ) { + final Map.Entry entry = iterator.next(); + final Object mapKey = entry.getKey(); + final Object mapValue = entry.getValue(); + final String subName = name + "[" + mapKey + "]"; + processFormItem(newNameValueList, configuration, subName, mapValue, target); + } + } + + + /** + * 处理Form表单中的项 + * @param newNameValueList 键值对列表 + * @param configuration Forest配置 + * @param name 表单项目名 + * @param value 表单项目值 + * @param target 请求目标位置 + */ + protected void processFormItem(List newNameValueList, ForestConfiguration configuration, String name, Object value, int target) { + if (StringUtils.isEmpty(name) && value == null) { + return; + } + if (value != null) { + final Class itemClass = value.getClass(); + boolean needCollapse = false; + if (value instanceof Collection) { + final Collection collection = (Collection) value; + if (collection.size() <= 8) { + for (Object item : collection) { + if (!ReflectUtils.isPrimaryType(item.getClass())) { + needCollapse = true; + break; + } + } + } + } else if (itemClass.isArray() && !ReflectUtils.isPrimaryArrayType(itemClass)) { + needCollapse = true; + } + if (needCollapse) { + if (value instanceof Collection) { + processFormCollectionItem(newNameValueList, configuration, name, (Collection) value, target); + } else if (itemClass.isArray()) { + processFormArrayItem(newNameValueList, configuration, name, value, target); + } + } else if (ReflectUtils.isPrimaryType(itemClass) + || ReflectUtils.isPrimaryArrayType(itemClass) + || value instanceof Collection) { + newNameValueList.add(new RequestNameValue(name, value, target)); + } else if (value instanceof Map) { + processFormMapItem(newNameValueList, configuration, name, (Map) value, target); + } else { + Map itemAttrs = ReflectUtils.convertObjectToMap(value, configuration); + for (Map.Entry entry : itemAttrs.entrySet()) { + String subAttrName = entry.getKey(); + Object subAttrValue = entry.getValue(); + String subName = name + "." + subAttrName; + processFormItem(newNameValueList, configuration, subName, subAttrValue, target); + } + } + } + } + + + + /** + * 处理Form表单中的键值对列表 + * + * @param request 请求对象 + * @param nameValueList 键值对列表 + * @param configuration Forest 配置对象 + * @param options 转换选项 + * @return 处理过的新键值对列表 + */ + protected List processFromNameValueList( + final ForestRequest request, + final List nameValueList, + final ForestConfiguration configuration, + final ConvertOptions options) { + final List newNameValueList = new LinkedList<>(); + for (RequestNameValue nameValue : nameValueList) { + final String name = nameValue.getName(); + if (options != null && options.shouldExclude(name)) { + continue; + } + Object value = nameValue.getValue(); + if (Lazy.isEvaluatingLazyValue(value, request)) { + continue; + } + if (options != null) { + value = options.getValue(value, request); + if (options.shouldIgnore(value)) { + continue; + } + } + final int target = nameValue.getTarget(); + processFormItem(newNameValueList, configuration, name, value, target); + } + return newNameValueList; + } + + private String formUrlEncodedString(List nameValueList, Charset charset) { + final ForestJsonConverter jsonConverter = configuration.getJsonConverter(); + final StringBuilder strBuilder = new StringBuilder(); + for (int i = 0; i < nameValueList.size(); i++) { + final RequestNameValue nameValue = nameValueList.get(i); + if (!nameValue.isInBody()) { + continue; + } + final String name = nameValue.getName(); + strBuilder.append(name); + Object value = nameValue.getValue(); + if (value != null) { + value = MappingTemplate.getFormValueString(jsonConverter, value); + strBuilder.append("=").append(CustomURLEncoder.FORM_VALUE.encode(String.valueOf(value), charset)); + } + if (i < nameValueList.size() - 1) { + strBuilder.append("&"); + } + } + return strBuilder.toString(); + } + + @Override + public byte[] encodeRequestBody(final ForestBody body, final Charset charset, final ConvertOptions options) { + final List nameValueList = new LinkedList<>(); + final Charset cs = charset != null ? charset : StandardCharsets.UTF_8; + final ForestRequest request = body.getRequest(); + for (ForestRequestBody bodyItem : body) { + if (bodyItem instanceof SupportFormUrlEncoded) { + nameValueList.addAll(((SupportFormUrlEncoded) bodyItem).getNameValueList(request)); + } + } + final List newNameValueList = + processFromNameValueList(request, nameValueList, configuration, options); + String strBody = formUrlEncodedString(newNameValueList, cs); + byte[] bytes = strBody.getBytes(cs); + return bytes; + } +} diff --git a/ruoyi-yunxin/src/main/java/com/ruoyi/yunxin/convertor/CustomURLEncoder.java b/ruoyi-yunxin/src/main/java/com/ruoyi/yunxin/convertor/CustomURLEncoder.java new file mode 100644 index 00000000..cd94c281 --- /dev/null +++ b/ruoyi-yunxin/src/main/java/com/ruoyi/yunxin/convertor/CustomURLEncoder.java @@ -0,0 +1,286 @@ +package com.ruoyi.yunxin.convertor; + +import com.dtflys.forest.exceptions.ForestRuntimeException; +import com.dtflys.forest.utils.StringUtils; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.BitSet; + +public class CustomURLEncoder { + + /** + * 空格字符 + */ + private static final char SPACE = ' '; + + /** + * 十六进制字符的大写字符数组 + */ + private static final char[] HEX_DIGITS_UPPER = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + + /** + * URI用户信息部分中不会被编码的字符集 + */ + private static final char[] USER_INFO_EXCLUDED_CHARACTERS = {'-', '.', '_', '+', '!', '(', ')', '*', ':', '=', '%'}; + + /** + * URI路径中不会被编码的字符集 + */ + private static final char[] PATH_EXCLUDED_CHARACTERS = {'-', '.', '_', '+', '!', '(', ')', '[', ']', '*', '/', ':', '?', '=', '$', '@', '&', '%', '~'}; + + /** + * 查询参数值中不会被编码的字符集 + */ + private static final char[] QUERY_VALUE_EXCLUDED_CHARACTERS = {'-', '.', '_', '+', '!', '(', ')', '[', ']', ',', '*', '/', ':', '?', '=', '%', '~'}; + + /** + * (带不转义大括号的) 查询参数值中不会被编码的字符集 + */ + private static final char[] QUERY_VALUE_EXCLUDED_CHARACTERS_WITH_BRACE = {'-', '.', '_', '+', '!', '{', '}', '(', ')', '[', ']', ',', '*', '/', ':', '?', '=', '%', '~'}; + + + /** + * 查询参数值中不会被编码的字符集 + */ + private static final char[] X_WWW_FORM_URLENCODED_VALUE_EXCLUDED_CHARACTERS = {'-', '.', '_', '!', '{', '}', '[', ']', ',', '"', '*', '/', ':', '?', '#', '='}; + + /** + * 强制全编码中不会被编码的字符集 + */ + private static final char[] ALL_EXCLUDED_CHARACTERS = {'*', '-', '.', '_'}; + + /** + * 用于用户验证信息的编码{@link CustomURLEncoder} + */ + public static final CustomURLEncoder USER_INFO = createUserInfoUrlEncoder(); + + /** + * 用于URI路径部分的编码{@link CustomURLEncoder} + */ + public static final CustomURLEncoder PATH = createPathUrlEncoder(); + + /** + * 用于查询参数值部分的编码{@link CustomURLEncoder} + */ + public static final CustomURLEncoder QUERY_VALUE = createQueryValueUrlEncoder(); + + /** + * 用于 (带不转义大括号的) 查询参数值部分的编码{@link CustomURLEncoder} + */ + public static final CustomURLEncoder QUERY_VALUE_WITH_BRACE = createQueryValueWithBraceUrlEncoder(); + + + /** + * 用于表单参数值部分的编码{@link CustomURLEncoder} + */ + public static final CustomURLEncoder FORM_VALUE = createXWwwFormUrlEncodedValueUrlEncoder(); + + + /** + * 强制全编码的编码{@link CustomURLEncoder} + */ + public static final CustomURLEncoder ALL = createAllUrlEncoder(); + + + private static CustomURLEncoder createURLEncoder(final char[] excludedCharacters, final boolean encodeSpaceAsPlus) { + final CustomURLEncoder encoder = new CustomURLEncoder(); + encoder.setEncodeSpaceAsPlus(encodeSpaceAsPlus); + final int len = excludedCharacters.length; + for (int i = 0; i < len; i++) { + final char ch = excludedCharacters[i]; + encoder.excludeCharacter(ch); + } + return encoder; + } + + /** + * 创建用于用户验证信息编码的{@link CustomURLEncoder} + * + * @return {@link CustomURLEncoder}实例 + */ + public static CustomURLEncoder createUserInfoUrlEncoder() { + return createURLEncoder(USER_INFO_EXCLUDED_CHARACTERS, false); + } + + + /** + * 创建用于URI路径编码的{@link CustomURLEncoder} + * + * @return {@link CustomURLEncoder}实例 + */ + public static CustomURLEncoder createPathUrlEncoder() { + return createURLEncoder(PATH_EXCLUDED_CHARACTERS, false); + } + + /** + * 创建用于查询参数值编码的{@link CustomURLEncoder} + * + * @return {@link CustomURLEncoder}实例 + */ + public static CustomURLEncoder createQueryValueUrlEncoder() { + return createURLEncoder(QUERY_VALUE_EXCLUDED_CHARACTERS, false); + } + + /** + * 创建用于 (带不转义大括号的) 查询参数值编码的{@link CustomURLEncoder} + * + * @return {@link CustomURLEncoder}实例 + */ + public static CustomURLEncoder createQueryValueWithBraceUrlEncoder() { + return createURLEncoder(QUERY_VALUE_EXCLUDED_CHARACTERS_WITH_BRACE, false); + } + + + /** + * 创建用于表单参数值编码的{@link CustomURLEncoder} + * + * @return {@link CustomURLEncoder}实例 + */ + public static CustomURLEncoder createXWwwFormUrlEncodedValueUrlEncoder() { + return createURLEncoder(X_WWW_FORM_URLENCODED_VALUE_EXCLUDED_CHARACTERS, false); + } + + + /** + * 创建用于强制编码的{@link CustomURLEncoder} + * + * @return {@link CustomURLEncoder}实例 + */ + public static CustomURLEncoder createAllUrlEncoder() { + return createURLEncoder(ALL_EXCLUDED_CHARACTERS, false); + } + + + /** + * 安全编码字符集 + */ + private final BitSet excludedCharacters; + + /** + * 是否将空格编码为+ + */ + private boolean encodeSpaceAsPlus = false; + + /** + * {@link CustomURLEncoder}构造函数 + */ + public CustomURLEncoder() { + this(new BitSet(256)); + // 排除所有小写英文字母 + for (char i = 'a'; i <= 'z'; i++) { + excludeCharacter(i); + } + // 排除所有大写英文字母 + for (char i = 'A'; i <= 'Z'; i++) { + excludeCharacter(i); + } + // 排除所有数字 + for (char i = '0'; i <= '9'; i++) { + excludeCharacter(i); + } + } + + /** + * URLEncoder构造函数 + * + * @param excludedCharacters 安全字符,安全字符不被编码 + */ + private CustomURLEncoder(BitSet excludedCharacters) { + this.excludedCharacters = excludedCharacters; + } + + /** + * 排除不被不被编码的字符 + * + * @param c 字符 + */ + public void excludeCharacter(char c) { + excludedCharacters.set(c); + } + + /** + * 是否将空格编码为+ + * + * @param encodeSpaceAsPlus 是否将空格编码为+ + */ + public void setEncodeSpaceAsPlus(boolean encodeSpaceAsPlus) { + this.encodeSpaceAsPlus = encodeSpaceAsPlus; + } + + public String encode(final String path, final String charset) { + if (path == null) { + return null; + } + try { + final Charset cs = StringUtils.isEmpty(charset) ? + StandardCharsets.UTF_8 : Charset.forName(charset); + return encode(path, cs); + } catch (Throwable th) { + throw new ForestRuntimeException(th); + } + } + + private boolean isURLEncoded(final char[] charArray, final int index) { + if (charArray[index] != '%') { + return false; + } + final int len = charArray.length; + if (index + 2 < len) { + final char ch1 = charArray[index + 1]; + final char ch2 = charArray[index + 2]; + return Character.isDigit(ch1) && Character.isDigit(ch2); + } + return false; + } + + + /** + * 将URL中的字符串编码为%形式 + * + * @param path 需要编码的字符串 + * @param charset 编码 + * @return 编码后的字符串 + */ + public String encode(final String path, final Charset charset) { + final StringBuilder builder = new StringBuilder(path.length()); + final ByteArrayOutputStream buf = new ByteArrayOutputStream(); + final OutputStreamWriter writer = new OutputStreamWriter(buf, charset); + final char[] charArray = path.toCharArray(); + final int len = charArray.length; + for (int i = 0; i < len; i++) { + final char ch = charArray[i]; + if (isURLEncoded(charArray, i)) { + builder.append(ch); + } else if (excludedCharacters.get(ch)) { + builder.append(ch); + } else if (encodeSpaceAsPlus && ch == SPACE) { + // 处理空格为加号+ + builder.append('+'); + } else { + try { + writer.write((char) ch); + writer.flush(); + } catch (IOException e) { + buf.reset(); + continue; + } + + final byte[] ba = buf.toByteArray(); + for (byte toEncode : ba) { + builder.append('%'); + final int high = (toEncode & 0xf0) >>> 4;//高位 + final int low = toEncode & 0x0f;//低位 + builder.append(HEX_DIGITS_UPPER[high]); + builder.append(HEX_DIGITS_UPPER[low]); + } + buf.reset(); + } + } + return builder.toString(); + } + +}