feat: 新增OpenList集成功能,完善TMDB刮削与配置加密
1. 新增用户OpenList连接配置模块,包括数据库表、实体类与相关服务 2. 为驱动服务添加定时心跳检测功能,监控OpenList连接状态 3. 重构电影实体与数据库字段,移除冗余的overview字段,统一使用description存储简介 4. 完善TMDB刮削逻辑,增加空指针校验与详细失败日志 5. 配置文件敏感信息加密,添加加密脚本工具 6. 新增网关路由规则支持OpenList接口 7. 完善刮削日志查询功能,支持查看详细执行结果
This commit is contained in:
parent
ac0d94bd00
commit
9f19d64afb
2
encrypt.bat
Normal file
2
encrypt.bat
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
@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!
|
||||||
5
encrypt.ps1
Normal file
5
encrypt.ps1
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
$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"
|
||||||
BIN
wangpan-common/deps/HikariCP-5.0.1.jar
Normal file
BIN
wangpan-common/deps/HikariCP-5.0.1.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/annotations-13.0.jar
Normal file
BIN
wangpan-common/deps/annotations-13.0.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/bcprov-jdk18on-1.76.jar
Normal file
BIN
wangpan-common/deps/bcprov-jdk18on-1.76.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/checker-qual-3.37.0.jar
Normal file
BIN
wangpan-common/deps/checker-qual-3.37.0.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/commons-compress-1.24.0.jar
Normal file
BIN
wangpan-common/deps/commons-compress-1.24.0.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/error_prone_annotations-2.21.1.jar
Normal file
BIN
wangpan-common/deps/error_prone_annotations-2.21.1.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/failureaccess-1.0.1.jar
Normal file
BIN
wangpan-common/deps/failureaccess-1.0.1.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/guava-32.1.3-jre.jar
Normal file
BIN
wangpan-common/deps/guava-32.1.3-jre.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/j2objc-annotations-2.8.jar
Normal file
BIN
wangpan-common/deps/j2objc-annotations-2.8.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/jackson-annotations-2.15.4.jar
Normal file
BIN
wangpan-common/deps/jackson-annotations-2.15.4.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/jackson-core-2.15.4.jar
Normal file
BIN
wangpan-common/deps/jackson-core-2.15.4.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/jackson-databind-2.15.4.jar
Normal file
BIN
wangpan-common/deps/jackson-databind-2.15.4.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/jackson-datatype-jdk8-2.15.4.jar
Normal file
BIN
wangpan-common/deps/jackson-datatype-jdk8-2.15.4.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/jackson-datatype-jsr310-2.15.4.jar
Normal file
BIN
wangpan-common/deps/jackson-datatype-jsr310-2.15.4.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/jackson-module-parameter-names-2.15.4.jar
Normal file
BIN
wangpan-common/deps/jackson-module-parameter-names-2.15.4.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/jakarta.annotation-api-2.1.1.jar
Normal file
BIN
wangpan-common/deps/jakarta.annotation-api-2.1.1.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/jasypt-1.9.3.jar
Normal file
BIN
wangpan-common/deps/jasypt-1.9.3.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/jsqlparser-4.9.jar
Normal file
BIN
wangpan-common/deps/jsqlparser-4.9.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/jsr305-3.0.2.jar
Normal file
BIN
wangpan-common/deps/jsr305-3.0.2.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/jul-to-slf4j-2.0.13.jar
Normal file
BIN
wangpan-common/deps/jul-to-slf4j-2.0.13.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/kotlin-stdlib-1.9.23.jar
Normal file
BIN
wangpan-common/deps/kotlin-stdlib-1.9.23.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/kotlin-stdlib-common-1.9.23.jar
Normal file
BIN
wangpan-common/deps/kotlin-stdlib-common-1.9.23.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/kotlin-stdlib-jdk7-1.9.23.jar
Normal file
BIN
wangpan-common/deps/kotlin-stdlib-jdk7-1.9.23.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/kotlin-stdlib-jdk8-1.9.23.jar
Normal file
BIN
wangpan-common/deps/kotlin-stdlib-jdk8-1.9.23.jar
Normal file
Binary file not shown.
Binary file not shown.
BIN
wangpan-common/deps/log4j-api-2.21.1.jar
Normal file
BIN
wangpan-common/deps/log4j-api-2.21.1.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/log4j-to-slf4j-2.21.1.jar
Normal file
BIN
wangpan-common/deps/log4j-to-slf4j-2.21.1.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/logback-classic-1.4.14.jar
Normal file
BIN
wangpan-common/deps/logback-classic-1.4.14.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/logback-core-1.4.14.jar
Normal file
BIN
wangpan-common/deps/logback-core-1.4.14.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/micrometer-commons-1.12.5.jar
Normal file
BIN
wangpan-common/deps/micrometer-commons-1.12.5.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/micrometer-observation-1.12.5.jar
Normal file
BIN
wangpan-common/deps/micrometer-observation-1.12.5.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/minio-8.5.7.jar
Normal file
BIN
wangpan-common/deps/minio-8.5.7.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/mybatis-3.5.16.jar
Normal file
BIN
wangpan-common/deps/mybatis-3.5.16.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/mybatis-plus-3.5.7.jar
Normal file
BIN
wangpan-common/deps/mybatis-plus-3.5.7.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/mybatis-plus-annotation-3.5.7.jar
Normal file
BIN
wangpan-common/deps/mybatis-plus-annotation-3.5.7.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/mybatis-plus-core-3.5.7.jar
Normal file
BIN
wangpan-common/deps/mybatis-plus-core-3.5.7.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/mybatis-plus-extension-3.5.7.jar
Normal file
BIN
wangpan-common/deps/mybatis-plus-extension-3.5.7.jar
Normal file
Binary file not shown.
Binary file not shown.
BIN
wangpan-common/deps/mybatis-plus-spring-boot3-starter-3.5.7.jar
Normal file
BIN
wangpan-common/deps/mybatis-plus-spring-boot3-starter-3.5.7.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/mybatis-spring-3.0.3.jar
Normal file
BIN
wangpan-common/deps/mybatis-spring-3.0.3.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/mysql-connector-j-8.0.33.jar
Normal file
BIN
wangpan-common/deps/mysql-connector-j-8.0.33.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/okhttp-4.12.0.jar
Normal file
BIN
wangpan-common/deps/okhttp-4.12.0.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/okio-3.6.0.jar
Normal file
BIN
wangpan-common/deps/okio-3.6.0.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/okio-jvm-3.6.0.jar
Normal file
BIN
wangpan-common/deps/okio-jvm-3.6.0.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/simple-xml-safe-2.7.1.jar
Normal file
BIN
wangpan-common/deps/simple-xml-safe-2.7.1.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/slf4j-api-2.0.13.jar
Normal file
BIN
wangpan-common/deps/slf4j-api-2.0.13.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/snakeyaml-2.2.jar
Normal file
BIN
wangpan-common/deps/snakeyaml-2.2.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/snappy-java-1.1.10.5.jar
Normal file
BIN
wangpan-common/deps/snappy-java-1.1.10.5.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/spring-aop-6.1.6.jar
Normal file
BIN
wangpan-common/deps/spring-aop-6.1.6.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/spring-beans-6.1.6.jar
Normal file
BIN
wangpan-common/deps/spring-beans-6.1.6.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/spring-boot-3.2.5.jar
Normal file
BIN
wangpan-common/deps/spring-boot-3.2.5.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/spring-boot-autoconfigure-3.2.5.jar
Normal file
BIN
wangpan-common/deps/spring-boot-autoconfigure-3.2.5.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/spring-boot-starter-3.2.5.jar
Normal file
BIN
wangpan-common/deps/spring-boot-starter-3.2.5.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/spring-boot-starter-jdbc-3.2.5.jar
Normal file
BIN
wangpan-common/deps/spring-boot-starter-jdbc-3.2.5.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/spring-boot-starter-json-3.2.5.jar
Normal file
BIN
wangpan-common/deps/spring-boot-starter-json-3.2.5.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/spring-boot-starter-logging-3.2.5.jar
Normal file
BIN
wangpan-common/deps/spring-boot-starter-logging-3.2.5.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/spring-boot-starter-tomcat-3.2.5.jar
Normal file
BIN
wangpan-common/deps/spring-boot-starter-tomcat-3.2.5.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/spring-boot-starter-web-3.2.5.jar
Normal file
BIN
wangpan-common/deps/spring-boot-starter-web-3.2.5.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/spring-context-6.1.6.jar
Normal file
BIN
wangpan-common/deps/spring-context-6.1.6.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/spring-core-6.1.6.jar
Normal file
BIN
wangpan-common/deps/spring-core-6.1.6.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/spring-expression-6.1.6.jar
Normal file
BIN
wangpan-common/deps/spring-expression-6.1.6.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/spring-jcl-6.1.6.jar
Normal file
BIN
wangpan-common/deps/spring-jcl-6.1.6.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/spring-jdbc-6.1.6.jar
Normal file
BIN
wangpan-common/deps/spring-jdbc-6.1.6.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/spring-tx-6.1.6.jar
Normal file
BIN
wangpan-common/deps/spring-tx-6.1.6.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/spring-web-6.1.6.jar
Normal file
BIN
wangpan-common/deps/spring-web-6.1.6.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/spring-webmvc-6.1.6.jar
Normal file
BIN
wangpan-common/deps/spring-webmvc-6.1.6.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/tomcat-embed-core-10.1.20.jar
Normal file
BIN
wangpan-common/deps/tomcat-embed-core-10.1.20.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/tomcat-embed-el-10.1.20.jar
Normal file
BIN
wangpan-common/deps/tomcat-embed-el-10.1.20.jar
Normal file
Binary file not shown.
BIN
wangpan-common/deps/tomcat-embed-websocket-10.1.20.jar
Normal file
BIN
wangpan-common/deps/tomcat-embed-websocket-10.1.20.jar
Normal file
Binary file not shown.
@ -95,9 +95,6 @@ public class DriveMovieDO implements Serializable {
|
|||||||
/** 标签(逗号分隔) */
|
/** 标签(逗号分隔) */
|
||||||
private String tags;
|
private String tags;
|
||||||
|
|
||||||
/** 电影简介(TMDB刮削) */
|
|
||||||
private String overview;
|
|
||||||
|
|
||||||
/** 主演(逗号分隔,TMDB刮削) */
|
/** 主演(逗号分隔,TMDB刮削) */
|
||||||
private String casts;
|
private String casts;
|
||||||
|
|
||||||
@ -192,9 +189,6 @@ public class DriveMovieDO implements Serializable {
|
|||||||
public String getTags() { return tags; }
|
public String getTags() { return tags; }
|
||||||
public void setTags(String tags) { this.tags = tags; }
|
public void setTags(String tags) { this.tags = tags; }
|
||||||
|
|
||||||
public String getOverview() { return overview; }
|
|
||||||
public void setOverview(String overview) { this.overview = overview; }
|
|
||||||
|
|
||||||
public String getCasts() { return casts; }
|
public String getCasts() { return casts; }
|
||||||
public void setCasts(String casts) { this.casts = casts; }
|
public void setCasts(String casts) { this.casts = casts; }
|
||||||
|
|
||||||
|
|||||||
@ -68,9 +68,6 @@ public class MovieDO implements Serializable {
|
|||||||
/** 标签(逗号分隔) */
|
/** 标签(逗号分隔) */
|
||||||
private String tags;
|
private String tags;
|
||||||
|
|
||||||
/** 电影简介(TMDB刮削) */
|
|
||||||
private String overview;
|
|
||||||
|
|
||||||
/** 主演(逗号分隔,TMDB刮削) */
|
/** 主演(逗号分隔,TMDB刮削) */
|
||||||
private String casts;
|
private String casts;
|
||||||
|
|
||||||
@ -132,9 +129,6 @@ public class MovieDO implements Serializable {
|
|||||||
public String getTags() { return tags; }
|
public String getTags() { return tags; }
|
||||||
public void setTags(String tags) { this.tags = tags; }
|
public void setTags(String tags) { this.tags = tags; }
|
||||||
|
|
||||||
public String getOverview() { return overview; }
|
|
||||||
public void setOverview(String overview) { this.overview = overview; }
|
|
||||||
|
|
||||||
public String getCasts() { return casts; }
|
public String getCasts() { return casts; }
|
||||||
public void setCasts(String casts) { this.casts = casts; }
|
public void setCasts(String casts) { this.casts = casts; }
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,120 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|||||||
import com.wangpan.common.dal.dataobject.DriveMovieDO;
|
import com.wangpan.common.dal.dataobject.DriveMovieDO;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Update;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -44,7 +45,6 @@ public interface DriveMovieMapper extends BaseMapper<DriveMovieDO> {
|
|||||||
poster_url TEXT DEFAULT NULL COMMENT '海报URL(竖版,加密存储)',
|
poster_url TEXT DEFAULT NULL COMMENT '海报URL(竖版,加密存储)',
|
||||||
backdrop_url TEXT DEFAULT NULL COMMENT '背景图URL(横版,加密存储)',
|
backdrop_url TEXT DEFAULT NULL COMMENT '背景图URL(横版,加密存储)',
|
||||||
tags VARCHAR(500) DEFAULT NULL COMMENT '标签(逗号分隔)',
|
tags VARCHAR(500) DEFAULT NULL COMMENT '标签(逗号分隔)',
|
||||||
overview TEXT DEFAULT NULL COMMENT '电影简介(TMDB刮削)',
|
|
||||||
casts VARCHAR(500) DEFAULT NULL COMMENT '主演(逗号分隔,TMDB刮削)',
|
casts VARCHAR(500) DEFAULT NULL COMMENT '主演(逗号分隔,TMDB刮削)',
|
||||||
directors VARCHAR(200) DEFAULT NULL COMMENT '导演(逗号分隔,TMDB刮削)',
|
directors VARCHAR(200) DEFAULT NULL COMMENT '导演(逗号分隔,TMDB刮削)',
|
||||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
@ -63,4 +63,16 @@ public interface DriveMovieMapper extends BaseMapper<DriveMovieDO> {
|
|||||||
* 复杂SQL,在XML中实现
|
* 复杂SQL,在XML中实现
|
||||||
*/
|
*/
|
||||||
List<Map<String, Object>> findDuplicateMovies(@Param("userId") Long userId);
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -73,9 +73,9 @@ public interface MovieMapper extends BaseMapper<MovieDO> {
|
|||||||
int updateScrapeStatus(@Param("id") Long id, @Param("scrapeStatus") String scrapeStatus, @Param("scrapeError") String scrapeError);
|
int updateScrapeStatus(@Param("id") Long id, @Param("scrapeStatus") String scrapeStatus, @Param("scrapeError") String scrapeError);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新电影简介(overview → description)
|
* 更新电影简介(description 字段)
|
||||||
*/
|
*/
|
||||||
@Update("UPDATE wp_drive_movie SET description = #{description}, overview = #{description}, update_time = NOW() WHERE id = #{id}")
|
@Update("UPDATE wp_drive_movie SET description = #{description}, update_time = NOW() WHERE id = #{id}")
|
||||||
int updateDescription(@Param("id") Long id, @Param("description") String description);
|
int updateDescription(@Param("id") Long id, @Param("description") String description);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -0,0 +1,38 @@
|
|||||||
|
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,27 +204,200 @@ public class DriveMovieRepository extends ServiceImpl<DriveMovieMapper, DriveMov
|
|||||||
return list;
|
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 构建更新条件
|
* 使用 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 originalName, String subtitle, String description,
|
public boolean update(Long id, String title, String description,
|
||||||
Integer rating, String tags, String category) {
|
Integer rating, String tags, String posterUrl, String backdropUrl,
|
||||||
|
String resourceLibrary, String maintainer) {
|
||||||
var wrapper = new LambdaUpdateWrapper<DriveMovieDO>()
|
var wrapper = new LambdaUpdateWrapper<DriveMovieDO>()
|
||||||
.eq(DriveMovieDO::getId, id)
|
.eq(DriveMovieDO::getId, id)
|
||||||
|
// 基础信息字段
|
||||||
.set(title != null, DriveMovieDO::getTitle, title)
|
.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(description != null, DriveMovieDO::getDescription, description)
|
||||||
.set(rating != null, DriveMovieDO::getRating, rating)
|
.set(rating != null, DriveMovieDO::getRating, rating)
|
||||||
.set(tags != null, DriveMovieDO::getTags, tags)
|
.set(tags != null, DriveMovieDO::getTags, tags)
|
||||||
.set(category != null, DriveMovieDO::getCategory, category)
|
// 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(DriveMovieDO::getUpdateTime, LocalDateTime.now());
|
.set(DriveMovieDO::getUpdateTime, LocalDateTime.now());
|
||||||
return update(wrapper);
|
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 构建更新条件
|
* 使用 MyBatis-Plus LambdaUpdateWrapper 构建更新条件
|
||||||
@ -319,7 +492,6 @@ public class DriveMovieRepository extends ServiceImpl<DriveMovieMapper, DriveMov
|
|||||||
movieDO.setPosterUrl(movie.getPosterUrl());
|
movieDO.setPosterUrl(movie.getPosterUrl());
|
||||||
movieDO.setBackdropUrl(movie.getBackdropUrl());
|
movieDO.setBackdropUrl(movie.getBackdropUrl());
|
||||||
movieDO.setTags(movie.getTags());
|
movieDO.setTags(movie.getTags());
|
||||||
movieDO.setOverview(movie.getOverview());
|
|
||||||
movieDO.setCasts(movie.getCasts());
|
movieDO.setCasts(movie.getCasts());
|
||||||
movieDO.setDirectors(movie.getDirectors());
|
movieDO.setDirectors(movie.getDirectors());
|
||||||
movieDO.setOpenlistFilePath(movie.getOpenlistFilePath());
|
movieDO.setOpenlistFilePath(movie.getOpenlistFilePath());
|
||||||
@ -357,7 +529,6 @@ public class DriveMovieRepository extends ServiceImpl<DriveMovieMapper, DriveMov
|
|||||||
movie.setPosterUrl(movieDO.getPosterUrl());
|
movie.setPosterUrl(movieDO.getPosterUrl());
|
||||||
movie.setBackdropUrl(movieDO.getBackdropUrl());
|
movie.setBackdropUrl(movieDO.getBackdropUrl());
|
||||||
movie.setTags(movieDO.getTags());
|
movie.setTags(movieDO.getTags());
|
||||||
movie.setOverview(movieDO.getOverview());
|
|
||||||
movie.setCasts(movieDO.getCasts());
|
movie.setCasts(movieDO.getCasts());
|
||||||
movie.setDirectors(movieDO.getDirectors());
|
movie.setDirectors(movieDO.getDirectors());
|
||||||
movie.setOpenlistFilePath(movieDO.getOpenlistFilePath());
|
movie.setOpenlistFilePath(movieDO.getOpenlistFilePath());
|
||||||
@ -389,7 +560,6 @@ public class DriveMovieRepository extends ServiceImpl<DriveMovieMapper, DriveMov
|
|||||||
map.put("scrapeError", movieDO.getScrapeError());
|
map.put("scrapeError", movieDO.getScrapeError());
|
||||||
map.put("rating", movieDO.getRating());
|
map.put("rating", movieDO.getRating());
|
||||||
map.put("tags", movieDO.getTags());
|
map.put("tags", movieDO.getTags());
|
||||||
map.put("overview", movieDO.getOverview());
|
|
||||||
map.put("casts", movieDO.getCasts());
|
map.put("casts", movieDO.getCasts());
|
||||||
map.put("directors", movieDO.getDirectors());
|
map.put("directors", movieDO.getDirectors());
|
||||||
map.put("posterUrl", movieDO.getPosterUrl());
|
map.put("posterUrl", movieDO.getPosterUrl());
|
||||||
|
|||||||
@ -315,7 +315,7 @@ public class MovieRepository extends ServiceImpl<MovieMapper, MovieDO> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新电影简介(overview → description)
|
* 更新电影简介(description 字段)
|
||||||
*/
|
*/
|
||||||
public int updateDescription(Long id, String description) {
|
public int updateDescription(Long id, String description) {
|
||||||
return baseMapper.updateDescription(id, description);
|
return baseMapper.updateDescription(id, description);
|
||||||
@ -357,11 +357,9 @@ public class MovieRepository extends ServiceImpl<MovieMapper, MovieDO> {
|
|||||||
movieDO.setPosterUrl(movie.getPosterUrl());
|
movieDO.setPosterUrl(movie.getPosterUrl());
|
||||||
movieDO.setBackdropUrl(movie.getBackdropUrl());
|
movieDO.setBackdropUrl(movie.getBackdropUrl());
|
||||||
movieDO.setTags(movie.getTags());
|
movieDO.setTags(movie.getTags());
|
||||||
movieDO.setOverview(movie.getOverview());
|
|
||||||
movieDO.setCasts(movie.getCasts());
|
movieDO.setCasts(movie.getCasts());
|
||||||
movieDO.setDirectors(movie.getDirectors());
|
movieDO.setDirectors(movie.getDirectors());
|
||||||
movieDO.setUpdateTime(movie.getUpdateTime());
|
movieDO.setUpdateTime(movie.getUpdateTime());
|
||||||
movieDO.setCreateTime(movie.getCreateTime());
|
|
||||||
return movieDO;
|
return movieDO;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -385,11 +383,9 @@ public class MovieRepository extends ServiceImpl<MovieMapper, MovieDO> {
|
|||||||
movie.setPosterUrl(movieDO.getPosterUrl());
|
movie.setPosterUrl(movieDO.getPosterUrl());
|
||||||
movie.setBackdropUrl(movieDO.getBackdropUrl());
|
movie.setBackdropUrl(movieDO.getBackdropUrl());
|
||||||
movie.setTags(movieDO.getTags());
|
movie.setTags(movieDO.getTags());
|
||||||
movie.setOverview(movieDO.getOverview());
|
|
||||||
movie.setCasts(movieDO.getCasts());
|
movie.setCasts(movieDO.getCasts());
|
||||||
movie.setDirectors(movieDO.getDirectors());
|
movie.setDirectors(movieDO.getDirectors());
|
||||||
movie.setUpdateTime(movieDO.getUpdateTime());
|
movie.setUpdateTime(movieDO.getUpdateTime());
|
||||||
movie.setCreateTime(movieDO.getCreateTime());
|
|
||||||
return movie;
|
return movie;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -411,7 +407,6 @@ public class MovieRepository extends ServiceImpl<MovieMapper, MovieDO> {
|
|||||||
map.put("posterUrl", movieDO.getPosterUrl());
|
map.put("posterUrl", movieDO.getPosterUrl());
|
||||||
map.put("backdropUrl", movieDO.getBackdropUrl());
|
map.put("backdropUrl", movieDO.getBackdropUrl());
|
||||||
map.put("tags", movieDO.getTags());
|
map.put("tags", movieDO.getTags());
|
||||||
map.put("overview", movieDO.getOverview());
|
|
||||||
map.put("casts", movieDO.getCasts());
|
map.put("casts", movieDO.getCasts());
|
||||||
map.put("directors", movieDO.getDirectors());
|
map.put("directors", movieDO.getDirectors());
|
||||||
map.put("updateTime", movieDO.getUpdateTime() != null ? movieDO.getUpdateTime().toString() : null);
|
map.put("updateTime", movieDO.getUpdateTime() != null ? movieDO.getUpdateTime().toString() : null);
|
||||||
|
|||||||
@ -0,0 +1,275 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -121,9 +121,6 @@ public class DriveMovie implements Serializable {
|
|||||||
/** 标签:逗号分隔,如 "科幻,IMAX,杜比视界" */
|
/** 标签:逗号分隔,如 "科幻,IMAX,杜比视界" */
|
||||||
private String tags;
|
private String tags;
|
||||||
|
|
||||||
/** 电影简介(TMDB刮削) */
|
|
||||||
private String overview;
|
|
||||||
|
|
||||||
/** 主演(逗号分隔,TMDB刮削) */
|
/** 主演(逗号分隔,TMDB刮削) */
|
||||||
private String casts;
|
private String casts;
|
||||||
|
|
||||||
@ -246,9 +243,6 @@ public class DriveMovie implements Serializable {
|
|||||||
public String getTags() { return tags; }
|
public String getTags() { return tags; }
|
||||||
public void setTags(String tags) { this.tags = tags; }
|
public void setTags(String tags) { this.tags = tags; }
|
||||||
|
|
||||||
public String getOverview() { return overview; }
|
|
||||||
public void setOverview(String overview) { this.overview = overview; }
|
|
||||||
|
|
||||||
public String getCasts() { return casts; }
|
public String getCasts() { return casts; }
|
||||||
public void setCasts(String casts) { this.casts = casts; }
|
public void setCasts(String casts) { this.casts = casts; }
|
||||||
|
|
||||||
|
|||||||
@ -93,9 +93,6 @@ public class Movie implements Serializable {
|
|||||||
/** 标签:逗号分隔,如"科幻, IMAX, 杜比视界" */
|
/** 标签:逗号分隔,如"科幻, IMAX, 杜比视界" */
|
||||||
private String tags;
|
private String tags;
|
||||||
|
|
||||||
/** 电影简介(TMDB刮削) */
|
|
||||||
private String overview;
|
|
||||||
|
|
||||||
/** 主演(逗号分隔,TMDB刮削) */
|
/** 主演(逗号分隔,TMDB刮削) */
|
||||||
private String casts;
|
private String casts;
|
||||||
|
|
||||||
@ -176,9 +173,6 @@ public class Movie implements Serializable {
|
|||||||
public String getTags() { return tags; }
|
public String getTags() { return tags; }
|
||||||
public void setTags(String tags) { this.tags = tags; }
|
public void setTags(String tags) { this.tags = tags; }
|
||||||
|
|
||||||
public String getOverview() { return overview; }
|
|
||||||
public void setOverview(String overview) { this.overview = overview; }
|
|
||||||
|
|
||||||
public String getCasts() { return casts; }
|
public String getCasts() { return casts; }
|
||||||
public void setCasts(String casts) { this.casts = casts; }
|
public void setCasts(String casts) { this.casts = casts; }
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,194 @@
|
|||||||
|
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 +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,89 +1,207 @@
|
|||||||
package com.wangpan.common.util;
|
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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.*;
|
import org.springframework.http.*;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OpenList API 客户端
|
* OpenList API 客户端(用户级连接)
|
||||||
*
|
*
|
||||||
* 封装 OpenList 的 REST API 调用,提供统一的文件操作接口。
|
* 每个用户绑定自己的 OpenList 实例(地址、账号、密码),互不干扰。
|
||||||
|
* Token 缓存到数据库(wp_user_openlist 表),避免每次调用都重新登录。
|
||||||
*
|
*
|
||||||
* 重要:OpenList 使用内存缓存验证 Token,服务重启后所有 Token 失效。
|
* 重要:OpenList 使用内存缓存验证 Token,服务重启后所有 Token 失效。
|
||||||
* 本客户端实现了自动重试机制:当 API 返回 401 "token is invalidated" 时,
|
* 本客户端实现了自动重试机制:当 API 返回 401 "token is invalidated" 时,
|
||||||
* 自动刷新 Token 并重试请求,保证 API 调用的连续性。
|
* 自动刷新 Token 并重试请求,保证 API 调用的连续性。
|
||||||
*
|
*
|
||||||
* 配置项(application.yml):
|
* 使用方式:
|
||||||
* openlist:
|
* 1. 前端调用 /api/openlist/bind 绑定 OpenList(传入地址、账号、密码)
|
||||||
* base-url: http://localhost:5244
|
* 2. 后端验证连接 → 获取 Token → 缓存到数据库
|
||||||
* username: admin
|
* 3. 后续 API 调用自动使用用户的 OpenList 配置
|
||||||
* password: your_password
|
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class OpenListClient {
|
public class OpenListClient {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(OpenListClient.class);
|
private static final Logger log = LoggerFactory.getLogger(OpenListClient.class);
|
||||||
|
|
||||||
@Value("${openlist.base-url:http://localhost:5244}")
|
@Autowired
|
||||||
private String baseUrl;
|
private UserOpenlistRepository userOpenlistRepository;
|
||||||
|
|
||||||
@Value("${openlist.username:admin}")
|
|
||||||
private String username;
|
|
||||||
|
|
||||||
@Value("${openlist.password:}")
|
|
||||||
private String password;
|
|
||||||
|
|
||||||
private final RestTemplate restTemplate = new RestTemplate();
|
private final RestTemplate restTemplate = new RestTemplate();
|
||||||
|
|
||||||
private String cachedToken;
|
|
||||||
private long tokenExpireTime;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取有效的 Token(自动刷新)
|
* 测试 OpenList 连接(不保存配置)
|
||||||
* Token 有效期 24 小时,提前 1 小时刷新
|
* 用于前端绑定前验证地址、账号、密码是否正确
|
||||||
*
|
*
|
||||||
* @return JWT Token
|
* @param openlistUrl OpenList 服务地址,如 http://ldfun.asia:13000
|
||||||
|
* @param openlistUsername OpenList 账号
|
||||||
|
* @param openlistPassword OpenList 密码
|
||||||
|
* @return 连接结果,包含 success(是否成功)、message(提示信息)、token(Token,成功时返回)
|
||||||
*/
|
*/
|
||||||
private String getValidToken() {
|
public Map<String, Object> testConnection(String openlistUrl, String openlistUsername, String openlistPassword) {
|
||||||
if (cachedToken == null || System.currentTimeMillis() > tokenExpireTime - 3600000) {
|
var result = new HashMap<String, Object>();
|
||||||
refreshToken();
|
|
||||||
|
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 cachedToken;
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 刷新 Token
|
* 绑定用户的 OpenList 连接配置
|
||||||
* 调用 OpenList 登录接口获取新的 JWT Token
|
* 验证连接 → 保存配置 → 缓存 Token
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param openlistUrl OpenList 服务地址
|
||||||
|
* @param openlistUsername OpenList 账号
|
||||||
|
* @param openlistPassword OpenList 密码
|
||||||
|
* @return 绑定结果,包含 success(是否成功)、message(提示信息)
|
||||||
*/
|
*/
|
||||||
private void refreshToken() {
|
public Map<String, Object> bindConnection(Long userId, String openlistUrl, String openlistUsername, String openlistPassword) {
|
||||||
String url = baseUrl + "/api/auth/login";
|
var result = new HashMap<String, Object>();
|
||||||
|
|
||||||
Map<String, Object> requestBody = new HashMap<>();
|
// 1. 测试连接
|
||||||
requestBody.put("username", username);
|
var testResult = testConnection(openlistUrl, openlistUsername, openlistPassword);
|
||||||
requestBody.put("password", password);
|
if (!(boolean) testResult.get("success")) {
|
||||||
|
result.put("success", false);
|
||||||
HttpHeaders headers = new HttpHeaders();
|
result.put("message", testResult.get("message"));
|
||||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
return result;
|
||||||
|
|
||||||
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");
|
|
||||||
cachedToken = (String) data.get("token");
|
|
||||||
// Token 有效期设为 24 小时
|
|
||||||
tokenExpireTime = System.currentTimeMillis() + 86400000;
|
|
||||||
log.info("OpenList Token 刷新成功");
|
|
||||||
} else {
|
|
||||||
throw new RuntimeException("OpenList 登录失败:" + (body != null ? body.get("message") : "未知错误"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 文件操作 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -100,11 +218,18 @@ public class OpenListClient {
|
|||||||
/**
|
/**
|
||||||
* 执行 POST 请求(带自动重试)
|
* 执行 POST 请求(带自动重试)
|
||||||
* 当 Token 失效时自动刷新并重试一次
|
* 当 Token 失效时自动刷新并重试一次
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param url 请求 URL
|
||||||
|
* @param requestBody 请求体
|
||||||
|
* @return 响应结果
|
||||||
*/
|
*/
|
||||||
private Map<String, Object> postWithRetry(String url, Map<String, Object> requestBody) {
|
private Map<String, Object> postWithRetry(Long userId, String url, Map<String, Object> requestBody) {
|
||||||
|
String token = getValidToken(userId);
|
||||||
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
headers.setBearerAuth(getValidToken());
|
headers.setBearerAuth(token);
|
||||||
|
|
||||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||||
ResponseEntity<Map> response = restTemplate.postForEntity(url, entity, Map.class);
|
ResponseEntity<Map> response = restTemplate.postForEntity(url, entity, Map.class);
|
||||||
@ -112,9 +237,9 @@ public class OpenListClient {
|
|||||||
|
|
||||||
// 如果 Token 失效,刷新后重试一次
|
// 如果 Token 失效,刷新后重试一次
|
||||||
if (isTokenInvalidated(result)) {
|
if (isTokenInvalidated(result)) {
|
||||||
log.info("OpenList Token 失效,自动刷新并重试");
|
log.info("用户 {} 的 OpenList Token 失效,自动刷新并重试", userId);
|
||||||
refreshToken();
|
token = getValidToken(userId); // 强制刷新
|
||||||
headers.setBearerAuth(cachedToken);
|
headers.setBearerAuth(token);
|
||||||
entity = new HttpEntity<>(requestBody, headers);
|
entity = new HttpEntity<>(requestBody, headers);
|
||||||
response = restTemplate.postForEntity(url, entity, Map.class);
|
response = restTemplate.postForEntity(url, entity, Map.class);
|
||||||
result = response.getBody();
|
result = response.getBody();
|
||||||
@ -126,13 +251,19 @@ public class OpenListClient {
|
|||||||
/**
|
/**
|
||||||
* 列出指定路径下的文件
|
* 列出指定路径下的文件
|
||||||
*
|
*
|
||||||
* @param path OpenList 路径,如 /movies
|
* @param userId 用户ID
|
||||||
* @param page 页码(从 1 开始)
|
* @param path OpenList 路径,如 /movies
|
||||||
|
* @param page 页码(从 1 开始)
|
||||||
* @param perPage 每页数量
|
* @param perPage 每页数量
|
||||||
* @return 文件列表响应
|
* @return 文件列表响应
|
||||||
*/
|
*/
|
||||||
public Map<String, Object> listFiles(String path, int page, int perPage) {
|
public Map<String, Object> listFiles(Long userId, String path, int page, int perPage) {
|
||||||
String url = baseUrl + "/api/fs/list";
|
UserOpenlist config = getUserOpenlist(userId);
|
||||||
|
if (config == null) {
|
||||||
|
throw new RuntimeException("用户未绑定 OpenList");
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = config.getOpenlistUrl() + "/api/fs/list";
|
||||||
|
|
||||||
Map<String, Object> requestBody = new HashMap<>();
|
Map<String, Object> requestBody = new HashMap<>();
|
||||||
requestBody.put("path", path);
|
requestBody.put("path", path);
|
||||||
@ -140,62 +271,142 @@ public class OpenListClient {
|
|||||||
requestBody.put("per_page", perPage);
|
requestBody.put("per_page", perPage);
|
||||||
requestBody.put("refresh", false);
|
requestBody.put("refresh", false);
|
||||||
|
|
||||||
return postWithRetry(url, requestBody);
|
return postWithRetry(userId, url, requestBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取文件详情和下载链接
|
* 获取文件详情和下载链接
|
||||||
*
|
*
|
||||||
* @param path 文件路径,如 /movies/阿凡达.mp4
|
* @param userId 用户ID
|
||||||
|
* @param path 文件路径,如 /movies/阿凡达.mp4
|
||||||
* @return 文件详情响应(包含 raw_url)
|
* @return 文件详情响应(包含 raw_url)
|
||||||
*/
|
*/
|
||||||
public Map<String, Object> getFileDetail(String path) {
|
public Map<String, Object> getFileDetail(Long userId, String path) {
|
||||||
String url = baseUrl + "/api/fs/get";
|
UserOpenlist config = getUserOpenlist(userId);
|
||||||
|
if (config == null) {
|
||||||
|
throw new RuntimeException("用户未绑定 OpenList");
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = config.getOpenlistUrl() + "/api/fs/get";
|
||||||
|
|
||||||
Map<String, Object> requestBody = new HashMap<>();
|
Map<String, Object> requestBody = new HashMap<>();
|
||||||
requestBody.put("path", path);
|
requestBody.put("path", path);
|
||||||
|
|
||||||
return postWithRetry(url, requestBody);
|
return postWithRetry(userId, url, requestBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建文件夹
|
* 创建文件夹
|
||||||
*
|
*
|
||||||
* @param path 文件夹路径,如 /movies/科幻
|
* @param userId 用户ID
|
||||||
|
* @param path 文件夹路径,如 /movies/科幻
|
||||||
* @return 创建结果
|
* @return 创建结果
|
||||||
*/
|
*/
|
||||||
public Map<String, Object> createFolder(String path) {
|
public Map<String, Object> createFolder(Long userId, String path) {
|
||||||
String url = baseUrl + "/api/fs/mkdir";
|
UserOpenlist config = getUserOpenlist(userId);
|
||||||
|
if (config == null) {
|
||||||
|
throw new RuntimeException("用户未绑定 OpenList");
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = config.getOpenlistUrl() + "/api/fs/mkdir";
|
||||||
|
|
||||||
Map<String, Object> requestBody = new HashMap<>();
|
Map<String, Object> requestBody = new HashMap<>();
|
||||||
requestBody.put("path", path);
|
requestBody.put("path", path);
|
||||||
|
|
||||||
return postWithRetry(url, requestBody);
|
return postWithRetry(userId, url, requestBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除文件或文件夹
|
* 删除文件或文件夹
|
||||||
*
|
*
|
||||||
* @param path 文件路径
|
* @param userId 用户ID
|
||||||
* @param names 文件名列表
|
* @param path 文件路径
|
||||||
|
* @param names 文件名列表
|
||||||
* @return 删除结果
|
* @return 删除结果
|
||||||
*/
|
*/
|
||||||
public Map<String, Object> deleteFiles(String path, String[] names) {
|
public Map<String, Object> deleteFiles(Long userId, String path, String[] names) {
|
||||||
String url = baseUrl + "/api/fs/remove";
|
UserOpenlist config = getUserOpenlist(userId);
|
||||||
|
if (config == null) {
|
||||||
|
throw new RuntimeException("用户未绑定 OpenList");
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = config.getOpenlistUrl() + "/api/fs/remove";
|
||||||
|
|
||||||
Map<String, Object> requestBody = new HashMap<>();
|
Map<String, Object> requestBody = new HashMap<>();
|
||||||
requestBody.put("dir", path);
|
requestBody.put("dir", path);
|
||||||
requestBody.put("names", names);
|
requestBody.put("names", names);
|
||||||
|
|
||||||
return postWithRetry(url, requestBody);
|
return postWithRetry(userId, url, requestBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取 OpenList 服务地址
|
* 获取用户的 OpenList 服务地址
|
||||||
*
|
*
|
||||||
* @return OpenList base URL
|
* @param userId 用户ID
|
||||||
|
* @return OpenList base URL,未绑定时返回 null
|
||||||
*/
|
*/
|
||||||
public String getBaseUrl() {
|
public String getUserBaseUrl(Long userId) {
|
||||||
return baseUrl;
|
UserOpenlist config = getUserOpenlist(userId);
|
||||||
|
return config != null ? config.getOpenlistUrl() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 同步相关 API ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定路径下的所有内容(自动处理分页,返回全部结果)
|
||||||
|
* 用于同步场景,需要获取目录下所有文件和文件夹
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param path OpenList 路径,如 / 或 /movies
|
||||||
|
* @return 内容列表(包含文件和文件夹信息)
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
requestBody.put("refresh", false);
|
||||||
|
|
||||||
|
Map<String, Object> result = postWithRetry(userId, url, requestBody);
|
||||||
|
|
||||||
|
if (result == null || (int) result.get("code") != 200) {
|
||||||
|
log.warn("用户 {} 列出路径 {} 第 {} 页失败: {}", userId, path, page, result);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> data = (Map<String, Object>) result.get("data");
|
||||||
|
if (data == null) break;
|
||||||
|
|
||||||
|
Map<String, Object> content = (Map<String, Object>) data.get("content");
|
||||||
|
if (content == null) break;
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<Map<String, Object>> items = (List<Map<String, Object>>) content.get("content");
|
||||||
|
if (items == null || items.isEmpty()) break;
|
||||||
|
|
||||||
|
allContent.addAll(items);
|
||||||
|
|
||||||
|
// 判断是否还有下一页
|
||||||
|
int total = (int) content.getOrDefault("total", 0);
|
||||||
|
if (page * perPage >= total) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
page++;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("用户 {} 路径 {} 共获取 {} 个内容项", userId, path, allContent.size());
|
||||||
|
return allContent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user