Files
openclaw-config/workspace-planner/tasks/dailyhot-job-review.md
T

7.4 KiB
Raw Blame History

DailyHotJob 代码审查报告

审查目标:DailyHotJob#execute() + JackettServiceImpl.dailyHot()
审查时间:2026-04-25
审查人:需求分析助手


1. 整体架构概览

DailyHotJob.execute(param)
  └─ JackettServiceImpl.dailyHot()
       ├─ getDailyHotIds()          → 爬取 wuji.me 获取 hot IDs
       │    └─ getPageContent(url)  → 通过 FlareSolverr 获取 HTML
       ├─ 逐条 selectOne 查重        → 过滤已存在的 ID
       ├─ 循环(逐条串行):
       │    ├─ searchSingleKeyword() → 调用 Jackett API
       │    ├─ 内存过滤(大小≥3G、当年发布)
       │    ├─ ResourcesUtils.saveFile() → 追加写到 hot.txt
       │    ├─ javInfoMapper.insert()    → 逐条插入
       │    └─ Thread.sleep()            → sleep 间隔
       └─ 结束

2. 🔴 P0 严重问题

2.1 异常全被吞,Job 日志永远显示「成功」

JobHandlerInvoker 依赖 exception != null 判断 Job 是否执行成功:

// JobHandlerInvoker 的关键逻辑
try {
    data = this.executeInternal(jobHandlerName, jobHandlerParam);
} catch (Throwable ex) {
    exception = ex;   // 只有这里 exception 才非 null
}

DailyHotJob.execute()dailyHot() 内的每一层都是:

// DailyHotJob
@Override
public String execute(String param) {
    jackettService.dailyHot();       // 没有 throws,异常全在里面吞了
    return "每日热点";               // 永远返回正常字符串
}

// dailyHot() 内部
for (...) {
    try {
        ...
    } catch (Exception e) {
        log.error("搜索 '{}' 时出错", javId, e.getMessage());  // 只打日志
    }
}

后果:即使远程 API 完全不可用、所有搜索全失败,Job 日志面板仍然显示「成功」。运维人员无法通过框架面板感知故障。

2.2 租户注解缺失问题

@Override
@TenantIgnore                          // ← 只有最外层加了
public void dailyHot() { ... }

dailyHot() 内部调用了 this.getDailyHotIds()this.getPageContent()this.searchSingleKeyword(),都是内部方法调用

在 Spring AOPJDK 动态代理)下,内部 this.method() 不会触发 @TenantIgnore 切面。如果 ruoyi 的租户 SQL 拦截器处于激活状态,可能导致这些方法执行时产生错误的 WHERE tenant_id = ? 过滤。

saveRedisIds() 方法也完全没加 @TenantIgnore,同样有风险。


3. 🟠 P1-P2 中等问题

3.1 N+1 数据库查询 + 逐条 insert

// N 次独立 selectOne
Set<String> newIds = hotIds.stream()
    .filter(javId -> javInfoMapper.selectOne(JavInfoDO::getJavId, javId) == null)
    .collect(Collectors.toSet());

// M 次独立 insert
for (String javId : newIds) {
    JavInfoDO javInfo = new JavInfoDO();
    javInfo.setJavId(javId);
    javInfoMapper.insert(javInfo);  // 逐条
}

如果 hotIds 有 200 条,就是 200 次 select + 若干次 insert,数据库连接往返开销巨大。

建议:改为 selectByJavIds(Collection) 批量查重 + insertBatch 批量插入。

3.2 逐条串行远程搜索 + sleep 阻塞

for (String javId : newIds) {
    List<JackettSearchResultVO> results = searchSingleKeyword(javId, 30);
    // 过滤...
    Thread.sleep(jackettProperties.getSearchDelay());  // 通常 1-3s
}

假设只差 50 条新 ID,每次 search 耗时 3s + sleep 2s = 5s/条 → 总共 250 秒(4+ 分钟)。如果匹配更多,耗时线性增长。

建议:使用并行流或 CompletableFuture 控制并发度(3-5),同时为 RestTemplate 配置连接/读取超时。

