缺陷修复: - GalleryUtil.parseGid 只捕 IndexOutOfBoundsException:非数字段抛 NumberFormatException、 link 为 null 抛 NPE,都会穿透成 500(按链接查询直接把用户输入喂进来)。现统一返回 null, 由调用方转成业务失败。 - 图片索引缓存不是整体生效:先写 gidToKey 再逐页写 ImageKeyCache,中途失败会留下 「gidToKey 命中但页 key 缺失」的半截缓存,后续请求直接返回已缓存而永远取不到图。 现任何异常都回滚已写入部分,并识别历史半截缓存自动重建。 - 全站请求日志把 AuthCode 明文记入 INFO,改为只记 present/absent。 - selectEnableAuthCode 的 SQL 与全量查询完全相同(都无 isEnable 条件),停用用户的 授权码刷新后仍放行,isEnable 形同虚设;补齐条件并允许 null 按默认 true 处理。 - /validate 手工 String.format 拼 JSON,用户名含引号或反斜杠会产出非法 JSON, 前端 JSON.parse 失败;改用 ObjectMapper 组装。 性能与并发: - Netty IO 线程不再直接做 JDBC:节点上报的任务状态改由单线程顺序执行器落库与推送, 既不打乱「按上报顺序覆盖状态」的语义,也不阻塞心跳与响应。 - 取订阅不再等待第三方归属地查询:先以 unknown 落库并立即分发,归属地由后台线程补齐。 - 搜索关键词与分页参数按 UTF-8 编码,&、#、中文不再破坏上游查询串。 - selectAllGallery 的收藏标记由 O(n*m) 嵌套循环改为集合查找。 - 复用 CustomUtil.objectMapper(8 处 new ObjectMapper);gid2MpvKey 改为有界 LRU(2048); 缩略图清理排序不再每次比较都重读文件属性;FileDownload 关闭响应流,避免大文件下载的 临时文件堆积到 Full GC;移除从未使用的 downloadThread 线程池。 测试:431 项全过(新增索引回滚、半截缓存重建、用户名特殊字符、畸形链接查询、 搜索编码、分发不等待定位、批量收藏标记等用例;异步状态处理用例加 awaitStatusApplied 屏障保持断言确定性)。
191 lines
7.2 KiB
Java
191 lines
7.2 KiB
Java
package com.lion.lionwebsite.Service;
|
|
|
|
import com.lion.lionwebsite.Dao.normal.CustomConfigurationMapper;
|
|
import com.lion.lionwebsite.Dao.normal.GalleryMapper;
|
|
import com.lion.lionwebsite.Domain.CustomConfiguration;
|
|
import com.lion.lionwebsite.Util.CustomUtil;
|
|
import com.lion.lionwebsite.Util.GalleryUtil;
|
|
import lombok.Data;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.apache.hc.client5.http.classic.methods.HttpGet;
|
|
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
|
|
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
|
|
import org.apache.hc.client5.http.impl.classic.HttpClients;
|
|
import org.apache.hc.core5.http.HttpEntity;
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
import org.springframework.scheduling.annotation.Scheduled;
|
|
import org.springframework.stereotype.Service;
|
|
|
|
import java.io.*;
|
|
import java.net.URLDecoder;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.nio.file.*;
|
|
import java.nio.file.attribute.BasicFileAttributes;
|
|
import java.nio.file.attribute.FileTime;
|
|
import java.time.LocalDateTime;
|
|
import java.time.format.DateTimeFormatter;
|
|
import java.util.*;
|
|
import java.util.regex.Matcher;
|
|
import java.util.regex.Pattern;
|
|
|
|
import static java.nio.file.FileVisitResult.CONTINUE;
|
|
|
|
@Service
|
|
@Data
|
|
@Slf4j
|
|
public class LocalService{
|
|
@Value("${local.dou-nai-clash:https://aaaa.gay/link/X7zEqkIx5gtIGugO?client=clashmeta}")
|
|
String DouNaiClash;
|
|
|
|
@Value("${local.dou-nai-v2ray:https://aaaa.gay/link/X7zEqkIx5gtIGugO?client=v2}")
|
|
String DouNaiV2ray;
|
|
|
|
private static final CloseableHttpClient httpClient = HttpClients.createDefault();
|
|
|
|
final CustomConfigurationMapper configurationMapper;
|
|
|
|
final GalleryMapper galleryMapper;
|
|
|
|
final PushService pushService;
|
|
|
|
final RemoteService remoteService;
|
|
|
|
final SubscriptionRefreshScheduler subscriptionRefreshScheduler;
|
|
|
|
/**
|
|
* 检查连接是否有效,如果无效自动重连
|
|
*/
|
|
@Scheduled(cron = "0 0/30 * * * *")
|
|
public void CheckConnectionAvailability(){
|
|
if (remoteService.isDead()){
|
|
remoteService.initChannel();
|
|
pushService.sendToMe("主动检测连接已断开,自动进行重连");
|
|
log.warn("主动检测连接已断开,自动进行重连");
|
|
return;
|
|
}
|
|
|
|
// -1 为对方没有返回
|
|
if (remoteService.checkAvailability() == -1) {
|
|
String result = switch (remoteService.reconnect()){
|
|
case 0 -> "重连成功";
|
|
case -1 -> "重连失败";
|
|
case -2 -> "当前未连接,不进行重连";
|
|
default -> "未知错误";
|
|
};
|
|
pushService.sendToMe("主动检测连接无数据返回,自动进行重连:" + result);
|
|
log.warn("主动检测连接无数据返回,自动进行重连:{}", result);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 每周周一四点重置额度
|
|
*/
|
|
@Scheduled(cron = "0 0 4 * * MON")
|
|
public void reset() {
|
|
configurationMapper.updateConfiguration(CustomConfiguration.WEEK_USED_AMOUNT, "0");
|
|
configurationMapper.updateConfiguration(CustomConfiguration.LAST_RESET_AMOUNT_TIME, CustomUtil.now());
|
|
}
|
|
|
|
/**
|
|
* 每天凌晨测试e-hentai cookie是否过期
|
|
*/
|
|
@Scheduled(cron = "0 0 0 * * *")
|
|
public void verifyCookie(){
|
|
try {
|
|
String content = GalleryUtil.requests("https://exhentai.org", "GET", null, null);
|
|
if(content.trim().isEmpty())
|
|
pushService.sendToMe("cookie过期");
|
|
} catch (IOException e) {
|
|
pushService.sendToMe("检测cookie异常:" + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 手动「更新订阅」:立即刷新全部启用账号,供管理页按钮调用。
|
|
*
|
|
* <p>定时刷新已移交 {@link SubscriptionRefreshScheduler}。原先这里还有一个
|
|
* {@code @Scheduled(fixedRate = 86400000)} 的定时入口,它会一次性刷新全部账号
|
|
* (十几个账号在十几秒内打满),且因为没有 initialDelay,每次重启都会立刻重刷一遍,
|
|
* 使上游看到的请求密度取决于部署频率。该入口已移除。
|
|
*
|
|
* <p>不再保留 {@code isManual} 参数:定时路径已不存在,留着它只会让人以为
|
|
* 「传 false 就是不刷新」,而实际语义是「什么都不做却报成功」。
|
|
*/
|
|
public boolean updateSub() {
|
|
boolean success = subscriptionRefreshScheduler.refreshAllNow();
|
|
remoteService.requestSubscriptionSync();
|
|
if (success)
|
|
configurationMapper.updateConfiguration(CustomConfiguration.LAST_UPDATE_SUB_TIME,
|
|
CustomUtil.dateTimeFormatter().format(LocalDateTime.now()));
|
|
return success;
|
|
}
|
|
|
|
/**
|
|
* 封装好的get方法
|
|
* @param url url
|
|
* @return 请求结果
|
|
* @throws IOException 网络异常
|
|
*/
|
|
public static ArrayList<String> Get(String url) throws IOException {
|
|
CloseableHttpResponse httpResponse;
|
|
HttpGet httpGet = new HttpGet(url);
|
|
|
|
httpResponse = httpClient.execute(httpGet);
|
|
|
|
HttpEntity responseEntity = httpResponse.getEntity();
|
|
int statusCode = httpResponse.getCode();
|
|
ArrayList<String> temp = new ArrayList<>();
|
|
|
|
if (statusCode == 200) {
|
|
BufferedReader reader = new BufferedReader(new InputStreamReader(responseEntity.getContent()));
|
|
String str;
|
|
while ((str = reader.readLine()) != null)
|
|
temp.add(str);
|
|
}
|
|
|
|
httpResponse.close();
|
|
return temp;
|
|
}
|
|
|
|
/**
|
|
* 每周,查看缩略图数量,按照访问时间排序,删除超出1w的部分
|
|
*/
|
|
@Scheduled(cron = "0 0 4 1 * *")
|
|
public void clearThumbnailCache(){
|
|
String cachePath = "/storage/hentaiCache/";
|
|
File directory = new File(cachePath);
|
|
// 排序需要文件的最后访问时间。原先在比较器里每次重新读属性(n log n 次系统调用),
|
|
// 且属性读取失败会让整个排序抛 RuntimeException;这里在遍历时一次性带出属性。
|
|
List<Map.Entry<Path, FileTime>> files = new ArrayList<>();
|
|
try {
|
|
Files.walkFileTree(directory.toPath(), new SimpleFileVisitor<>() {
|
|
@Override
|
|
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
|
|
if (attrs.isRegularFile()) {
|
|
files.add(Map.entry(file, attrs.lastAccessTime()));
|
|
}
|
|
return CONTINUE;
|
|
}
|
|
});
|
|
} catch (IOException e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
|
|
files.sort(Map.Entry.comparingByValue());
|
|
|
|
if (files.size() > 10000) {
|
|
List<Map.Entry<Path, FileTime>> toDelete = files.subList(0, files.size() - 10000);
|
|
for (Map.Entry<Path, FileTime> entry : toDelete) {
|
|
try {
|
|
Files.delete(entry.getKey());
|
|
}catch (IOException e){
|
|
log.warn("删除缩略图缓存文件失败: {}", entry.getKey(), e);
|
|
}
|
|
}
|
|
log.info("Deleted {} files", toDelete.size());
|
|
} else {
|
|
log.info("No files to delete");
|
|
}
|
|
}
|
|
}
|