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 转义用户输入, 避免把消息里的