3.3 文件无限追加不轮转

ResourcesUtils.saveFile(resultVO.getMagnetUri(),
    jackettProperties.getWujiBaseDirectory(), "hot");

每次都追加到同一个 hot.txt,永不清理。每天几十上百条磁力链接,一年后文件可能几十上百 MB,I/O 持续劣化。

建议:按日期分文件,如 hot/2026-04-25.txt;或改用数据库存储。

3.4 年度过滤器不符合「热点」语义

if (resultVO.getPublishDate().getYear() != currentYear) {
    continue;
}

如果今天是 2026-04-25,这个条件会接受 2026-01-01 到 2026-04-24 的所有结果,而不是「最近发布的结果」。

建议:改为 isAfter(LocalDateTime.now().minusHours(N)) 时间窗口过滤。


4. 🟡 P3 潜在坑

4.1 Redis Key 硬编码且已过时

String key = "\"jav:rank:2023\"";
  • 2023 硬编码,距离现在已经 3 年
  • Key 带多余的双引号,实际可能是 jav:rank:2023
  • 如果这是保存历史 ID 的入口,2023 早已不是有效数据源

4.2 非幂等 + 中断无保护

Job 执行到一半时应用关闭:

  • 已插入的 JavInfoDO 已提交(无事务包裹)
  • 已写入文件的磁力链已持久化
  • 下次执行时已入库的 ID 被跳过,但 hot.txt 用 APPEND 追加,产生重复行

4.3 Redis 数据清洗掩盖上游 Bug

private String cleanString(String raw) {
    // 去首尾引号 + 转义
    String cleaned = raw;
    if (cleaned.startsWith("\"")) cleaned = cleaned.substring(1);
    if (cleaned.endsWith("\"")) cleaned = cleaned.substring(0, cleaned.length() - 1);
    cleaned = cleaned.replace("\\\"", "\"");
    return cleaned.isEmpty() ? null : cleaned;
}

Redis 的 Set 中存储的值不应该带序列化引号或转义。出现这种情况说明上游写入数据的方式有问题。消费侧做清洗只是遮盖症状,不是根本解决。

4.4 saveFile() 两个重载路径冲突

// 重载1dailyHot 调用的)
saveFile(content, path, fileName)   Paths.get(path, fileName + ".txt")

// 重载2
saveFile(content, name)             dir = "/app/daily"; Paths.get(dir, name + ".txt")

重载2 硬编码了 /app/daily,如果其他调用方使用了这个重载但没有意识到路径硬编码,可能产生预期外的写入位置。

4.5 Thread.sleep 占用 Quartz 线程

Thread.sleep 是阻塞操作,在 Quartz 的工作线程中执行会让该线程无法处理其他 Job。虽然 @DisallowConcurrentExecution 避免了同 Job 并行,但如果 JackettServiceImpl 被多个 Job 共享,sleep 期间资源浪费更明显。


5. 改造建议优先级

优先级 问题 建议方案
P0 异常吞没,运维盲区 dailyHot() 改为 throws Exception,仅捕获可恢复异常,向上传播不可恢复异常
P0 租户注解内部调用不生效 getDailyHotIds()searchSingleKeyword()saveRedisIds() 上也加 @TenantIgnore
P1 N+1 数据库操作 替换为 selectByJavIds 批量查重 + insertBatch 批量插入
P1 串行远程搜索过慢 CompletableFuture 并发(并发度 3-5),配置 RestTemplate 超时(connect+read
P2 文件追加不轮转 按日期分文件:hot/yyyy-MM-dd.txt
P2 年度过滤语义不对 改为最近 N 小时/天的时间窗口过滤
P2 Thread.sleep 阻塞 使用 ThreadPoolTaskSchedulerScheduledThreadPoolExecutor 提交异步任务
P3 Redis Key 硬编码 提取为配置项 wuji.redis-rank-key
P3 非幂等设计 改造为支持幂等(基于 jav_id 唯一约束的 ON DUPLICATE KEY UPDATE,文件去重写入)