修复 Jackson 3 迁移引入的看图 500,并大幅补充单元测试

生产缺陷(2026-09-14 Spring Boot 4.1.1 / Jackson 2→3 迁移引入):
- GalleryUtil.parseImageKeys 解析 mpv 页 imagelist 时,行尾分号属 JS 语法,
  Jackson 3 默认 FAIL_ON_TRAILING_TOKENS=true 会抛 StreamReadException。
  改为先由 parseImagelist 剥掉 JS 语句外壳(前缀 + 行尾分号)再交给 JSON 解析。
  已用线上真实页面验证:修复前必抛异常,修复后正确解析出 154 页 imgkey。
- GalleryManageService.cacheImagesKey 只 catch IOException,而 Jackson 3 的
  JacksonException 继承 RuntimeException,异常穿透 Controller(无 @ControllerAdvice)
  表现为 HTTP 500。改为同时兜 JacksonException 并按业务失败返回。
  同时用 parseGidFromLink 拦住段数不足的畸形链接(原先会抛
  ArrayIndexOutOfBoundsException 变成 500)。

既有行为修正:
- PersonalService.deleteFile 原先依据 hutool FileUtil.del 的返回值报成功,
  而该函数对不存在的目标返回 true,导致路径写错也显示「删除成功」;
  改为先校验存在性,不存在则回「文件不存在」。
- PersonalService.getFiles 的 listFiles()==null 分支原先报「文件夹为空」,
  但该返回值只在 I/O 出错时出现,会掩盖权限/磁盘问题,改为「读取文件夹失败」。

测试:143 → 379 用例,指令覆盖率 36.2% → 80.2%(分支 34.6% → 69.8%)。
新增控制器(MockMvc standaloneSetup)、服务层、拦截器/过滤器、WebSocket、
配置装配与 MyBatis AOT hints 的测试;全部离线可跑,不加载 Spring 上下文、不连库、不占端口。
This commit is contained in:
root
2026-09-15 13:42:22 +08:00
parent a288846552
commit a0959618ca
25 changed files with 4609 additions and 7 deletions
@@ -11,6 +11,7 @@ import com.lion.lionwebsite.Util.ImageFileCache;
import java.nio.file.Path;
import com.lion.lionwebsite.Util.GalleryUtil;
import com.lion.lionwebsite.Util.Response;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@@ -371,7 +372,17 @@ public class GalleryManageService {
public String cacheImagesKey(String url) {
Response response = Response.generateResponse();
String gid = String.valueOf(GalleryUtil.parseGid(url));
// 畸形链接在这里就拦掉:段数不足会让 url.split("/")[5] 抛 ArrayIndexOutOfBoundsException,
// 而 parseGid 也会对无 /g/ 的链接返回 null,两者都不该变成 500。
String[] segments = url == null ? null : url.split("/");
Integer parsedGid = parseGidFromLink(url);
if (parsedGid == null || segments == null || segments.length <= 5) {
response.failure("链接错误");
return response.toJSONString();
}
String gid = String.valueOf(parsedGid);
GidToKey gidToKey = imageCacheMapper.selectKeyByGid(gid);
//已缓存过,直接返回
if(gidToKey != null) {
@@ -381,7 +392,7 @@ public class GalleryManageService {
try {
gidToKey = new GidToKey();
gidToKey.setGid(gid);
gidToKey.setKey(url.split("/")[5].strip());
gidToKey.setKey(segments[5].strip());
ArrayList<ImageKeyCache> imageKeyCaches = GalleryUtil.parseImageKeys(url);
if(imageKeyCaches == null)
return response.failure("该图片已下架或已被删除").toJSONString();
@@ -390,8 +401,10 @@ public class GalleryManageService {
for (ImageKeyCache imageKeyCache : imageKeyCaches)
imageCacheMapper.insertImageKeyCache(imageKeyCache);
response.success(objectMapper.valueToTree(gidToKey));
}catch (IOException e){
log.error(e.getMessage());
}catch (IOException | JacksonException e){
// Jackson 3 的解析异常继承 RuntimeException 而非 IOException,
// 只捕 IOException 会漏掉第三方页面格式变化,穿透成 500。
log.warn("缓存图片索引失败 gid={} errorType={}", gid, e.getClass().getSimpleName());
response.failure("网络波动或其他异常");
}
return response.toJSONString();
@@ -117,7 +117,9 @@ public class PersonalService{
response.success(new ObjectMapper().valueToTree(fileMaps).toString());
}
else
response.failure("文件夹为空");
// listFiles() 只在 I/O 出错时返回 null(可读空目录返回长度为 0 的数组),
// 报「文件夹为空」会掩盖权限/磁盘问题,这里如实提示。
response.failure("读取文件夹失败");
}
return response.toJSONString();
}
@@ -359,7 +361,11 @@ public class PersonalService{
path = URLDecoder.decode(path, StandardCharsets.UTF_8);
File file = new File(path);
if(FileUtil.del(file))
// hutool 的 FileUtil.del 对不存在的目标返回 true(幂等语义),
// 直接据此报成功会让「路径写错/文件已不存在」也回「删除成功」,前端误以为已删除。
if (!file.exists())
response.failure("文件不存在");
else if (FileUtil.del(file))
response.success("删除成功");
else
response.failure("删除失败");
@@ -185,7 +185,7 @@ public class GalleryUtil {
ArrayList<ImageKeyCache> imageKeyCaches = new ArrayList<>();
AtomicInteger page = new AtomicInteger(1);
gid2MpvKey.put(gid, scripts[1].split("=")[1].replace(";", "").replace("\"", "").replace(" ", ""));
JsonNode nodes = objectMapper.readValue(scripts[2].replace("var imagelist = ", ""), JsonNode.class);
JsonNode nodes = parseImagelist(scripts[2]);
nodes.forEach((n) -> {
ImageKeyCache imageKeyCache = new ImageKeyCache();
imageKeyCache.setGid(gid);
@@ -196,6 +196,19 @@ public class GalleryUtil {
return imageKeyCaches;
}
/**
* 解析 mpv 页里的 imagelist 行。
* 该行是 JS 赋值语句(`var imagelist = [...];`),行尾分号属 JavaScript 语法而非 JSON,
* 因此先剥掉「var imagelist = 」前缀与行尾分号,再交给 JSON 解析。
* Jackson 3 默认开启 FAIL_ON_TRAILING_TOKENS,若把分号留给它会在解析时报错。
*/
private static JsonNode parseImagelist(String scriptLine) {
String json = scriptLine.replace("var imagelist = ", "").trim();
if (json.endsWith(";"))
json = json.substring(0, json.length() - 1).trim();
return objectMapper.readValue(json, JsonNode.class);
}
public static String getMpvKey(String url){
String gid = String.valueOf(parseGid(url));
String key = gid2MpvKey.get(gid);