Compare commits
No commits in common. "2b538c4520ff522fdff875be30dd1af8106bfa06" and "6f3b6f9f28b3c118384fc8b3f1a1cef25dc21662" have entirely different histories.
2b538c4520
...
6f3b6f9f28
24
.gitignore
vendored
24
.gitignore
vendored
@ -1,24 +0,0 @@
|
||||
# 编译输出目录
|
||||
target/
|
||||
|
||||
# 依赖 JAR 包
|
||||
wangpan-common/deps/
|
||||
|
||||
# IDE 配置文件
|
||||
.idea/
|
||||
*.iml
|
||||
.vscode/
|
||||
.settings/
|
||||
.classpath
|
||||
.project
|
||||
|
||||
# 日志文件
|
||||
*.log
|
||||
/opt/wangpan/logs/
|
||||
|
||||
# 系统文件
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# 环境配置
|
||||
.env
|
||||
@ -1,73 +0,0 @@
|
||||
|
||||
. ____ _ __ _ _
|
||||
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
|
||||
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
|
||||
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
|
||||
' |____| .__|_| |_|_| |_\__, | / / / /
|
||||
=========|_|==============|___/=/_/_/_/
|
||||
:: Spring Boot :: (v3.2.5)
|
||||
|
||||
2026-07-02T14:26:40.211+08:00 INFO 151212 --- [wangpan-drive-service] [ main] com.wangpan.DriveServiceApplication : Starting DriveServiceApplication v1.0-SNAPSHOT using Java 17.0.8 with PID 151212 (D:\java2\wangpan\java\wangpan-drive-service\target\wangpan-drive-service-1.0-SNAPSHOT.jar started by 李荣强 in D:\java2\wangpan\java)
|
||||
2026-07-02T14:26:40.217+08:00 INFO 151212 --- [wangpan-drive-service] [ main] com.wangpan.DriveServiceApplication : No active profile set, falling back to 1 default profile: "default"
|
||||
2026-07-02T14:26:41.606+08:00 INFO 151212 --- [wangpan-drive-service] [ main] ptablePropertiesBeanFactoryPostProcessor : Post-processing PropertySource instances
|
||||
2026-07-02T14:26:41.608+08:00 INFO 151212 --- [wangpan-drive-service] [ main] c.u.j.EncryptablePropertySourceConverter : Skipping PropertySource configurationProperties [class org.springframework.boot.context.properties.source.ConfigurationPropertySourcesPropertySource
|
||||
2026-07-02T14:26:41.610+08:00 INFO 151212 --- [wangpan-drive-service] [ main] c.u.j.EncryptablePropertySourceConverter : Skipping PropertySource servletConfigInitParams [class org.springframework.core.env.PropertySource$StubPropertySource
|
||||
2026-07-02T14:26:41.610+08:00 INFO 151212 --- [wangpan-drive-service] [ main] c.u.j.EncryptablePropertySourceConverter : Skipping PropertySource servletContextInitParams [class org.springframework.core.env.PropertySource$StubPropertySource
|
||||
2026-07-02T14:26:41.610+08:00 INFO 151212 --- [wangpan-drive-service] [ main] c.u.j.EncryptablePropertySourceConverter : Converting PropertySource systemProperties [org.springframework.core.env.PropertiesPropertySource] to EncryptableMapPropertySourceWrapper
|
||||
2026-07-02T14:26:41.611+08:00 INFO 151212 --- [wangpan-drive-service] [ main] c.u.j.EncryptablePropertySourceConverter : Converting PropertySource systemEnvironment [org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor$OriginAwareSystemEnvironmentPropertySource] to EncryptableSystemEnvironmentPropertySourceWrapper
|
||||
2026-07-02T14:26:41.611+08:00 INFO 151212 --- [wangpan-drive-service] [ main] c.u.j.EncryptablePropertySourceConverter : Converting PropertySource random [org.springframework.boot.env.RandomValuePropertySource] to EncryptablePropertySourceWrapper
|
||||
2026-07-02T14:26:41.611+08:00 INFO 151212 --- [wangpan-drive-service] [ main] c.u.j.EncryptablePropertySourceConverter : Converting PropertySource Config resource 'class path resource [application.yml]' via location 'optional:classpath:/' [org.springframework.boot.env.OriginTrackedMapPropertySource] to EncryptableMapPropertySourceWrapper
|
||||
2026-07-02T14:26:41.842+08:00 INFO 151212 --- [wangpan-drive-service] [ main] c.u.j.filter.DefaultLazyPropertyFilter : Property Filter custom Bean not found with name 'encryptablePropertyFilter'. Initializing Default Property Filter
|
||||
2026-07-02T14:26:41.854+08:00 INFO 151212 --- [wangpan-drive-service] [ main] c.u.j.r.DefaultLazyPropertyResolver : Property Resolver custom Bean not found with name 'encryptablePropertyResolver'. Initializing Default Property Resolver
|
||||
2026-07-02T14:26:41.856+08:00 INFO 151212 --- [wangpan-drive-service] [ main] c.u.j.d.DefaultLazyPropertyDetector : Property Detector custom Bean not found with name 'encryptablePropertyDetector'. Initializing Default Property Detector
|
||||
2026-07-02T14:26:42.108+08:00 INFO 151212 --- [wangpan-drive-service] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8082 (http)
|
||||
2026-07-02T14:26:42.121+08:00 INFO 151212 --- [wangpan-drive-service] [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
|
||||
2026-07-02T14:26:42.122+08:00 INFO 151212 --- [wangpan-drive-service] [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.20]
|
||||
2026-07-02T14:26:42.179+08:00 INFO 151212 --- [wangpan-drive-service] [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
|
||||
2026-07-02T14:26:42.180+08:00 INFO 151212 --- [wangpan-drive-service] [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1857 ms
|
||||
2026-07-02T14:26:42.353+08:00 INFO 151212 --- [wangpan-drive-service] [ main] c.u.j.encryptor.DefaultLazyEncryptor : String Encryptor custom Bean not found with name 'jasyptStringEncryptor'. Initializing Default String Encryptor
|
||||
2026-07-02T14:26:42.367+08:00 INFO 151212 --- [wangpan-drive-service] [ main] c.u.j.c.StringEncryptorBuilder : Encryptor config not found for property jasypt.encryptor.provider-name, using default value: null
|
||||
2026-07-02T14:26:42.368+08:00 INFO 151212 --- [wangpan-drive-service] [ main] c.u.j.c.StringEncryptorBuilder : Encryptor config not found for property jasypt.encryptor.provider-class-name, using default value: null
|
||||
? 数据库配置加载成功: jdbc:mysql://118.178.238.159:33306/wangpan?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
========================================
|
||||
[DatabaseInitializer] 网盘服务数据库初始化...
|
||||
========================================
|
||||
? 表已存在:wp_drive
|
||||
? 表已存在:wp_drive_folder
|
||||
? 表已存在:wp_drive_movie
|
||||
? 表已存在:wp_icon
|
||||
? 表已存在:wp_user_openlist
|
||||
? [wp_drive] logo_url 字段已存在
|
||||
? [wp_drive] 表创建完成
|
||||
? [wp_drive_folder] 表创建完成
|
||||
? [wp_drive_movie] 表创建完成
|
||||
? 数据库初始化检查完成!
|
||||
========================================
|
||||
? 数据库初始化成功!耗时:743ms
|
||||
========================================
|
||||
_ _ |_ _ _|_. ___ _ | _
|
||||
| | |\/|_)(_| | |_\ |_)||_|_\
|
||||
/ |
|
||||
3.5.7
|
||||
2026-07-02T14:26:44.370+08:00 INFO 151212 --- [wangpan-drive-service] [ main] c.wangpan.listener.IconCleanupScheduler : [IconCleanupScheduler] 定时清理任务已启动 | 首次延迟=10分钟 | 间隔=24小时
|
||||
2026-07-02T14:26:44.963+08:00 INFO 151212 --- [wangpan-drive-service] [ main] com.wangpan.common.util.MinioUtil : [MinioUtil] 存储桶已存在: wangpan
|
||||
2026-07-02T14:26:44.964+08:00 INFO 151212 --- [wangpan-drive-service] [ main] com.wangpan.common.util.MinioUtil : [MinioUtil] MinIO 客户端初始化成功: http://118.178.238.159:39000
|
||||
2026-07-02T14:26:45.450+08:00 INFO 151212 --- [wangpan-drive-service] [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 3 endpoint(s) beneath base path '/actuator'
|
||||
2026-07-02T14:26:45.539+08:00 WARN 151212 --- [wangpan-drive-service] [ main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.context.ApplicationContextException: Failed to start bean 'webServerStartStop'
|
||||
2026-07-02T14:26:45.572+08:00 INFO 151212 --- [wangpan-drive-service] [ main] .s.b.a.l.ConditionEvaluationReportLogger :
|
||||
|
||||
Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
|
||||
2026-07-02T14:26:45.604+08:00 ERROR 151212 --- [wangpan-drive-service] [ main] o.s.b.d.LoggingFailureAnalysisReporter :
|
||||
|
||||
***************************
|
||||
APPLICATION FAILED TO START
|
||||
***************************
|
||||
|
||||
Description:
|
||||
|
||||
Web server failed to start. Port 8082 was already in use.
|
||||
|
||||
Action:
|
||||
|
||||
Identify and stop the process that's listening on port 8082 or configure this application to listen on another port.
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
@echo off
|
||||
java -cp wangpan-common\target\wangpan-common-1.0-SNAPSHOT.jar;D:\maven_repository\org\jasypt\jasypt\1.9.3\jasypt-1.9.3.jar com.wangpan.common.util.EncryptUtil encrypt Djl1022!
|
||||
@ -1,5 +0,0 @@
|
||||
$cp = "wangpan-common\target\wangpan-common-1.0-SNAPSHOT.jar;D:\maven_repository\org\jasypt\jasypt\1.9.3\jasypt-1.9.3.jar"
|
||||
$env:JASYPT_ENCRYPTOR_PASSWORD = "wangpan-secret-key-2024"
|
||||
java -cp $cp com.wangpan.common.util.EncryptUtil encrypt "http://ldfun.asia:13000"
|
||||
Write-Host "---"
|
||||
java -cp $cp com.wangpan.common.util.EncryptUtil encrypt "djl1022"
|
||||
@ -56,7 +56,7 @@ foreach ($service in $services) {
|
||||
Write-Host " ERROR: Jar not found: $jarPath" -ForegroundColor Red
|
||||
continue
|
||||
}
|
||||
$process = Start-Process -FilePath "java" -ArgumentList "-DJASYPT_ENCRYPTOR_PASSWORD=$JASYPT_PASSWORD", "-jar", $jarPath -WindowStyle Hidden -PassThru
|
||||
$process = Start-Process -FilePath "java" -ArgumentList "-jar", $jarPath -WindowStyle Hidden -PassThru
|
||||
if ($process) {
|
||||
Write-Host " Started $name (PID: $($process.Id))" -ForegroundColor Green
|
||||
} else {
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
package com.wangpan.common.config;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* MyBatis-Plus 配置
|
||||
* 注册分页插件,使 Page 分页查询生效
|
||||
*/
|
||||
@Configuration
|
||||
public class MybatisPlusConfig {
|
||||
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
// 注册分页插件,指定数据库类型为 MySQL
|
||||
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
|
||||
// 单页最大条数限制,防止误传超大 pageSize 导致 OOM
|
||||
paginationInterceptor.setMaxLimit(500L);
|
||||
interceptor.addInnerInterceptor(paginationInterceptor);
|
||||
return interceptor;
|
||||
}
|
||||
}
|
||||
@ -39,18 +39,6 @@ public class DriveDO implements Serializable {
|
||||
/** 网盘 Logo URL(MinIO 存储链接) */
|
||||
private String logoUrl;
|
||||
|
||||
/** Logo 最后访问时间(用于回收策略) */
|
||||
private LocalDateTime logoLastAccessTime;
|
||||
|
||||
/** Logo 状态:normal(正常)/recycling(回收中)/deleted(已删除) */
|
||||
private String logoStatus;
|
||||
|
||||
/** OpenList 存储ID:关联 OpenList 中的存储配置 */
|
||||
private String openlistStorageId;
|
||||
|
||||
/** OpenList 挂载路径:如 /movies */
|
||||
private String openlistMountPath;
|
||||
|
||||
/** 文件夹数量 */
|
||||
private Integer folderCount;
|
||||
|
||||
@ -88,18 +76,6 @@ public class DriveDO implements Serializable {
|
||||
public String getLogoUrl() { return logoUrl; }
|
||||
public void setLogoUrl(String logoUrl) { this.logoUrl = logoUrl; }
|
||||
|
||||
public LocalDateTime getLogoLastAccessTime() { return logoLastAccessTime; }
|
||||
public void setLogoLastAccessTime(LocalDateTime logoLastAccessTime) { this.logoLastAccessTime = logoLastAccessTime; }
|
||||
|
||||
public String getLogoStatus() { return logoStatus; }
|
||||
public void setLogoStatus(String logoStatus) { this.logoStatus = logoStatus; }
|
||||
|
||||
public String getOpenlistStorageId() { return openlistStorageId; }
|
||||
public void setOpenlistStorageId(String openlistStorageId) { this.openlistStorageId = openlistStorageId; }
|
||||
|
||||
public String getOpenlistMountPath() { return openlistMountPath; }
|
||||
public void setOpenlistMountPath(String openlistMountPath) { this.openlistMountPath = openlistMountPath; }
|
||||
|
||||
public Integer getFolderCount() { return folderCount; }
|
||||
public void setFolderCount(Integer folderCount) { this.folderCount = folderCount; }
|
||||
|
||||
|
||||
@ -95,18 +95,6 @@ public class DriveMovieDO implements Serializable {
|
||||
/** 标签(逗号分隔) */
|
||||
private String tags;
|
||||
|
||||
/** 主演(逗号分隔,TMDB刮削) */
|
||||
private String casts;
|
||||
|
||||
/** 导演(逗号分隔,TMDB刮削) */
|
||||
private String directors;
|
||||
|
||||
/** OpenList 文件路径:如 /movies/阿凡达.mp4 */
|
||||
private String openlistFilePath;
|
||||
|
||||
/** 存储类型:metadata(仅元数据)/ openlist(OpenList 管理实际文件) */
|
||||
private String storageType;
|
||||
|
||||
/** 更新时间 */
|
||||
@TableField(update = "NOW()")
|
||||
private LocalDateTime updateTime;
|
||||
@ -189,18 +177,6 @@ public class DriveMovieDO implements Serializable {
|
||||
public String getTags() { return tags; }
|
||||
public void setTags(String tags) { this.tags = tags; }
|
||||
|
||||
public String getCasts() { return casts; }
|
||||
public void setCasts(String casts) { this.casts = casts; }
|
||||
|
||||
public String getDirectors() { return directors; }
|
||||
public void setDirectors(String directors) { this.directors = directors; }
|
||||
|
||||
public String getOpenlistFilePath() { return openlistFilePath; }
|
||||
public void setOpenlistFilePath(String openlistFilePath) { this.openlistFilePath = openlistFilePath; }
|
||||
|
||||
public String getStorageType() { return storageType; }
|
||||
public void setStorageType(String storageType) { this.storageType = storageType; }
|
||||
|
||||
public LocalDateTime getUpdateTime() { return updateTime; }
|
||||
public void setUpdateTime(LocalDateTime updateTime) { this.updateTime = updateTime; }
|
||||
|
||||
|
||||
@ -68,12 +68,6 @@ public class MovieDO implements Serializable {
|
||||
/** 标签(逗号分隔) */
|
||||
private String tags;
|
||||
|
||||
/** 主演(逗号分隔,TMDB刮削) */
|
||||
private String casts;
|
||||
|
||||
/** 导演(逗号分隔,TMDB刮削) */
|
||||
private String directors;
|
||||
|
||||
/** 更新时间 */
|
||||
@TableField(update = "NOW()")
|
||||
private LocalDateTime updateTime;
|
||||
@ -129,12 +123,6 @@ public class MovieDO implements Serializable {
|
||||
public String getTags() { return tags; }
|
||||
public void setTags(String tags) { this.tags = tags; }
|
||||
|
||||
public String getCasts() { return casts; }
|
||||
public void setCasts(String casts) { this.casts = casts; }
|
||||
|
||||
public String getDirectors() { return directors; }
|
||||
public void setDirectors(String directors) { this.directors = directors; }
|
||||
|
||||
public LocalDateTime getUpdateTime() { return updateTime; }
|
||||
public void setUpdateTime(LocalDateTime updateTime) { this.updateTime = updateTime; }
|
||||
|
||||
|
||||
@ -1,120 +0,0 @@
|
||||
package com.wangpan.common.dal.dataobject;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 用户 OpenList 连接配置数据对象(Data Object)
|
||||
* 对应数据库表:wp_user_openlist
|
||||
*
|
||||
* 设计说明:
|
||||
* - 每个用户只能绑定一个 OpenList 实例(user_id 唯一索引)
|
||||
* - 敏感信息(URL、账号、密码)加密存储
|
||||
* - Token 缓存到数据库,避免每次调用 OpenList API 都重新登录
|
||||
* - DO 字段名使用 Java 驼峰命名,MyBatis-Plus 自动映射为数据库 snake_case 列
|
||||
*/
|
||||
@TableName(value = "wp_user_openlist", autoResultMap = true)
|
||||
public class UserOpenlistDO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 主键ID */
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 用户ID(关联 wp_user 表) */
|
||||
private Long userId;
|
||||
|
||||
/** OpenList 服务地址(加密存储) */
|
||||
private String openlistUrl;
|
||||
|
||||
/** OpenList 账号(加密存储) */
|
||||
private String openlistUsername;
|
||||
|
||||
/** OpenList 密码(加密存储) */
|
||||
private String openlistPassword;
|
||||
|
||||
/** OpenList 存储ID(用户在 OpenList 后台配置的存储) */
|
||||
private String storageId;
|
||||
|
||||
/** 挂载路径,如 /uc */
|
||||
private String mountPath;
|
||||
|
||||
/** OpenList API Token(缓存,避免重复登录) */
|
||||
private String token;
|
||||
|
||||
/** Token 过期时间戳(毫秒) */
|
||||
private Long tokenExpireTime;
|
||||
|
||||
/** 状态:active / disabled */
|
||||
private String status;
|
||||
|
||||
/** 最后一次心跳检测时间 */
|
||||
private LocalDateTime lastHeartbeatTime;
|
||||
|
||||
/** 心跳状态:success / failed */
|
||||
private String heartbeatStatus;
|
||||
|
||||
/** 心跳检测失败原因 */
|
||||
private String heartbeatError;
|
||||
|
||||
/** 更新时间 */
|
||||
@TableField(update = "NOW()")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
/** 创建时间 */
|
||||
@TableField(fill = com.baomidou.mybatisplus.annotation.FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
// ==================== Getter 和 Setter ====================
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
|
||||
public String getOpenlistUrl() { return openlistUrl; }
|
||||
public void setOpenlistUrl(String openlistUrl) { this.openlistUrl = openlistUrl; }
|
||||
|
||||
public String getOpenlistUsername() { return openlistUsername; }
|
||||
public void setOpenlistUsername(String openlistUsername) { this.openlistUsername = openlistUsername; }
|
||||
|
||||
public String getOpenlistPassword() { return openlistPassword; }
|
||||
public void setOpenlistPassword(String openlistPassword) { this.openlistPassword = openlistPassword; }
|
||||
|
||||
public String getStorageId() { return storageId; }
|
||||
public void setStorageId(String storageId) { this.storageId = storageId; }
|
||||
|
||||
public String getMountPath() { return mountPath; }
|
||||
public void setMountPath(String mountPath) { this.mountPath = mountPath; }
|
||||
|
||||
public String getToken() { return token; }
|
||||
public void setToken(String token) { this.token = token; }
|
||||
|
||||
public Long getTokenExpireTime() { return tokenExpireTime; }
|
||||
public void setTokenExpireTime(Long tokenExpireTime) { this.tokenExpireTime = tokenExpireTime; }
|
||||
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
|
||||
public LocalDateTime getLastHeartbeatTime() { return lastHeartbeatTime; }
|
||||
public void setLastHeartbeatTime(LocalDateTime lastHeartbeatTime) { this.lastHeartbeatTime = lastHeartbeatTime; }
|
||||
|
||||
public String getHeartbeatStatus() { return heartbeatStatus; }
|
||||
public void setHeartbeatStatus(String heartbeatStatus) { this.heartbeatStatus = heartbeatStatus; }
|
||||
|
||||
public String getHeartbeatError() { return heartbeatError; }
|
||||
public void setHeartbeatError(String heartbeatError) { this.heartbeatError = heartbeatError; }
|
||||
|
||||
public LocalDateTime getUpdateTime() { return updateTime; }
|
||||
public void setUpdateTime(LocalDateTime updateTime) { this.updateTime = updateTime; }
|
||||
|
||||
public LocalDateTime getCreateTime() { return createTime; }
|
||||
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
|
||||
}
|
||||
@ -16,22 +16,19 @@ public interface DriveMapper extends BaseMapper<DriveDO> {
|
||||
/** 建表SQL(保留供 DatabaseInitializer 使用) */
|
||||
String CREATE_TABLE_SQL = """
|
||||
CREATE TABLE IF NOT EXISTS wp_drive (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '网盘ID',
|
||||
user_id BIGINT NOT NULL COMMENT '所属用户ID',
|
||||
name VARCHAR(100) NOT NULL COMMENT '网盘名称',
|
||||
description VARCHAR(500) DEFAULT NULL COMMENT '网盘描述',
|
||||
logo_url TEXT DEFAULT NULL COMMENT '网盘Logo URL(MinIO存储链接)',
|
||||
logo_last_access_time DATETIME DEFAULT NULL COMMENT 'Logo最后访问时间(用于回收策略)',
|
||||
logo_status VARCHAR(20) NOT NULL DEFAULT 'normal' COMMENT 'Logo状态:normal(正常)/recycling(回收中)/deleted(已删除)',
|
||||
folder_count INT NOT NULL DEFAULT 0 COMMENT '文件夹数量',
|
||||
file_count INT NOT NULL DEFAULT 0 COMMENT '文件数量',
|
||||
total_size BIGINT NOT NULL DEFAULT 0 COMMENT '总大小(字节)',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态:active/disabled',
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '网盘ID',
|
||||
user_id BIGINT NOT NULL COMMENT '所属用户ID',
|
||||
name VARCHAR(100) NOT NULL COMMENT '网盘名称',
|
||||
description VARCHAR(500) DEFAULT NULL COMMENT '网盘描述',
|
||||
logo_url TEXT DEFAULT NULL COMMENT '网盘Logo URL(MinIO存储链接)',
|
||||
folder_count INT NOT NULL DEFAULT 0 COMMENT '文件夹数量',
|
||||
file_count INT NOT NULL DEFAULT 0 COMMENT '文件数量',
|
||||
total_size BIGINT NOT NULL DEFAULT 0 COMMENT '总大小(字节)',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态:active/disabled',
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_logo_status (logo_status)
|
||||
INDEX idx_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='网盘表'
|
||||
""";
|
||||
}
|
||||
|
||||
@ -4,7 +4,6 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.wangpan.common.dal.dataobject.DriveMovieDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -45,8 +44,6 @@ public interface DriveMovieMapper extends BaseMapper<DriveMovieDO> {
|
||||
poster_url TEXT DEFAULT NULL COMMENT '海报URL(竖版,加密存储)',
|
||||
backdrop_url TEXT DEFAULT NULL COMMENT '背景图URL(横版,加密存储)',
|
||||
tags VARCHAR(500) DEFAULT NULL COMMENT '标签(逗号分隔)',
|
||||
casts VARCHAR(500) DEFAULT NULL COMMENT '主演(逗号分隔,TMDB刮削)',
|
||||
directors VARCHAR(200) DEFAULT NULL COMMENT '导演(逗号分隔,TMDB刮削)',
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
INDEX idx_drive_id (drive_id),
|
||||
@ -63,16 +60,4 @@ public interface DriveMovieMapper extends BaseMapper<DriveMovieDO> {
|
||||
* 复杂SQL,在XML中实现
|
||||
*/
|
||||
List<Map<String, Object>> findDuplicateMovies(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 更新电影主演
|
||||
*/
|
||||
@Update("UPDATE wp_drive_movie SET casts = #{casts}, update_time = NOW() WHERE id = #{id}")
|
||||
int updateCasts(@Param("id") Long id, @Param("casts") String casts);
|
||||
|
||||
/**
|
||||
* 更新电影导演
|
||||
*/
|
||||
@Update("UPDATE wp_drive_movie SET directors = #{directors}, update_time = NOW() WHERE id = #{id}")
|
||||
int updateDirectors(@Param("id") Long id, @Param("directors") String directors);
|
||||
}
|
||||
|
||||
@ -71,22 +71,4 @@ public interface MovieMapper extends BaseMapper<MovieDO> {
|
||||
*/
|
||||
@Update("UPDATE wp_drive_movie SET scrape_status = #{scrapeStatus}, scrape_error = #{scrapeError}, update_time = NOW() WHERE id = #{id}")
|
||||
int updateScrapeStatus(@Param("id") Long id, @Param("scrapeStatus") String scrapeStatus, @Param("scrapeError") String scrapeError);
|
||||
|
||||
/**
|
||||
* 更新电影简介(description 字段)
|
||||
*/
|
||||
@Update("UPDATE wp_drive_movie SET description = #{description}, update_time = NOW() WHERE id = #{id}")
|
||||
int updateDescription(@Param("id") Long id, @Param("description") String description);
|
||||
|
||||
/**
|
||||
* 更新电影主演
|
||||
*/
|
||||
@Update("UPDATE wp_drive_movie SET casts = #{casts}, update_time = NOW() WHERE id = #{id}")
|
||||
int updateCasts(@Param("id") Long id, @Param("casts") String casts);
|
||||
|
||||
/**
|
||||
* 更新电影导演
|
||||
*/
|
||||
@Update("UPDATE wp_drive_movie SET directors = #{directors}, update_time = NOW() WHERE id = #{id}")
|
||||
int updateDirectors(@Param("id") Long id, @Param("directors") String directors);
|
||||
}
|
||||
|
||||
@ -1,38 +0,0 @@
|
||||
package com.wangpan.common.dal.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.wangpan.common.dal.dataobject.UserOpenlistDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 用户 OpenList 连接配置 Mapper 接口
|
||||
*
|
||||
* 继承 MyBatis-Plus BaseMapper,自动获得 CRUD 能力
|
||||
* 每个用户只能绑定一个 OpenList 实例(user_id 唯一索引)
|
||||
*/
|
||||
@Mapper
|
||||
public interface UserOpenlistMapper extends BaseMapper<UserOpenlistDO> {
|
||||
|
||||
/** 建表SQL(保留供 DatabaseInitializer 使用) */
|
||||
String CREATE_TABLE_SQL = """
|
||||
CREATE TABLE IF NOT EXISTS wp_user_openlist (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
|
||||
user_id BIGINT NOT NULL COMMENT '用户ID(关联 wp_user 表)',
|
||||
openlist_url VARCHAR(200) NOT NULL COMMENT 'OpenList 服务地址(加密存储)',
|
||||
openlist_username VARCHAR(100) NOT NULL COMMENT 'OpenList 账号(加密存储)',
|
||||
openlist_password VARCHAR(200) NOT NULL COMMENT 'OpenList 密码(加密存储)',
|
||||
storage_id VARCHAR(50) DEFAULT NULL COMMENT 'OpenList 存储ID(用户在 OpenList 后台配置的存储)',
|
||||
mount_path VARCHAR(200) DEFAULT NULL COMMENT '挂载路径,如 /uc',
|
||||
token VARCHAR(500) DEFAULT NULL COMMENT 'OpenList API Token(缓存,避免重复登录)',
|
||||
token_expire_time BIGINT DEFAULT NULL COMMENT 'Token 过期时间戳(毫秒)',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态:active/disabled',
|
||||
last_heartbeat_time DATETIME DEFAULT NULL COMMENT '最后一次心跳检测时间',
|
||||
heartbeat_status VARCHAR(20) DEFAULT NULL COMMENT '心跳状态:success/failed',
|
||||
heartbeat_error VARCHAR(500) DEFAULT NULL COMMENT '心跳检测失败原因',
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
UNIQUE KEY uk_user_id (user_id),
|
||||
INDEX idx_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户 OpenList 连接配置表'
|
||||
""";
|
||||
}
|
||||
@ -204,200 +204,27 @@ public class DriveMovieRepository extends ServiceImpl<DriveMovieMapper, DriveMov
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户所有网盘下所有未删除的电影(分页,支持关键词搜索和排序)
|
||||
* 适用于大数据量场景(1万+),使用索引优化查询
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param keyword 关键词(可选,搜索标题)
|
||||
* @param sortBy 排序字段(可选:createTime/updateTime/rating/title)
|
||||
* @param sortOrder 排序方向(可选:asc/desc)
|
||||
* @param pageNo 页码
|
||||
* @param pageSize 每页大小
|
||||
* @return 分页结果(包含总数、总页数、当前页数据)
|
||||
*/
|
||||
public Map<String, Object> queryAllMoviesByUser(Long userId, String keyword, String sortBy, String sortOrder,
|
||||
int pageNo, int pageSize) {
|
||||
var page = new Page<DriveMovieDO>(pageNo, pageSize);
|
||||
var wrapper = new LambdaQueryWrapper<DriveMovieDO>()
|
||||
.eq(DriveMovieDO::getUserId, userId)
|
||||
.eq(DriveMovieDO::getStatus, "active");
|
||||
|
||||
// 关键词搜索(标题模糊匹配)
|
||||
if (keyword != null && !keyword.isBlank()) {
|
||||
wrapper.like(DriveMovieDO::getTitle, keyword);
|
||||
}
|
||||
|
||||
// 排序字段
|
||||
if ("rating".equals(sortBy)) {
|
||||
wrapper.orderBy(true, "asc".equalsIgnoreCase(sortOrder), DriveMovieDO::getRating);
|
||||
} else if ("title".equals(sortBy)) {
|
||||
wrapper.orderBy(true, "asc".equalsIgnoreCase(sortOrder), DriveMovieDO::getTitle);
|
||||
} else if ("updateTime".equals(sortBy)) {
|
||||
wrapper.orderBy(true, "asc".equalsIgnoreCase(sortOrder), DriveMovieDO::getUpdateTime);
|
||||
} else {
|
||||
// 默认按创建时间倒序
|
||||
wrapper.orderByDesc(DriveMovieDO::getCreateTime);
|
||||
}
|
||||
|
||||
var result = page(page, wrapper);
|
||||
var list = new ArrayList<Map<String, Object>>();
|
||||
for (var movieDO : result.getRecords()) {
|
||||
list.add(toMap(movieDO));
|
||||
}
|
||||
|
||||
// 构建分页信息
|
||||
var pageInfo = new HashMap<String, Object>();
|
||||
pageInfo.put("total", result.getTotal());
|
||||
pageInfo.put("pages", result.getPages());
|
||||
pageInfo.put("pageNo", result.getCurrent());
|
||||
pageInfo.put("pageSize", result.getSize());
|
||||
pageInfo.put("list", list);
|
||||
|
||||
return pageInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机查询指定数量的电影(用于每日推荐)
|
||||
* 优化方案:先 COUNT 总数,再随机计算偏移量,避免 ORDER BY RAND() 全表扫描
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param limit 查询数量
|
||||
* @return 电影列表
|
||||
*/
|
||||
public List<DriveMovieDO> queryRandomMovies(Long userId, int limit) {
|
||||
// 1. 统计符合条件的电影总数
|
||||
var countWrapper = new LambdaQueryWrapper<DriveMovieDO>()
|
||||
.eq(DriveMovieDO::getUserId, userId)
|
||||
.eq(DriveMovieDO::getStatus, "active");
|
||||
long total = count(countWrapper);
|
||||
|
||||
if (total == 0) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 2. 随机计算偏移量,确保不越界
|
||||
int offset = 0;
|
||||
if (total > limit) {
|
||||
offset = new Random().nextInt((int) (total - limit));
|
||||
}
|
||||
|
||||
// 3. 使用 LIMIT offset, limit 查询,避免全表排序
|
||||
var wrapper = new LambdaQueryWrapper<DriveMovieDO>()
|
||||
.eq(DriveMovieDO::getUserId, userId)
|
||||
.eq(DriveMovieDO::getStatus, "active")
|
||||
.last(String.format("LIMIT %d, %d", offset, limit));
|
||||
return list(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户所有网盘下所有未删除的电影(排除指定ID,分页)
|
||||
* 用于每日推荐场景:先随机20部,再查询剩余电影
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param excludeIds 排除的电影ID列表
|
||||
* @param keyword 关键词(可选)
|
||||
* @param sortBy 排序字段(可选)
|
||||
* @param sortOrder 排序方向(可选)
|
||||
* @param pageNo 页码
|
||||
* @param pageSize 每页大小
|
||||
* @return 分页结果
|
||||
*/
|
||||
public Map<String, Object> queryAllMoviesExcludeIds(Long userId, List<Long> excludeIds, String keyword,
|
||||
String sortBy, String sortOrder, int pageNo, int pageSize) {
|
||||
var page = new Page<DriveMovieDO>(pageNo, pageSize);
|
||||
var wrapper = new LambdaQueryWrapper<DriveMovieDO>()
|
||||
.eq(DriveMovieDO::getUserId, userId)
|
||||
.eq(DriveMovieDO::getStatus, "active");
|
||||
|
||||
// 排除指定ID
|
||||
if (excludeIds != null && !excludeIds.isEmpty()) {
|
||||
wrapper.notIn(DriveMovieDO::getId, excludeIds);
|
||||
}
|
||||
|
||||
// 关键词搜索
|
||||
if (keyword != null && !keyword.isBlank()) {
|
||||
wrapper.like(DriveMovieDO::getTitle, keyword);
|
||||
}
|
||||
|
||||
// 排序字段
|
||||
if ("rating".equals(sortBy)) {
|
||||
wrapper.orderBy(true, "asc".equalsIgnoreCase(sortOrder), DriveMovieDO::getRating);
|
||||
} else if ("title".equals(sortBy)) {
|
||||
wrapper.orderBy(true, "asc".equalsIgnoreCase(sortOrder), DriveMovieDO::getTitle);
|
||||
} else if ("updateTime".equals(sortBy)) {
|
||||
wrapper.orderBy(true, "asc".equalsIgnoreCase(sortOrder), DriveMovieDO::getUpdateTime);
|
||||
} else {
|
||||
wrapper.orderByDesc(DriveMovieDO::getCreateTime);
|
||||
}
|
||||
|
||||
var result = page(page, wrapper);
|
||||
var list = new ArrayList<Map<String, Object>>();
|
||||
for (var movieDO : result.getRecords()) {
|
||||
list.add(toMap(movieDO));
|
||||
}
|
||||
|
||||
var pageInfo = new HashMap<String, Object>();
|
||||
pageInfo.put("total", result.getTotal());
|
||||
pageInfo.put("pages", result.getPages());
|
||||
pageInfo.put("pageNo", result.getCurrent());
|
||||
pageInfo.put("pageSize", result.getSize());
|
||||
pageInfo.put("list", list);
|
||||
|
||||
return pageInfo;
|
||||
}
|
||||
|
||||
// ==================== 更新 ====================
|
||||
|
||||
/**
|
||||
* 更新电影基本信息(支持所有可更新字段,所有字段可选)
|
||||
* 更新电影基本信息
|
||||
* 使用 MyBatis-Plus LambdaUpdateWrapper 构建更新条件
|
||||
* 注意:casts 和 directors 字段通过单独的 updateCasts/updateDirectors 方法更新
|
||||
*
|
||||
* @param id 电影ID
|
||||
* @param title 电影标题(可选)
|
||||
* @param description 描述(可选)
|
||||
* @param rating 评分(可选)
|
||||
* @param tags 标签(可选)
|
||||
* @param posterUrl 海报URL(可选)
|
||||
* @param backdropUrl 背景图URL(可选)
|
||||
* @param resourceLibrary 资源库来源(可选)
|
||||
* @param maintainer 维护者(可选)
|
||||
*/
|
||||
public boolean update(Long id, String title, String description,
|
||||
Integer rating, String tags, String posterUrl, String backdropUrl,
|
||||
String resourceLibrary, String maintainer) {
|
||||
public boolean update(Long id, String title, String originalName, String subtitle, String description,
|
||||
Integer rating, String tags, String category) {
|
||||
var wrapper = new LambdaUpdateWrapper<DriveMovieDO>()
|
||||
.eq(DriveMovieDO::getId, id)
|
||||
// 基础信息字段
|
||||
.set(title != null, DriveMovieDO::getTitle, title)
|
||||
.set(originalName != null, DriveMovieDO::getOriginalName, originalName)
|
||||
.set(subtitle != null, DriveMovieDO::getSubtitle, subtitle)
|
||||
.set(description != null, DriveMovieDO::getDescription, description)
|
||||
.set(rating != null, DriveMovieDO::getRating, rating)
|
||||
.set(tags != null, DriveMovieDO::getTags, tags)
|
||||
// TMDB 刮削相关字段(用户可手动补充或修正)
|
||||
.set(posterUrl != null, DriveMovieDO::getPosterUrl, posterUrl)
|
||||
.set(backdropUrl != null, DriveMovieDO::getBackdropUrl, backdropUrl)
|
||||
.set(resourceLibrary != null, DriveMovieDO::getResourceLibrary, resourceLibrary)
|
||||
.set(maintainer != null, DriveMovieDO::getMaintainer, maintainer)
|
||||
// 更新时间
|
||||
.set(category != null, DriveMovieDO::getCategory, category)
|
||||
.set(DriveMovieDO::getUpdateTime, LocalDateTime.now());
|
||||
return update(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新电影主演
|
||||
*/
|
||||
public int updateCasts(Long id, String casts) {
|
||||
return baseMapper.updateCasts(id, casts);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新电影导演
|
||||
*/
|
||||
public int updateDirectors(Long id, String directors) {
|
||||
return baseMapper.updateDirectors(id, directors);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新电影刮削信息
|
||||
* 使用 MyBatis-Plus LambdaUpdateWrapper 构建更新条件
|
||||
@ -492,10 +319,6 @@ public class DriveMovieRepository extends ServiceImpl<DriveMovieMapper, DriveMov
|
||||
movieDO.setPosterUrl(movie.getPosterUrl());
|
||||
movieDO.setBackdropUrl(movie.getBackdropUrl());
|
||||
movieDO.setTags(movie.getTags());
|
||||
movieDO.setCasts(movie.getCasts());
|
||||
movieDO.setDirectors(movie.getDirectors());
|
||||
movieDO.setOpenlistFilePath(movie.getOpenlistFilePath());
|
||||
movieDO.setStorageType(movie.getStorageType());
|
||||
movieDO.setUpdateTime(movie.getUpdateTime());
|
||||
return movieDO;
|
||||
}
|
||||
@ -529,10 +352,6 @@ public class DriveMovieRepository extends ServiceImpl<DriveMovieMapper, DriveMov
|
||||
movie.setPosterUrl(movieDO.getPosterUrl());
|
||||
movie.setBackdropUrl(movieDO.getBackdropUrl());
|
||||
movie.setTags(movieDO.getTags());
|
||||
movie.setCasts(movieDO.getCasts());
|
||||
movie.setDirectors(movieDO.getDirectors());
|
||||
movie.setOpenlistFilePath(movieDO.getOpenlistFilePath());
|
||||
movie.setStorageType(movieDO.getStorageType());
|
||||
movie.setUpdateTime(movieDO.getUpdateTime());
|
||||
movie.setCreateTime(movieDO.getCreateTime());
|
||||
return movie;
|
||||
@ -547,11 +366,13 @@ public class DriveMovieRepository extends ServiceImpl<DriveMovieMapper, DriveMov
|
||||
map.put("driveId", movieDO.getDriveId());
|
||||
map.put("folderId", movieDO.getFolderId());
|
||||
map.put("title", movieDO.getTitle());
|
||||
map.put("subtitle", movieDO.getSubtitle());
|
||||
map.put("originalName", movieDO.getOriginalName());
|
||||
map.put("mimeType", movieDO.getMimeType());
|
||||
map.put("fileSize", movieDO.getFileSize());
|
||||
map.put("extension", movieDO.getExtension());
|
||||
map.put("description", movieDO.getDescription());
|
||||
map.put("resourceLibrary", movieDO.getResourceLibrary());
|
||||
map.put("maintainer", movieDO.getMaintainer());
|
||||
map.put("status", movieDO.getStatus());
|
||||
map.put("category", movieDO.getCategory());
|
||||
@ -560,13 +381,9 @@ public class DriveMovieRepository extends ServiceImpl<DriveMovieMapper, DriveMov
|
||||
map.put("scrapeError", movieDO.getScrapeError());
|
||||
map.put("rating", movieDO.getRating());
|
||||
map.put("tags", movieDO.getTags());
|
||||
map.put("casts", movieDO.getCasts());
|
||||
map.put("directors", movieDO.getDirectors());
|
||||
map.put("posterUrl", movieDO.getPosterUrl());
|
||||
map.put("backdropUrl", movieDO.getBackdropUrl());
|
||||
map.put("minioUrl", movieDO.getMinioUrl());
|
||||
map.put("openlistFilePath", movieDO.getOpenlistFilePath());
|
||||
map.put("storageType", movieDO.getStorageType());
|
||||
map.put("createTime", movieDO.getCreateTime() != null ? movieDO.getCreateTime().toString() : null);
|
||||
map.put("updateTime", movieDO.getUpdateTime() != null ? movieDO.getUpdateTime().toString() : null);
|
||||
return map;
|
||||
|
||||
@ -90,61 +90,6 @@ public class DriveRepository extends ServiceImpl<DriveMapper, DriveDO> {
|
||||
return update(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新网盘 Logo 最后访问时间(访问时调用,刷新回收策略计时)
|
||||
* 如果 Logo 处于回收模式(recycling),则恢复正常模式(normal)并重新计时
|
||||
*/
|
||||
public boolean updateLogoAccessTime(Long id) {
|
||||
var wrapper = new LambdaUpdateWrapper<DriveDO>()
|
||||
.eq(DriveDO::getId, id)
|
||||
.isNotNull(DriveDO::getLogoUrl)
|
||||
.set(DriveDO::getLogoLastAccessTime, LocalDateTime.now())
|
||||
.set(DriveDO::getLogoStatus, "normal");
|
||||
return update(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询所有有 Logo 且状态为 normal 的网盘(用于回收策略检查)
|
||||
*/
|
||||
public List<DriveDO> findDrivesWithLogoNormal() {
|
||||
var wrapper = new LambdaQueryWrapper<DriveDO>()
|
||||
.isNotNull(DriveDO::getLogoUrl)
|
||||
.eq(DriveDO::getLogoStatus, "normal");
|
||||
return list(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询所有处于回收模式(recycling)的网盘(用于删除策略检查)
|
||||
*/
|
||||
public List<DriveDO> findDrivesWithLogoRecycling() {
|
||||
var wrapper = new LambdaQueryWrapper<DriveDO>()
|
||||
.isNotNull(DriveDO::getLogoUrl)
|
||||
.eq(DriveDO::getLogoStatus, "recycling");
|
||||
return list(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Logo 状态更新为回收模式(recycling)
|
||||
*/
|
||||
public boolean updateLogoStatusToRecycling(Long id) {
|
||||
var wrapper = new LambdaUpdateWrapper<DriveDO>()
|
||||
.eq(DriveDO::getId, id)
|
||||
.set(DriveDO::getLogoStatus, "recycling");
|
||||
return update(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 Logo:清空 logo_url,设置状态为 deleted
|
||||
*/
|
||||
public boolean deleteLogo(Long id) {
|
||||
var wrapper = new LambdaUpdateWrapper<DriveDO>()
|
||||
.eq(DriveDO::getId, id)
|
||||
.set(DriveDO::getLogoUrl, null)
|
||||
.set(DriveDO::getLogoStatus, "deleted")
|
||||
.set(DriveDO::getLogoLastAccessTime, null);
|
||||
return update(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新网盘统计信息
|
||||
* 使用 MyBatis-Plus LambdaUpdateWrapper 构建更新条件
|
||||
@ -198,10 +143,6 @@ public class DriveRepository extends ServiceImpl<DriveMapper, DriveDO> {
|
||||
driveDO.setName(drive.getName());
|
||||
driveDO.setDescription(drive.getDescription());
|
||||
driveDO.setLogoUrl(drive.getLogoUrl());
|
||||
driveDO.setLogoLastAccessTime(drive.getLogoLastAccessTime());
|
||||
driveDO.setLogoStatus(drive.getLogoStatus());
|
||||
driveDO.setOpenlistStorageId(drive.getOpenlistStorageId());
|
||||
driveDO.setOpenlistMountPath(drive.getOpenlistMountPath());
|
||||
driveDO.setFolderCount(drive.getFolderCount());
|
||||
driveDO.setFileCount(drive.getFileCount());
|
||||
driveDO.setTotalSize(drive.getTotalSize());
|
||||
@ -221,8 +162,6 @@ public class DriveRepository extends ServiceImpl<DriveMapper, DriveDO> {
|
||||
drive.setName(driveDO.getName());
|
||||
drive.setDescription(driveDO.getDescription());
|
||||
drive.setLogoUrl(driveDO.getLogoUrl());
|
||||
drive.setOpenlistStorageId(driveDO.getOpenlistStorageId());
|
||||
drive.setOpenlistMountPath(driveDO.getOpenlistMountPath());
|
||||
drive.setFolderCount(driveDO.getFolderCount());
|
||||
drive.setFileCount(driveDO.getFileCount());
|
||||
drive.setTotalSize(driveDO.getTotalSize());
|
||||
@ -242,10 +181,6 @@ public class DriveRepository extends ServiceImpl<DriveMapper, DriveDO> {
|
||||
map.put("name", driveDO.getName());
|
||||
map.put("description", driveDO.getDescription());
|
||||
map.put("logoUrl", driveDO.getLogoUrl());
|
||||
map.put("logoLastAccessTime", driveDO.getLogoLastAccessTime() != null ? driveDO.getLogoLastAccessTime().toString() : null);
|
||||
map.put("logoStatus", driveDO.getLogoStatus());
|
||||
map.put("openlistStorageId", driveDO.getOpenlistStorageId());
|
||||
map.put("openlistMountPath", driveDO.getOpenlistMountPath());
|
||||
map.put("folderCount", driveDO.getFolderCount());
|
||||
map.put("fileCount", driveDO.getFileCount());
|
||||
map.put("totalSize", driveDO.getTotalSize());
|
||||
|
||||
@ -72,19 +72,16 @@ public class MovieRepository extends ServiceImpl<MovieMapper, MovieDO> {
|
||||
|
||||
/**
|
||||
* 根据电影ID列表批量查询电影(用于每日推荐缓存回查)
|
||||
* 自动过滤已被逻辑删除(status = deleted)的电影
|
||||
*
|
||||
* @param movieIds 电影ID列表
|
||||
* @return 电影Map列表(保持传入ID的顺序,不含已删除的电影)
|
||||
* @return 电影Map列表(保持传入ID的顺序)
|
||||
*/
|
||||
public List<Map<String, Object>> findByIds(List<Long> movieIds) {
|
||||
if (movieIds == null || movieIds.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
// 查询时排除已被逻辑删除的电影
|
||||
var wrapper = new LambdaQueryWrapper<MovieDO>()
|
||||
.in(MovieDO::getId, movieIds)
|
||||
.ne(MovieDO::getStatus, "deleted");
|
||||
.in(MovieDO::getId, movieIds);
|
||||
var movieDOs = list(wrapper);
|
||||
// 按传入的ID顺序排序
|
||||
var movieMap = movieDOs.stream().collect(Collectors.toMap(MovieDO::getId, m -> m));
|
||||
@ -230,17 +227,14 @@ public class MovieRepository extends ServiceImpl<MovieMapper, MovieDO> {
|
||||
/**
|
||||
* 随机查询指定数量的电影(用于今日推荐)
|
||||
* 使用 MySQL 的 RAND() 函数实现随机排序
|
||||
* 自动过滤已被逻辑删除(status = deleted)的电影
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param limit 返回数量,默认3
|
||||
* @return 随机电影列表(不含已删除的电影)
|
||||
* @return 随机电影列表
|
||||
*/
|
||||
public List<Map<String, Object>> findRandomMovies(Long userId, int limit) {
|
||||
// 查询时排除已被逻辑删除的电影
|
||||
var wrapper = new LambdaQueryWrapper<MovieDO>()
|
||||
.eq(MovieDO::getUserId, userId)
|
||||
.ne(MovieDO::getStatus, "deleted")
|
||||
.last("ORDER BY RAND() LIMIT " + limit);
|
||||
|
||||
var movieDOs = list(wrapper);
|
||||
@ -314,27 +308,6 @@ public class MovieRepository extends ServiceImpl<MovieMapper, MovieDO> {
|
||||
return baseMapper.updateScrapeStatus(id, scrapeStatus, scrapeError);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新电影简介(description 字段)
|
||||
*/
|
||||
public int updateDescription(Long id, String description) {
|
||||
return baseMapper.updateDescription(id, description);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新电影主演
|
||||
*/
|
||||
public int updateCasts(Long id, String casts) {
|
||||
return baseMapper.updateCasts(id, casts);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新电影导演
|
||||
*/
|
||||
public int updateDirectors(Long id, String directors) {
|
||||
return baseMapper.updateDirectors(id, directors);
|
||||
}
|
||||
|
||||
// ==================== Entity ↔ DO 转换 ====================
|
||||
|
||||
/**
|
||||
@ -357,9 +330,8 @@ public class MovieRepository extends ServiceImpl<MovieMapper, MovieDO> {
|
||||
movieDO.setPosterUrl(movie.getPosterUrl());
|
||||
movieDO.setBackdropUrl(movie.getBackdropUrl());
|
||||
movieDO.setTags(movie.getTags());
|
||||
movieDO.setCasts(movie.getCasts());
|
||||
movieDO.setDirectors(movie.getDirectors());
|
||||
movieDO.setUpdateTime(movie.getUpdateTime());
|
||||
movieDO.setCreateTime(movie.getCreateTime());
|
||||
return movieDO;
|
||||
}
|
||||
|
||||
@ -383,9 +355,8 @@ public class MovieRepository extends ServiceImpl<MovieMapper, MovieDO> {
|
||||
movie.setPosterUrl(movieDO.getPosterUrl());
|
||||
movie.setBackdropUrl(movieDO.getBackdropUrl());
|
||||
movie.setTags(movieDO.getTags());
|
||||
movie.setCasts(movieDO.getCasts());
|
||||
movie.setDirectors(movieDO.getDirectors());
|
||||
movie.setUpdateTime(movieDO.getUpdateTime());
|
||||
movie.setCreateTime(movieDO.getCreateTime());
|
||||
return movie;
|
||||
}
|
||||
|
||||
@ -397,6 +368,8 @@ public class MovieRepository extends ServiceImpl<MovieMapper, MovieDO> {
|
||||
map.put("id", movieDO.getId());
|
||||
map.put("userId", movieDO.getUserId());
|
||||
map.put("title", movieDO.getTitle());
|
||||
map.put("subtitle", movieDO.getSubtitle());
|
||||
map.put("resourceLibrary", movieDO.getResourceLibrary());
|
||||
map.put("maintainer", movieDO.getMaintainer());
|
||||
map.put("status", movieDO.getStatus());
|
||||
map.put("category", movieDO.getCategory());
|
||||
@ -407,8 +380,6 @@ public class MovieRepository extends ServiceImpl<MovieMapper, MovieDO> {
|
||||
map.put("posterUrl", movieDO.getPosterUrl());
|
||||
map.put("backdropUrl", movieDO.getBackdropUrl());
|
||||
map.put("tags", movieDO.getTags());
|
||||
map.put("casts", movieDO.getCasts());
|
||||
map.put("directors", movieDO.getDirectors());
|
||||
map.put("updateTime", movieDO.getUpdateTime() != null ? movieDO.getUpdateTime().toString() : null);
|
||||
map.put("createTime", movieDO.getCreateTime() != null ? movieDO.getCreateTime().toString() : null);
|
||||
return map;
|
||||
|
||||
@ -1,275 +0,0 @@
|
||||
package com.wangpan.common.dal.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.wangpan.common.dal.dataobject.UserOpenlistDO;
|
||||
import com.wangpan.common.dal.mapper.UserOpenlistMapper;
|
||||
import com.wangpan.common.entity.UserOpenlist;
|
||||
import com.wangpan.common.util.EncryptUtil;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 用户 OpenList 连接配置 Repository 层
|
||||
*
|
||||
* 继承 MyBatis-Plus ServiceImpl,自动获得 CRUD 能力
|
||||
* 职责:
|
||||
* 1. 对外提供业务维度的数据访问接口(使用 Entity/Map)
|
||||
* 2. 处理敏感信息的加密/解密(URL、账号、密码)
|
||||
* 3. 处理 Entity ↔ UserOpenlistDO 的转换
|
||||
*/
|
||||
@Repository
|
||||
public class UserOpenlistRepository extends ServiceImpl<UserOpenlistMapper, UserOpenlistDO> {
|
||||
|
||||
// ==================== 查询 ====================
|
||||
|
||||
/**
|
||||
* 根据用户ID查询 OpenList 连接配置(返回 Entity)
|
||||
* 敏感信息自动解密
|
||||
*/
|
||||
public UserOpenlist findByUserId(Long userId) {
|
||||
var wrapper = new LambdaQueryWrapper<UserOpenlistDO>()
|
||||
.eq(UserOpenlistDO::getUserId, userId);
|
||||
var doObj = getOne(wrapper);
|
||||
return doObj != null ? toEntity(doObj) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询所有活跃状态的 OpenList 配置(用于定时心跳检测)
|
||||
* 敏感信息自动解密
|
||||
*/
|
||||
public List<UserOpenlist> findAllActive() {
|
||||
var wrapper = new LambdaQueryWrapper<UserOpenlistDO>()
|
||||
.eq(UserOpenlistDO::getStatus, "active");
|
||||
var list = list(wrapper);
|
||||
var result = new java.util.ArrayList<UserOpenlist>();
|
||||
for (var doObj : list) {
|
||||
result.add(toEntity(doObj));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户ID查询 OpenList 连接配置(返回 Map)
|
||||
* 敏感信息不返回明文,仅返回脱敏后的信息
|
||||
*/
|
||||
public Map<String, Object> findInfoByUserId(Long userId) {
|
||||
var wrapper = new LambdaQueryWrapper<UserOpenlistDO>()
|
||||
.eq(UserOpenlistDO::getUserId, userId);
|
||||
var doObj = getOne(wrapper);
|
||||
if (doObj == null) {
|
||||
return null;
|
||||
}
|
||||
// 先解密敏感字段,再脱敏显示
|
||||
String decryptedUrl = EncryptUtil.decrypt(doObj.getOpenlistUrl());
|
||||
String decryptedUsername = EncryptUtil.decrypt(doObj.getOpenlistUsername());
|
||||
var map = new LinkedHashMap<String, Object>();
|
||||
map.put("id", doObj.getId());
|
||||
map.put("openlistUrl", maskUrl(decryptedUrl));
|
||||
map.put("openlistUsername", maskString(decryptedUsername));
|
||||
map.put("storageId", doObj.getStorageId());
|
||||
map.put("mountPath", doObj.getMountPath());
|
||||
map.put("status", doObj.getStatus());
|
||||
map.put("tokenValid", doObj.getToken() != null && doObj.getTokenExpireTime() != null
|
||||
&& System.currentTimeMillis() < doObj.getTokenExpireTime() - 3600000);
|
||||
map.put("createTime", doObj.getCreateTime() != null ? doObj.getCreateTime().toString() : null);
|
||||
map.put("updateTime", doObj.getUpdateTime() != null ? doObj.getUpdateTime().toString() : null);
|
||||
return map;
|
||||
}
|
||||
|
||||
// ==================== 插入/更新 ====================
|
||||
|
||||
/**
|
||||
* 绑定或更新用户的 OpenList 连接配置
|
||||
* 敏感信息自动加密后存储
|
||||
*
|
||||
* @param entity 用户 OpenList 配置(明文)
|
||||
* @return 记录ID
|
||||
*/
|
||||
public Long saveOrUpdateConfig(UserOpenlist entity) {
|
||||
var doObj = toDO(entity);
|
||||
// 加密敏感信息
|
||||
doObj.setOpenlistUrl(EncryptUtil.encrypt(entity.getOpenlistUrl()));
|
||||
doObj.setOpenlistUsername(EncryptUtil.encrypt(entity.getOpenlistUsername()));
|
||||
doObj.setOpenlistPassword(EncryptUtil.encrypt(entity.getOpenlistPassword()));
|
||||
|
||||
// 检查是否已存在
|
||||
var existing = findByUserId(entity.getUserId());
|
||||
if (existing != null) {
|
||||
// 更新:保留已有的 Token 缓存
|
||||
doObj.setId(existing.getId());
|
||||
doObj.setToken(existing.getToken());
|
||||
doObj.setTokenExpireTime(existing.getTokenExpireTime());
|
||||
updateById(doObj);
|
||||
return doObj.getId();
|
||||
} else {
|
||||
// 新增
|
||||
save(doObj);
|
||||
return doObj.getId();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 Token 缓存
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param token OpenList API Token
|
||||
* @param expireTime Token 过期时间戳(毫秒)
|
||||
*/
|
||||
public void updateToken(Long userId, String token, Long expireTime) {
|
||||
var wrapper = new LambdaUpdateWrapper<UserOpenlistDO>()
|
||||
.eq(UserOpenlistDO::getUserId, userId)
|
||||
.set(UserOpenlistDO::getToken, token)
|
||||
.set(UserOpenlistDO::getTokenExpireTime, expireTime)
|
||||
.set(UserOpenlistDO::getUpdateTime, LocalDateTime.now());
|
||||
update(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新存储配置(storageId、mountPath)
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param storageId 存储ID
|
||||
* @param mountPath 挂载路径
|
||||
*/
|
||||
public void updateStorageConfig(Long userId, String storageId, String mountPath) {
|
||||
var wrapper = new LambdaUpdateWrapper<UserOpenlistDO>()
|
||||
.eq(UserOpenlistDO::getUserId, userId)
|
||||
.set(UserOpenlistDO::getStorageId, storageId)
|
||||
.set(UserOpenlistDO::getMountPath, mountPath)
|
||||
.set(UserOpenlistDO::getUpdateTime, LocalDateTime.now());
|
||||
update(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新心跳检测结果
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param heartbeatStatus 心跳状态:success / failed
|
||||
* @param heartbeatError 失败原因(成功时为 null)
|
||||
*/
|
||||
public void updateHeartbeat(Long userId, String heartbeatStatus, String heartbeatError) {
|
||||
var wrapper = new LambdaUpdateWrapper<UserOpenlistDO>()
|
||||
.eq(UserOpenlistDO::getUserId, userId)
|
||||
.set(UserOpenlistDO::getLastHeartbeatTime, LocalDateTime.now())
|
||||
.set(UserOpenlistDO::getHeartbeatStatus, heartbeatStatus)
|
||||
.set(UserOpenlistDO::getHeartbeatError, heartbeatError)
|
||||
.set(UserOpenlistDO::getUpdateTime, LocalDateTime.now());
|
||||
update(wrapper);
|
||||
}
|
||||
|
||||
// ==================== 删除 ====================
|
||||
|
||||
/**
|
||||
* 解绑用户的 OpenList 连接配置
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 是否删除成功
|
||||
*/
|
||||
public boolean unbind(Long userId) {
|
||||
var wrapper = new LambdaQueryWrapper<UserOpenlistDO>()
|
||||
.eq(UserOpenlistDO::getUserId, userId);
|
||||
return remove(wrapper);
|
||||
}
|
||||
|
||||
// ==================== Entity ↔ DO 转换 ====================
|
||||
|
||||
/**
|
||||
* DO → Entity 转换
|
||||
* 敏感信息自动解密
|
||||
*/
|
||||
private UserOpenlist toEntity(UserOpenlistDO doObj) {
|
||||
var entity = new UserOpenlist();
|
||||
entity.setId(doObj.getId());
|
||||
entity.setUserId(doObj.getUserId());
|
||||
// 解密敏感信息
|
||||
entity.setOpenlistUrl(EncryptUtil.decrypt(doObj.getOpenlistUrl()));
|
||||
entity.setOpenlistUsername(EncryptUtil.decrypt(doObj.getOpenlistUsername()));
|
||||
entity.setOpenlistPassword(EncryptUtil.decrypt(doObj.getOpenlistPassword()));
|
||||
entity.setStorageId(doObj.getStorageId());
|
||||
entity.setMountPath(doObj.getMountPath());
|
||||
entity.setToken(doObj.getToken());
|
||||
entity.setTokenExpireTime(doObj.getTokenExpireTime());
|
||||
entity.setStatus(doObj.getStatus());
|
||||
entity.setLastHeartbeatTime(doObj.getLastHeartbeatTime());
|
||||
entity.setHeartbeatStatus(doObj.getHeartbeatStatus());
|
||||
entity.setHeartbeatError(doObj.getHeartbeatError());
|
||||
entity.setCreateTime(doObj.getCreateTime());
|
||||
entity.setUpdateTime(doObj.getUpdateTime());
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entity → DO 转换
|
||||
* 注意:敏感信息加密由 saveOrUpdateConfig 方法处理,这里不加密
|
||||
*/
|
||||
private UserOpenlistDO toDO(UserOpenlist entity) {
|
||||
var doObj = new UserOpenlistDO();
|
||||
doObj.setId(entity.getId());
|
||||
doObj.setUserId(entity.getUserId());
|
||||
// 这里不加密,由 saveOrUpdateConfig 统一处理
|
||||
doObj.setOpenlistUrl(entity.getOpenlistUrl());
|
||||
doObj.setOpenlistUsername(entity.getOpenlistUsername());
|
||||
doObj.setOpenlistPassword(entity.getOpenlistPassword());
|
||||
doObj.setStorageId(entity.getStorageId());
|
||||
doObj.setMountPath(entity.getMountPath());
|
||||
doObj.setToken(entity.getToken());
|
||||
doObj.setTokenExpireTime(entity.getTokenExpireTime());
|
||||
doObj.setStatus(entity.getStatus());
|
||||
doObj.setLastHeartbeatTime(entity.getLastHeartbeatTime());
|
||||
doObj.setHeartbeatStatus(entity.getHeartbeatStatus());
|
||||
doObj.setHeartbeatError(entity.getHeartbeatError());
|
||||
doObj.setCreateTime(entity.getCreateTime());
|
||||
doObj.setUpdateTime(entity.getUpdateTime());
|
||||
return doObj;
|
||||
}
|
||||
|
||||
/**
|
||||
* URL 脱敏显示
|
||||
* 例如:http://ldfun.asia:13000 → http://ld***:13000
|
||||
*/
|
||||
private String maskUrl(String url) {
|
||||
if (url == null || url.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
// 提取域名部分进行脱敏
|
||||
int start = url.indexOf("://");
|
||||
if (start < 0) return url;
|
||||
String protocol = url.substring(0, start + 3);
|
||||
String rest = url.substring(start + 3);
|
||||
int colonIdx = rest.indexOf(':');
|
||||
int slashIdx = rest.indexOf('/');
|
||||
int endIdx = colonIdx > 0 ? colonIdx : (slashIdx > 0 ? slashIdx : rest.length());
|
||||
String host = rest.substring(0, endIdx);
|
||||
String suffix = rest.substring(endIdx);
|
||||
if (host.length() > 3) {
|
||||
return protocol + host.substring(0, 2) + "***" + suffix;
|
||||
}
|
||||
return protocol + "***" + suffix;
|
||||
} catch (Exception e) {
|
||||
return "***";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 字符串脱敏显示
|
||||
* 保留前2位和后2位,中间用 *** 替换
|
||||
* 例如:djl1022 → dj***22
|
||||
* 例如:Djl1022! → Dj***2!
|
||||
*/
|
||||
private String maskString(String str) {
|
||||
if (str == null || str.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
if (str.length() <= 4) {
|
||||
return str.substring(0, 1) + "***" + str.substring(str.length() - 1);
|
||||
}
|
||||
return str.substring(0, 2) + "***" + str.substring(str.length() - 2);
|
||||
}
|
||||
}
|
||||
@ -223,15 +223,6 @@ public class UserRepository extends ServiceImpl<UserMapper, UserDO> {
|
||||
|
||||
/**
|
||||
* Entity → DO 转换
|
||||
*
|
||||
* 时间字段兜底说明:
|
||||
* UserDO.createTime 标注了 @TableField(fill = FieldFill.INSERT),
|
||||
* 期望由 MyBatis-Plus 的 MetaObjectHandler 自动填充。
|
||||
* 但本项目未配置 MetaObjectHandler(参考 IconRepository.toIconDO 的处理),
|
||||
* 若调用方未显式设置时间,会导致 INSERT 语句带上 create_time = null,
|
||||
* 而 wp_user.create_time 为 NOT NULL,会抛出
|
||||
* SQLIntegrityConstraintViolationException: Column 'create_time' cannot be null。
|
||||
* 因此在此处统一兜底:时间为 null 时取当前时间,保证写入安全。
|
||||
*/
|
||||
private UserDO toUserDO(User user) {
|
||||
var userDO = new UserDO();
|
||||
@ -249,10 +240,8 @@ public class UserRepository extends ServiceImpl<UserMapper, UserDO> {
|
||||
userDO.setLockUntil(user.getLockUntil());
|
||||
userDO.setFailCount(user.getFailCount());
|
||||
userDO.setTmdbApiKey(user.getTmdbApiKey());
|
||||
// 时间字段兜底:未设置时使用当前时间,避免 NOT NULL 约束报错
|
||||
var now = LocalDateTime.now();
|
||||
userDO.setCreateTime(user.getCreateTime() != null ? user.getCreateTime() : now);
|
||||
userDO.setUpdateTime(user.getUpdateTime() != null ? user.getUpdateTime() : now);
|
||||
userDO.setCreateTime(user.getCreateTime());
|
||||
userDO.setUpdateTime(user.getUpdateTime());
|
||||
userDO.setIsDeleted(user.getIsDeleted());
|
||||
userDO.setRemark(user.getRemark());
|
||||
return userDO;
|
||||
|
||||
@ -40,12 +40,6 @@ public class Drive implements Serializable {
|
||||
/** 网盘 Logo URL:存储在 MinIO 中的图片链接 */
|
||||
private String logoUrl;
|
||||
|
||||
/** Logo 最后访问时间(用于回收策略) */
|
||||
private LocalDateTime logoLastAccessTime;
|
||||
|
||||
/** Logo 状态:normal(正常)/recycling(回收中)/deleted(已删除) */
|
||||
private String logoStatus;
|
||||
|
||||
// ==================== 统计属性 ====================
|
||||
|
||||
/** 文件夹数量 */
|
||||
@ -57,14 +51,6 @@ public class Drive implements Serializable {
|
||||
/** 总大小(字节) */
|
||||
private Long totalSize;
|
||||
|
||||
// ==================== OpenList 关联属性 ====================
|
||||
|
||||
/** OpenList 存储ID:关联 OpenList 中的存储配置 */
|
||||
private String openlistStorageId;
|
||||
|
||||
/** OpenList 挂载路径:如 /movies */
|
||||
private String openlistMountPath;
|
||||
|
||||
// ==================== 状态属性 ====================
|
||||
|
||||
/**
|
||||
@ -122,12 +108,6 @@ public class Drive implements Serializable {
|
||||
public String getLogoUrl() { return logoUrl; }
|
||||
public void setLogoUrl(String logoUrl) { this.logoUrl = logoUrl; }
|
||||
|
||||
public LocalDateTime getLogoLastAccessTime() { return logoLastAccessTime; }
|
||||
public void setLogoLastAccessTime(LocalDateTime logoLastAccessTime) { this.logoLastAccessTime = logoLastAccessTime; }
|
||||
|
||||
public String getLogoStatus() { return logoStatus; }
|
||||
public void setLogoStatus(String logoStatus) { this.logoStatus = logoStatus; }
|
||||
|
||||
public Integer getFolderCount() { return folderCount; }
|
||||
public void setFolderCount(Integer folderCount) { this.folderCount = folderCount; }
|
||||
|
||||
@ -146,12 +126,6 @@ public class Drive implements Serializable {
|
||||
public LocalDateTime getCreateTime() { return createTime; }
|
||||
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
|
||||
|
||||
public String getOpenlistStorageId() { return openlistStorageId; }
|
||||
public void setOpenlistStorageId(String openlistStorageId) { this.openlistStorageId = openlistStorageId; }
|
||||
|
||||
public String getOpenlistMountPath() { return openlistMountPath; }
|
||||
public void setOpenlistMountPath(String openlistMountPath) { this.openlistMountPath = openlistMountPath; }
|
||||
|
||||
// ==================== Object方法重写 ====================
|
||||
|
||||
@Override
|
||||
|
||||
@ -121,20 +121,6 @@ public class DriveMovie implements Serializable {
|
||||
/** 标签:逗号分隔,如 "科幻,IMAX,杜比视界" */
|
||||
private String tags;
|
||||
|
||||
/** 主演(逗号分隔,TMDB刮削) */
|
||||
private String casts;
|
||||
|
||||
/** 导演(逗号分隔,TMDB刮削) */
|
||||
private String directors;
|
||||
|
||||
// ==================== OpenList 关联属性 ====================
|
||||
|
||||
/** OpenList 文件路径:如 /movies/阿凡达.mp4 */
|
||||
private String openlistFilePath;
|
||||
|
||||
/** 存储类型:metadata(仅元数据)/ openlist(OpenList 管理实际文件) */
|
||||
private String storageType;
|
||||
|
||||
// ==================== 时间戳属性 ====================
|
||||
|
||||
/** 更新时间 */
|
||||
@ -243,24 +229,12 @@ public class DriveMovie implements Serializable {
|
||||
public String getTags() { return tags; }
|
||||
public void setTags(String tags) { this.tags = tags; }
|
||||
|
||||
public String getCasts() { return casts; }
|
||||
public void setCasts(String casts) { this.casts = casts; }
|
||||
|
||||
public String getDirectors() { return directors; }
|
||||
public void setDirectors(String directors) { this.directors = directors; }
|
||||
|
||||
public LocalDateTime getUpdateTime() { return updateTime; }
|
||||
public void setUpdateTime(LocalDateTime updateTime) { this.updateTime = updateTime; }
|
||||
|
||||
public LocalDateTime getCreateTime() { return createTime; }
|
||||
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
|
||||
|
||||
public String getOpenlistFilePath() { return openlistFilePath; }
|
||||
public void setOpenlistFilePath(String openlistFilePath) { this.openlistFilePath = openlistFilePath; }
|
||||
|
||||
public String getStorageType() { return storageType; }
|
||||
public void setStorageType(String storageType) { this.storageType = storageType; }
|
||||
|
||||
// ==================== Object方法重写 ====================
|
||||
|
||||
@Override
|
||||
|
||||
@ -93,12 +93,6 @@ public class Movie implements Serializable {
|
||||
/** 标签:逗号分隔,如"科幻, IMAX, 杜比视界" */
|
||||
private String tags;
|
||||
|
||||
/** 主演(逗号分隔,TMDB刮削) */
|
||||
private String casts;
|
||||
|
||||
/** 导演(逗号分隔,TMDB刮削) */
|
||||
private String directors;
|
||||
|
||||
// ==================== 时间戳属性 ====================
|
||||
|
||||
/** 更新时间:资源最后更新时间 */
|
||||
@ -173,12 +167,6 @@ public class Movie implements Serializable {
|
||||
public String getTags() { return tags; }
|
||||
public void setTags(String tags) { this.tags = tags; }
|
||||
|
||||
public String getCasts() { return casts; }
|
||||
public void setCasts(String casts) { this.casts = casts; }
|
||||
|
||||
public String getDirectors() { return directors; }
|
||||
public void setDirectors(String directors) { this.directors = directors; }
|
||||
|
||||
public LocalDateTime getUpdateTime() { return updateTime; }
|
||||
public void setUpdateTime(LocalDateTime updateTime) { this.updateTime = updateTime; }
|
||||
|
||||
|
||||
@ -1,194 +0,0 @@
|
||||
package com.wangpan.common.entity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 用户 OpenList 连接配置实体类
|
||||
* 对应数据库表:wp_user_openlist
|
||||
*
|
||||
* 功能:
|
||||
* 1. 存储用户绑定的 OpenList 实例信息(地址、账号、密码)
|
||||
* 2. 缓存 OpenList API Token,避免每次调用都重新登录
|
||||
* 3. 每个用户只能绑定一个 OpenList 实例(user_id 唯一)
|
||||
*
|
||||
* 设计说明:
|
||||
* - 敏感信息(URL、账号、密码)在数据库中加密存储
|
||||
* - Token 有有效期,过期后需要重新登录获取
|
||||
* - 支持禁用/启用状态
|
||||
*/
|
||||
public class UserOpenlist implements Serializable {
|
||||
|
||||
/** 序列化版本号 */
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
// ==================== 基础属性 ====================
|
||||
|
||||
/** 主键ID */
|
||||
private Long id;
|
||||
|
||||
/** 用户ID(关联 wp_user 表) */
|
||||
private Long userId;
|
||||
|
||||
/** OpenList 服务地址(明文,用于调用 API) */
|
||||
private String openlistUrl;
|
||||
|
||||
/** OpenList 账号(明文,用于登录) */
|
||||
private String openlistUsername;
|
||||
|
||||
/** OpenList 密码(明文,用于登录) */
|
||||
private String openlistPassword;
|
||||
|
||||
// ==================== OpenList 配置属性 ====================
|
||||
|
||||
/** OpenList 存储ID(用户在 OpenList 后台配置的存储) */
|
||||
private String storageId;
|
||||
|
||||
/** 挂载路径,如 /uc */
|
||||
private String mountPath;
|
||||
|
||||
// ==================== Token 缓存属性 ====================
|
||||
|
||||
/** OpenList API Token(缓存) */
|
||||
private String token;
|
||||
|
||||
/** Token 过期时间戳(毫秒) */
|
||||
private Long tokenExpireTime;
|
||||
|
||||
// ==================== 状态属性 ====================
|
||||
|
||||
/** 状态:active / disabled */
|
||||
private String status;
|
||||
|
||||
/** 最后一次心跳检测时间 */
|
||||
private LocalDateTime lastHeartbeatTime;
|
||||
|
||||
/** 心跳状态:success / failed */
|
||||
private String heartbeatStatus;
|
||||
|
||||
/** 心跳检测失败原因 */
|
||||
private String heartbeatError;
|
||||
|
||||
// ==================== 时间戳属性 ====================
|
||||
|
||||
/** 创建时间 */
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/** 更新时间 */
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
// ==================== 构造方法 ====================
|
||||
|
||||
/** 无参构造方法 */
|
||||
public UserOpenlist() {}
|
||||
|
||||
/**
|
||||
* 基础构造方法
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param openlistUrl OpenList 服务地址
|
||||
* @param openlistUsername OpenList 账号
|
||||
* @param openlistPassword OpenList 密码
|
||||
*/
|
||||
public UserOpenlist(Long userId, String openlistUrl, String openlistUsername, String openlistPassword) {
|
||||
this.userId = userId;
|
||||
this.openlistUrl = openlistUrl;
|
||||
this.openlistUsername = openlistUsername;
|
||||
this.openlistPassword = openlistPassword;
|
||||
this.status = "active";
|
||||
this.createTime = LocalDateTime.now();
|
||||
}
|
||||
|
||||
// ==================== 业务方法 ====================
|
||||
|
||||
/**
|
||||
* 检查 Token 是否有效
|
||||
* Token 有效期 24 小时,提前 1 小时视为过期
|
||||
*
|
||||
* @return true 表示 Token 有效,false 表示已过期或不存在
|
||||
*/
|
||||
public boolean isTokenValid() {
|
||||
if (token == null || token.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
if (tokenExpireTime == null) {
|
||||
return false;
|
||||
}
|
||||
// 提前 1 小时刷新
|
||||
return System.currentTimeMillis() < tokenExpireTime - 3600000;
|
||||
}
|
||||
|
||||
// ==================== Getter和Setter方法 ====================
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
|
||||
public String getOpenlistUrl() { return openlistUrl; }
|
||||
public void setOpenlistUrl(String openlistUrl) { this.openlistUrl = openlistUrl; }
|
||||
|
||||
public String getOpenlistUsername() { return openlistUsername; }
|
||||
public void setOpenlistUsername(String openlistUsername) { this.openlistUsername = openlistUsername; }
|
||||
|
||||
public String getOpenlistPassword() { return openlistPassword; }
|
||||
public void setOpenlistPassword(String openlistPassword) { this.openlistPassword = openlistPassword; }
|
||||
|
||||
public String getStorageId() { return storageId; }
|
||||
public void setStorageId(String storageId) { this.storageId = storageId; }
|
||||
|
||||
public String getMountPath() { return mountPath; }
|
||||
public void setMountPath(String mountPath) { this.mountPath = mountPath; }
|
||||
|
||||
public String getToken() { return token; }
|
||||
public void setToken(String token) { this.token = token; }
|
||||
|
||||
public Long getTokenExpireTime() { return tokenExpireTime; }
|
||||
public void setTokenExpireTime(Long tokenExpireTime) { this.tokenExpireTime = tokenExpireTime; }
|
||||
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
|
||||
public LocalDateTime getLastHeartbeatTime() { return lastHeartbeatTime; }
|
||||
public void setLastHeartbeatTime(LocalDateTime lastHeartbeatTime) { this.lastHeartbeatTime = lastHeartbeatTime; }
|
||||
|
||||
public String getHeartbeatStatus() { return heartbeatStatus; }
|
||||
public void setHeartbeatStatus(String heartbeatStatus) { this.heartbeatStatus = heartbeatStatus; }
|
||||
|
||||
public String getHeartbeatError() { return heartbeatError; }
|
||||
public void setHeartbeatError(String heartbeatError) { this.heartbeatError = heartbeatError; }
|
||||
|
||||
public LocalDateTime getCreateTime() { return createTime; }
|
||||
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
|
||||
|
||||
public LocalDateTime getUpdateTime() { return updateTime; }
|
||||
public void setUpdateTime(LocalDateTime updateTime) { this.updateTime = updateTime; }
|
||||
|
||||
// ==================== Object方法重写 ====================
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
UserOpenlist that = (UserOpenlist) o;
|
||||
return Objects.equals(id, that.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "UserOpenlist{" +
|
||||
"id=" + id +
|
||||
", userId=" + userId +
|
||||
", openlistUrl='" + openlistUrl + '\'' +
|
||||
", status='" + status + '\'' +
|
||||
", createTime=" + createTime +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@ -33,16 +33,20 @@ public class EncryptUtil {
|
||||
private static final String ENCRYPT_SUFFIX = ")";
|
||||
|
||||
/**
|
||||
* 默认加密算法 - 与 Spring Jasypt 配置保持一致
|
||||
* 必须与 application.yml 中 jasypt.encryptor.algorithm 相同
|
||||
* 默认加密算法 - PBEWithMD5AndDES
|
||||
* PBE = Password Based Encryption(基于密码的加密)
|
||||
* MD5 + DES 是经典组合,兼容性好
|
||||
* 如需更高安全性可改为 PBEWITHSHA256AND256BITAES-CBC-BC
|
||||
*/
|
||||
private static final String DEFAULT_ALGORITHM = "PBEWITHHMACSHA512ANDAES_256";
|
||||
private static final String DEFAULT_ALGORITHM = "PBEWithMD5AndDES";
|
||||
|
||||
/**
|
||||
* 默认加密密钥 - 与 Spring Jasypt 配置保持一致
|
||||
* 必须与 application.yml 中 jasypt.encryptor.password 相同
|
||||
* 默认加密密钥(仅用于开发环境!)
|
||||
* 生产环境必须通过环境变量覆盖此值:
|
||||
* -Djasypt.encryptor.password=你的密钥
|
||||
* 或设置环境变量:JASYPT_ENCRYPTOR_PASSWORD=你的密钥
|
||||
*/
|
||||
private static final String DEFAULT_PASSWORD = "wangpan-secret-key-2024";
|
||||
private static final String DEFAULT_PASSWORD = "wangpan2024";
|
||||
|
||||
// ==================== 单例实例 ====================
|
||||
|
||||
@ -179,14 +183,13 @@ public class EncryptUtil {
|
||||
private static StandardPBEStringEncryptor createEncryptor() {
|
||||
StandardPBEStringEncryptor newEncryptor = new StandardPBEStringEncryptor();
|
||||
|
||||
// 创建配置对象(使用SimplePBEConfig实现类)
|
||||
SimplePBEConfig config = new SimplePBEConfig();
|
||||
config.setAlgorithm(DEFAULT_ALGORITHM);
|
||||
config.setPassword(getEncryptionPassword());
|
||||
config.setKeyObtentionIterations(1000);
|
||||
config.setIvGeneratorClassName("org.jasypt.iv.RandomIvGenerator");
|
||||
config.setKeyObtentionIterations(1000); // 密钥派生迭代次数
|
||||
|
||||
newEncryptor.setConfig(config);
|
||||
newEncryptor.setStringOutputType("base64");
|
||||
return newEncryptor;
|
||||
}
|
||||
|
||||
|
||||
@ -51,8 +51,8 @@ public class MinioUtil {
|
||||
@Value("${minio.bucketName}")
|
||||
private String bucketName;
|
||||
|
||||
/** 预签名 URL 有效期(365天,确保在资源生命周期内 URL 始终有效) */
|
||||
private static final int PRESIGNED_URL_EXPIRY_DAYS = 365;
|
||||
/** 预签名 URL 有效期(7天) */
|
||||
private static final int PRESIGNED_URL_EXPIRY_DAYS = 7;
|
||||
|
||||
/** MinIO 客户端实例(单例模式) */
|
||||
private static volatile MinioClient client;
|
||||
|
||||
@ -1,638 +0,0 @@
|
||||
package com.wangpan.common.util;
|
||||
|
||||
import com.wangpan.common.dal.repository.UserOpenlistRepository;
|
||||
import com.wangpan.common.entity.UserOpenlist;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* OpenList API 客户端(用户级连接)
|
||||
*
|
||||
* 每个用户绑定自己的 OpenList 实例(地址、账号、密码),互不干扰。
|
||||
* Token 缓存到数据库(wp_user_openlist 表),避免每次调用都重新登录。
|
||||
*
|
||||
* 重要:OpenList 使用内存缓存验证 Token,服务重启后所有 Token 失效。
|
||||
* 本客户端实现了自动重试机制:当 API 返回 401 "token is invalidated" 时,
|
||||
* 自动刷新 Token 并重试请求,保证 API 调用的连续性。
|
||||
*
|
||||
* 使用方式:
|
||||
* 1. 前端调用 /api/openlist/bind 绑定 OpenList(传入地址、账号、密码)
|
||||
* 2. 后端验证连接 → 获取 Token → 缓存到数据库
|
||||
* 3. 后续 API 调用自动使用用户的 OpenList 配置
|
||||
*/
|
||||
@Component
|
||||
public class OpenListClient {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(OpenListClient.class);
|
||||
|
||||
@Autowired
|
||||
private UserOpenlistRepository userOpenlistRepository;
|
||||
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
/**
|
||||
* 测试 OpenList 连接(不保存配置)
|
||||
* 用于前端绑定前验证地址、账号、密码是否正确
|
||||
*
|
||||
* @param openlistUrl OpenList 服务地址,如 http://ldfun.asia:13000
|
||||
* @param openlistUsername OpenList 账号
|
||||
* @param openlistPassword OpenList 密码
|
||||
* @return 连接结果,包含 success(是否成功)、message(提示信息)、token(Token,成功时返回)
|
||||
*/
|
||||
public Map<String, Object> testConnection(String openlistUrl, String openlistUsername, String openlistPassword) {
|
||||
var result = new HashMap<String, Object>();
|
||||
|
||||
try {
|
||||
// 调用登录接口
|
||||
String url = openlistUrl + "/api/auth/login";
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("username", openlistUsername);
|
||||
requestBody.put("password", openlistPassword);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||
ResponseEntity<Map> response = restTemplate.postForEntity(url, entity, Map.class);
|
||||
|
||||
Map<String, Object> body = response.getBody();
|
||||
if (body != null && (int) body.get("code") == 200) {
|
||||
Map<String, Object> data = (Map<String, Object>) body.get("data");
|
||||
String token = (String) data.get("token");
|
||||
|
||||
result.put("success", true);
|
||||
result.put("message", "连接成功");
|
||||
result.put("token", token);
|
||||
log.info("OpenList 连接测试成功: {}", openlistUrl);
|
||||
} else {
|
||||
result.put("success", false);
|
||||
result.put("message", "连接失败:" + (body != null ? body.get("message") : "未知错误"));
|
||||
log.warn("OpenList 连接测试失败: {}", openlistUrl);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
result.put("success", false);
|
||||
result.put("message", "连接异常:" + e.getMessage());
|
||||
log.error("OpenList 连接测试异常: {}", openlistUrl, e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定用户的 OpenList 连接配置
|
||||
* 验证连接 → 保存配置 → 缓存 Token
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param openlistUrl OpenList 服务地址
|
||||
* @param openlistUsername OpenList 账号
|
||||
* @param openlistPassword OpenList 密码
|
||||
* @return 绑定结果,包含 success(是否成功)、message(提示信息)
|
||||
*/
|
||||
public Map<String, Object> bindConnection(Long userId, String openlistUrl, String openlistUsername, String openlistPassword) {
|
||||
var result = new HashMap<String, Object>();
|
||||
|
||||
// 1. 测试连接
|
||||
var testResult = testConnection(openlistUrl, openlistUsername, openlistPassword);
|
||||
if (!(boolean) testResult.get("success")) {
|
||||
result.put("success", false);
|
||||
result.put("message", testResult.get("message"));
|
||||
return result;
|
||||
}
|
||||
|
||||
// 2. 保存配置(敏感信息自动加密)
|
||||
UserOpenlist entity = new UserOpenlist(userId, openlistUrl, openlistUsername, openlistPassword);
|
||||
userOpenlistRepository.saveOrUpdateConfig(entity);
|
||||
|
||||
// 3. 缓存 Token(Token 有效期 24 小时)
|
||||
String token = (String) testResult.get("token");
|
||||
long expireTime = System.currentTimeMillis() + 86400000; // 24 小时
|
||||
userOpenlistRepository.updateToken(userId, token, expireTime);
|
||||
|
||||
result.put("success", true);
|
||||
result.put("message", "绑定成功");
|
||||
log.info("用户 {} 绑定 OpenList 成功: {}", userId, openlistUrl);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的 OpenList 连接配置(Entity)
|
||||
* 敏感信息自动解密
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 用户 OpenList 配置,未绑定时返回 null
|
||||
*/
|
||||
public UserOpenlist getUserOpenlist(Long userId) {
|
||||
return userOpenlistRepository.findByUserId(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的 OpenList 连接信息(脱敏)
|
||||
* 用于前端展示,不返回明文密码
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 连接信息 Map,未绑定时返回 null
|
||||
*/
|
||||
public Map<String, Object> getUserOpenlistInfo(Long userId) {
|
||||
return userOpenlistRepository.findInfoByUserId(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解绑用户的 OpenList 连接配置
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 解绑结果,包含 success(是否成功)、message(提示信息)
|
||||
*/
|
||||
public Map<String, Object> unbindConnection(Long userId) {
|
||||
var result = new HashMap<String, Object>();
|
||||
|
||||
boolean deleted = userOpenlistRepository.unbind(userId);
|
||||
if (deleted) {
|
||||
result.put("success", true);
|
||||
result.put("message", "解绑成功");
|
||||
log.info("用户 {} 解绑 OpenList 成功", userId);
|
||||
} else {
|
||||
result.put("success", false);
|
||||
result.put("message", "解绑失败:未找到绑定记录");
|
||||
log.warn("用户 {} 解绑 OpenList 失败:未找到绑定记录", userId);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制刷新并验证 Token(公开方法,供 Controller 调用)
|
||||
* 用于在同步前确保 Token 有效
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 是否成功刷新并验证 Token
|
||||
*/
|
||||
public boolean refreshAndVerifyToken(Long userId) {
|
||||
UserOpenlist config = getUserOpenlist(userId);
|
||||
if (config == null) {
|
||||
log.warn("用户 {} 未绑定 OpenList,无法刷新 Token", userId);
|
||||
return false;
|
||||
}
|
||||
|
||||
log.info("用户 {} 开始强制刷新并验证 OpenList Token", userId);
|
||||
var testResult = testConnection(config.getOpenlistUrl(), config.getOpenlistUsername(), config.getOpenlistPassword());
|
||||
boolean success = (boolean) testResult.get("success");
|
||||
|
||||
if (success && testResult.get("token") != null) {
|
||||
String newToken = (String) testResult.get("token");
|
||||
long expireTime = System.currentTimeMillis() + 86400000; // 24 小时
|
||||
userOpenlistRepository.updateToken(userId, newToken, expireTime);
|
||||
log.info("用户 {} 的 OpenList Token 已强制刷新并验证成功", userId);
|
||||
return true;
|
||||
} else {
|
||||
log.warn("用户 {} 的 OpenList Token 刷新失败: {}", userId, testResult.get("message"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 文件操作 API(使用用户级连接) ====================
|
||||
|
||||
/**
|
||||
* 获取有效的 Token(自动刷新)
|
||||
* 从数据库读取 Token,如果过期则重新登录获取
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return JWT Token
|
||||
*/
|
||||
private String getValidToken(Long userId) {
|
||||
UserOpenlist config = getUserOpenlist(userId);
|
||||
if (config == null) {
|
||||
throw new RuntimeException("用户未绑定 OpenList");
|
||||
}
|
||||
|
||||
// 检查 Token 是否有效
|
||||
if (config.isTokenValid()) {
|
||||
return config.getToken();
|
||||
}
|
||||
|
||||
// Token 过期,重新登录获取
|
||||
log.info("用户 {} 的 OpenList Token 已过期,自动刷新", userId);
|
||||
var testResult = testConnection(config.getOpenlistUrl(), config.getOpenlistUsername(), config.getOpenlistPassword());
|
||||
if (!(boolean) testResult.get("success")) {
|
||||
throw new RuntimeException("OpenList Token 刷新失败:" + testResult.get("message"));
|
||||
}
|
||||
|
||||
// 缓存新 Token
|
||||
String token = (String) testResult.get("token");
|
||||
long expireTime = System.currentTimeMillis() + 86400000; // 24 小时
|
||||
userOpenlistRepository.updateToken(userId, token, expireTime);
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查响应是否因 Token 失效而失败
|
||||
* OpenList 返回格式:{"code": 401, "message": "token is invalidated", ...}
|
||||
*
|
||||
* @param response 响应体 Map
|
||||
* @return 是否 Token 失效
|
||||
*/
|
||||
private boolean isTokenInvalidated(Map<String, Object> response) {
|
||||
if (response == null) return false;
|
||||
|
||||
Object code = response.get("code");
|
||||
Object message = response.get("message");
|
||||
|
||||
log.info("检查 Token 失效: code={}, message={}", code, message);
|
||||
|
||||
// 更健壮的类型转换:支持 Number、String 等多种类型
|
||||
int codeValue = 0;
|
||||
if (code instanceof Number) {
|
||||
codeValue = ((Number) code).intValue();
|
||||
} else if (code instanceof String) {
|
||||
try {
|
||||
codeValue = Integer.parseInt((String) code);
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("无法解析 code 值: {}", code);
|
||||
return false;
|
||||
}
|
||||
} else if (code != null) {
|
||||
log.warn("未知的 code 类型: {}, 值: {}", code.getClass().getName(), code);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 大小写不敏感匹配,兼容 "token is invalidated"、"Token is invalidated" 等
|
||||
boolean isInvalidated = codeValue == 401 && message != null
|
||||
&& message.toString().toLowerCase().contains("token is invalidated");
|
||||
if (isInvalidated) {
|
||||
log.info("检测到 Token 失效: code={}, message={}", code, message);
|
||||
}
|
||||
|
||||
return isInvalidated;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 POST 请求(带自动重试)
|
||||
* 当 Token 失效时自动刷新并重试一次
|
||||
*
|
||||
* 重要:OpenList 在 Token 失效时可能返回 HTTP 401 状态码,
|
||||
* RestTemplate 会抛出 HttpClientErrorException 异常,
|
||||
* 而不是返回响应体。因此需要捕获异常并检查状态码。
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param url 请求 URL
|
||||
* @param requestBody 请求体
|
||||
* @return 响应结果
|
||||
*/
|
||||
private Map<String, Object> postWithRetry(Long userId, String url, Map<String, Object> requestBody) {
|
||||
String token = getValidToken(userId);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("Authorization", token);
|
||||
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||
Map<String, Object> result = null;
|
||||
|
||||
try {
|
||||
ResponseEntity<Map> response = restTemplate.postForEntity(url, entity, Map.class);
|
||||
result = response.getBody();
|
||||
} catch (org.springframework.web.client.HttpClientErrorException e) {
|
||||
// 捕获 HTTP 4xx 错误(包括 401 Unauthorized)
|
||||
log.warn("用户 {} 请求 OpenList API 失败: HTTP {} - {}", userId, e.getStatusCode(), e.getMessage());
|
||||
|
||||
// 检查是否是 401 Token 失效
|
||||
if (e.getStatusCode() == HttpStatus.UNAUTHORIZED) {
|
||||
log.info("用户 {} 的 OpenList Token 失效(HTTP 401),强制刷新并重试", userId);
|
||||
token = refreshToken(userId);
|
||||
headers.set("Authorization", token);
|
||||
entity = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
try {
|
||||
ResponseEntity<Map> retryResponse = restTemplate.postForEntity(url, entity, Map.class);
|
||||
result = retryResponse.getBody();
|
||||
log.info("用户 {} Token 刷新后重试成功", userId);
|
||||
} catch (Exception retryEx) {
|
||||
log.error("用户 {} Token 刷新后重试仍然失败", userId, retryEx);
|
||||
throw new RuntimeException("OpenList API 调用失败(重试后): " + retryEx.getMessage(), retryEx);
|
||||
}
|
||||
} else {
|
||||
// 非 401 错误,直接抛出
|
||||
throw new RuntimeException("OpenList API 调用失败: HTTP " + e.getStatusCode() + " - " + e.getMessage(), e);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 其他异常(网络错误等)
|
||||
log.error("用户 {} 请求 OpenList API 异常", userId, e);
|
||||
throw new RuntimeException("OpenList API 调用异常: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
// 检查响应体中的 Token 失效标志(某些情况下 OpenList 可能返回 HTTP 200 但 body 中 code=401)
|
||||
if (isTokenInvalidated(result)) {
|
||||
log.info("用户 {} 的 OpenList Token 失效(响应体 code=401),强制刷新并重试", userId);
|
||||
token = refreshToken(userId);
|
||||
|
||||
// 验证新获取的 Token 是否有效
|
||||
log.info("用户 {} 新 Token: {}", userId, token != null ? token.substring(0, Math.min(20, token.length())) + "..." : "null");
|
||||
|
||||
headers.set("Authorization", token);
|
||||
entity = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
try {
|
||||
ResponseEntity<Map> retryResponse = restTemplate.postForEntity(url, entity, Map.class);
|
||||
result = retryResponse.getBody();
|
||||
|
||||
// 检查重试后的结果是否仍然 Token 失效
|
||||
if (isTokenInvalidated(result)) {
|
||||
log.error("用户 {} Token 刷新后重试仍然返回 code=401,新 Token 无效!", userId);
|
||||
throw new RuntimeException("OpenList Token 刷新后仍然失效,请检查用户名和密码是否正确");
|
||||
}
|
||||
|
||||
log.info("用户 {} Token 刷新后重试成功", userId);
|
||||
} catch (org.springframework.web.client.HttpClientErrorException e) {
|
||||
log.error("用户 {} Token 刷新后重试 HTTP 错误: {}", userId, e.getStatusCode(), e);
|
||||
throw new RuntimeException("OpenList API 调用失败(重试后): HTTP " + e.getStatusCode() + " - " + e.getMessage(), e);
|
||||
} catch (Exception retryEx) {
|
||||
log.error("用户 {} Token 刷新后重试仍然失败", userId, retryEx);
|
||||
throw new RuntimeException("OpenList API 调用失败(重试后): " + retryEx.getMessage(), retryEx);
|
||||
}
|
||||
}
|
||||
|
||||
// 最终检查:确保不会返回 Token 失效的响应
|
||||
// 如果经过多次重试后仍然返回 code=401,说明 Token 刷新机制存在问题
|
||||
if (isTokenInvalidated(result)) {
|
||||
log.error("用户 {} 经过多次重试后仍然返回 Token 失效,请检查 OpenList 用户名和密码", userId);
|
||||
throw new RuntimeException("OpenList Token 持续失效,请检查用户名和密码是否正确,或 OpenList 服务是否正常运行");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制刷新 Token(忽略缓存,重新登录获取)
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 新的 JWT Token
|
||||
*/
|
||||
private String refreshToken(Long userId) {
|
||||
UserOpenlist config = getUserOpenlist(userId);
|
||||
if (config == null) {
|
||||
throw new RuntimeException("用户未绑定 OpenList");
|
||||
}
|
||||
|
||||
log.info("用户 {} 强制刷新 OpenList Token", userId);
|
||||
var testResult = testConnection(config.getOpenlistUrl(), config.getOpenlistUsername(), config.getOpenlistPassword());
|
||||
if (!(boolean) testResult.get("success")) {
|
||||
throw new RuntimeException("OpenList Token 刷新失败:" + testResult.get("message"));
|
||||
}
|
||||
|
||||
// 缓存新 Token
|
||||
String token = (String) testResult.get("token");
|
||||
long expireTime = System.currentTimeMillis() + 86400000; // 24 小时
|
||||
userOpenlistRepository.updateToken(userId, token, expireTime);
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出指定路径下的文件
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param path OpenList 路径,如 /movies
|
||||
* @param page 页码(从 1 开始)
|
||||
* @param perPage 每页数量
|
||||
* @return 文件列表响应
|
||||
*/
|
||||
public Map<String, Object> listFiles(Long userId, String path, int page, int perPage) {
|
||||
UserOpenlist config = getUserOpenlist(userId);
|
||||
if (config == null) {
|
||||
throw new RuntimeException("用户未绑定 OpenList");
|
||||
}
|
||||
|
||||
String url = config.getOpenlistUrl() + "/api/fs/list";
|
||||
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("path", path);
|
||||
requestBody.put("page", page);
|
||||
requestBody.put("per_page", perPage);
|
||||
requestBody.put("refresh", false);
|
||||
|
||||
return postWithRetry(userId, url, requestBody);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件详情和下载链接
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param path 文件路径,如 /movies/阿凡达.mp4
|
||||
* @return 文件详情响应(包含 raw_url)
|
||||
*/
|
||||
public Map<String, Object> getFileDetail(Long userId, String path) {
|
||||
UserOpenlist config = getUserOpenlist(userId);
|
||||
if (config == null) {
|
||||
throw new RuntimeException("用户未绑定 OpenList");
|
||||
}
|
||||
|
||||
String url = config.getOpenlistUrl() + "/api/fs/get";
|
||||
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("path", path);
|
||||
|
||||
return postWithRetry(userId, url, requestBody);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建文件夹
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param path 文件夹路径,如 /movies/科幻
|
||||
* @return 创建结果
|
||||
*/
|
||||
public Map<String, Object> createFolder(Long userId, String path) {
|
||||
UserOpenlist config = getUserOpenlist(userId);
|
||||
if (config == null) {
|
||||
throw new RuntimeException("用户未绑定 OpenList");
|
||||
}
|
||||
|
||||
String url = config.getOpenlistUrl() + "/api/fs/mkdir";
|
||||
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("path", path);
|
||||
|
||||
return postWithRetry(userId, url, requestBody);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件或文件夹
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param path 文件路径
|
||||
* @param names 文件名列表
|
||||
* @return 删除结果
|
||||
*/
|
||||
public Map<String, Object> deleteFiles(Long userId, String path, String[] names) {
|
||||
UserOpenlist config = getUserOpenlist(userId);
|
||||
if (config == null) {
|
||||
throw new RuntimeException("用户未绑定 OpenList");
|
||||
}
|
||||
|
||||
String url = config.getOpenlistUrl() + "/api/fs/remove";
|
||||
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("dir", path);
|
||||
requestBody.put("names", names);
|
||||
|
||||
return postWithRetry(userId, url, requestBody);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的 OpenList 服务地址
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return OpenList base URL,未绑定时返回 null
|
||||
*/
|
||||
public String getUserBaseUrl(Long userId) {
|
||||
UserOpenlist config = getUserOpenlist(userId);
|
||||
return config != null ? config.getOpenlistUrl() : null;
|
||||
}
|
||||
|
||||
// ==================== 同步相关 API ====================
|
||||
|
||||
/**
|
||||
* 获取用户的所有存储驱动器列表
|
||||
* OpenList 中的每个存储(如阿里云盘、百度网盘等)都是一个独立的驱动器
|
||||
* 注意:此 API 需要管理员权限,普通用户调用会失败
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 存储驱动器列表,每个驱动器包含 id、mount_path、driver 等信息
|
||||
* @throws RuntimeException 当 API 调用失败时抛出异常
|
||||
*/
|
||||
public List<Map<String, Object>> getStorageList(Long userId) {
|
||||
UserOpenlist config = getUserOpenlist(userId);
|
||||
if (config == null) {
|
||||
throw new RuntimeException("用户未绑定 OpenList");
|
||||
}
|
||||
|
||||
String url = config.getOpenlistUrl() + "/api/admin/storage/list";
|
||||
log.info("用户 {} 尝试获取存储列表(需要管理员权限),URL: {}", userId, url);
|
||||
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
Map<String, Object> result = postWithRetry(userId, url, requestBody);
|
||||
|
||||
log.info("用户 {} 存储列表 API 响应: {}", userId, result);
|
||||
|
||||
if (result == null) {
|
||||
throw new RuntimeException("存储列表 API 响应为 null");
|
||||
}
|
||||
|
||||
Object codeObj = result.get("code");
|
||||
int code = codeObj != null ? (int) codeObj : -1;
|
||||
if (code != 200) {
|
||||
String message = result.get("message") != null ? result.get("message").toString() : "未知错误";
|
||||
throw new RuntimeException("存储列表 API 返回错误: code=" + code + ", message=" + message);
|
||||
}
|
||||
|
||||
Object dataObj = result.get("data");
|
||||
log.info("用户 {} 存储列表 data 类型: {}, 内容: {}",
|
||||
userId,
|
||||
dataObj != null ? dataObj.getClass().getSimpleName() : "null",
|
||||
dataObj != null ? dataObj.toString() : "null");
|
||||
|
||||
if (dataObj == null) {
|
||||
throw new RuntimeException("存储列表 data 为空");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> storages = (List<Map<String, Object>>) dataObj;
|
||||
log.info("用户 {} 共有 {} 个存储驱动器", userId, storages.size());
|
||||
|
||||
// 打印每个存储的详细信息
|
||||
for (int i = 0; i < storages.size(); i++) {
|
||||
var storage = storages.get(i);
|
||||
log.info("存储 {}: id={}, mount_path={}, driver={}, status={}",
|
||||
i, storage.get("id"), storage.get("mount_path"),
|
||||
storage.get("driver"), storage.get("status"));
|
||||
}
|
||||
|
||||
return storages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定路径下的所有内容(自动处理分页,返回全部结果)
|
||||
* 用于同步场景,需要获取目录下所有文件和文件夹
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param path OpenList 路径,如 / 或 /movies
|
||||
* @return 内容列表(包含文件和文件夹信息)
|
||||
* @throws RuntimeException 当 API 调用失败时抛出异常
|
||||
*/
|
||||
public List<Map<String, Object>> listAll(Long userId, String path) {
|
||||
UserOpenlist config = getUserOpenlist(userId);
|
||||
if (config == null) {
|
||||
throw new RuntimeException("用户未绑定 OpenList");
|
||||
}
|
||||
|
||||
String url = config.getOpenlistUrl() + "/api/fs/list";
|
||||
List<Map<String, Object>> allContent = new ArrayList<>();
|
||||
|
||||
int page = 1;
|
||||
int perPage = 100; // 每页 100 条
|
||||
|
||||
while (true) {
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("path", path);
|
||||
requestBody.put("page", page);
|
||||
requestBody.put("per_page", perPage);
|
||||
|
||||
log.info("用户 {} 请求列出路径 {} 第 {} 页,参数: {}", userId, path, page, requestBody);
|
||||
Map<String, Object> result = postWithRetry(userId, url, requestBody);
|
||||
|
||||
if (result == null) {
|
||||
throw new RuntimeException("列出路径 " + path + " 失败: API 响应为 null");
|
||||
}
|
||||
|
||||
Object codeObj = result.get("code");
|
||||
int code = codeObj != null ? (int) codeObj : -1;
|
||||
if (code != 200) {
|
||||
String message = result.get("message") != null ? result.get("message").toString() : "未知错误";
|
||||
throw new RuntimeException("列出路径 " + path + " 失败: code=" + code + ", message=" + message);
|
||||
}
|
||||
|
||||
Object dataObj = result.get("data");
|
||||
log.info("用户 {} 路径 {} 第 {} 页响应 data 类型: {}, 内容: {}",
|
||||
userId, path, page,
|
||||
dataObj != null ? dataObj.getClass().getSimpleName() : "null",
|
||||
dataObj != null ? dataObj.toString().substring(0, Math.min(500, dataObj.toString().length())) : "null");
|
||||
|
||||
if (dataObj == null) {
|
||||
throw new RuntimeException("列出路径 " + path + " 失败: data 为 null");
|
||||
}
|
||||
|
||||
Map<String, Object> data = (Map<String, Object>) dataObj;
|
||||
|
||||
// data.content 是文件/文件夹数组,data.total 是总数
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> items = (List<Map<String, Object>>) data.get("content");
|
||||
if (items == null || items.isEmpty()) {
|
||||
log.info("用户 {} 路径 {} 第 {} 页 content 为空(目录可能为空)", userId, path, page);
|
||||
break;
|
||||
}
|
||||
|
||||
allContent.addAll(items);
|
||||
|
||||
// 判断是否还有下一页
|
||||
int total = (int) data.getOrDefault("total", 0);
|
||||
log.info("用户 {} 路径 {} 第 {} 页获取 {} 项,总计 {} 项", userId, path, page, items.size(), total);
|
||||
if (page * perPage >= total) {
|
||||
break;
|
||||
}
|
||||
page++;
|
||||
}
|
||||
|
||||
log.info("用户 {} 路径 {} 共获取 {} 个内容项", userId, path, allContent.size());
|
||||
return allContent;
|
||||
}
|
||||
}
|
||||
BIN
wangpan-common/target/classes/com/wangpan/common/Result.class
Normal file
BIN
wangpan-common/target/classes/com/wangpan/common/Result.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
|
||||
<!-- 网盘文件夹 Mapper XML:存放复杂SQL,简单CRUD由MyBatis-Plus BaseMapper自动提供 -->
|
||||
<mapper namespace="com.wangpan.common.dal.mapper.DriveFolderMapper">
|
||||
|
||||
</mapper>
|
||||
Binary file not shown.
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
|
||||
<!-- 网盘 Mapper XML:存放复杂SQL,简单CRUD由MyBatis-Plus BaseMapper自动提供 -->
|
||||
<mapper namespace="com.wangpan.common.dal.mapper.DriveMapper">
|
||||
|
||||
</mapper>
|
||||
Binary file not shown.
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
|
||||
<!-- 网盘电影 Mapper XML:存放复杂SQL,简单CRUD由MyBatis-Plus BaseMapper自动提供 -->
|
||||
<mapper namespace="com.wangpan.common.dal.mapper.DriveMovieMapper">
|
||||
|
||||
<!-- 查找用户所有网盘下的重复电影(按 title 分组,出现次数 > 1) -->
|
||||
<select id="findDuplicateMovies" resultType="java.util.Map">
|
||||
SELECT title, COUNT(*) AS cnt
|
||||
FROM wp_drive_movie
|
||||
WHERE user_id = #{userId} AND status = 'active'
|
||||
GROUP BY title
|
||||
HAVING COUNT(*) > 1
|
||||
ORDER BY cnt DESC
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
Binary file not shown.
Binary file not shown.
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
|
||||
<!-- 登录日志 Mapper XML:存放复杂SQL,简单CRUD由MyBatis-Plus BaseMapper自动提供 -->
|
||||
<mapper namespace="com.wangpan.common.dal.mapper.LoginLogMapper">
|
||||
|
||||
</mapper>
|
||||
Binary file not shown.
@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
|
||||
<!-- 电影 Mapper XML:存放复杂SQL,简单CRUD由MyBatis-Plus BaseMapper自动提供 -->
|
||||
<mapper namespace="com.wangpan.common.dal.mapper.MovieMapper">
|
||||
|
||||
<!-- 查找用户所有网盘下的重复电影(按 title 分组,出现次数 > 1) -->
|
||||
<select id="findDuplicateMovies" resultType="java.util.Map">
|
||||
SELECT title, COUNT(*) AS cnt
|
||||
FROM wp_drive_movie
|
||||
WHERE user_id = #{userId} AND status = 'active'
|
||||
GROUP BY title
|
||||
HAVING COUNT(*) > 1
|
||||
ORDER BY cnt DESC
|
||||
</select>
|
||||
|
||||
<!-- 查找所有配置了TMDB API Key的用户(供定时刮削使用) -->
|
||||
<select id="findUsersWithTmdbKey" resultType="java.util.Map">
|
||||
SELECT u.id, u.username
|
||||
FROM wp_user u
|
||||
WHERE u.tmdb_api_key IS NOT NULL
|
||||
AND u.tmdb_api_key != ''
|
||||
AND u.status = 1
|
||||
</select>
|
||||
|
||||
<!-- 查找指定用户需要刮削的电影(包含 scrape_type='none' 的未刮削电影) -->
|
||||
<select id="findMoviesNeedingScrape" resultType="com.wangpan.common.dal.dataobject.MovieDO">
|
||||
SELECT m.* FROM wp_drive_movie m
|
||||
LEFT JOIN wp_tmdb_cache c ON m.id = c.movie_id AND c.status = 'active'
|
||||
WHERE m.user_id = #{userId}
|
||||
AND m.title IS NOT NULL AND m.title != ''
|
||||
AND (c.id IS NULL OR c.cache_time < DATE_SUB(NOW(), INTERVAL #{expireDays} DAY))
|
||||
AND m.status = 'active'
|
||||
ORDER BY m.update_time DESC
|
||||
LIMIT 100
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
Binary file not shown.
@ -0,0 +1,90 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
|
||||
<!-- TMDB刮削缓存 Mapper XML:存放复杂SQL,简单CRUD由MyBatis-Plus BaseMapper自动提供 -->
|
||||
<mapper namespace="com.wangpan.common.dal.mapper.TmdbCacheMapper">
|
||||
|
||||
<!-- 根据电影ID查询有效缓存 -->
|
||||
<select id="findByMovieId" resultType="com.wangpan.common.dal.dataobject.TmdbCacheDO">
|
||||
SELECT * FROM wp_tmdb_cache WHERE movie_id = #{movieId} AND status = 'active'
|
||||
</select>
|
||||
|
||||
<!-- 根据TMDB ID查询有效缓存列表 -->
|
||||
<select id="findByTmdbId" resultType="com.wangpan.common.dal.dataobject.TmdbCacheDO">
|
||||
SELECT * FROM wp_tmdb_cache WHERE tmdb_id = #{tmdbId} AND status = 'active'
|
||||
</select>
|
||||
|
||||
<!-- 分页查询用户缓存(含关联电影标题) -->
|
||||
<select id="queryByUserId" resultType="java.util.Map">
|
||||
SELECT c.*, m.title AS movie_title
|
||||
FROM wp_tmdb_cache c
|
||||
LEFT JOIN wp_drive_movie m ON c.movie_id = m.id
|
||||
WHERE c.user_id = #{userId} AND c.status = 'active'
|
||||
ORDER BY c.cache_time DESC
|
||||
LIMIT #{pageSize} OFFSET #{pageNo}
|
||||
</select>
|
||||
|
||||
<!-- 查询所有过期的缓存ID列表 -->
|
||||
<select id="findExpiredCaches" resultType="java.lang.Long">
|
||||
SELECT id FROM wp_tmdb_cache WHERE cache_time < #{expireBefore} AND status = 'active'
|
||||
</select>
|
||||
|
||||
<!-- 新增或更新缓存记录(UPSERT) -->
|
||||
<insert id="saveOrUpdate" parameterType="map">
|
||||
INSERT INTO wp_tmdb_cache (
|
||||
movie_id, user_id, tmdb_id, title, original_title, overview,
|
||||
release_date, poster_path, backdrop_path, local_poster_url,
|
||||
tmdb_rating, vote_count, runtime, genres, directors, casts,
|
||||
original_language, production_countries, status, cache_time
|
||||
) VALUES (
|
||||
#{data.movieId}, #{data.userId}, #{data.tmdbId}, #{data.title}, #{data.originalTitle}, #{data.overview},
|
||||
#{data.releaseDate}, #{data.posterPath}, #{data.backdropPath}, #{data.localPosterUrl},
|
||||
#{data.tmdbRating}, #{data.voteCount}, #{data.runtime}, #{data.genres}, #{data.directors}, #{data.casts},
|
||||
#{data.originalLanguage}, #{data.productionCountries}, 'active', NOW()
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
tmdb_id = VALUES(tmdb_id),
|
||||
title = VALUES(title),
|
||||
original_title = VALUES(original_title),
|
||||
overview = VALUES(overview),
|
||||
release_date = VALUES(release_date),
|
||||
poster_path = VALUES(poster_path),
|
||||
backdrop_path = VALUES(backdrop_path),
|
||||
local_poster_url = VALUES(local_poster_url),
|
||||
tmdb_rating = VALUES(tmdb_rating),
|
||||
vote_count = VALUES(vote_count),
|
||||
runtime = VALUES(runtime),
|
||||
genres = VALUES(genres),
|
||||
directors = VALUES(directors),
|
||||
casts = VALUES(casts),
|
||||
original_language = VALUES(original_language),
|
||||
production_countries = VALUES(production_countries),
|
||||
status = 'active',
|
||||
cache_time = NOW()
|
||||
</insert>
|
||||
|
||||
<!-- 批量将缓存标记为过期 -->
|
||||
<update id="markAsExpired">
|
||||
UPDATE wp_tmdb_cache SET status = 'expired' WHERE id IN
|
||||
<foreach collection="cacheIds" item="id" open="(" separator="," close=")">
|
||||
#{id}
|
||||
</foreach>
|
||||
</update>
|
||||
|
||||
<!-- 删除指定电影的缓存 -->
|
||||
<delete id="deleteByMovieId">
|
||||
DELETE FROM wp_tmdb_cache WHERE movie_id = #{movieId}
|
||||
</delete>
|
||||
|
||||
<!-- 获取用户的缓存统计信息 -->
|
||||
<select id="getStatistics" resultType="java.util.Map">
|
||||
SELECT
|
||||
COUNT(*) AS totalCached,
|
||||
SUM(CASE WHEN cache_time >= DATE_SUB(NOW(), INTERVAL 1 DAY) THEN 1 ELSE 0 END) AS todayCached,
|
||||
SUM(CASE WHEN cache_time < DATE_SUB(NOW(), INTERVAL 7 DAY) THEN 1 ELSE 0 END) AS expiredCount
|
||||
FROM wp_tmdb_cache
|
||||
WHERE user_id = #{userId}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
Binary file not shown.
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
|
||||
<!-- TMDB定时刮削执行记录 Mapper XML:存放复杂SQL,简单CRUD由MyBatis-Plus BaseMapper自动提供 -->
|
||||
<mapper namespace="com.wangpan.common.dal.mapper.TmdbScraperLogMapper">
|
||||
|
||||
<!-- 插入一条执行记录 -->
|
||||
<insert id="insertLog" parameterType="map">
|
||||
INSERT INTO wp_tmdb_scraper_log (
|
||||
trigger_type, status, total_processed, total_success, total_skipped,
|
||||
total_failed, user_count, detail_json, error_message, duration_ms,
|
||||
start_time, end_time
|
||||
) VALUES (
|
||||
#{log.triggerType}, #{log.status}, #{log.totalProcessed}, #{log.totalSuccess}, #{log.totalSkipped},
|
||||
#{log.totalFailed}, #{log.userCount}, #{log.detailJson}, #{log.errorMessage}, #{log.durationMs},
|
||||
#{log.startTime}, #{log.endTime}
|
||||
)
|
||||
</insert>
|
||||
|
||||
<!-- 查询执行记录总数 -->
|
||||
<select id="countAll" resultType="long">
|
||||
SELECT COUNT(*) FROM wp_tmdb_scraper_log
|
||||
</select>
|
||||
|
||||
<!-- 分页查询最近的执行记录 -->
|
||||
<select id="queryRecent" resultType="java.util.Map">
|
||||
SELECT * FROM wp_tmdb_scraper_log ORDER BY start_time DESC
|
||||
LIMIT #{pageSize} OFFSET #{offset}
|
||||
</select>
|
||||
|
||||
<!-- 查询最近的N条执行记录 -->
|
||||
<select id="queryRecentByLimit" resultType="java.util.Map">
|
||||
SELECT * FROM wp_tmdb_scraper_log ORDER BY start_time DESC LIMIT #{limit}
|
||||
</select>
|
||||
|
||||
<!-- 根据ID查询单条记录 -->
|
||||
<select id="findById" resultType="java.util.Map">
|
||||
SELECT * FROM wp_tmdb_scraper_log WHERE id = #{id}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
Binary file not shown.
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
|
||||
<!-- 用户令牌 Mapper XML:存放复杂SQL,简单CRUD由MyBatis-Plus BaseMapper自动提供 -->
|
||||
<mapper namespace="com.wangpan.common.dal.mapper.TokenMapper">
|
||||
|
||||
</mapper>
|
||||
Binary file not shown.
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
|
||||
<!-- 用户 Mapper XML:存放复杂SQL,简单CRUD由MyBatis-Plus BaseMapper自动提供 -->
|
||||
<mapper namespace="com.wangpan.common.dal.mapper.UserMapper">
|
||||
|
||||
</mapper>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
3
wangpan-common/target/maven-archiver/pom.properties
Normal file
3
wangpan-common/target/maven-archiver/pom.properties
Normal file
@ -0,0 +1,3 @@
|
||||
artifactId=wangpan-common
|
||||
groupId=com.wangpan
|
||||
version=1.0-SNAPSHOT
|
||||
@ -0,0 +1,52 @@
|
||||
com\wangpan\common\dal\repository\IconRepository.class
|
||||
com\wangpan\common\util\EncryptUtil.class
|
||||
com\wangpan\common\dal\dataobject\MovieDO.class
|
||||
com\wangpan\common\dal\dataobject\TmdbCacheDO.class
|
||||
com\wangpan\common\interceptor\AuthInterceptor.class
|
||||
com\wangpan\common\dal\repository\TokenRepository.class
|
||||
com\wangpan\common\entity\Drive.class
|
||||
com\wangpan\common\dal\mapper\TmdbCacheMapper.class
|
||||
com\wangpan\common\util\DBUtil.class
|
||||
com\wangpan\common\entity\DriveMovie.class
|
||||
com\wangpan\common\config\WebMvcConfig.class
|
||||
com\wangpan\common\dal\mapper\DriveMovieMapper.class
|
||||
com\wangpan\common\util\TokenUtil.class
|
||||
com\wangpan\common\dal\mapper\MovieMapper.class
|
||||
com\wangpan\common\dal\mapper\TmdbScraperLogMapper.class
|
||||
com\wangpan\common\dal\mapper\DriveMapper.class
|
||||
com\wangpan\common\dal\mapper\IconMapper.class
|
||||
com\wangpan\common\dal\dataobject\DriveFolderDO.class
|
||||
com\wangpan\common\dal\repository\DriveRepository.class
|
||||
com\wangpan\common\dal\repository\DriveMovieRepository.class
|
||||
com\wangpan\common\util\TokenUtil$TokenInfo.class
|
||||
com\wangpan\common\util\SchemaUtil.class
|
||||
com\wangpan\common\dal\mapper\TokenMapper.class
|
||||
com\wangpan\common\dal\repository\DriveFolderRepository.class
|
||||
com\wangpan\common\entity\DriveFolder.class
|
||||
com\wangpan\common\entity\Icon.class
|
||||
com\wangpan\common\dal\dataobject\UserDO.class
|
||||
com\wangpan\common\dal\dataobject\DriveDO.class
|
||||
com\wangpan\common\util\AuthUtil.class
|
||||
com\wangpan\common\dal\dataobject\TmdbScraperLogDO.class
|
||||
com\wangpan\common\dal\dataobject\TokenDO.class
|
||||
com\wangpan\common\dal\mapper\UserMapper.class
|
||||
com\wangpan\common\dal\dataobject\IconDO.class
|
||||
com\wangpan\common\dal\mapper\LoginLogMapper.class
|
||||
com\wangpan\common\annotation\RequireAuth.class
|
||||
com\wangpan\common\dal\dataobject\LoginLogDO.class
|
||||
com\wangpan\common\util\MinioUtil.class
|
||||
com\wangpan\common\dal\repository\LoginLogRepository.class
|
||||
com\wangpan\common\dal\mapper\DailyRecommendMapper.class
|
||||
com\wangpan\common\dal\repository\TmdbScraperLogRepository.class
|
||||
com\wangpan\common\entity\Movie.class
|
||||
com\wangpan\common\dal\dataobject\DailyRecommendDO.class
|
||||
com\wangpan\common\dal\repository\MovieRepository.class
|
||||
com\wangpan\common\entity\User.class
|
||||
com\wangpan\common\dal\mapper\DriveFolderMapper.class
|
||||
com\wangpan\common\annotation\RequireAdmin.class
|
||||
com\wangpan\common\dal\repository\TmdbCacheRepository.class
|
||||
com\wangpan\common\util\IconLogoFetcher.class
|
||||
com\wangpan\common\dal\dataobject\DriveMovieDO.class
|
||||
com\wangpan\common\dal\repository\DailyRecommendRepository.class
|
||||
com\wangpan\common\Result.class
|
||||
com\wangpan\common\dal\repository\UserRepository.class
|
||||
@ -0,0 +1,51 @@
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\util\SchemaUtil.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\util\AuthUtil.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\mapper\TmdbScraperLogMapper.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\mapper\UserMapper.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\entity\DriveMovie.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\interceptor\AuthInterceptor.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\repository\TmdbCacheRepository.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\dataobject\DriveFolderDO.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\util\TokenUtil.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\repository\LoginLogRepository.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\entity\Drive.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\repository\UserRepository.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\entity\Icon.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\util\IconLogoFetcher.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\mapper\DriveMapper.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\repository\DriveMovieRepository.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\repository\DriveRepository.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\entity\Movie.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\mapper\LoginLogMapper.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\Result.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\repository\TmdbScraperLogRepository.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\repository\DailyRecommendRepository.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\dataobject\MovieDO.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\dataobject\TmdbCacheDO.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\util\MinioUtil.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\repository\MovieRepository.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\dataobject\DriveMovieDO.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\repository\DriveFolderRepository.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\mapper\TmdbCacheMapper.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\dataobject\LoginLogDO.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\repository\TokenRepository.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\dataobject\UserDO.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\entity\User.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\dataobject\DailyRecommendDO.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\repository\IconRepository.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\dataobject\TmdbScraperLogDO.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\dataobject\DriveDO.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\mapper\IconMapper.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\mapper\TokenMapper.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\entity\DriveFolder.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\mapper\MovieMapper.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\dataobject\TokenDO.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\mapper\DailyRecommendMapper.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\annotation\RequireAdmin.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\dataobject\IconDO.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\mapper\DriveFolderMapper.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\config\WebMvcConfig.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\mapper\DriveMovieMapper.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\annotation\RequireAuth.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\util\EncryptUtil.java
|
||||
D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\util\DBUtil.java
|
||||
BIN
wangpan-common/target/wangpan-common-1.0-SNAPSHOT.jar
Normal file
BIN
wangpan-common/target/wangpan-common-1.0-SNAPSHOT.jar
Normal file
Binary file not shown.
@ -2,7 +2,6 @@ package com.wangpan;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import com.ulisesbocchio.jasyptspringboot.annotation.EnableEncryptableProperties;
|
||||
|
||||
/**
|
||||
@ -11,7 +10,6 @@ import com.ulisesbocchio.jasyptspringboot.annotation.EnableEncryptableProperties
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@EnableEncryptableProperties
|
||||
@EnableScheduling
|
||||
public class DriveServiceApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@ -4,7 +4,6 @@ import com.wangpan.common.dal.mapper.DriveFolderMapper;
|
||||
import com.wangpan.common.dal.mapper.DriveMapper;
|
||||
import com.wangpan.common.dal.mapper.DriveMovieMapper;
|
||||
import com.wangpan.common.dal.mapper.IconMapper;
|
||||
import com.wangpan.common.dal.mapper.UserOpenlistMapper;
|
||||
import com.wangpan.common.util.DBUtil;
|
||||
import com.wangpan.common.util.SchemaUtil;
|
||||
import jakarta.annotation.Resource;
|
||||
@ -65,12 +64,11 @@ public class DatabaseInitializer {
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建网盘相关表(使用各 Mapper 中定义的建表 SQL)
|
||||
// 创建网盘相关表(使用各 Mapper 中定义的建表SQL)
|
||||
SchemaUtil.createTableIfNotExists(conn, "wp_drive", DriveMapper.CREATE_TABLE_SQL);
|
||||
SchemaUtil.createTableIfNotExists(conn, "wp_drive_folder", DriveFolderMapper.CREATE_TABLE_SQL);
|
||||
SchemaUtil.createTableIfNotExists(conn, "wp_drive_movie", DriveMovieMapper.CREATE_TABLE_SQL);
|
||||
SchemaUtil.createTableIfNotExists(conn, "wp_icon", IconMapper.CREATE_TABLE_SQL);
|
||||
SchemaUtil.createTableIfNotExists(conn, "wp_user_openlist", UserOpenlistMapper.CREATE_TABLE_SQL);
|
||||
|
||||
// 检查并添加新字段(兼容旧表结构升级)
|
||||
SchemaUtil.ensureColumnExists(conn, "wp_drive", "logo_url",
|
||||
@ -88,30 +86,6 @@ public class DatabaseInitializer {
|
||||
SchemaUtil.ensureColumnExists(conn, "wp_drive_movie", "backdrop_url",
|
||||
"TEXT DEFAULT NULL COMMENT '背景图URL(横版,加密存储)'");
|
||||
|
||||
// 检查 wp_drive 表是否有 openlist_storage_id 字段(OpenList 存储ID)
|
||||
SchemaUtil.ensureColumnExists(conn, "wp_drive", "openlist_storage_id",
|
||||
"VARCHAR(50) DEFAULT NULL COMMENT 'OpenList 存储ID'");
|
||||
|
||||
// 检查 wp_drive 表是否有 openlist_mount_path 字段(OpenList 挂载路径)
|
||||
SchemaUtil.ensureColumnExists(conn, "wp_drive", "openlist_mount_path",
|
||||
"VARCHAR(200) DEFAULT NULL COMMENT 'OpenList 挂载路径,如 /movies'");
|
||||
|
||||
// 检查 wp_drive_movie 表是否有 openlist_file_path 字段(OpenList 文件路径)
|
||||
SchemaUtil.ensureColumnExists(conn, "wp_drive_movie", "openlist_file_path",
|
||||
"VARCHAR(500) DEFAULT NULL COMMENT 'OpenList 文件路径,如 /movies/阿凡达.mp4'");
|
||||
|
||||
// 检查 wp_drive_movie 表是否有 storage_type 字段(存储类型)
|
||||
SchemaUtil.ensureColumnExists(conn, "wp_drive_movie", "storage_type",
|
||||
"VARCHAR(20) NOT NULL DEFAULT 'metadata' COMMENT '存储类型:metadata(仅元数据)/ openlist(OpenList 管理)'");
|
||||
|
||||
// 检查 wp_drive_movie 表是否有 casts 字段(主演)
|
||||
SchemaUtil.ensureColumnExists(conn, "wp_drive_movie", "casts",
|
||||
"VARCHAR(500) DEFAULT NULL COMMENT '主演(逗号分隔,TMDB刮削)'");
|
||||
|
||||
// 检查 wp_drive_movie 表是否有 directors 字段(导演)
|
||||
SchemaUtil.ensureColumnExists(conn, "wp_drive_movie", "directors",
|
||||
"VARCHAR(200) DEFAULT NULL COMMENT '导演(逗号分隔,TMDB刮削)'");
|
||||
|
||||
// 检查 wp_icon 表是否有 logo_url 字段
|
||||
SchemaUtil.ensureColumnExists(conn, "wp_icon", "logo_url",
|
||||
"VARCHAR(500) DEFAULT NULL COMMENT 'Logo 图片 URL(MinIO 存储)'");
|
||||
@ -124,26 +98,6 @@ public class DatabaseInitializer {
|
||||
SchemaUtil.ensureColumnExists(conn, "wp_icon", "link_type",
|
||||
"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 '心跳检测失败原因'");
|
||||
|
||||
// 检查 wp_drive 表是否有 logo_last_access_time 字段(Logo 最后访问时间)
|
||||
SchemaUtil.ensureColumnExists(conn, "wp_drive", "logo_last_access_time",
|
||||
"DATETIME DEFAULT NULL COMMENT 'Logo最后访问时间(用于回收策略)'");
|
||||
|
||||
// 检查 wp_drive 表是否有 logo_status 字段(Logo 状态)
|
||||
SchemaUtil.ensureColumnExists(conn, "wp_drive", "logo_status",
|
||||
"VARCHAR(20) NOT NULL DEFAULT 'normal' COMMENT 'Logo状态:normal(正常)/recycling(回收中)/deleted(已删除)'");
|
||||
|
||||
// 移除已废弃的 icon 列(已被 logo_url 替代)
|
||||
try (var checkStmt = conn.createStatement();
|
||||
var checkRs = checkStmt.executeQuery("SHOW COLUMNS FROM wp_drive LIKE 'icon'")) {
|
||||
@ -153,15 +107,6 @@ public class DatabaseInitializer {
|
||||
}
|
||||
}
|
||||
|
||||
// 移除已废弃的 overview 列(已与 description 合并,不再单独存储)
|
||||
try (var checkStmt = conn.createStatement();
|
||||
var checkRs = checkStmt.executeQuery("SHOW COLUMNS FROM wp_drive_movie LIKE 'overview'")) {
|
||||
if (checkRs.next()) {
|
||||
conn.createStatement().executeUpdate("ALTER TABLE wp_drive_movie DROP COLUMN overview");
|
||||
System.out.println(" ✓ [wp_drive_movie] 已移除废弃的 overview 列(已合并到 description)");
|
||||
}
|
||||
}
|
||||
|
||||
// 验证字段是否添加成功
|
||||
try (var checkStmt = conn.createStatement();
|
||||
var checkRs = checkStmt.executeQuery("SHOW COLUMNS FROM wp_drive LIKE 'logo_url'")) {
|
||||
|
||||
@ -11,11 +11,9 @@ import com.wangpan.common.entity.DriveMovie;
|
||||
import com.wangpan.common.entity.User;
|
||||
import com.wangpan.common.interceptor.AuthInterceptor;
|
||||
import com.wangpan.common.util.MinioUtil;
|
||||
import com.wangpan.common.util.OpenListClient;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
@ -43,9 +41,6 @@ public class DriveController {
|
||||
private final DriveFolderRepository folderRepository;
|
||||
private final DriveMovieRepository movieRepository;
|
||||
|
||||
@Autowired
|
||||
private OpenListClient openListClient;
|
||||
|
||||
/**
|
||||
* 构造函数注入 Repository 依赖
|
||||
*/
|
||||
@ -87,9 +82,6 @@ public class DriveController {
|
||||
try {
|
||||
String logoUrl = uploadDriveLogo(logoData);
|
||||
drive.setLogoUrl(logoUrl);
|
||||
// 设置 Logo 初始状态为 normal,并记录首次访问时间
|
||||
drive.setLogoStatus("normal");
|
||||
drive.setLogoLastAccessTime(java.time.LocalDateTime.now());
|
||||
} catch (Exception e) {
|
||||
log.error("Logo上传失败", e);
|
||||
return Result.error("创建网盘失败:Logo图片上传失败");
|
||||
@ -120,15 +112,6 @@ public class DriveController {
|
||||
var list = driveRepository.queryByUserId(user.getId(), pageNo, pageSize);
|
||||
var total = driveRepository.countByUserId(user.getId());
|
||||
|
||||
// 更新所有有 Logo 的网盘的最后访问时间(触发回收策略重置)
|
||||
for (var driveMap : list) {
|
||||
Long driveId = (Long) driveMap.get("id");
|
||||
String logoUrl = (String) driveMap.get("logoUrl");
|
||||
if (logoUrl != null && !logoUrl.isBlank()) {
|
||||
driveRepository.updateLogoAccessTime(driveId);
|
||||
}
|
||||
}
|
||||
|
||||
var result = new LinkedHashMap<String, Object>();
|
||||
result.put("list", list);
|
||||
result.put("total", total);
|
||||
@ -151,11 +134,6 @@ public class DriveController {
|
||||
return Result.error("查询失败:网盘不存在或无权访问");
|
||||
}
|
||||
|
||||
// 更新 Logo 最后访问时间(触发回收策略重置)
|
||||
if (drive.getLogoUrl() != null && !drive.getLogoUrl().isBlank()) {
|
||||
driveRepository.updateLogoAccessTime(id);
|
||||
}
|
||||
|
||||
return Result.success("查询成功", driveToMap(drive));
|
||||
}
|
||||
|
||||
@ -187,8 +165,6 @@ public class DriveController {
|
||||
if (logoData != null && !logoData.isBlank()) {
|
||||
try {
|
||||
logoUrl = uploadDriveLogo(logoData);
|
||||
// 更新 Logo 时重置状态为 normal,并刷新访问时间
|
||||
driveRepository.updateLogoAccessTime(id);
|
||||
} catch (Exception e) {
|
||||
log.error("Logo更新失败", e);
|
||||
return Result.error("更新网盘失败:Logo图片上传失败");
|
||||
@ -410,16 +386,6 @@ public class DriveController {
|
||||
movie.setMimeType(detectMimeType(title));
|
||||
movie.setExtension(getExtension(title));
|
||||
|
||||
// 如果用户绑定了 OpenList,设置文件路径和存储类型
|
||||
var userOpenlist = openListClient.getUserOpenlist(user.getId());
|
||||
if (userOpenlist != null && userOpenlist.getMountPath() != null) {
|
||||
String openlistPath = buildOpenlistPath(userOpenlist.getMountPath(), folderId, title.trim());
|
||||
movie.setOpenlistFilePath(openlistPath);
|
||||
movie.setStorageType("openlist");
|
||||
} else {
|
||||
movie.setStorageType("metadata");
|
||||
}
|
||||
|
||||
Long id = movieRepository.insert(movie);
|
||||
if (id != null) {
|
||||
movie.setId(id);
|
||||
@ -493,21 +459,18 @@ public class DriveController {
|
||||
* 更新电影信息
|
||||
* PUT /api/drive/{driveId}/movies/{movieId}
|
||||
*
|
||||
* 请求体(所有字段可选,只传需要更新的字段):
|
||||
* 请求体(所有字段可选):
|
||||
* {
|
||||
* "title": "新标题",
|
||||
* "originalName": "新文件名.mp4",
|
||||
* "subtitle": "副标题",
|
||||
* "description": "描述",
|
||||
* "rating": 85,
|
||||
* "tags": "标签1,标签2",
|
||||
* "posterUrl": "海报URL",
|
||||
* "backdropUrl": "背景图URL",
|
||||
* "casts": "主演列表",
|
||||
* "directors": "导演列表",
|
||||
* "resourceLibrary": "资源库来源",
|
||||
* "maintainer": "维护者"
|
||||
* "category": "分类"
|
||||
* }
|
||||
*
|
||||
* 说明:用于 TMDB 刮削有误时,用户自定义修改文件名等信息,也支持手动补充刮削字段
|
||||
* 说明:用于 TMDB 刮削有误时,用户自定义修改文件名等信息
|
||||
*/
|
||||
@PutMapping("/{driveId}/movies/{movieId}")
|
||||
public Result<?> updateMovie(@PathVariable("driveId") Long driveId,
|
||||
@ -521,38 +484,19 @@ public class DriveController {
|
||||
return Result.error("更新失败:电影不存在或无权访问");
|
||||
}
|
||||
|
||||
// 基础信息字段
|
||||
String title = (String) data.get("title");
|
||||
String originalName = (String) data.get("originalName");
|
||||
String subtitle = (String) data.get("subtitle");
|
||||
String description = (String) data.get("description");
|
||||
String tags = (String) data.get("tags");
|
||||
|
||||
// 评分字段(需要类型转换)
|
||||
Object ratingObj = data.get("rating");
|
||||
Integer rating = null;
|
||||
if (ratingObj != null) {
|
||||
rating = ((Number) ratingObj).intValue();
|
||||
}
|
||||
String tags = (String) data.get("tags");
|
||||
String category = (String) data.get("category");
|
||||
|
||||
// TMDB 刮削相关字段(用户可手动补充或修正)
|
||||
String posterUrl = (String) data.get("posterUrl");
|
||||
String backdropUrl = (String) data.get("backdropUrl");
|
||||
String casts = (String) data.get("casts");
|
||||
String directors = (String) data.get("directors");
|
||||
String resourceLibrary = (String) data.get("resourceLibrary");
|
||||
String maintainer = (String) data.get("maintainer");
|
||||
|
||||
// 调用扩展后的 Repository 更新方法(基础字段)
|
||||
boolean success = movieRepository.update(movieId, title, description,
|
||||
rating, tags, posterUrl, backdropUrl, resourceLibrary, maintainer);
|
||||
|
||||
// 显式更新 casts 和 directors 字段(使用 @Update 注解直接执行 SQL)
|
||||
if (casts != null) {
|
||||
movieRepository.updateCasts(movieId, casts);
|
||||
}
|
||||
if (directors != null) {
|
||||
movieRepository.updateDirectors(movieId, directors);
|
||||
}
|
||||
|
||||
boolean success = movieRepository.update(movieId, title, originalName, subtitle, description, rating, tags, category);
|
||||
if (success) {
|
||||
return Result.success("更新成功");
|
||||
} else {
|
||||
@ -675,37 +619,6 @@ public class DriveController {
|
||||
return fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 OpenList 文件路径
|
||||
* 例如:/uc/科幻/阿凡达.mp4
|
||||
*
|
||||
* @param mountPath 挂载路径(如 /uc)
|
||||
* @param folderId 文件夹 ID(0 表示根目录)
|
||||
* @param fileName 文件名
|
||||
* @return OpenList 文件路径
|
||||
*/
|
||||
private String buildOpenlistPath(String mountPath, Long folderId, String fileName) {
|
||||
StringBuilder path = new StringBuilder();
|
||||
|
||||
// 添加挂载路径
|
||||
if (mountPath != null && !mountPath.equals("/")) {
|
||||
path.append(mountPath);
|
||||
}
|
||||
|
||||
// 添加文件夹路径
|
||||
if (folderId > 0) {
|
||||
DriveFolder folder = folderRepository.findById(folderId);
|
||||
if (folder != null) {
|
||||
path.append("/").append(folder.getName());
|
||||
}
|
||||
}
|
||||
|
||||
// 添加文件名
|
||||
path.append("/").append(fileName);
|
||||
|
||||
return path.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Drive 实体转 Map
|
||||
*/
|
||||
@ -715,8 +628,6 @@ public class DriveController {
|
||||
map.put("name", drive.getName());
|
||||
map.put("description", drive.getDescription());
|
||||
map.put("logoUrl", drive.getLogoUrl());
|
||||
map.put("logoStatus", drive.getLogoStatus());
|
||||
map.put("logoLastAccessTime", drive.getLogoLastAccessTime() != null ? drive.getLogoLastAccessTime().toString() : null);
|
||||
map.put("folderCount", drive.getFolderCount());
|
||||
map.put("fileCount", drive.getFileCount());
|
||||
map.put("totalSize", drive.getTotalSize());
|
||||
@ -771,11 +682,6 @@ public class DriveController {
|
||||
|
||||
/**
|
||||
* DriveMovie 实体转 Map
|
||||
*
|
||||
* TMDB 刮削字段(casts、directors、posterUrl、backdropUrl、rating)
|
||||
* 仅在刮削成功(scrapeStatus = "success")时返回实际值,否则返回 null
|
||||
*
|
||||
* 已移除字段:subtitle、originalName、category、overview(不再对外暴露)
|
||||
*/
|
||||
private Map<String, Object> movieToMap(DriveMovie movie) {
|
||||
var map = new LinkedHashMap<String, Object>();
|
||||
@ -783,6 +689,8 @@ public class DriveController {
|
||||
map.put("driveId", movie.getDriveId());
|
||||
map.put("folderId", movie.getFolderId());
|
||||
map.put("title", movie.getTitle());
|
||||
map.put("subtitle", movie.getSubtitle());
|
||||
map.put("originalName", movie.getOriginalName());
|
||||
map.put("mimeType", movie.getMimeType());
|
||||
map.put("fileSize", movie.getFileSize());
|
||||
map.put("extension", movie.getExtension());
|
||||
@ -790,159 +698,16 @@ public class DriveController {
|
||||
map.put("resourceLibrary", movie.getResourceLibrary());
|
||||
map.put("maintainer", movie.getMaintainer());
|
||||
map.put("status", movie.getStatus());
|
||||
map.put("category", movie.getCategory());
|
||||
map.put("scrapeType", movie.getScrapeType());
|
||||
map.put("scrapeStatus", movie.getScrapeStatus());
|
||||
map.put("scrapeError", movie.getScrapeError());
|
||||
map.put("rating", movie.getRating());
|
||||
map.put("tags", movie.getTags());
|
||||
map.put("openlistFilePath", movie.getOpenlistFilePath());
|
||||
map.put("storageType", movie.getStorageType());
|
||||
map.put("minioUrl", movie.getMinioUrl());
|
||||
map.put("posterUrl", movie.getPosterUrl());
|
||||
map.put("createTime", movie.getCreateTime() != null ? movie.getCreateTime().toString() : null);
|
||||
map.put("updateTime", movie.getUpdateTime() != null ? movie.getUpdateTime().toString() : null);
|
||||
|
||||
// TMDB 刮削字段:仅在刮削成功时返回实际值,未刮削/刮削失败返回 null
|
||||
boolean scrapeSuccess = "success".equals(movie.getScrapeStatus());
|
||||
map.put("rating", scrapeSuccess ? movie.getRating() : null);
|
||||
map.put("casts", scrapeSuccess ? movie.getCasts() : null);
|
||||
map.put("directors", scrapeSuccess ? movie.getDirectors() : null);
|
||||
map.put("posterUrl", scrapeSuccess ? movie.getPosterUrl() : null);
|
||||
map.put("backdropUrl", scrapeSuccess ? movie.getBackdropUrl() : null);
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
// ==================== OpenList 集成接口 ====================
|
||||
|
||||
/**
|
||||
* 获取电影播放链接(从 OpenList 获取真实下载地址)
|
||||
* GET /api/drive/{driveId}/movies/{movieId}/play-url
|
||||
*
|
||||
* 返回 OpenList 的真实下载链接,用于前端播放
|
||||
* 使用当前用户的 OpenList 连接配置
|
||||
*/
|
||||
@GetMapping("/{driveId}/movies/{movieId}/play-url")
|
||||
public Result<?> getMoviePlayUrl(@PathVariable("driveId") Long driveId,
|
||||
@PathVariable("movieId") Long movieId,
|
||||
HttpServletRequest request) {
|
||||
User user = getCurrentUser(request);
|
||||
|
||||
DriveMovie movie = movieRepository.findById(movieId);
|
||||
if (movie == null || !movie.getUserId().equals(user.getId())) {
|
||||
return Result.error("获取失败:电影不存在或无权访问");
|
||||
}
|
||||
|
||||
// 检查是否是 OpenList 管理的文件
|
||||
if (!"openlist".equals(movie.getStorageType())) {
|
||||
return Result.error("获取失败:该电影不是 OpenList 管理的文件");
|
||||
}
|
||||
|
||||
try {
|
||||
// 调用 OpenList API 获取下载链接(使用用户级连接)
|
||||
Map<String, Object> result = openListClient.getFileDetail(user.getId(), movie.getOpenlistFilePath());
|
||||
|
||||
if (result != null && (int) result.get("code") == 200) {
|
||||
Map<String, Object> data = (Map<String, Object>) result.get("data");
|
||||
String rawUrl = (String) data.get("raw_url");
|
||||
|
||||
Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("playUrl", rawUrl);
|
||||
response.put("fileName", movie.getTitle());
|
||||
response.put("fileSize", movie.getFileSize());
|
||||
|
||||
return Result.success("获取成功", response);
|
||||
} else {
|
||||
return Result.error("获取播放链接失败:" + result.get("message"));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("获取播放链接异常", e);
|
||||
return Result.error("获取播放链接失败:系统异常");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 OpenList 同步文件夹下的文件列表
|
||||
* POST /api/drive/{driveId}/folders/{folderId}/sync-files
|
||||
*
|
||||
* 从 OpenList 获取实际文件列表,并同步到数据库
|
||||
* 使用当前用户的 OpenList 连接配置
|
||||
*/
|
||||
@PostMapping("/{driveId}/folders/{folderId}/sync-files")
|
||||
public Result<?> syncFilesFromOpenList(@PathVariable("driveId") Long driveId,
|
||||
@PathVariable("folderId") Long folderId,
|
||||
HttpServletRequest request) {
|
||||
User user = getCurrentUser(request);
|
||||
|
||||
Drive drive = driveRepository.findById(driveId);
|
||||
if (drive == null || !drive.getUserId().equals(user.getId())) {
|
||||
return Result.error("同步失败:网盘不存在或无权访问");
|
||||
}
|
||||
|
||||
DriveFolder folder = folderRepository.findById(folderId);
|
||||
if (folder == null || !folder.getUserId().equals(user.getId())) {
|
||||
return Result.error("同步失败:文件夹不存在或无权访问");
|
||||
}
|
||||
|
||||
// 检查用户是否绑定了 OpenList
|
||||
var userOpenlist = openListClient.getUserOpenlist(user.getId());
|
||||
if (userOpenlist == null || userOpenlist.getMountPath() == null) {
|
||||
return Result.error("同步失败:用户未绑定 OpenList,请先在设置中绑定 OpenList 连接");
|
||||
}
|
||||
|
||||
try {
|
||||
// 构建 OpenList 路径
|
||||
String openlistPath = buildOpenlistPath(userOpenlist.getMountPath(), folderId, "");
|
||||
|
||||
// 调用 OpenList API 获取文件列表(使用用户级连接)
|
||||
Map<String, Object> result = openListClient.listFiles(user.getId(), openlistPath, 1, 100);
|
||||
|
||||
if (result != null && (int) result.get("code") == 200) {
|
||||
Map<String, Object> data = (Map<String, Object>) result.get("data");
|
||||
@SuppressWarnings("unchecked")
|
||||
java.util.List<Map<String, Object>> files = (java.util.List<Map<String, Object>>) data.get("content");
|
||||
|
||||
int syncCount = 0;
|
||||
for (Map<String, Object> file : files) {
|
||||
String fileName = (String) file.get("name");
|
||||
boolean isDir = (boolean) file.get("is_dir");
|
||||
long fileSize = ((Number) file.get("size")).longValue();
|
||||
|
||||
if (!isDir) {
|
||||
// 检查文件是否已存在(通过 title 和 folderId 判断)
|
||||
var existingList = movieRepository.queryByDriveAndFolder(driveId, folderId, user.getId(), 1, 1000);
|
||||
boolean exists = existingList.stream()
|
||||
.anyMatch(m -> fileName.equals(m.get("title")));
|
||||
|
||||
if (!exists) {
|
||||
// 创建新的电影记录
|
||||
DriveMovie movie = new DriveMovie(driveId, folderId, user.getId(), fileName, fileName);
|
||||
movie.setMimeType(detectMimeType(fileName));
|
||||
movie.setExtension(getExtension(fileName));
|
||||
movie.setFileSize(fileSize);
|
||||
movie.setStorageType("openlist");
|
||||
movie.setOpenlistFilePath(openlistPath + "/" + fileName);
|
||||
|
||||
movieRepository.insert(movie);
|
||||
syncCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新统计信息
|
||||
updateDriveStats(driveId, user.getId());
|
||||
if (folderId > 0) {
|
||||
updateFolderStats(folderId, user.getId());
|
||||
}
|
||||
|
||||
Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("syncCount", syncCount);
|
||||
response.put("totalFiles", files.size());
|
||||
|
||||
return Result.success("同步成功", response);
|
||||
} else {
|
||||
return Result.error("同步失败:" + result.get("message"));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("同步文件异常", e);
|
||||
return Result.error("同步失败:系统异常");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,353 +0,0 @@
|
||||
package com.wangpan.controller;
|
||||
|
||||
import com.wangpan.common.Result;
|
||||
import com.wangpan.common.annotation.RequireAuth;
|
||||
import com.wangpan.common.dal.repository.UserOpenlistRepository;
|
||||
import com.wangpan.common.entity.User;
|
||||
import com.wangpan.common.entity.UserOpenlist;
|
||||
import com.wangpan.common.interceptor.AuthInterceptor;
|
||||
import com.wangpan.common.util.OpenListClient;
|
||||
import com.wangpan.service.OpenListSyncService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* OpenList 连接管理控制器
|
||||
*
|
||||
* 提供用户级 OpenList 实例的绑定、解绑、测试连接、查询信息接口。
|
||||
* 每个用户可以绑定自己的 OpenList 实例(地址、账号、密码),互不干扰。
|
||||
*
|
||||
* 核心流程:
|
||||
* 1. 前端传入 OpenList 地址、账号、密码
|
||||
* 2. 后端验证连接 → 获取 Token → 加密存储到数据库
|
||||
* 3. 后续文件操作(同步、播放)自动使用用户的 OpenList 配置
|
||||
*
|
||||
* 安全设计:
|
||||
* - 敏感信息(URL、账号、密码)使用 Jasypt 加密存储
|
||||
* - Token 缓存到数据库,避免每次调用都重新登录
|
||||
* - 查询接口返回脱敏信息,不暴露明文密码
|
||||
*/
|
||||
@RestController
|
||||
@RequireAuth
|
||||
@RequestMapping("/api/openlist")
|
||||
@CrossOrigin(origins = {"*"}, allowCredentials = "false")
|
||||
public class OpenListController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(OpenListController.class);
|
||||
|
||||
@Autowired
|
||||
private OpenListClient openListClient;
|
||||
|
||||
@Autowired
|
||||
private OpenListSyncService openListSyncService;
|
||||
|
||||
@Autowired
|
||||
private UserOpenlistRepository userOpenlistRepository;
|
||||
|
||||
/**
|
||||
* 从请求属性中获取当前登录用户(由 AuthInterceptor 注入)
|
||||
*/
|
||||
private User getCurrentUser(HttpServletRequest request) {
|
||||
return (User) request.getAttribute(AuthInterceptor.CURRENT_USER_ATTR);
|
||||
}
|
||||
|
||||
// ==================== 连接管理接口 ====================
|
||||
|
||||
/**
|
||||
* 测试 OpenList 连接(不保存配置)
|
||||
* 用于前端绑定前验证地址、账号、密码是否正确
|
||||
*
|
||||
* POST /api/openlist/test
|
||||
*
|
||||
* 请求体:
|
||||
* {
|
||||
* "openlistUrl": "http://ldfun.asia:13000",
|
||||
* "openlistUsername": "djl1022",
|
||||
* "openlistPassword": "Djl1022!"
|
||||
* }
|
||||
*
|
||||
* 响应:
|
||||
* {
|
||||
* "code": 200,
|
||||
* "message": "连接成功",
|
||||
* "data": {
|
||||
* "success": true,
|
||||
* "message": "连接成功"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
@PostMapping("/test")
|
||||
public Result<?> testConnection(@RequestBody Map<String, Object> data) {
|
||||
String openlistUrl = (String) data.get("openlistUrl");
|
||||
String openlistUsername = (String) data.get("openlistUsername");
|
||||
String openlistPassword = (String) data.get("openlistPassword");
|
||||
|
||||
// 参数校验
|
||||
if (openlistUrl == null || openlistUrl.isBlank()) {
|
||||
return Result.error("测试连接失败:OpenList 地址不能为空");
|
||||
}
|
||||
if (openlistUsername == null || openlistUsername.isBlank()) {
|
||||
return Result.error("测试连接失败:账号不能为空");
|
||||
}
|
||||
if (openlistPassword == null || openlistPassword.isBlank()) {
|
||||
return Result.error("测试连接失败:密码不能为空");
|
||||
}
|
||||
|
||||
// 去除 URL 末尾的斜杠
|
||||
openlistUrl = openlistUrl.trim().replaceAll("/+$", "");
|
||||
|
||||
var result = openListClient.testConnection(openlistUrl, openlistUsername, openlistPassword);
|
||||
boolean success = (boolean) result.get("success");
|
||||
String message = (String) result.get("message");
|
||||
|
||||
if (success) {
|
||||
return Result.success(message);
|
||||
} else {
|
||||
return Result.error(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定 OpenList 连接
|
||||
* 验证连接 → 保存配置(加密) → 缓存 Token
|
||||
* 每个用户只能绑定一个 OpenList 实例,重复绑定会更新配置
|
||||
*
|
||||
* POST /api/openlist/bind
|
||||
*
|
||||
* 请求体:
|
||||
* {
|
||||
* "openlistUrl": "http://ldfun.asia:13000",
|
||||
* "openlistUsername": "djl1022",
|
||||
* "openlistPassword": "Djl1022!"
|
||||
* }
|
||||
*
|
||||
* 响应:
|
||||
* {
|
||||
* "code": 200,
|
||||
* "message": "绑定成功",
|
||||
* "data": {
|
||||
* "success": true,
|
||||
* "message": "绑定成功"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
@PostMapping("/bind")
|
||||
public Result<?> bindConnection(@RequestBody Map<String, Object> data, HttpServletRequest request) {
|
||||
User user = getCurrentUser(request);
|
||||
|
||||
String openlistUrl = (String) data.get("openlistUrl");
|
||||
String openlistUsername = (String) data.get("openlistUsername");
|
||||
String openlistPassword = (String) data.get("openlistPassword");
|
||||
|
||||
// 参数校验
|
||||
if (openlistUrl == null || openlistUrl.isBlank()) {
|
||||
return Result.error("绑定失败:OpenList 地址不能为空");
|
||||
}
|
||||
if (openlistUsername == null || openlistUsername.isBlank()) {
|
||||
return Result.error("绑定失败:账号不能为空");
|
||||
}
|
||||
if (openlistPassword == null || openlistPassword.isBlank()) {
|
||||
return Result.error("绑定失败:密码不能为空");
|
||||
}
|
||||
|
||||
// 去除 URL 末尾的斜杠
|
||||
openlistUrl = openlistUrl.trim().replaceAll("/+$", "");
|
||||
|
||||
var result = openListClient.bindConnection(user.getId(), openlistUrl, openlistUsername, openlistPassword);
|
||||
boolean success = (boolean) result.get("success");
|
||||
String message = (String) result.get("message");
|
||||
|
||||
if (success) {
|
||||
// 绑定成功后,异步触发同步
|
||||
triggerAsyncSync(user.getId());
|
||||
return Result.success(message + ",正在后台同步文件...");
|
||||
} else {
|
||||
return Result.error(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步触发同步(不阻塞绑定响应)
|
||||
*/
|
||||
private void triggerAsyncSync(Long userId) {
|
||||
// 使用新线程异步执行同步,避免阻塞 HTTP 响应
|
||||
new Thread(() -> {
|
||||
try {
|
||||
log.info("用户 {} 绑定成功,开始异步同步 OpenList 文件", userId);
|
||||
var syncResult = openListSyncService.syncFromOpenList(userId);
|
||||
log.info("用户 {} 异步同步完成: 文件夹={}, 电影={}, 错误={}",
|
||||
userId, syncResult.folderCount, syncResult.movieCount, syncResult.errors.size());
|
||||
} catch (Exception e) {
|
||||
log.error("用户 {} 异步同步失败", userId, e);
|
||||
}
|
||||
}, "openlist-sync-" + userId).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解绑 OpenList 连接
|
||||
* 删除用户的 OpenList 配置(包括加密的 URL、账号、密码和 Token 缓存)
|
||||
*
|
||||
* DELETE /api/openlist/unbind
|
||||
*
|
||||
* 响应:
|
||||
* {
|
||||
* "code": 200,
|
||||
* "message": "解绑成功",
|
||||
* "data": {
|
||||
* "success": true,
|
||||
* "message": "解绑成功"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
@DeleteMapping("/unbind")
|
||||
public Result<?> unbindConnection(HttpServletRequest request) {
|
||||
User user = getCurrentUser(request);
|
||||
|
||||
var result = openListClient.unbindConnection(user.getId());
|
||||
boolean success = (boolean) result.get("success");
|
||||
String message = (String) result.get("message");
|
||||
|
||||
if (success) {
|
||||
return Result.success(message);
|
||||
} else {
|
||||
return Result.error(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前用户的 OpenList 连接信息(脱敏)
|
||||
* 返回脱敏后的 URL、账号、存储配置、Token 状态、连接测试结果等,不返回明文密码
|
||||
* 会实际调用 OpenList 登录接口验证连接是否可用
|
||||
*
|
||||
* GET /api/openlist/info
|
||||
*
|
||||
* 响应(已绑定):
|
||||
* {
|
||||
* "code": 200,
|
||||
* "data": {
|
||||
* "id": 1,
|
||||
* "openlistUrl": "http://ld***:13000",
|
||||
* "openlistUsername": "dj***22",
|
||||
* "storageId": "1",
|
||||
* "mountPath": "/uc",
|
||||
* "status": "active",
|
||||
* "tokenValid": true,
|
||||
* "connected": true,
|
||||
* "connectMessage": "连接成功",
|
||||
* "createTime": "2026-06-26T15:00:00",
|
||||
* "updateTime": "2026-06-26T15:00:00"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* 响应(未绑定):
|
||||
* {
|
||||
* "code": 200,
|
||||
* "data": null
|
||||
* }
|
||||
*/
|
||||
@GetMapping("/info")
|
||||
public Result<?> getConnectionInfo(HttpServletRequest request) {
|
||||
User user = getCurrentUser(request);
|
||||
|
||||
var info = openListClient.getUserOpenlistInfo(user.getId());
|
||||
if (info == null) {
|
||||
return Result.success(null);
|
||||
}
|
||||
|
||||
// 检查当前 Token 是否有效,如果有效就不重新登录
|
||||
// 避免每次查看 info 都登录 OpenList,导致之前的 Token 被作废
|
||||
// OpenList 是"最后登录有效"模式,每次登录会使旧 Token 失效
|
||||
UserOpenlist config = openListClient.getUserOpenlist(user.getId());
|
||||
if (config != null) {
|
||||
if (config.isTokenValid()) {
|
||||
// Token 缓存仍然有效,不需要重新登录
|
||||
info.put("connected", true);
|
||||
info.put("connectMessage", "连接正常");
|
||||
log.info("用户 {} 的 OpenList Token 缓存有效,无需重新登录", user.getId());
|
||||
} else {
|
||||
// Token 已过期或不存在,需要重新登录验证
|
||||
var testResult = openListClient.testConnection(
|
||||
config.getOpenlistUrl(),
|
||||
config.getOpenlistUsername(),
|
||||
config.getOpenlistPassword()
|
||||
);
|
||||
boolean connected = (boolean) testResult.get("success");
|
||||
info.put("connected", connected);
|
||||
info.put("connectMessage", testResult.get("message"));
|
||||
|
||||
// 如果连接成功,将新获取的 token 保存到数据库
|
||||
if (connected && testResult.get("token") != null) {
|
||||
String newToken = (String) testResult.get("token");
|
||||
long expireTime = System.currentTimeMillis() + 86400000; // 24 小时
|
||||
userOpenlistRepository.updateToken(user.getId(), newToken, expireTime);
|
||||
log.info("用户 {} 的 OpenList Token 已更新到数据库", user.getId());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
info.put("connected", false);
|
||||
info.put("connectMessage", "未找到连接配置");
|
||||
}
|
||||
|
||||
return Result.success(info);
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动触发 OpenList 文件同步
|
||||
* 从 OpenList 根目录递归遍历,同步文件夹层级和视频文件到本地数据库
|
||||
* 只同步视频格式文件(mp4, mkv, avi 等),跳过非视频文件
|
||||
*
|
||||
* POST /api/openlist/sync
|
||||
*
|
||||
* 响应:
|
||||
* {
|
||||
* "code": 200,
|
||||
* "data": {
|
||||
* "driveCount": 1,
|
||||
* "folderCount": 10,
|
||||
* "movieCount": 50,
|
||||
* "skippedFileCount": 5,
|
||||
* "skippedFolderCount": 2,
|
||||
* "errorCount": 0,
|
||||
* "errors": []
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
@PostMapping("/sync")
|
||||
public Result<?> syncFiles(HttpServletRequest request) {
|
||||
User user = getCurrentUser(request);
|
||||
|
||||
// 检查用户是否已绑定 OpenList
|
||||
UserOpenlist config = openListClient.getUserOpenlist(user.getId());
|
||||
if (config == null) {
|
||||
return Result.error("同步失败:用户未绑定 OpenList,请先绑定");
|
||||
}
|
||||
|
||||
log.info("用户 {} 手动触发 OpenList 同步", user.getId());
|
||||
|
||||
// 在同步前强制刷新 Token,确保使用的是最新有效的 Token
|
||||
// 避免因为 info 接口登录后 Token 被更新,但 sync 接口使用的是旧 Token
|
||||
boolean tokenRefreshed = openListClient.refreshAndVerifyToken(user.getId());
|
||||
if (!tokenRefreshed) {
|
||||
return Result.error("同步失败:无法刷新 OpenList Token,请检查用户名和密码");
|
||||
}
|
||||
|
||||
var syncResult = openListSyncService.syncFromOpenList(user.getId());
|
||||
|
||||
// 根据是否有错误返回不同的结果
|
||||
if (syncResult.errors.isEmpty()) {
|
||||
// 同步完全成功
|
||||
return Result.success("同步完成", syncResult.toMap());
|
||||
} else {
|
||||
// 同步失败,将错误信息拼接在 message 中返回
|
||||
String errorDetails = String.join("; ", syncResult.errors);
|
||||
String message = "同步失败: " + errorDetails;
|
||||
return Result.error(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,180 +0,0 @@
|
||||
package com.wangpan.listener;
|
||||
|
||||
import com.wangpan.common.dal.dataobject.DriveDO;
|
||||
import com.wangpan.common.dal.repository.DriveRepository;
|
||||
import com.wangpan.common.util.MinioUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
/**
|
||||
* Logo 回收定时任务
|
||||
*
|
||||
* 回收策略:
|
||||
* 1. 超过 30 天没有访问的 Logo 进入回收模式(recycling)
|
||||
* 2. 回收模式下 7 天内访问一次则恢复正常模式(normal)并重新计时 30 天
|
||||
* 3. 否则将直接从 MinIO 中删除
|
||||
*/
|
||||
@Component
|
||||
public class LogoRecycleScheduler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(LogoRecycleScheduler.class);
|
||||
|
||||
/** 正常模式:30 天未访问进入回收 */
|
||||
private static final int NORMAL_MODE_DAYS = 30;
|
||||
|
||||
/** 回收模式:7 天未访问则删除 */
|
||||
private static final int RECYCLE_MODE_DAYS = 7;
|
||||
|
||||
@Autowired
|
||||
private DriveRepository driveRepository;
|
||||
|
||||
/**
|
||||
* 每天凌晨 2 点执行回收检查
|
||||
*/
|
||||
@Scheduled(cron = "0 0 2 * * ?")
|
||||
public void checkAndRecycleLogos() {
|
||||
log.info("========== Logo 回收检查开始 ==========");
|
||||
|
||||
try {
|
||||
// 检查正常模式的 Logo,超过 30 天未访问则进入回收模式
|
||||
checkNormalModeLogos();
|
||||
|
||||
// 检查回收模式的 Logo,超过 7 天未访问则删除
|
||||
checkRecycleModeLogos();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Logo 回收检查失败", e);
|
||||
}
|
||||
|
||||
log.info("========== Logo 回收检查完成 ==========");
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查正常模式的 Logo
|
||||
* 超过 30 天未访问则进入回收模式
|
||||
*/
|
||||
private void checkNormalModeLogos() {
|
||||
var drives = driveRepository.findDrivesWithLogoNormal();
|
||||
log.info("检查正常模式 Logo:共 {} 个", drives.size());
|
||||
|
||||
int recycledCount = 0;
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
for (var drive : drives) {
|
||||
if (drive.getLogoLastAccessTime() == null) {
|
||||
// 从未记录访问时间,使用创建时间
|
||||
if (drive.getCreateTime() != null) {
|
||||
long daysSinceCreation = ChronoUnit.DAYS.between(drive.getCreateTime(), now);
|
||||
if (daysSinceCreation >= NORMAL_MODE_DAYS) {
|
||||
// 进入回收模式
|
||||
driveRepository.updateLogoStatusToRecycling(drive.getId());
|
||||
log.info("网盘 {} 的 Logo 进入回收模式(创建超过 {} 天未访问)", drive.getId(), daysSinceCreation);
|
||||
recycledCount++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
long daysSinceLastAccess = ChronoUnit.DAYS.between(drive.getLogoLastAccessTime(), now);
|
||||
if (daysSinceLastAccess >= NORMAL_MODE_DAYS) {
|
||||
// 进入回收模式
|
||||
driveRepository.updateLogoStatusToRecycling(drive.getId());
|
||||
log.info("网盘 {} 的 Logo 进入回收模式({} 天未访问)", drive.getId(), daysSinceLastAccess);
|
||||
recycledCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.info("正常模式检查完成:{} 个 Logo 进入回收模式", recycledCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查回收模式的 Logo
|
||||
* 超过 7 天未访问则从 MinIO 删除
|
||||
*/
|
||||
private void checkRecycleModeLogos() {
|
||||
var drives = driveRepository.findDrivesWithLogoRecycling();
|
||||
log.info("检查回收模式 Logo:共 {} 个", drives.size());
|
||||
|
||||
int deletedCount = 0;
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
for (var drive : drives) {
|
||||
if (drive.getLogoLastAccessTime() != null) {
|
||||
long daysSinceLastAccess = ChronoUnit.DAYS.between(drive.getLogoLastAccessTime(), now);
|
||||
if (daysSinceLastAccess >= RECYCLE_MODE_DAYS) {
|
||||
// 从 MinIO 删除 Logo
|
||||
deleteLogoFromMinIO(drive);
|
||||
deletedCount++;
|
||||
}
|
||||
} else {
|
||||
// 回收模式下没有访问时间记录,直接删除
|
||||
deleteLogoFromMinIO(drive);
|
||||
deletedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
log.info("回收模式检查完成:{} 个 Logo 被删除", deletedCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 MinIO 删除 Logo 并更新数据库状态
|
||||
*/
|
||||
private void deleteLogoFromMinIO(DriveDO drive) {
|
||||
String logoUrl = drive.getLogoUrl();
|
||||
if (logoUrl == null || logoUrl.isBlank()) {
|
||||
driveRepository.deleteLogo(drive.getId());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 从 URL 中提取 objectName
|
||||
// URL 格式:http://endpoint/bucket/objectName
|
||||
String objectName = extractObjectNameFromUrl(logoUrl);
|
||||
|
||||
if (objectName != null && !objectName.isBlank()) {
|
||||
// 调用 MinIO 删除文件
|
||||
boolean deleted = MinioUtil.deleteFile(objectName);
|
||||
if (deleted) {
|
||||
log.info("MinIO 文件删除成功:{}", objectName);
|
||||
} else {
|
||||
log.warn("MinIO 文件删除失败:{}", objectName);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新数据库状态
|
||||
driveRepository.deleteLogo(drive.getId());
|
||||
log.info("网盘 {} 的 Logo 已删除", drive.getId());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("删除网盘 {} 的 Logo 失败", drive.getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 MinIO URL 中提取 objectName
|
||||
* 例如:http://minio.example.com/bucket/path/to/file.jpg -> path/to/file.jpg
|
||||
*/
|
||||
private String extractObjectNameFromUrl(String url) {
|
||||
try {
|
||||
// 简单解析:找到第三个 / 之后的部分
|
||||
int firstSlash = url.indexOf("://");
|
||||
if (firstSlash == -1) return null;
|
||||
|
||||
int secondSlash = url.indexOf("/", firstSlash + 3);
|
||||
if (secondSlash == -1) return null;
|
||||
|
||||
int thirdSlash = url.indexOf("/", secondSlash + 1);
|
||||
if (thirdSlash == -1) return null;
|
||||
|
||||
return url.substring(thirdSlash + 1);
|
||||
} catch (Exception e) {
|
||||
log.error("解析 MinIO URL 失败:{}", url, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,108 +0,0 @@
|
||||
package com.wangpan.listener;
|
||||
|
||||
import com.wangpan.common.dal.repository.UserOpenlistRepository;
|
||||
import com.wangpan.common.entity.UserOpenlist;
|
||||
import com.wangpan.common.util.OpenListClient;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* OpenList 心跳检测定时任务
|
||||
*
|
||||
* 每 24 小时自动检测所有已绑定 OpenList 用户的连接状态。
|
||||
* 检测结果记录到数据库(last_heartbeat_time、heartbeat_status、heartbeat_error),
|
||||
* 前端可通过 /api/openlist/info 接口查看心跳状态。
|
||||
*
|
||||
* 检测逻辑:
|
||||
* 1. 查询所有状态为 active 的用户 OpenList 配置
|
||||
* 2. 逐个调用 OpenList 登录接口验证连接
|
||||
* 3. 将检测结果(成功/失败、失败原因、检测时间)写入数据库
|
||||
*/
|
||||
@Component
|
||||
public class OpenListHeartbeatScheduler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(OpenListHeartbeatScheduler.class);
|
||||
|
||||
@Autowired
|
||||
private UserOpenlistRepository userOpenlistRepository;
|
||||
|
||||
@Autowired
|
||||
private OpenListClient openListClient;
|
||||
|
||||
/**
|
||||
* 每 24 小时执行一次心跳检测
|
||||
* cron 表达式:0 0 0/24 * * ? 表示每 24 小时整点执行
|
||||
* 首次启动后延迟 5 分钟执行,避免启动时集中检测
|
||||
*/
|
||||
@Scheduled(fixedDelay = 86400000, initialDelay = 300000)
|
||||
public void checkHeartbeat() {
|
||||
log.info("========== OpenList 心跳检测开始 ==========");
|
||||
|
||||
List<UserOpenlist> configs = userOpenlistRepository.findAllActive();
|
||||
if (configs.isEmpty()) {
|
||||
log.info("没有活跃的用户 OpenList 配置,跳过心跳检测");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("共检测到 {} 个用户 OpenList 配置,开始逐个检测", configs.size());
|
||||
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
|
||||
for (UserOpenlist config : configs) {
|
||||
Long userId = config.getUserId();
|
||||
try {
|
||||
// 心跳检测:如果 Token 仍然有效,说明连接正常,不需要重新登录
|
||||
// 避免心跳登录导致正在使用的 Token 被作废
|
||||
if (config.isTokenValid()) {
|
||||
userOpenlistRepository.updateHeartbeat(userId, "success", null);
|
||||
successCount++;
|
||||
log.info("用户 {} OpenList 心跳检测成功(Token 缓存有效): {}", userId, config.getOpenlistUrl());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Token 已过期,需要重新登录验证连接
|
||||
Map<String, Object> testResult = openListClient.testConnection(
|
||||
config.getOpenlistUrl(),
|
||||
config.getOpenlistUsername(),
|
||||
config.getOpenlistPassword()
|
||||
);
|
||||
|
||||
boolean success = (boolean) testResult.get("success");
|
||||
String message = (String) testResult.get("message");
|
||||
|
||||
if (success) {
|
||||
// 连接成功,更新心跳记录
|
||||
userOpenlistRepository.updateHeartbeat(userId, "success", null);
|
||||
// 同时刷新 Token 缓存
|
||||
String token = (String) testResult.get("token");
|
||||
if (token != null) {
|
||||
long expireTime = System.currentTimeMillis() + 86400000;
|
||||
userOpenlistRepository.updateToken(userId, token, expireTime);
|
||||
}
|
||||
successCount++;
|
||||
log.info("用户 {} OpenList 心跳检测成功: {}", userId, config.getOpenlistUrl());
|
||||
} else {
|
||||
// 连接失败,记录失败原因
|
||||
userOpenlistRepository.updateHeartbeat(userId, "failed", message);
|
||||
failCount++;
|
||||
log.warn("用户 {} OpenList 心跳检测失败: {} - {}", userId, config.getOpenlistUrl(), message);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 检测过程异常,记录异常信息
|
||||
String errorMsg = "检测异常:" + e.getMessage();
|
||||
userOpenlistRepository.updateHeartbeat(userId, "failed", errorMsg);
|
||||
failCount++;
|
||||
log.error("用户 {} OpenList 心跳检测异常: {}", userId, config.getOpenlistUrl(), e);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("========== OpenList 心跳检测完成:成功 {} 个,失败 {} 个 ==========", successCount, failCount);
|
||||
}
|
||||
}
|
||||
@ -1,427 +0,0 @@
|
||||
package com.wangpan.service;
|
||||
|
||||
import com.wangpan.common.dal.repository.DriveFolderRepository;
|
||||
import com.wangpan.common.dal.repository.DriveMovieRepository;
|
||||
import com.wangpan.common.dal.repository.DriveRepository;
|
||||
import com.wangpan.common.entity.Drive;
|
||||
import com.wangpan.common.entity.DriveFolder;
|
||||
import com.wangpan.common.entity.DriveMovie;
|
||||
import com.wangpan.common.entity.UserOpenlist;
|
||||
import com.wangpan.common.util.OpenListClient;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* OpenList 同步服务
|
||||
*
|
||||
* 负责将用户 OpenList 中的文件夹层级和视频文件同步到本地数据库。
|
||||
* 同步策略:
|
||||
* 1. 自动为用户创建一个关联 OpenList 的网盘(Drive)
|
||||
* 2. 递归遍历 OpenList 目录树,在本地创建对应的文件夹(DriveFolder)
|
||||
* 3. 只同步视频格式文件,创建电影记录(DriveMovie)
|
||||
* 4. 非视频文件和非视频文件夹会被跳过
|
||||
*
|
||||
* 视频格式白名单:mp4, mkv, avi, mov, wmv, flv, webm, m4v, mpg, mpeg, 3gp, ts, rm, rmvb, vob, iso
|
||||
*/
|
||||
@Service
|
||||
public class OpenListSyncService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(OpenListSyncService.class);
|
||||
|
||||
/** 支持的视频文件扩展名(小写) */
|
||||
private static final Set<String> VIDEO_EXTENSIONS = Set.of(
|
||||
"mp4", "mkv", "avi", "mov", "wmv", "flv", "webm",
|
||||
"m4v", "mpg", "mpeg", "3gp", "ts", "rm", "rmvb",
|
||||
"vob", "iso"
|
||||
);
|
||||
|
||||
@Autowired
|
||||
private OpenListClient openListClient;
|
||||
|
||||
@Autowired
|
||||
private DriveRepository driveRepository;
|
||||
|
||||
@Autowired
|
||||
private DriveFolderRepository folderRepository;
|
||||
|
||||
@Autowired
|
||||
private DriveMovieRepository movieRepository;
|
||||
|
||||
/**
|
||||
* 同步结果统计
|
||||
*/
|
||||
public static class SyncResult {
|
||||
public int driveCount = 0;
|
||||
public int folderCount = 0;
|
||||
public int movieCount = 0;
|
||||
public int skippedFileCount = 0;
|
||||
public int skippedFolderCount = 0;
|
||||
public List<String> errors = new ArrayList<>();
|
||||
|
||||
public Map<String, Object> toMap() {
|
||||
var map = new LinkedHashMap<String, Object>();
|
||||
map.put("driveCount", driveCount);
|
||||
map.put("folderCount", folderCount);
|
||||
map.put("movieCount", movieCount);
|
||||
map.put("skippedFileCount", skippedFileCount);
|
||||
map.put("skippedFolderCount", skippedFolderCount);
|
||||
map.put("errorCount", errors.size());
|
||||
map.put("errors", errors);
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行全量同步
|
||||
* 从 OpenList 根目录列出所有文件夹,每个文件夹作为一个独立的网盘
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 同步结果统计
|
||||
*/
|
||||
public SyncResult syncFromOpenList(Long userId) {
|
||||
SyncResult result = new SyncResult();
|
||||
|
||||
// 1. 检查用户是否已绑定 OpenList
|
||||
UserOpenlist config = openListClient.getUserOpenlist(userId);
|
||||
if (config == null) {
|
||||
result.errors.add("用户未绑定 OpenList");
|
||||
return result;
|
||||
}
|
||||
|
||||
log.info("用户 {} 开始从 OpenList 同步: {}", userId, config.getOpenlistUrl());
|
||||
|
||||
// 2. 直接从根目录列出所有文件夹(不再使用需要管理员权限的存储API)
|
||||
log.info("用户 {} 尝试从根目录列出所有内容", userId);
|
||||
return syncFromRootDirectory(userId, config, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从根目录列表识别存储并同步
|
||||
* 列出 OpenList 根目录下的所有文件夹,每个文件夹作为一个独立的网盘
|
||||
*/
|
||||
private SyncResult syncFromRootDirectory(Long userId, UserOpenlist config, SyncResult result) {
|
||||
List<Map<String, Object>> rootItems;
|
||||
try {
|
||||
log.info("用户 {} 开始列出 OpenList 根目录内容", userId);
|
||||
rootItems = openListClient.listAll(userId, "/");
|
||||
} catch (Exception e) {
|
||||
log.error("用户 {} 列出 OpenList 根目录失败", userId, e);
|
||||
result.errors.add("列出 OpenList 根目录失败: " + e.getMessage());
|
||||
return result;
|
||||
}
|
||||
|
||||
if (rootItems.isEmpty()) {
|
||||
result.errors.add("OpenList 根目录为空,请检查 OpenList 是否已添加存储");
|
||||
return result;
|
||||
}
|
||||
|
||||
log.info("用户 {} 的 OpenList 根目录有 {} 个项目", userId, rootItems.size());
|
||||
|
||||
// 分离存储(文件夹)和文件
|
||||
List<Map<String, Object>> storages = new ArrayList<>();
|
||||
List<Map<String, Object>> rootFiles = new ArrayList<>();
|
||||
|
||||
for (var item : rootItems) {
|
||||
Object isDirObj = item.get("is_dir");
|
||||
boolean isDir = false;
|
||||
if (isDirObj instanceof Boolean) {
|
||||
isDir = (Boolean) isDirObj;
|
||||
} else if (isDirObj instanceof Integer) {
|
||||
isDir = ((Integer) isDirObj) == 1;
|
||||
} else if (isDirObj instanceof String) {
|
||||
isDir = "true".equalsIgnoreCase((String) isDirObj) || "1".equals(isDirObj);
|
||||
}
|
||||
|
||||
if (isDir) {
|
||||
storages.add(item);
|
||||
} else {
|
||||
rootFiles.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("用户 {} 的 OpenList 根目录有 {} 个文件夹,{} 个文件",
|
||||
userId, storages.size(), rootFiles.size());
|
||||
|
||||
// 为每个文件夹创建网盘
|
||||
for (var storage : storages) {
|
||||
String storageName = (String) storage.get("name");
|
||||
String storagePath = "/" + storageName;
|
||||
|
||||
Drive drive = findOrCreateDriveForStorage(userId, config, storageName, storagePath);
|
||||
if (drive == null) {
|
||||
result.errors.add("创建存储 " + storageName + " 的网盘失败");
|
||||
continue;
|
||||
}
|
||||
result.driveCount++;
|
||||
|
||||
syncDirectory(userId, drive.getId(), storagePath, 0L, 0, result);
|
||||
updateDriveStats(drive.getId(), userId);
|
||||
}
|
||||
|
||||
// 处理根目录文件
|
||||
if (!rootFiles.isEmpty()) {
|
||||
Drive defaultDrive = findOrCreateDrive(userId, config);
|
||||
if (defaultDrive != null) {
|
||||
result.driveCount++;
|
||||
syncRootFiles(userId, defaultDrive.getId(), rootFiles, result);
|
||||
updateDriveStats(defaultDrive.getId(), userId);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为指定存储创建或查找网盘
|
||||
*/
|
||||
private Drive findOrCreateDriveForStorage(Long userId, UserOpenlist config, String storageName, String storagePath) {
|
||||
// 查询用户所有网盘,查找匹配该存储路径的
|
||||
var drives = driveRepository.queryByUserId(userId, 1, 100);
|
||||
for (var driveMap : drives) {
|
||||
String mountPath = (String) driveMap.get("openlistMountPath");
|
||||
if (storagePath.equals(mountPath)) {
|
||||
log.info("复用已有网盘: id={}, mountPath={}", driveMap.get("id"), mountPath);
|
||||
return driveRepository.findById(((Number) driveMap.get("id")).longValue());
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新网盘
|
||||
Drive drive = new Drive(userId, storageName, "从 OpenList 同步的网盘: " + storagePath);
|
||||
drive.setOpenlistMountPath(storagePath);
|
||||
Long driveId = driveRepository.insert(drive);
|
||||
drive.setId(driveId);
|
||||
log.info("创建新网盘: id={}, name={}, mountPath={}", driveId, storageName, storagePath);
|
||||
return drive;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步根目录的文件(不属于任何存储的文件)
|
||||
*/
|
||||
private void syncRootFiles(Long userId, Long driveId, List<Map<String, Object>> rootFiles, SyncResult result) {
|
||||
for (var fileItem : rootFiles) {
|
||||
String fileName = (String) fileItem.get("name");
|
||||
if (isVideoFile(fileName)) {
|
||||
long fileSize = ((Number) fileItem.getOrDefault("size", 0L)).longValue();
|
||||
String extension = getFileExtension(fileName);
|
||||
String openlistFilePath = "/" + fileName;
|
||||
|
||||
DriveMovie movie = new DriveMovie(driveId, 0L, userId,
|
||||
getMovieTitle(fileName), fileName);
|
||||
movie.setFileSize(fileSize);
|
||||
movie.setExtension(extension);
|
||||
movie.setMimeType("video/" + extension);
|
||||
movie.setOpenlistFilePath(openlistFilePath);
|
||||
movie.setStorageType("openlist");
|
||||
movie.setScrapeType("none");
|
||||
movie.setScrapeStatus("pending");
|
||||
|
||||
movieRepository.insert(movie);
|
||||
result.movieCount++;
|
||||
} else {
|
||||
result.skippedFileCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新网盘统计信息
|
||||
*/
|
||||
private void updateDriveStats(Long driveId, Long userId) {
|
||||
int totalFolders = folderRepository.countByDrive(driveId, userId);
|
||||
int totalMovies = movieRepository.countByDrive(driveId, userId);
|
||||
driveRepository.updateStats(driveId, totalFolders, totalMovies, 0L);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找或创建关联网盘
|
||||
* 查找用户已有的 OpenList 网盘,如果没有则创建新的
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param config OpenList 配置
|
||||
* @return Drive 对象
|
||||
*/
|
||||
private Drive findOrCreateDrive(Long userId, UserOpenlist config) {
|
||||
// 查询用户所有网盘,查找已关联网盘的
|
||||
var drives = driveRepository.queryByUserId(userId, 1, 100);
|
||||
for (var driveMap : drives) {
|
||||
String mountPath = (String) driveMap.get("openlistMountPath");
|
||||
if (mountPath != null && !mountPath.isBlank()) {
|
||||
// 复用已有网盘
|
||||
log.info("复用已有网盘: id={}", driveMap.get("id"));
|
||||
return driveRepository.findById(((Number) driveMap.get("id")).longValue());
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新网盘
|
||||
Drive drive = new Drive(userId, "OpenList 网盘", "从 OpenList 同步的网盘");
|
||||
drive.setOpenlistMountPath(config.getMountPath() != null ? config.getMountPath() : "/");
|
||||
Long driveId = driveRepository.insert(drive);
|
||||
drive.setId(driveId);
|
||||
log.info("创建新网盘: id={}", driveId);
|
||||
return drive;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归同步目录
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param driveId 网盘ID
|
||||
* @param openlistPath OpenList 中的路径
|
||||
* @param parentFolderId 本地父文件夹ID(0 表示根目录)
|
||||
* @param depth 当前层级深度
|
||||
* @param result 同步结果统计
|
||||
*/
|
||||
private void syncDirectory(Long userId, Long driveId, String openlistPath,
|
||||
Long parentFolderId, int depth, SyncResult result) {
|
||||
try {
|
||||
// 获取目录下所有内容
|
||||
List<Map<String, Object>> items = openListClient.listAll(userId, openlistPath);
|
||||
log.info("用户 {} 路径 {} 获取到 {} 个内容项", userId, openlistPath, items.size());
|
||||
if (items.isEmpty()) {
|
||||
log.warn("用户 {} 路径 {} 为空,跳过同步", userId, openlistPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// 打印前几个项目用于调试
|
||||
for (int i = 0; i < Math.min(3, items.size()); i++) {
|
||||
var item = items.get(i);
|
||||
log.info("项目 {}: name={}, is_dir={}, type={}", i, item.get("name"), item.get("is_dir"), item.get("type"));
|
||||
}
|
||||
|
||||
// 分离文件夹和视频文件
|
||||
List<Map<String, Object>> folders = new ArrayList<>();
|
||||
List<Map<String, Object>> videoFiles = new ArrayList<>();
|
||||
|
||||
for (var item : items) {
|
||||
String name = (String) item.get("name");
|
||||
|
||||
// 处理 is_dir 字段可能是 Boolean、Integer 或 String 类型
|
||||
Object isDirObj = item.get("is_dir");
|
||||
boolean isDir = false;
|
||||
if (isDirObj instanceof Boolean) {
|
||||
isDir = (Boolean) isDirObj;
|
||||
} else if (isDirObj instanceof Integer) {
|
||||
isDir = ((Integer) isDirObj) == 1;
|
||||
} else if (isDirObj instanceof String) {
|
||||
isDir = "true".equalsIgnoreCase((String) isDirObj) || "1".equals(isDirObj);
|
||||
}
|
||||
|
||||
if (isDir) {
|
||||
folders.add(item);
|
||||
} else {
|
||||
// 判断是否为视频文件
|
||||
if (isVideoFile(name)) {
|
||||
videoFiles.add(item);
|
||||
} else {
|
||||
result.skippedFileCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果该目录下没有任何视频文件且没有子文件夹,跳过整个目录
|
||||
if (folders.isEmpty() && videoFiles.isEmpty()) {
|
||||
result.skippedFolderCount++;
|
||||
return;
|
||||
}
|
||||
|
||||
// 同步文件夹(递归)
|
||||
for (var folderItem : folders) {
|
||||
String folderName = (String) folderItem.get("name");
|
||||
String childPath = normalizePath(openlistPath + "/" + folderName);
|
||||
|
||||
// 在本地创建文件夹记录
|
||||
DriveFolder folder = new DriveFolder(driveId, userId, parentFolderId, folderName, null);
|
||||
folder.setDepth(depth + 1);
|
||||
Long folderId = folderRepository.insert(folder);
|
||||
result.folderCount++;
|
||||
|
||||
log.debug("同步文件夹: {} -> 本地ID={}", childPath, folderId);
|
||||
|
||||
// 递归同步子目录
|
||||
syncDirectory(userId, driveId, childPath, folderId, depth + 1, result);
|
||||
}
|
||||
|
||||
// 同步视频文件
|
||||
for (var fileItem : videoFiles) {
|
||||
String fileName = (String) fileItem.get("name");
|
||||
long fileSize = ((Number) fileItem.getOrDefault("size", 0L)).longValue();
|
||||
String extension = getFileExtension(fileName);
|
||||
|
||||
// 构建 OpenList 文件路径
|
||||
String openlistFilePath = normalizePath(openlistPath + "/" + fileName);
|
||||
|
||||
// 创建电影记录
|
||||
DriveMovie movie = new DriveMovie(driveId, parentFolderId, userId,
|
||||
getMovieTitle(fileName), fileName);
|
||||
movie.setFileSize(fileSize);
|
||||
movie.setExtension(extension);
|
||||
movie.setMimeType("video/" + extension);
|
||||
movie.setOpenlistFilePath(openlistFilePath);
|
||||
movie.setStorageType("openlist");
|
||||
movie.setScrapeType("none");
|
||||
movie.setScrapeStatus("pending");
|
||||
|
||||
movieRepository.insert(movie);
|
||||
result.movieCount++;
|
||||
|
||||
log.debug("同步视频: {} ({} bytes) -> 本地ID={}", openlistFilePath, fileSize, movie.getId());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
String errorMsg = "同步目录 " + openlistPath + " 失败: " + e.getMessage();
|
||||
result.errors.add(errorMsg);
|
||||
log.error("用户 {} 同步目录 {} 失败", userId, openlistPath, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断文件是否为视频格式
|
||||
*/
|
||||
private boolean isVideoFile(String fileName) {
|
||||
if (fileName == null || fileName.isBlank()) return false;
|
||||
String ext = getFileExtension(fileName).toLowerCase();
|
||||
return VIDEO_EXTENSIONS.contains(ext);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件扩展名
|
||||
*/
|
||||
private String getFileExtension(String fileName) {
|
||||
if (fileName == null || !fileName.contains(".")) return "";
|
||||
return fileName.substring(fileName.lastIndexOf('.') + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件名提取电影标题(去掉扩展名)
|
||||
*/
|
||||
private String getMovieTitle(String fileName) {
|
||||
if (fileName == null) return "";
|
||||
String title = fileName;
|
||||
if (title.contains(".")) {
|
||||
title = title.substring(0, title.lastIndexOf('.'));
|
||||
}
|
||||
return title.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化路径:去除多余斜杠,确保以 / 开头
|
||||
*/
|
||||
private String normalizePath(String path) {
|
||||
if (path == null) return "/";
|
||||
// 替换多个连续斜杠为单个斜杠
|
||||
path = path.replaceAll("/+", "/");
|
||||
// 确保以 / 开头
|
||||
if (!path.startsWith("/")) {
|
||||
path = "/" + path;
|
||||
}
|
||||
// 去除末尾斜杠(根目录除外)
|
||||
if (path.length() > 1 && path.endsWith("/")) {
|
||||
path = path.substring(0, path.length() - 1);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
}
|
||||
@ -61,9 +61,3 @@ mybatis-plus:
|
||||
global-config:
|
||||
db-config:
|
||||
id-type: auto
|
||||
|
||||
# OpenList 配置
|
||||
openlist:
|
||||
base-url: ENC(nAus1Dw7kxDsHnaqF+0g1Np+4wsYAcRlIqXPxaag1knMYJlBQsf/U3AKhetuIJywsGvENyNH7cscCtlDsv4oLA==)
|
||||
username: ENC(s4yi9ARKcY1dY1whS0MKMr7RjM6FMzePB6FYfmCeEilAbV927glsrdP8EUZblGun)
|
||||
password: ENC(TUkBEpl8iK5kIYcuFE5tLsMeozUDfqdeep1se21GTxE9D+HfcdfsVTJEgNuol2N+)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user