opt(openlist): 新增OpenList多存储同步支持,完善心跳检测字段

1. 为wp_user_openlist表新增心跳检测相关字段
2. 新增获取OpenList存储列表API
3. 重构OpenList同步逻辑,支持多存储同步
4. 优化网盘创建逻辑,按存储ID匹配复用已有网盘
This commit is contained in:
liRQ 2026-06-30 17:44:06 +08:00
parent bd921cc161
commit d8aab63d85
75 changed files with 124 additions and 22 deletions

View File

@ -350,6 +350,53 @@ public class OpenListClient {
return config != null ? config.getOpenlistUrl() : null; return config != null ? config.getOpenlistUrl() : null;
} }
// ==================== 存储管理 API ====================
/**
* 获取用户的所有存储列表
* OpenList 支持多个存储storage每个存储有独立的挂载路径
*
* @param userId 用户ID
* @return 存储列表每个存储包含 idmount_pathdriver 等信息
*/
public List<Map<String, Object>> listStorages(Long userId) {
UserOpenlist config = getUserOpenlist(userId);
if (config == null) {
throw new RuntimeException("用户未绑定 OpenList");
}
String url = config.getOpenlistUrl() + "/api/me/storages";
try {
String token = getValidToken(userId);
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
HttpEntity<Void> entity = new HttpEntity<>(headers);
ResponseEntity<Map> response = restTemplate.exchange(url, HttpMethod.GET, entity, Map.class);
Map<String, Object> result = response.getBody();
if (result == null || (int) result.get("code") != 200) {
log.warn("用户 {} 获取存储列表失败: {}", userId, result);
return new ArrayList<>();
}
@SuppressWarnings("unchecked")
List<Map<String, Object>> storages = (List<Map<String, Object>>) result.get("data");
if (storages == null) {
return new ArrayList<>();
}
log.info("用户 {} 共有 {} 个存储", userId, storages.size());
return storages;
} catch (Exception e) {
log.error("用户 {} 获取存储列表异常", userId, e);
return new ArrayList<>();
}
}
// ==================== 同步相关 API ==================== // ==================== 同步相关 API ====================
/** /**

View File

@ -124,6 +124,18 @@ public class DatabaseInitializer {
SchemaUtil.ensureColumnExists(conn, "wp_icon", "link_type", SchemaUtil.ensureColumnExists(conn, "wp_icon", "link_type",
"VARCHAR(20) NOT NULL DEFAULT 'external' COMMENT '链接类型external仅外网/ internal仅内网/ both内外网'"); "VARCHAR(20) NOT NULL DEFAULT 'external' COMMENT '链接类型external仅外网/ internal仅内网/ both内外网'");
// 检查 wp_user_openlist 表是否有 last_heartbeat_time 字段心跳检测时间
SchemaUtil.ensureColumnExists(conn, "wp_user_openlist", "last_heartbeat_time",
"DATETIME DEFAULT NULL COMMENT '最后一次心跳检测时间'");
// 检查 wp_user_openlist 表是否有 heartbeat_status 字段心跳状态
SchemaUtil.ensureColumnExists(conn, "wp_user_openlist", "heartbeat_status",
"VARCHAR(20) DEFAULT NULL COMMENT '心跳状态success/failed'");
// 检查 wp_user_openlist 表是否有 heartbeat_error 字段心跳失败原因
SchemaUtil.ensureColumnExists(conn, "wp_user_openlist", "heartbeat_error",
"VARCHAR(500) DEFAULT NULL COMMENT '心跳检测失败原因'");
// 移除已废弃的 icon 已被 logo_url 替代 // 移除已废弃的 icon 已被 logo_url 替代
try (var checkStmt = conn.createStatement(); try (var checkStmt = conn.createStatement();
var checkRs = checkStmt.executeQuery("SHOW COLUMNS FROM wp_drive LIKE 'icon'")) { var checkRs = checkStmt.executeQuery("SHOW COLUMNS FROM wp_drive LIKE 'icon'")) {

View File

@ -94,51 +94,94 @@ public class OpenListSyncService {
log.info("用户 {} 开始从 OpenList 同步: {}", userId, config.getOpenlistUrl()); log.info("用户 {} 开始从 OpenList 同步: {}", userId, config.getOpenlistUrl());
// 2. 查找或创建关联网盘 // 2. 获取 OpenList 的所有存储
Drive drive = findOrCreateDrive(userId, config); List<Map<String, Object>> storages = openListClient.listStorages(userId);
if (drive == null) { if (storages.isEmpty()) {
result.errors.add("创建网盘失败"); log.warn("用户 {} 的 OpenList 没有可用存储,尝试从根目录同步", userId);
// 降级处理从根目录同步创建单个 Drive
Drive drive = findOrCreateDrive(userId, config, null, null);
if (drive == null) {
result.errors.add("创建网盘失败");
return result;
}
result.driveCount = 1;
syncDirectory(userId, drive.getId(), "/", 0L, 0, result);
updateDriveStats(drive.getId(), userId);
return result; return result;
} }
result.driveCount = 1;
log.info("用户 {} 使用网盘: {} (ID={})", userId, drive.getName(), drive.getId());
// 3. 从根目录开始递归同步 log.info("用户 {} 的 OpenList 共有 {} 个存储", userId, storages.size());
String rootPath = "/";
syncDirectory(userId, drive.getId(), rootPath, 0L, 0, result);
// 4. 更新网盘统计信息 // 3. 为每个存储创建或查找对应的 Drive并同步内容
int totalFolders = folderRepository.countByDrive(drive.getId(), userId); for (Map<String, Object> storage : storages) {
int totalMovies = movieRepository.countByDrive(drive.getId(), userId); String storageId = String.valueOf(storage.get("id"));
driveRepository.updateStats(drive.getId(), totalFolders, totalMovies, 0L); String mountPath = (String) storage.get("mount_path");
String driver = (String) storage.get("driver");
log.info("用户 {} 同步存储: id={}, mountPath={}, driver={}", userId, storageId, mountPath, driver);
// 查找或创建该存储对应的 Drive
Drive drive = findOrCreateDrive(userId, config, storageId, mountPath);
if (drive == null) {
result.errors.add("创建存储 " + mountPath + " 的网盘失败");
continue;
}
result.driveCount++;
// 从该存储的挂载路径开始同步
String rootPath = mountPath != null ? mountPath : "/";
syncDirectory(userId, drive.getId(), rootPath, 0L, 0, result);
// 更新该网盘的统计信息
updateDriveStats(drive.getId(), userId);
}
log.info("用户 {} 同步完成: 网盘={}, 文件夹={}, 电影={}, 跳过文件={}, 跳过文件夹={}, 错误={}", log.info("用户 {} 同步完成: 网盘={}, 文件夹={}, 电影={}, 跳过文件={}, 跳过文件夹={}, 错误={}",
userId, drive.getName(), result.folderCount, result.movieCount, userId, result.driveCount, result.folderCount, result.movieCount,
result.skippedFileCount, result.skippedFolderCount, result.errors.size()); result.skippedFileCount, result.skippedFolderCount, result.errors.size());
return result; return result;
} }
/** /**
* 查找或创建关联 OpenList 的网盘 * 更新网盘统计信息
* 如果用户已有 openlistMountPath 不为空的网盘则复用否则创建新的
*/ */
private Drive findOrCreateDrive(Long userId, UserOpenlist config) { private void updateDriveStats(Long driveId, Long userId) {
// 查询用户所有网盘查找已关联 OpenList int totalFolders = folderRepository.countByDrive(driveId, userId);
int totalMovies = movieRepository.countByDrive(driveId, userId);
driveRepository.updateStats(driveId, totalFolders, totalMovies, 0L);
}
/**
* 查找或创建关联网盘
* 根据 storageId 查找已有的 Drive如果没有则创建新的
*
* @param userId 用户ID
* @param config OpenList 配置
* @param storageId OpenList 存储ID可为 null
* @param mountPath 挂载路径可为 null
* @return Drive 对象
*/
private Drive findOrCreateDrive(Long userId, UserOpenlist config, String storageId, String mountPath) {
// 查询用户所有网盘查找匹配 storageId
var drives = driveRepository.queryByUserId(userId, 1, 100); var drives = driveRepository.queryByUserId(userId, 1, 100);
for (var driveMap : drives) { for (var driveMap : drives) {
String mountPath = (String) driveMap.get("openlistMountPath"); String existingStorageId = (String) driveMap.get("openlistStorageId");
if (mountPath != null && !mountPath.isBlank()) { if (storageId != null && storageId.equals(existingStorageId)) {
// 复用已有网盘 // 复用已有网盘
log.info("复用已有网盘: id={}, storageId={}", driveMap.get("id"), storageId);
return driveRepository.findById(((Number) driveMap.get("id")).longValue()); return driveRepository.findById(((Number) driveMap.get("id")).longValue());
} }
} }
// 创建新网盘 // 创建新网盘
Drive drive = new Drive(userId, "OpenList 网盘", "从 OpenList 同步的网盘"); String driveName = mountPath != null ? "OpenList: " + mountPath : "OpenList 网盘";
drive.setOpenlistMountPath(config.getMountPath() != null ? config.getMountPath() : "/"); Drive drive = new Drive(userId, driveName, "从 OpenList 同步的网盘");
drive.setOpenlistStorageId(storageId);
drive.setOpenlistMountPath(mountPath != null ? mountPath : "/");
Long driveId = driveRepository.insert(drive); Long driveId = driveRepository.insert(drive);
drive.setId(driveId); drive.setId(driveId);
log.info("创建新网盘: id={}, storageId={}, mountPath={}", driveId, storageId, mountPath);
return drive; return drive;
} }