sync from WSL host: latest openclaw config update
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
# 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 是否执行成功:
|
||||
|
||||
```java
|
||||
// JobHandlerInvoker 的关键逻辑
|
||||
try {
|
||||
data = this.executeInternal(jobHandlerName, jobHandlerParam);
|
||||
} catch (Throwable ex) {
|
||||
exception = ex; // 只有这里 exception 才非 null
|
||||
}
|
||||
```
|
||||
|
||||
但 `DailyHotJob.execute()` 和 `dailyHot()` 内的每一层都是:
|
||||
|
||||
```java
|
||||
// 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 租户注解缺失问题
|
||||
|
||||
```java
|
||||
@Override
|
||||
@TenantIgnore // ← 只有最外层加了
|
||||
public void dailyHot() { ... }
|
||||
```
|
||||
|
||||
`dailyHot()` 内部调用了 `this.getDailyHotIds()`、`this.getPageContent()`、`this.searchSingleKeyword()`,都是**内部方法调用**。
|
||||
|
||||
在 Spring AOP(JDK 动态代理)下,内部 `this.method()` 不会触发 `@TenantIgnore` 切面。如果 ruoyi 的租户 SQL 拦截器处于激活状态,可能导致这些方法执行时产生错误的 `WHERE tenant_id = ?` 过滤。
|
||||
|
||||
`saveRedisIds()` 方法也完全没加 `@TenantIgnore`,同样有风险。
|
||||
|
||||
---
|
||||
|
||||
## 3. 🟠 P1-P2 中等问题
|
||||
|
||||
### 3.1 N+1 数据库查询 + 逐条 insert
|
||||
|
||||
```java
|
||||
// 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 阻塞
|
||||
|
||||
```java
|
||||
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 文件无限追加不轮转
|
||||
|
||||
```java
|
||||
ResourcesUtils.saveFile(resultVO.getMagnetUri(),
|
||||
jackettProperties.getWujiBaseDirectory(), "hot");
|
||||
```
|
||||
|
||||
每次都追加到同一个 `hot.txt`,永不清理。每天几十上百条磁力链接,一年后文件可能几十上百 MB,I/O 持续劣化。
|
||||
|
||||
**建议**:按日期分文件,如 `hot/2026-04-25.txt`;或改用数据库存储。
|
||||
|
||||
### 3.4 年度过滤器不符合「热点」语义
|
||||
|
||||
```java
|
||||
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 硬编码且已过时
|
||||
|
||||
```java
|
||||
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
|
||||
|
||||
```java
|
||||
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<String> 中存储的值不应该带序列化引号或转义。出现这种情况说明**上游写入数据的方式有问题**。消费侧做清洗只是遮盖症状,不是根本解决。
|
||||
|
||||
### 4.4 `saveFile()` 两个重载路径冲突
|
||||
|
||||
```java
|
||||
// 重载1(dailyHot 调用的)
|
||||
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 阻塞 | 使用 `ThreadPoolTaskScheduler` 或 `ScheduledThreadPoolExecutor` 提交异步任务 |
|
||||
| **P3** | Redis Key 硬编码 | 提取为配置项 `wuji.redis-rank-key` |
|
||||
| **P3** | 非幂等设计 | 改造为支持幂等(基于 jav_id 唯一约束的 `ON DUPLICATE KEY UPDATE`,文件去重写入) |
|
||||
Reference in New Issue
Block a user