diff --git a/core/src/main/java/info/xiaomo/core/utils/CastUtil.java b/core/src/main/java/info/xiaomo/core/utils/CastUtil.java index e3bf4938..f0f5b514 100644 --- a/core/src/main/java/info/xiaomo/core/utils/CastUtil.java +++ b/core/src/main/java/info/xiaomo/core/utils/CastUtil.java @@ -1,6 +1,7 @@ package info.xiaomo.core.utils; import java.io.*; +import java.math.BigDecimal; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.HashMap; @@ -74,49 +75,31 @@ public static int toInteger(String str) { } public static int isNumeric(String str) { - if (str == null) { + if (str == null || str.isEmpty()) { return 0; - } else { - boolean isdouble = false; - boolean hasE = false; - int i = str.length(); - - while (true) { - while (true) { - char c; - do { - --i; - if (i < 0) { - if (isdouble) { - return 2; - } - - return 1; - } - - c = str.charAt(i); - } while (i == 0 && c == 45); - - if (c == 46) { - if (isdouble) { - return 0; - } - - isdouble = true; - } else if (c != 69 && c != 101) { - if (!Character.isDigit(str.charAt(i))) { - return 0; - } - } else { - if (hasE) { - return 0; - } - - hasE = true; - } - } + } + String check = str; + if (check.charAt(0) == '-') { + // 允许唯一的负号前缀(负整数), 与旧实现一致; "-" 单独出现视为非数字 + check = check.substring(1); + if (check.isEmpty()) { + return 0; + } + } else if (check.charAt(0) == '+') { + return 0; + } + try { + // 用 BigDecimal 严格校验, "1.2.3" 之类非法数字返回 0, 不再抛运行期异常 + new BigDecimal(check); + } catch (NumberFormatException e) { + return 0; + } + for (int i = 0; i < check.length(); i++) { + if (!Character.isDigit(check.charAt(i))) { + return 2; } } + return 1; } public static HashMap copyMap(HashMap map) { diff --git a/core/src/main/java/info/xiaomo/core/utils/ExcelUtil.java b/core/src/main/java/info/xiaomo/core/utils/ExcelUtil.java index 8d4fc80b..ca84130f 100644 --- a/core/src/main/java/info/xiaomo/core/utils/ExcelUtil.java +++ b/core/src/main/java/info/xiaomo/core/utils/ExcelUtil.java @@ -102,11 +102,11 @@ private static String[][] getData(File file, int ignoreRows) } break; case FORMULA: - // 导入时如果为公式生成的数据则无值 - if (!"".equals(cell.getStringCellValue())) { + // 公式单元格可能是字符串或数值, 分别取值, 避免类型不匹配时抛异常 + try { value = cell.getStringCellValue(); - } else { - value = cell.getNumericCellValue() + ""; + } catch (IllegalStateException e) { + value = String.valueOf(cell.getNumericCellValue()); } break; case BLANK: diff --git a/core/src/main/java/info/xiaomo/core/utils/FileUtil.java b/core/src/main/java/info/xiaomo/core/utils/FileUtil.java index 0e8e965c..0d50087a 100644 --- a/core/src/main/java/info/xiaomo/core/utils/FileUtil.java +++ b/core/src/main/java/info/xiaomo/core/utils/FileUtil.java @@ -856,11 +856,11 @@ private static String sanitizeNamePart(String part) { public static boolean isImage(String imageName) { String fileType = FileUtil.getFileType(imageName); - return !("bmp".equals(fileType) || "BMP".equals(fileType) - || "jpg".equals(fileType) || "JPG".equals(fileType) - || "jpeg".equals(fileType) || "JPEG".equals(fileType) - || "git".equals(fileType) || "GIF".equals(fileType) - || "png".equals(fileType) || "PNG".equals(fileType)); + return "bmp".equalsIgnoreCase(fileType) + || "jpg".equalsIgnoreCase(fileType) + || "jpeg".equalsIgnoreCase(fileType) + || "gif".equalsIgnoreCase(fileType) + || "png".equalsIgnoreCase(fileType); } } diff --git a/core/src/main/java/info/xiaomo/core/utils/IDUtil.java b/core/src/main/java/info/xiaomo/core/utils/IDUtil.java index 7e12cfa2..66744220 100644 --- a/core/src/main/java/info/xiaomo/core/utils/IDUtil.java +++ b/core/src/main/java/info/xiaomo/core/utils/IDUtil.java @@ -25,7 +25,10 @@ public static void main(String[] args) { } /** - * 获取唯一一个id + * 获取唯一一个id。 + * + *

注意: 该实现只在单个 JVM 进程内(秒内 65000 个)保证单调不回退, 并非跨进程、 + * 跨重启全局唯一的 snowflake 方案, 不适用于分布式主键场景。 * * @return long */ diff --git a/core/src/main/java/info/xiaomo/core/utils/MailUtil.java b/core/src/main/java/info/xiaomo/core/utils/MailUtil.java index bd91d7d5..7b2ef621 100644 --- a/core/src/main/java/info/xiaomo/core/utils/MailUtil.java +++ b/core/src/main/java/info/xiaomo/core/utils/MailUtil.java @@ -3,9 +3,10 @@ import jakarta.mail.*; import jakarta.mail.internet.InternetAddress; import jakarta.mail.internet.MimeMessage; -import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; import java.util.Date; +import java.util.Objects; import java.util.Properties; /** @@ -31,11 +32,15 @@ public class MailUtil { */ private static Session getSession() throws IOException { Properties props = new Properties(); - String dir = System.getProperty("user.dir"); - FileInputStream is = new FileInputStream(dir + "/website/src/main/resources/config/application.properties"); - props.load(is); - USERNAME = String.valueOf(props.get("mail.username")); - PASSWORD = String.valueOf(props.get("mail.password")); + // 从 classpath 读取配置, 不再依赖进程工作目录(user.dir)下的绝对路径 + try (InputStream is = MailUtil.class.getClassLoader().getResourceAsStream("config/application.properties")) { + if (is == null) { + throw new IOException("未找到 classpath 下的 config/application.properties"); + } + props.load(is); + } + USERNAME = Objects.toString(props.get("mail.username"), ""); + PASSWORD = Objects.toString(props.get("mail.password"), ""); Authenticator authenticator = new Authenticator() { @Override protected PasswordAuthentication getPasswordAuthentication() { @@ -61,34 +66,4 @@ public static void send(String toEmail, String subject, String content) { mex.printStackTrace(); } } - - /** - * 返回激活链接 - * - * @param email email - * @return 有3个参数 email password time - */ - public static String redirectValidateUrl(String email, String password) { - Long now = TimeUtil.getNowOfMills(); - StringBuilder sb = new StringBuilder("点击下面链接激活账号,48小时生效,否则重新注册账号,链接只能使用一次,请尽快激活!
"); - sb.append(""); - sb.append("http://localhost:8080/user/validate?email="); - sb.append(email); - sb.append("&password="); - sb.append(password); - sb.append("&time="); - sb.append(now); - sb.append("
"); - sb.append("小莫
"); - sb.append(""); - sb.append(TimeUtil.getFormatDate()); - sb.append("
"); - return sb.toString(); - } } diff --git a/core/src/main/java/info/xiaomo/core/utils/Md5Util.java b/core/src/main/java/info/xiaomo/core/utils/Md5Util.java index 309ab9ec..f1554c33 100644 --- a/core/src/main/java/info/xiaomo/core/utils/Md5Util.java +++ b/core/src/main/java/info/xiaomo/core/utils/Md5Util.java @@ -1,5 +1,6 @@ package info.xiaomo.core.utils; +import java.nio.charset.StandardCharsets; import java.security.MessageDigest; /** @@ -70,7 +71,7 @@ public static String encode(String password, String salt) { try { resultString = password + salt; MessageDigest md = MessageDigest.getInstance("md5"); - resultString = byteArrayToString(md.digest(resultString.getBytes())); + resultString = byteArrayToString(md.digest(resultString.getBytes(StandardCharsets.UTF_8))); } catch (Exception ex) { ex.printStackTrace(); } diff --git a/core/src/main/java/info/xiaomo/core/utils/RegExUtil.java b/core/src/main/java/info/xiaomo/core/utils/RegExUtil.java index 5f24b994..da9e13ad 100644 --- a/core/src/main/java/info/xiaomo/core/utils/RegExUtil.java +++ b/core/src/main/java/info/xiaomo/core/utils/RegExUtil.java @@ -2,6 +2,8 @@ import org.apache.commons.lang3.StringUtils; +import java.util.ArrayList; +import java.util.List; import java.util.Vector; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -167,13 +169,11 @@ public static Vector splitTags2Vector(String pattern, String str) throws public static String[] splitTags(String pattern, String str) { Pattern p = Pattern.compile(pattern); Matcher m = p.matcher(str); - String[] array = new String[m.groupCount()]; - int i = 0; + List array = new ArrayList<>(); while (m.find()) { - array[i] = eregReplace("(\\[\\#)|(\\#\\])", "", m.group()); - i++; + array.add(eregReplace("(\\[\\#)|(\\#\\])", "", m.group())); } - return array; + return array.toArray(new String[0]); } @@ -206,13 +206,11 @@ public static Vector regMatchAll2Vector(String pattern, String str) thro public static String[] regMatchAll2Array(String pattern, String str) throws PatternSyntaxException { Pattern p = Pattern.compile(pattern); Matcher m = p.matcher(str); - String[] array = new String[m.groupCount()]; - int i = 0; + List array = new ArrayList<>(); while (m.find()) { - array[i] = m.group(); - i++; + array.add(m.group()); } - return array; + return array.toArray(new String[0]); } /** diff --git a/core/src/main/java/info/xiaomo/core/utils/SerializeUtil.java b/core/src/main/java/info/xiaomo/core/utils/SerializeUtil.java index 3dcac2de..db2fe20e 100644 --- a/core/src/main/java/info/xiaomo/core/utils/SerializeUtil.java +++ b/core/src/main/java/info/xiaomo/core/utils/SerializeUtil.java @@ -41,7 +41,7 @@ public static String serialize(Object object) { oos = new ObjectOutputStream(baos); oos.writeObject(object); byte[] bytes = baos.toByteArray(); - return new String(bytes, StandardCharsets.UTF_8); + return new String(bytes, StandardCharsets.ISO_8859_1); } catch (Exception e) { e.printStackTrace(); } @@ -51,8 +51,8 @@ public static String serialize(Object object) { /** * 反序列化 */ - public static Object unserialize(String s) throws UnsupportedEncodingException { - byte[] bytes = s.getBytes(StandardCharsets.UTF_8); + public static Object unserialize(String s) { + byte[] bytes = s.getBytes(StandardCharsets.ISO_8859_1); ByteArrayInputStream bais; try { //反序列化 diff --git a/core/src/main/java/info/xiaomo/core/utils/SqlUtil.java b/core/src/main/java/info/xiaomo/core/utils/SqlUtil.java index 66e53e00..bbdb246d 100644 --- a/core/src/main/java/info/xiaomo/core/utils/SqlUtil.java +++ b/core/src/main/java/info/xiaomo/core/utils/SqlUtil.java @@ -53,6 +53,9 @@ private String delNewSQlString(String sql) { } private String delSQlString(String sql) { + if (sql == null || sql.isEmpty()) { + return "in()"; + } StringBuilder delSql = new StringBuilder("in("); StringTokenizer tokenizer = new StringTokenizer(sql, "|"); diff --git a/core/src/main/java/info/xiaomo/core/utils/StringUtil.java b/core/src/main/java/info/xiaomo/core/utils/StringUtil.java index 78f27dd1..e616106c 100644 --- a/core/src/main/java/info/xiaomo/core/utils/StringUtil.java +++ b/core/src/main/java/info/xiaomo/core/utils/StringUtil.java @@ -194,8 +194,10 @@ public static String replace(String s, Map map) { char ch = '}'; for (int start, end; (start = s.indexOf(str, cursor)) != -1 && (end = s.indexOf(ch, start)) != -1; ) { sb.append(s.substring(cursor, start)); - String key = s.substring(start + 2, end); - sb.append(map.get(StringUtils.trim(key))); + String key = StringUtils.trim(s.substring(start + 2, end)); + String value = map.get(key); + // 缺失的 key 原样保留占位符, 而不是拼出字面量 "null" + sb.append(value != null ? value : str + key + ch); cursor = end + 1; } sb.append(s.substring(cursor, s.length())); @@ -209,32 +211,37 @@ public static String replace(String s, Map map) { * @return ip 如果返回null,说明是一个不合法的ip地址格式 */ public static String getIP(HttpServletRequest request) { - String ip = request.getHeader("X-Requested-For"); - String unknown = "unknown"; - if (StringUtils.isBlank(ip) || unknown.equalsIgnoreCase(ip)) { - ip = request.getHeader("X-Forwarded-For"); - } - if (StringUtils.isBlank(ip) || unknown.equalsIgnoreCase(ip)) { - ip = request.getHeader("Proxy-Client-IP"); - } - if (StringUtils.isBlank(ip) || unknown.equalsIgnoreCase(ip)) { - ip = request.getHeader("WL-Proxy-Client-IP"); - } - if (StringUtils.isBlank(ip) || unknown.equalsIgnoreCase(ip)) { - ip = request.getHeader("HTTP_CLIENT_IP"); - } - if (StringUtils.isBlank(ip) || unknown.equalsIgnoreCase(ip)) { - ip = request.getHeader("HTTP_X_FORWARDED_FOR"); - } - if (StringUtils.isBlank(ip) || unknown.equalsIgnoreCase(ip)) { + // 反代场景下这些头可能是逗号分隔的多级代理列表, 取第一段(真实客户端地址)并跳过 "unknown" + String ip = firstCandidate(request.getHeader("X-Forwarded-For"), + request.getHeader("X-Requested-For"), + request.getHeader("Proxy-Client-IP"), + request.getHeader("WL-Proxy-Client-IP"), + request.getHeader("HTTP_CLIENT_IP"), + request.getHeader("HTTP_X_FORWARDED_FOR")); + if (ip == null) { ip = request.getRemoteAddr(); } - if (!ip.matches(IP_REGEX)) { + if (ip == null || !ip.matches(IP_REGEX)) { return null; } return ip; } + private static String firstCandidate(String... headers) { + for (String header : headers) { + if (StringUtils.isBlank(header)) { + continue; + } + for (String part : header.split(",")) { + String candidate = part.trim(); + if (StringUtils.isNotBlank(candidate) && !"unknown".equalsIgnoreCase(candidate)) { + return candidate; + } + } + } + return null; + } + /** * 判断字符串是否为空,并删除首尾空格 * @@ -467,7 +474,7 @@ public static String format(String str, Object... params) { param = obj.toString(); } } - m.appendReplacement(sb, param); + m.appendReplacement(sb, Matcher.quoteReplacement(param)); } m.appendTail(sb); return sb.toString(); @@ -964,12 +971,25 @@ public static String decode(String s) { || ch == ')') { sbuf.append((char) ch); } else if (ch == '%') { + // 末尾残缺的 % (如 "abc", "abc%u123") 直接原样输出剩余部分, 不再越界访问 + if (i + 1 >= len) { + sbuf.append(s.substring(i)); + break; + } int cint = 0; if ('u' != s.charAt(i + 1)) { + if (i + 3 > len) { + sbuf.append(s.substring(i)); + break; + } cint = (cint << 4) | VAL[s.charAt(i + 1)]; cint = (cint << 4) | VAL[s.charAt(i + 2)]; i += 2; } else { + if (i + 6 > len) { + sbuf.append(s.substring(i)); + break; + } cint = (cint << 4) | VAL[s.charAt(i + 2)]; cint = (cint << 4) | VAL[s.charAt(i + 3)]; cint = (cint << 4) | VAL[s.charAt(i + 4)]; diff --git a/core/src/main/java/info/xiaomo/core/utils/TimeUtil.java b/core/src/main/java/info/xiaomo/core/utils/TimeUtil.java index aab097dc..86072eb4 100644 --- a/core/src/main/java/info/xiaomo/core/utils/TimeUtil.java +++ b/core/src/main/java/info/xiaomo/core/utils/TimeUtil.java @@ -53,7 +53,7 @@ public class TimeUtil { public static final String DATE = MONTH + SymbolConst.HENGXIAN + DAY; public static final String TIME_PATTERN = DATE_PATTERN_WITH_HENGXIAN + " HH:mm:ss"; - public static final String DATE_PATTERN = " HH:mm:ss"; + public static final String DATE_PATTERN = "HH:mm:ss"; public static final FastDateFormat DATE_FORMAT = FastDateFormat.getInstance("yyyy-MM-dd"); public static final String DATE_FORMAT_STRING = "yyyyMMddHHmmss"; @@ -743,51 +743,24 @@ public static long getTimeFromString(String timeStr, String format) { * 例如:1天20小时5分0秒,20小时0分0秒,1秒 */ public static String getLeftTimeString(long leftTime) { - StringBuilder sb = new StringBuilder(); - // 剩余秒数 - int leftSecond = (int) (leftTime / 1000); - // 秒数 - int second = leftSecond % 60; - if (second > 0) { - sb.insert(0, second + "秒"); - } - // 剩余分钟数 - int leftMinute = leftSecond / 60; - // 分钟数 - int minute = leftMinute % 60; - if (minute > 0) { - sb.insert(0, minute + "分"); - } - // 剩余小时 - int leftHour = leftMinute / 60; - int hour = leftHour % 24; - if (hour > 0) { - sb.insert(0, hour + "小时"); + if (leftTime <= 0) { + return "0秒"; } - // 剩余天数 - int leftDay = leftHour / 24; - if (leftDay > 0) { - sb.insert(0, leftDay + "天"); - } - // 获取剩余天数 - int day = (int) (leftTime / ONE_DAY_IN_MILLISECONDS); - // 1天及以上的显示剩余天 + long day = leftTime / ONE_DAY_IN_MILLISECONDS; + long hour = (leftTime % ONE_DAY_IN_MILLISECONDS) / ONE_HOUR_IN_MILLISECONDS; + long minute = (leftTime % ONE_HOUR_IN_MILLISECONDS) / ONE_MINUTE_IN_MILLISECONDS; + long second = (leftTime % ONE_MINUTE_IN_MILLISECONDS) / 1000; + StringBuilder sb = new StringBuilder(); if (day > 0) { sb.append(day).append("天"); - leftTime -= (day * ONE_DAY_IN_MILLISECONDS); } - hour = (int) (leftTime / ONE_HOUR_IN_MILLISECONDS); - // 1小时及以上或者前面显示了天数则后面需要小时 - if (hour > 0 || sb.length() > 0) { + if (day > 0 || hour > 0) { sb.append(hour).append("小时"); - leftTime -= (hour * ONE_HOUR_IN_MILLISECONDS); } - minute = (int) (leftTime / ONE_MINUTE_IN_MILLISECONDS); - if (minute > 0 || sb.length() > 0) { + if (day > 0 || hour > 0 || minute > 0) { sb.append(minute).append("分"); - leftTime -= (minute * ONE_MINUTE_IN_MILLISECONDS); } - sb.append(leftTime / 1000).append("秒"); + sb.append(second).append("秒"); return sb.toString(); } diff --git a/core/src/main/java/info/xiaomo/core/utils/TokenUtil.java b/core/src/main/java/info/xiaomo/core/utils/TokenUtil.java index 17a05852..8ce1f331 100644 --- a/core/src/main/java/info/xiaomo/core/utils/TokenUtil.java +++ b/core/src/main/java/info/xiaomo/core/utils/TokenUtil.java @@ -39,8 +39,10 @@ public static String getAccessToken(String string) { if (m2.find()) { accessToken = m2.group(1); } else { - String temp = string.split(SymbolConst.DENGHAO)[1]; - accessToken = temp.split(SymbolConst.AND)[0]; + String[] parts = string.split(SymbolConst.DENGHAO); + if (parts.length > 1) { + accessToken = parts[1].split(SymbolConst.AND)[0]; + } } } } diff --git a/core/src/test/java/info/xiaomo/core/utils/UtilsBugFixTest.java b/core/src/test/java/info/xiaomo/core/utils/UtilsBugFixTest.java new file mode 100644 index 00000000..5c56e3f9 --- /dev/null +++ b/core/src/test/java/info/xiaomo/core/utils/UtilsBugFixTest.java @@ -0,0 +1,112 @@ +package info.xiaomo.core.utils; + +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpServletRequest; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * 覆盖本次 bug 修复涉及的几个工具方法, 防止改回去。 + */ +class UtilsBugFixTest { + + @Test + void isImage应当识别常见图片扩展名而不误判() { + assertThat(FileUtil.isImage("avatar.jpg")).isTrue(); + assertThat(FileUtil.isImage("avatar.JPEG")).isTrue(); + assertThat(FileUtil.isImage("avatar.Png")).isTrue(); + assertThat(FileUtil.isImage("avatar.gif")).isTrue(); + assertThat(FileUtil.isImage("avatar.bmp")).isTrue(); + + assertThat(FileUtil.isImage("readme.txt")).isFalse(); + assertThat(FileUtil.isImage("video.mp4")).isFalse(); + } + + @Test + void 剩余天数小时应按既定格式输出() { + // 1天 + 20小时 + 5分 + 0秒 + assertThat(TimeUtil.getLeftTimeString(1L * 86400000 + 20 * 3600000 + 5 * 60000)) + .isEqualTo("1天20小时5分0秒"); + // 20小时 + 0分 + 0秒 + assertThat(TimeUtil.getLeftTimeString(20L * 3600000)).isEqualTo("20小时0分0秒"); + assertThat(TimeUtil.getLeftTimeString(1000)).isEqualTo("1秒"); + } + + @Test + void 剩余时间为零或负数时应当返回零秒() { + assertThat(TimeUtil.getLeftTimeString(0)).isEqualTo("0秒"); + assertThat(TimeUtil.getLeftTimeString(-1)).isEqualTo("0秒"); + } + + @Test + void 序列化应当能无损往返() { + String pole = SerializeUtil.serialize("中文abc123!@#"); + Object back = SerializeUtil.unserialize(pole); + + assertThat(back).isEqualTo("中文abc123!@#"); + } + + @Test + void 数值判断应当区分非数字整数与小数() { + assertThat(CastUtil.isNumeric("123")).isEqualTo(1); + // 负整数仍应视为整数 + assertThat(CastUtil.isNumeric("-5")).isEqualTo(1); + assertThat(CastUtil.isNumeric("3.14")).isEqualTo(2); + assertThat(CastUtil.isNumeric("1e3")).isEqualTo(2); + assertThat(CastUtil.isNumeric("2.147483647E9")).isEqualTo(2); + // 这些以前会抛 NumberFormatException/越界 + assertThat(CastUtil.isNumeric("1.2.3")).isEqualTo(0); + assertThat(CastUtil.isNumeric("abc")).isEqualTo(0); + assertThat(CastUtil.isNumeric(null)).isEqualTo(0); + assertThat(CastUtil.isNumeric("")).isEqualTo(0); + } + + @Test + void 正则全量匹配无可匹配时应当返回空数组() { + assertThat(RegExUtil.regMatchAll2Array("\\d+", "abc")).isEmpty(); + assertThat(RegExUtil.splitTags("\\[#\\w+#\\]", "没有标签")).isEmpty(); + } + + @Test + void 正则全量匹配应当返回所有匹配项() { + assertThat(RegExUtil.regMatchAll2Array("\\d+", "a1b22c")) + .containsExactly("1", "22"); + assertThat(RegExUtil.splitTags("\\[#\\w+#\\]", "[#tag1#] [#tag2#]")) + .containsExactly("tag1", "tag2"); + } + + @Test + void 取IP应当优先取转发头第一段并跳过unknown() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("X-Forwarded-For", "203.0.113.7, 10.0.0.1"); + assertThat(StringUtil.getIP(request)).isEqualTo("203.0.113.7"); + + MockHttpServletRequest request2 = new MockHttpServletRequest(); + request2.addHeader("X-Forwarded-For", "unknown, 203.0.113.9"); + request2.setRemoteAddr("192.168.1.5"); + assertThat(StringUtil.getIP(request2)).isEqualTo("203.0.113.9"); + } + + @Test + void 取IP无转发头时应当回退到远端地址() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRemoteAddr("192.168.1.5"); + assertThat(StringUtil.getIP(request)).isEqualTo("192.168.1.5"); + } + + @Test + void 解码尾部残缺百分号时不应越界() { + // 以前会 charAt(i+1) 越界抛异常 + assertThat(StringUtil.decode("abc%")).isEqualTo("abc%"); + assertThat(StringUtil.decode("a%20b")).isEqualTo("a b"); + // 残缺的 %u 转义(不足 4 位十六进制)原样保留, 不越界 + assertThat(StringUtil.decode("abc%u12A")).isEqualTo("abc%u12A"); + } + + @Test + void md5编码应当使用UTF8且稳定() { + // md5("") 的经典结果, 顺带锁定输出为 32 位小写 hex + assertThat(Md5Util.encode("", "")).isEqualTo("d41d8cd98f00b204e9800998ecf8427e"); + assertThat(Md5Util.encode("abc", "123")).hasSize(32).matches("[0-9a-f]{32}"); + } +} \ No newline at end of file diff --git a/crawler/src/main/java/info/xiaomo/crawler/spider/OnnmyoujiSpider.java b/crawler/src/main/java/info/xiaomo/crawler/spider/OnnmyoujiSpider.java index f3a2ac86..47eabef3 100644 --- a/crawler/src/main/java/info/xiaomo/crawler/spider/OnnmyoujiSpider.java +++ b/crawler/src/main/java/info/xiaomo/crawler/spider/OnnmyoujiSpider.java @@ -28,7 +28,9 @@ public static List getShikigamiModel() { List shikigamiDetailInfoUrl = getShikigamiDetailInfoUrl(); for (String url : shikigamiDetailInfoUrl) { ShikigamiModel shikigamiModel = getShikigami(url); - list.add(shikigamiModel); + if (shikigamiModel != null) { + list.add(shikigamiModel); + } } return list; } @@ -41,11 +43,16 @@ private static List getShikigamiDetailInfoUrl() { List list = new ArrayList<>(); String html = HttpUtil.get(URL); Document doc = Jsoup.parse(html); - Element select = doc.select(".heroList-2").get(0); - Elements liElement = select.select("a"); + Elements select = doc.select(".heroList-2"); + if (select.isEmpty()) { + return list; + } + Elements liElement = select.get(0).select("a"); for (Element element : liElement) { String href = element.attr("href"); - list.add(href); + if (href != null && !href.isEmpty()) { + list.add(href); + } } return list; } @@ -58,11 +65,16 @@ private static List getMitamaDetailInfoUrl() { List list = new ArrayList<>(); String html = HttpUtil.get(URL); Document doc = Jsoup.parse(html); - Element select = doc.select(".heroList-1").get(0); - Elements liElement = select.select("a"); + Elements select = doc.select(".heroList-1"); + if (select.isEmpty()) { + return list; + } + Elements liElement = select.get(0).select("a"); for (Element element : liElement) { String href = element.attr("href"); - list.add(href); + if (href != null && !href.isEmpty()) { + list.add(href); + } } return list; } @@ -72,20 +84,32 @@ private static List getMitamaDetailInfoUrl() { * 获取式神信息 */ private static ShikigamiModel getShikigami(String url) { - ShikigamiModel a = new ShikigamiModel(); - String html = HttpUtil.get(url); - Document doc = Jsoup.parse(html); - Elements selects = doc.select("table").get(1).select("tr").get(2).select("td"); - String seiyou = selects.get(0).text(); - String name = selects.get(1).text(); - String star = selects.get(2).text(); - String sex = selects.get(3).text(); - String level = selects.get(4).text(); - String getWay = selects.get(5).text(); - String image = doc.select("table").get(0).select("tr").get(0).select("img").attr("src"); - return new ShikigamiModel(name, image, seiyou, sex, star, getWay, level, ""); - - + try { + ShikigamiModel a = new ShikigamiModel(); + String html = HttpUtil.get(url); + Document doc = Jsoup.parse(html); + Elements tables = doc.select("table"); + if (tables.size() < 2) { + return null; + } + Elements trs = tables.get(1).select("tr"); + if (trs.size() < 3) { + return null; + } + Elements selects = trs.get(2).select("td"); + String seiyou = selects.size() > 0 ? selects.get(0).text() : ""; + String name = selects.size() > 1 ? selects.get(1).text() : ""; + String star = selects.size() > 2 ? selects.get(2).text() : ""; + String sex = selects.size() > 3 ? selects.get(3).text() : ""; + String level = selects.size() > 4 ? selects.get(4).text() : ""; + String getWay = selects.size() > 5 ? selects.get(5).text() : ""; + String image = tables.get(0).select("tr").isEmpty() + ? "" : tables.get(0).select("tr").get(0).select("img").attr("src"); + return new ShikigamiModel(name, image, seiyou, sex, star, getWay, level, ""); + } catch (Exception e) { + // 页面结构变化或网络异常时跳过该条, 交由调用方过滤 null + return null; + } } } diff --git a/socket/src/main/java/info/xiaomo/chat/socket/MyWebSocket.java b/socket/src/main/java/info/xiaomo/chat/socket/MyWebSocket.java index 92bb7e02..c5be0ff7 100644 --- a/socket/src/main/java/info/xiaomo/chat/socket/MyWebSocket.java +++ b/socket/src/main/java/info/xiaomo/chat/socket/MyWebSocket.java @@ -96,9 +96,10 @@ public void onClose() { @OnMessage public void onMessage(String message) throws IOException { String date = "" + TimeUtil.getDateNow(TimeUtil.DATE_PATTERN) + "
"; - // 群发消息 + // 群发前先 HTML 转义用户输入, 避免把消息里的