diff --git a/encrypt.bat b/encrypt.bat new file mode 100644 index 0000000..e9aaa8e --- /dev/null +++ b/encrypt.bat @@ -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! diff --git a/encrypt.ps1 b/encrypt.ps1 new file mode 100644 index 0000000..38a96b2 --- /dev/null +++ b/encrypt.ps1 @@ -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" diff --git a/wangpan-common/deps/HikariCP-5.0.1.jar b/wangpan-common/deps/HikariCP-5.0.1.jar new file mode 100644 index 0000000..9d7eafb Binary files /dev/null and b/wangpan-common/deps/HikariCP-5.0.1.jar differ diff --git a/wangpan-common/deps/annotations-13.0.jar b/wangpan-common/deps/annotations-13.0.jar new file mode 100644 index 0000000..fb794be Binary files /dev/null and b/wangpan-common/deps/annotations-13.0.jar differ diff --git a/wangpan-common/deps/bcprov-jdk18on-1.76.jar b/wangpan-common/deps/bcprov-jdk18on-1.76.jar new file mode 100644 index 0000000..c0c8994 Binary files /dev/null and b/wangpan-common/deps/bcprov-jdk18on-1.76.jar differ diff --git a/wangpan-common/deps/checker-qual-3.37.0.jar b/wangpan-common/deps/checker-qual-3.37.0.jar new file mode 100644 index 0000000..b4f9db9 Binary files /dev/null and b/wangpan-common/deps/checker-qual-3.37.0.jar differ diff --git a/wangpan-common/deps/commons-compress-1.24.0.jar b/wangpan-common/deps/commons-compress-1.24.0.jar new file mode 100644 index 0000000..cb2950e Binary files /dev/null and b/wangpan-common/deps/commons-compress-1.24.0.jar differ diff --git a/wangpan-common/deps/error_prone_annotations-2.21.1.jar b/wangpan-common/deps/error_prone_annotations-2.21.1.jar new file mode 100644 index 0000000..00f26ae Binary files /dev/null and b/wangpan-common/deps/error_prone_annotations-2.21.1.jar differ diff --git a/wangpan-common/deps/failureaccess-1.0.1.jar b/wangpan-common/deps/failureaccess-1.0.1.jar new file mode 100644 index 0000000..9b56dc7 Binary files /dev/null and b/wangpan-common/deps/failureaccess-1.0.1.jar differ diff --git a/wangpan-common/deps/guava-32.1.3-jre.jar b/wangpan-common/deps/guava-32.1.3-jre.jar new file mode 100644 index 0000000..9dc8235 Binary files /dev/null and b/wangpan-common/deps/guava-32.1.3-jre.jar differ diff --git a/wangpan-common/deps/j2objc-annotations-2.8.jar b/wangpan-common/deps/j2objc-annotations-2.8.jar new file mode 100644 index 0000000..3595c4f Binary files /dev/null and b/wangpan-common/deps/j2objc-annotations-2.8.jar differ diff --git a/wangpan-common/deps/jackson-annotations-2.15.4.jar b/wangpan-common/deps/jackson-annotations-2.15.4.jar new file mode 100644 index 0000000..5dc55f6 Binary files /dev/null and b/wangpan-common/deps/jackson-annotations-2.15.4.jar differ diff --git a/wangpan-common/deps/jackson-core-2.15.4.jar b/wangpan-common/deps/jackson-core-2.15.4.jar new file mode 100644 index 0000000..2e2c83c Binary files /dev/null and b/wangpan-common/deps/jackson-core-2.15.4.jar differ diff --git a/wangpan-common/deps/jackson-databind-2.15.4.jar b/wangpan-common/deps/jackson-databind-2.15.4.jar new file mode 100644 index 0000000..ffa9fee Binary files /dev/null and b/wangpan-common/deps/jackson-databind-2.15.4.jar differ diff --git a/wangpan-common/deps/jackson-datatype-jdk8-2.15.4.jar b/wangpan-common/deps/jackson-datatype-jdk8-2.15.4.jar new file mode 100644 index 0000000..98459aa Binary files /dev/null and b/wangpan-common/deps/jackson-datatype-jdk8-2.15.4.jar differ diff --git a/wangpan-common/deps/jackson-datatype-jsr310-2.15.4.jar b/wangpan-common/deps/jackson-datatype-jsr310-2.15.4.jar new file mode 100644 index 0000000..70c54e5 Binary files /dev/null and b/wangpan-common/deps/jackson-datatype-jsr310-2.15.4.jar differ diff --git a/wangpan-common/deps/jackson-module-parameter-names-2.15.4.jar b/wangpan-common/deps/jackson-module-parameter-names-2.15.4.jar new file mode 100644 index 0000000..dd5023b Binary files /dev/null and b/wangpan-common/deps/jackson-module-parameter-names-2.15.4.jar differ diff --git a/wangpan-common/deps/jakarta.annotation-api-2.1.1.jar b/wangpan-common/deps/jakarta.annotation-api-2.1.1.jar new file mode 100644 index 0000000..e13b7df Binary files /dev/null and b/wangpan-common/deps/jakarta.annotation-api-2.1.1.jar differ diff --git a/wangpan-common/deps/jasypt-1.9.3.jar b/wangpan-common/deps/jasypt-1.9.3.jar new file mode 100644 index 0000000..f4c4606 Binary files /dev/null and b/wangpan-common/deps/jasypt-1.9.3.jar differ diff --git a/wangpan-common/deps/jsqlparser-4.9.jar b/wangpan-common/deps/jsqlparser-4.9.jar new file mode 100644 index 0000000..3a3aac1 Binary files /dev/null and b/wangpan-common/deps/jsqlparser-4.9.jar differ diff --git a/wangpan-common/deps/jsr305-3.0.2.jar b/wangpan-common/deps/jsr305-3.0.2.jar new file mode 100644 index 0000000..59222d9 Binary files /dev/null and b/wangpan-common/deps/jsr305-3.0.2.jar differ diff --git a/wangpan-common/deps/jul-to-slf4j-2.0.13.jar b/wangpan-common/deps/jul-to-slf4j-2.0.13.jar new file mode 100644 index 0000000..cbd370c Binary files /dev/null and b/wangpan-common/deps/jul-to-slf4j-2.0.13.jar differ diff --git a/wangpan-common/deps/kotlin-stdlib-1.9.23.jar b/wangpan-common/deps/kotlin-stdlib-1.9.23.jar new file mode 100644 index 0000000..b0728ef Binary files /dev/null and b/wangpan-common/deps/kotlin-stdlib-1.9.23.jar differ diff --git a/wangpan-common/deps/kotlin-stdlib-common-1.9.23.jar b/wangpan-common/deps/kotlin-stdlib-common-1.9.23.jar new file mode 100644 index 0000000..a873c90 Binary files /dev/null and b/wangpan-common/deps/kotlin-stdlib-common-1.9.23.jar differ diff --git a/wangpan-common/deps/kotlin-stdlib-jdk7-1.9.23.jar b/wangpan-common/deps/kotlin-stdlib-jdk7-1.9.23.jar new file mode 100644 index 0000000..6fea5e1 Binary files /dev/null and b/wangpan-common/deps/kotlin-stdlib-jdk7-1.9.23.jar differ diff --git a/wangpan-common/deps/kotlin-stdlib-jdk8-1.9.23.jar b/wangpan-common/deps/kotlin-stdlib-jdk8-1.9.23.jar new file mode 100644 index 0000000..aeebf1f Binary files /dev/null and b/wangpan-common/deps/kotlin-stdlib-jdk8-1.9.23.jar differ diff --git a/wangpan-common/deps/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar b/wangpan-common/deps/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar new file mode 100644 index 0000000..45832c0 Binary files /dev/null and b/wangpan-common/deps/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar differ diff --git a/wangpan-common/deps/log4j-api-2.21.1.jar b/wangpan-common/deps/log4j-api-2.21.1.jar new file mode 100644 index 0000000..238d9aa Binary files /dev/null and b/wangpan-common/deps/log4j-api-2.21.1.jar differ diff --git a/wangpan-common/deps/log4j-to-slf4j-2.21.1.jar b/wangpan-common/deps/log4j-to-slf4j-2.21.1.jar new file mode 100644 index 0000000..7bc23a2 Binary files /dev/null and b/wangpan-common/deps/log4j-to-slf4j-2.21.1.jar differ diff --git a/wangpan-common/deps/logback-classic-1.4.14.jar b/wangpan-common/deps/logback-classic-1.4.14.jar new file mode 100644 index 0000000..93602a7 Binary files /dev/null and b/wangpan-common/deps/logback-classic-1.4.14.jar differ diff --git a/wangpan-common/deps/logback-core-1.4.14.jar b/wangpan-common/deps/logback-core-1.4.14.jar new file mode 100644 index 0000000..0213073 Binary files /dev/null and b/wangpan-common/deps/logback-core-1.4.14.jar differ diff --git a/wangpan-common/deps/micrometer-commons-1.12.5.jar b/wangpan-common/deps/micrometer-commons-1.12.5.jar new file mode 100644 index 0000000..27a96a9 Binary files /dev/null and b/wangpan-common/deps/micrometer-commons-1.12.5.jar differ diff --git a/wangpan-common/deps/micrometer-observation-1.12.5.jar b/wangpan-common/deps/micrometer-observation-1.12.5.jar new file mode 100644 index 0000000..c6deecb Binary files /dev/null and b/wangpan-common/deps/micrometer-observation-1.12.5.jar differ diff --git a/wangpan-common/deps/minio-8.5.7.jar b/wangpan-common/deps/minio-8.5.7.jar new file mode 100644 index 0000000..8ca67ee Binary files /dev/null and b/wangpan-common/deps/minio-8.5.7.jar differ diff --git a/wangpan-common/deps/mybatis-3.5.16.jar b/wangpan-common/deps/mybatis-3.5.16.jar new file mode 100644 index 0000000..3347062 Binary files /dev/null and b/wangpan-common/deps/mybatis-3.5.16.jar differ diff --git a/wangpan-common/deps/mybatis-plus-3.5.7.jar b/wangpan-common/deps/mybatis-plus-3.5.7.jar new file mode 100644 index 0000000..61bb38b Binary files /dev/null and b/wangpan-common/deps/mybatis-plus-3.5.7.jar differ diff --git a/wangpan-common/deps/mybatis-plus-annotation-3.5.7.jar b/wangpan-common/deps/mybatis-plus-annotation-3.5.7.jar new file mode 100644 index 0000000..44db327 Binary files /dev/null and b/wangpan-common/deps/mybatis-plus-annotation-3.5.7.jar differ diff --git a/wangpan-common/deps/mybatis-plus-core-3.5.7.jar b/wangpan-common/deps/mybatis-plus-core-3.5.7.jar new file mode 100644 index 0000000..fdbfafd Binary files /dev/null and b/wangpan-common/deps/mybatis-plus-core-3.5.7.jar differ diff --git a/wangpan-common/deps/mybatis-plus-extension-3.5.7.jar b/wangpan-common/deps/mybatis-plus-extension-3.5.7.jar new file mode 100644 index 0000000..8e30627 Binary files /dev/null and b/wangpan-common/deps/mybatis-plus-extension-3.5.7.jar differ diff --git a/wangpan-common/deps/mybatis-plus-spring-boot-autoconfigure-3.5.7.jar b/wangpan-common/deps/mybatis-plus-spring-boot-autoconfigure-3.5.7.jar new file mode 100644 index 0000000..54a8262 Binary files /dev/null and b/wangpan-common/deps/mybatis-plus-spring-boot-autoconfigure-3.5.7.jar differ diff --git a/wangpan-common/deps/mybatis-plus-spring-boot3-starter-3.5.7.jar b/wangpan-common/deps/mybatis-plus-spring-boot3-starter-3.5.7.jar new file mode 100644 index 0000000..552e3e9 Binary files /dev/null and b/wangpan-common/deps/mybatis-plus-spring-boot3-starter-3.5.7.jar differ diff --git a/wangpan-common/deps/mybatis-spring-3.0.3.jar b/wangpan-common/deps/mybatis-spring-3.0.3.jar new file mode 100644 index 0000000..95c4542 Binary files /dev/null and b/wangpan-common/deps/mybatis-spring-3.0.3.jar differ diff --git a/wangpan-common/deps/mysql-connector-j-8.0.33.jar b/wangpan-common/deps/mysql-connector-j-8.0.33.jar new file mode 100644 index 0000000..3f741f5 Binary files /dev/null and b/wangpan-common/deps/mysql-connector-j-8.0.33.jar differ diff --git a/wangpan-common/deps/okhttp-4.12.0.jar b/wangpan-common/deps/okhttp-4.12.0.jar new file mode 100644 index 0000000..faf3fa8 Binary files /dev/null and b/wangpan-common/deps/okhttp-4.12.0.jar differ diff --git a/wangpan-common/deps/okio-3.6.0.jar b/wangpan-common/deps/okio-3.6.0.jar new file mode 100644 index 0000000..f313756 Binary files /dev/null and b/wangpan-common/deps/okio-3.6.0.jar differ diff --git a/wangpan-common/deps/okio-jvm-3.6.0.jar b/wangpan-common/deps/okio-jvm-3.6.0.jar new file mode 100644 index 0000000..ec8ad90 Binary files /dev/null and b/wangpan-common/deps/okio-jvm-3.6.0.jar differ diff --git a/wangpan-common/deps/simple-xml-safe-2.7.1.jar b/wangpan-common/deps/simple-xml-safe-2.7.1.jar new file mode 100644 index 0000000..d4ce9f1 Binary files /dev/null and b/wangpan-common/deps/simple-xml-safe-2.7.1.jar differ diff --git a/wangpan-common/deps/slf4j-api-2.0.13.jar b/wangpan-common/deps/slf4j-api-2.0.13.jar new file mode 100644 index 0000000..a800cc2 Binary files /dev/null and b/wangpan-common/deps/slf4j-api-2.0.13.jar differ diff --git a/wangpan-common/deps/snakeyaml-2.2.jar b/wangpan-common/deps/snakeyaml-2.2.jar new file mode 100644 index 0000000..275dd57 Binary files /dev/null and b/wangpan-common/deps/snakeyaml-2.2.jar differ diff --git a/wangpan-common/deps/snappy-java-1.1.10.5.jar b/wangpan-common/deps/snappy-java-1.1.10.5.jar new file mode 100644 index 0000000..7707e58 Binary files /dev/null and b/wangpan-common/deps/snappy-java-1.1.10.5.jar differ diff --git a/wangpan-common/deps/spring-aop-6.1.6.jar b/wangpan-common/deps/spring-aop-6.1.6.jar new file mode 100644 index 0000000..2cbe890 Binary files /dev/null and b/wangpan-common/deps/spring-aop-6.1.6.jar differ diff --git a/wangpan-common/deps/spring-beans-6.1.6.jar b/wangpan-common/deps/spring-beans-6.1.6.jar new file mode 100644 index 0000000..bcfa3c6 Binary files /dev/null and b/wangpan-common/deps/spring-beans-6.1.6.jar differ diff --git a/wangpan-common/deps/spring-boot-3.2.5.jar b/wangpan-common/deps/spring-boot-3.2.5.jar new file mode 100644 index 0000000..7efbaca Binary files /dev/null and b/wangpan-common/deps/spring-boot-3.2.5.jar differ diff --git a/wangpan-common/deps/spring-boot-autoconfigure-3.2.5.jar b/wangpan-common/deps/spring-boot-autoconfigure-3.2.5.jar new file mode 100644 index 0000000..b2d8cc9 Binary files /dev/null and b/wangpan-common/deps/spring-boot-autoconfigure-3.2.5.jar differ diff --git a/wangpan-common/deps/spring-boot-starter-3.2.5.jar b/wangpan-common/deps/spring-boot-starter-3.2.5.jar new file mode 100644 index 0000000..12a50a6 Binary files /dev/null and b/wangpan-common/deps/spring-boot-starter-3.2.5.jar differ diff --git a/wangpan-common/deps/spring-boot-starter-jdbc-3.2.5.jar b/wangpan-common/deps/spring-boot-starter-jdbc-3.2.5.jar new file mode 100644 index 0000000..88d0999 Binary files /dev/null and b/wangpan-common/deps/spring-boot-starter-jdbc-3.2.5.jar differ diff --git a/wangpan-common/deps/spring-boot-starter-json-3.2.5.jar b/wangpan-common/deps/spring-boot-starter-json-3.2.5.jar new file mode 100644 index 0000000..4d86634 Binary files /dev/null and b/wangpan-common/deps/spring-boot-starter-json-3.2.5.jar differ diff --git a/wangpan-common/deps/spring-boot-starter-logging-3.2.5.jar b/wangpan-common/deps/spring-boot-starter-logging-3.2.5.jar new file mode 100644 index 0000000..d855189 Binary files /dev/null and b/wangpan-common/deps/spring-boot-starter-logging-3.2.5.jar differ diff --git a/wangpan-common/deps/spring-boot-starter-tomcat-3.2.5.jar b/wangpan-common/deps/spring-boot-starter-tomcat-3.2.5.jar new file mode 100644 index 0000000..e06b3c7 Binary files /dev/null and b/wangpan-common/deps/spring-boot-starter-tomcat-3.2.5.jar differ diff --git a/wangpan-common/deps/spring-boot-starter-web-3.2.5.jar b/wangpan-common/deps/spring-boot-starter-web-3.2.5.jar new file mode 100644 index 0000000..242fd46 Binary files /dev/null and b/wangpan-common/deps/spring-boot-starter-web-3.2.5.jar differ diff --git a/wangpan-common/deps/spring-context-6.1.6.jar b/wangpan-common/deps/spring-context-6.1.6.jar new file mode 100644 index 0000000..750b3a6 Binary files /dev/null and b/wangpan-common/deps/spring-context-6.1.6.jar differ diff --git a/wangpan-common/deps/spring-core-6.1.6.jar b/wangpan-common/deps/spring-core-6.1.6.jar new file mode 100644 index 0000000..c6b7025 Binary files /dev/null and b/wangpan-common/deps/spring-core-6.1.6.jar differ diff --git a/wangpan-common/deps/spring-expression-6.1.6.jar b/wangpan-common/deps/spring-expression-6.1.6.jar new file mode 100644 index 0000000..a649814 Binary files /dev/null and b/wangpan-common/deps/spring-expression-6.1.6.jar differ diff --git a/wangpan-common/deps/spring-jcl-6.1.6.jar b/wangpan-common/deps/spring-jcl-6.1.6.jar new file mode 100644 index 0000000..14c446a Binary files /dev/null and b/wangpan-common/deps/spring-jcl-6.1.6.jar differ diff --git a/wangpan-common/deps/spring-jdbc-6.1.6.jar b/wangpan-common/deps/spring-jdbc-6.1.6.jar new file mode 100644 index 0000000..aebcafe Binary files /dev/null and b/wangpan-common/deps/spring-jdbc-6.1.6.jar differ diff --git a/wangpan-common/deps/spring-tx-6.1.6.jar b/wangpan-common/deps/spring-tx-6.1.6.jar new file mode 100644 index 0000000..7032a98 Binary files /dev/null and b/wangpan-common/deps/spring-tx-6.1.6.jar differ diff --git a/wangpan-common/deps/spring-web-6.1.6.jar b/wangpan-common/deps/spring-web-6.1.6.jar new file mode 100644 index 0000000..67f06e0 Binary files /dev/null and b/wangpan-common/deps/spring-web-6.1.6.jar differ diff --git a/wangpan-common/deps/spring-webmvc-6.1.6.jar b/wangpan-common/deps/spring-webmvc-6.1.6.jar new file mode 100644 index 0000000..b55874a Binary files /dev/null and b/wangpan-common/deps/spring-webmvc-6.1.6.jar differ diff --git a/wangpan-common/deps/tomcat-embed-core-10.1.20.jar b/wangpan-common/deps/tomcat-embed-core-10.1.20.jar new file mode 100644 index 0000000..e1929eb Binary files /dev/null and b/wangpan-common/deps/tomcat-embed-core-10.1.20.jar differ diff --git a/wangpan-common/deps/tomcat-embed-el-10.1.20.jar b/wangpan-common/deps/tomcat-embed-el-10.1.20.jar new file mode 100644 index 0000000..0feb7d1 Binary files /dev/null and b/wangpan-common/deps/tomcat-embed-el-10.1.20.jar differ diff --git a/wangpan-common/deps/tomcat-embed-websocket-10.1.20.jar b/wangpan-common/deps/tomcat-embed-websocket-10.1.20.jar new file mode 100644 index 0000000..ffad16e Binary files /dev/null and b/wangpan-common/deps/tomcat-embed-websocket-10.1.20.jar differ diff --git a/wangpan-common/src/main/java/com/wangpan/common/dal/dataobject/DriveMovieDO.java b/wangpan-common/src/main/java/com/wangpan/common/dal/dataobject/DriveMovieDO.java index 22a9d23..aae36f4 100644 --- a/wangpan-common/src/main/java/com/wangpan/common/dal/dataobject/DriveMovieDO.java +++ b/wangpan-common/src/main/java/com/wangpan/common/dal/dataobject/DriveMovieDO.java @@ -95,9 +95,6 @@ public class DriveMovieDO implements Serializable { /** 标签(逗号分隔) */ private String tags; - /** 电影简介(TMDB刮削) */ - private String overview; - /** 主演(逗号分隔,TMDB刮削) */ private String casts; @@ -192,9 +189,6 @@ public class DriveMovieDO implements Serializable { public String getTags() { return 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 void setCasts(String casts) { this.casts = casts; } diff --git a/wangpan-common/src/main/java/com/wangpan/common/dal/dataobject/MovieDO.java b/wangpan-common/src/main/java/com/wangpan/common/dal/dataobject/MovieDO.java index 6afc9ca..cf3ab5b 100644 --- a/wangpan-common/src/main/java/com/wangpan/common/dal/dataobject/MovieDO.java +++ b/wangpan-common/src/main/java/com/wangpan/common/dal/dataobject/MovieDO.java @@ -68,9 +68,6 @@ public class MovieDO implements Serializable { /** 标签(逗号分隔) */ private String tags; - /** 电影简介(TMDB刮削) */ - private String overview; - /** 主演(逗号分隔,TMDB刮削) */ private String casts; @@ -132,9 +129,6 @@ public class MovieDO implements Serializable { public String getTags() { return 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 void setCasts(String casts) { this.casts = casts; } diff --git a/wangpan-common/src/main/java/com/wangpan/common/dal/dataobject/UserOpenlistDO.java b/wangpan-common/src/main/java/com/wangpan/common/dal/dataobject/UserOpenlistDO.java new file mode 100644 index 0000000..3a95d9d --- /dev/null +++ b/wangpan-common/src/main/java/com/wangpan/common/dal/dataobject/UserOpenlistDO.java @@ -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; } +} diff --git a/wangpan-common/src/main/java/com/wangpan/common/dal/mapper/DriveMovieMapper.java b/wangpan-common/src/main/java/com/wangpan/common/dal/mapper/DriveMovieMapper.java index e4ccdf1..3db978f 100644 --- a/wangpan-common/src/main/java/com/wangpan/common/dal/mapper/DriveMovieMapper.java +++ b/wangpan-common/src/main/java/com/wangpan/common/dal/mapper/DriveMovieMapper.java @@ -4,6 +4,7 @@ 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; @@ -44,7 +45,6 @@ public interface DriveMovieMapper extends BaseMapper { poster_url TEXT DEFAULT NULL COMMENT '海报URL(竖版,加密存储)', backdrop_url TEXT DEFAULT NULL COMMENT '背景图URL(横版,加密存储)', tags VARCHAR(500) DEFAULT NULL COMMENT '标签(逗号分隔)', - overview TEXT DEFAULT NULL COMMENT '电影简介(TMDB刮削)', 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 '更新时间', @@ -63,4 +63,16 @@ public interface DriveMovieMapper extends BaseMapper { * 复杂SQL,在XML中实现 */ List> 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); } diff --git a/wangpan-common/src/main/java/com/wangpan/common/dal/mapper/MovieMapper.java b/wangpan-common/src/main/java/com/wangpan/common/dal/mapper/MovieMapper.java index 347e8d3..cb16d08 100644 --- a/wangpan-common/src/main/java/com/wangpan/common/dal/mapper/MovieMapper.java +++ b/wangpan-common/src/main/java/com/wangpan/common/dal/mapper/MovieMapper.java @@ -73,9 +73,9 @@ public interface MovieMapper extends BaseMapper { 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); /** diff --git a/wangpan-common/src/main/java/com/wangpan/common/dal/mapper/UserOpenlistMapper.java b/wangpan-common/src/main/java/com/wangpan/common/dal/mapper/UserOpenlistMapper.java new file mode 100644 index 0000000..e544eb7 --- /dev/null +++ b/wangpan-common/src/main/java/com/wangpan/common/dal/mapper/UserOpenlistMapper.java @@ -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 { + + /** 建表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 连接配置表' + """; +} diff --git a/wangpan-common/src/main/java/com/wangpan/common/dal/repository/DriveMovieRepository.java b/wangpan-common/src/main/java/com/wangpan/common/dal/repository/DriveMovieRepository.java index ba580b2..c19541d 100644 --- a/wangpan-common/src/main/java/com/wangpan/common/dal/repository/DriveMovieRepository.java +++ b/wangpan-common/src/main/java/com/wangpan/common/dal/repository/DriveMovieRepository.java @@ -204,27 +204,200 @@ public class DriveMovieRepository extends ServiceImpl queryAllMoviesByUser(Long userId, String keyword, String sortBy, String sortOrder, + int pageNo, int pageSize) { + var page = new Page(pageNo, pageSize); + var wrapper = new LambdaQueryWrapper() + .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>(); + for (var movieDO : result.getRecords()) { + list.add(toMap(movieDO)); + } + + // 构建分页信息 + var pageInfo = new HashMap(); + 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 queryRandomMovies(Long userId, int limit) { + // 1. 统计符合条件的电影总数 + var countWrapper = new LambdaQueryWrapper() + .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() + .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 queryAllMoviesExcludeIds(Long userId, List excludeIds, String keyword, + String sortBy, String sortOrder, int pageNo, int pageSize) { + var page = new Page(pageNo, pageSize); + var wrapper = new LambdaQueryWrapper() + .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>(); + for (var movieDO : result.getRecords()) { + list.add(toMap(movieDO)); + } + + var pageInfo = new HashMap(); + 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 originalName, String subtitle, String description, - Integer rating, String tags, String category) { + public boolean update(Long id, String title, String description, + Integer rating, String tags, String posterUrl, String backdropUrl, + String resourceLibrary, String maintainer) { var wrapper = new LambdaUpdateWrapper() .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) - .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()); 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 构建更新条件 @@ -319,7 +492,6 @@ public class DriveMovieRepository extends ServiceImpl { } /** - * 更新电影简介(overview → description) + * 更新电影简介(description 字段) */ public int updateDescription(Long id, String description) { return baseMapper.updateDescription(id, description); @@ -357,11 +357,9 @@ public class MovieRepository extends ServiceImpl { movieDO.setPosterUrl(movie.getPosterUrl()); movieDO.setBackdropUrl(movie.getBackdropUrl()); movieDO.setTags(movie.getTags()); - movieDO.setOverview(movie.getOverview()); movieDO.setCasts(movie.getCasts()); movieDO.setDirectors(movie.getDirectors()); movieDO.setUpdateTime(movie.getUpdateTime()); - movieDO.setCreateTime(movie.getCreateTime()); return movieDO; } @@ -385,11 +383,9 @@ public class MovieRepository extends ServiceImpl { movie.setPosterUrl(movieDO.getPosterUrl()); movie.setBackdropUrl(movieDO.getBackdropUrl()); movie.setTags(movieDO.getTags()); - movie.setOverview(movieDO.getOverview()); movie.setCasts(movieDO.getCasts()); movie.setDirectors(movieDO.getDirectors()); movie.setUpdateTime(movieDO.getUpdateTime()); - movie.setCreateTime(movieDO.getCreateTime()); return movie; } @@ -411,7 +407,6 @@ public class MovieRepository extends ServiceImpl { map.put("posterUrl", movieDO.getPosterUrl()); map.put("backdropUrl", movieDO.getBackdropUrl()); map.put("tags", movieDO.getTags()); - map.put("overview", movieDO.getOverview()); map.put("casts", movieDO.getCasts()); map.put("directors", movieDO.getDirectors()); map.put("updateTime", movieDO.getUpdateTime() != null ? movieDO.getUpdateTime().toString() : null); diff --git a/wangpan-common/src/main/java/com/wangpan/common/dal/repository/UserOpenlistRepository.java b/wangpan-common/src/main/java/com/wangpan/common/dal/repository/UserOpenlistRepository.java new file mode 100644 index 0000000..a4b7d51 --- /dev/null +++ b/wangpan-common/src/main/java/com/wangpan/common/dal/repository/UserOpenlistRepository.java @@ -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 { + + // ==================== 查询 ==================== + + /** + * 根据用户ID查询 OpenList 连接配置(返回 Entity) + * 敏感信息自动解密 + */ + public UserOpenlist findByUserId(Long userId) { + var wrapper = new LambdaQueryWrapper() + .eq(UserOpenlistDO::getUserId, userId); + var doObj = getOne(wrapper); + return doObj != null ? toEntity(doObj) : null; + } + + /** + * 查询所有活跃状态的 OpenList 配置(用于定时心跳检测) + * 敏感信息自动解密 + */ + public List findAllActive() { + var wrapper = new LambdaQueryWrapper() + .eq(UserOpenlistDO::getStatus, "active"); + var list = list(wrapper); + var result = new java.util.ArrayList(); + for (var doObj : list) { + result.add(toEntity(doObj)); + } + return result; + } + + /** + * 根据用户ID查询 OpenList 连接配置(返回 Map) + * 敏感信息不返回明文,仅返回脱敏后的信息 + */ + public Map findInfoByUserId(Long userId) { + var wrapper = new LambdaQueryWrapper() + .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(); + 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() + .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() + .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() + .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() + .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); + } +} diff --git a/wangpan-common/src/main/java/com/wangpan/common/entity/DriveMovie.java b/wangpan-common/src/main/java/com/wangpan/common/entity/DriveMovie.java index df4871d..d5ab580 100644 --- a/wangpan-common/src/main/java/com/wangpan/common/entity/DriveMovie.java +++ b/wangpan-common/src/main/java/com/wangpan/common/entity/DriveMovie.java @@ -121,9 +121,6 @@ public class DriveMovie implements Serializable { /** 标签:逗号分隔,如 "科幻,IMAX,杜比视界" */ private String tags; - /** 电影简介(TMDB刮削) */ - private String overview; - /** 主演(逗号分隔,TMDB刮削) */ private String casts; @@ -246,9 +243,6 @@ public class DriveMovie implements Serializable { public String getTags() { return 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 void setCasts(String casts) { this.casts = casts; } diff --git a/wangpan-common/src/main/java/com/wangpan/common/entity/Movie.java b/wangpan-common/src/main/java/com/wangpan/common/entity/Movie.java index 5af1031..7c137c6 100644 --- a/wangpan-common/src/main/java/com/wangpan/common/entity/Movie.java +++ b/wangpan-common/src/main/java/com/wangpan/common/entity/Movie.java @@ -93,9 +93,6 @@ public class Movie implements Serializable { /** 标签:逗号分隔,如"科幻, IMAX, 杜比视界" */ private String tags; - /** 电影简介(TMDB刮削) */ - private String overview; - /** 主演(逗号分隔,TMDB刮削) */ private String casts; @@ -176,9 +173,6 @@ public class Movie implements Serializable { public String getTags() { return 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 void setCasts(String casts) { this.casts = casts; } diff --git a/wangpan-common/src/main/java/com/wangpan/common/entity/UserOpenlist.java b/wangpan-common/src/main/java/com/wangpan/common/entity/UserOpenlist.java new file mode 100644 index 0000000..c2d19cc --- /dev/null +++ b/wangpan-common/src/main/java/com/wangpan/common/entity/UserOpenlist.java @@ -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 + + '}'; + } +} diff --git a/wangpan-common/src/main/java/com/wangpan/common/util/OpenListClient.java b/wangpan-common/src/main/java/com/wangpan/common/util/OpenListClient.java index 784bdce..f96ebee 100644 --- a/wangpan-common/src/main/java/com/wangpan/common/util/OpenListClient.java +++ b/wangpan-common/src/main/java/com/wangpan/common/util/OpenListClient.java @@ -1,89 +1,207 @@ 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.Value; +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 API 客户端(用户级连接) * - * 封装 OpenList 的 REST API 调用,提供统一的文件操作接口。 + * 每个用户绑定自己的 OpenList 实例(地址、账号、密码),互不干扰。 + * Token 缓存到数据库(wp_user_openlist 表),避免每次调用都重新登录。 * * 重要:OpenList 使用内存缓存验证 Token,服务重启后所有 Token 失效。 * 本客户端实现了自动重试机制:当 API 返回 401 "token is invalidated" 时, * 自动刷新 Token 并重试请求,保证 API 调用的连续性。 * - * 配置项(application.yml): - * openlist: - * base-url: http://localhost:5244 - * username: admin - * password: your_password + * 使用方式: + * 1. 前端调用 /api/openlist/bind 绑定 OpenList(传入地址、账号、密码) + * 2. 后端验证连接 → 获取 Token → 缓存到数据库 + * 3. 后续 API 调用自动使用用户的 OpenList 配置 */ @Component public class OpenListClient { private static final Logger log = LoggerFactory.getLogger(OpenListClient.class); - @Value("${openlist.base-url:http://localhost:5244}") - private String baseUrl; - - @Value("${openlist.username:admin}") - private String username; - - @Value("${openlist.password:}") - private String password; + @Autowired + private UserOpenlistRepository userOpenlistRepository; private final RestTemplate restTemplate = new RestTemplate(); - private String cachedToken; - private long tokenExpireTime; - /** - * 获取有效的 Token(自动刷新) - * Token 有效期 24 小时,提前 1 小时刷新 - * - * @return JWT Token + * 测试 OpenList 连接(不保存配置) + * 用于前端绑定前验证地址、账号、密码是否正确 + * + * @param openlistUrl OpenList 服务地址,如 http://ldfun.asia:13000 + * @param openlistUsername OpenList 账号 + * @param openlistPassword OpenList 密码 + * @return 连接结果,包含 success(是否成功)、message(提示信息)、token(Token,成功时返回) */ - private String getValidToken() { - if (cachedToken == null || System.currentTimeMillis() > tokenExpireTime - 3600000) { - refreshToken(); + public Map testConnection(String openlistUrl, String openlistUsername, String openlistPassword) { + var result = new HashMap(); + + try { + // 调用登录接口 + String url = openlistUrl + "/api/auth/login"; + Map requestBody = new HashMap<>(); + requestBody.put("username", openlistUsername); + requestBody.put("password", openlistPassword); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity> entity = new HttpEntity<>(requestBody, headers); + ResponseEntity response = restTemplate.postForEntity(url, entity, Map.class); + + Map body = response.getBody(); + if (body != null && (int) body.get("code") == 200) { + Map data = (Map) 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 登录接口获取新的 JWT Token + * 绑定用户的 OpenList 连接配置 + * 验证连接 → 保存配置 → 缓存 Token + * + * @param userId 用户ID + * @param openlistUrl OpenList 服务地址 + * @param openlistUsername OpenList 账号 + * @param openlistPassword OpenList 密码 + * @return 绑定结果,包含 success(是否成功)、message(提示信息) */ - private void refreshToken() { - String url = baseUrl + "/api/auth/login"; + public Map bindConnection(Long userId, String openlistUrl, String openlistUsername, String openlistPassword) { + var result = new HashMap(); - Map requestBody = new HashMap<>(); - requestBody.put("username", username); - requestBody.put("password", password); - - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - - HttpEntity> entity = new HttpEntity<>(requestBody, headers); - ResponseEntity response = restTemplate.postForEntity(url, entity, Map.class); - - Map body = response.getBody(); - if (body != null && (int) body.get("code") == 200) { - Map data = (Map) 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") : "未知错误")); + // 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 getUserOpenlistInfo(Long userId) { + return userOpenlistRepository.findInfoByUserId(userId); + } + + /** + * 解绑用户的 OpenList 连接配置 + * + * @param userId 用户ID + * @return 解绑结果,包含 success(是否成功)、message(提示信息) + */ + public Map unbindConnection(Long userId) { + var result = new HashMap(); + + 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 请求(带自动重试) * 当 Token 失效时自动刷新并重试一次 + * + * @param userId 用户ID + * @param url 请求 URL + * @param requestBody 请求体 + * @return 响应结果 */ - private Map postWithRetry(String url, Map requestBody) { + private Map postWithRetry(Long userId, String url, Map requestBody) { + String token = getValidToken(userId); + HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); - headers.setBearerAuth(getValidToken()); + headers.setBearerAuth(token); HttpEntity> entity = new HttpEntity<>(requestBody, headers); ResponseEntity response = restTemplate.postForEntity(url, entity, Map.class); @@ -112,9 +237,9 @@ public class OpenListClient { // 如果 Token 失效,刷新后重试一次 if (isTokenInvalidated(result)) { - log.info("OpenList Token 失效,自动刷新并重试"); - refreshToken(); - headers.setBearerAuth(cachedToken); + log.info("用户 {} 的 OpenList Token 失效,自动刷新并重试", userId); + token = getValidToken(userId); // 强制刷新 + headers.setBearerAuth(token); entity = new HttpEntity<>(requestBody, headers); response = restTemplate.postForEntity(url, entity, Map.class); result = response.getBody(); @@ -125,14 +250,20 @@ public class OpenListClient { /** * 列出指定路径下的文件 - * - * @param path OpenList 路径,如 /movies - * @param page 页码(从 1 开始) + * + * @param userId 用户ID + * @param path OpenList 路径,如 /movies + * @param page 页码(从 1 开始) * @param perPage 每页数量 * @return 文件列表响应 */ - public Map listFiles(String path, int page, int perPage) { - String url = baseUrl + "/api/fs/list"; + public Map 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 requestBody = new HashMap<>(); requestBody.put("path", path); @@ -140,62 +271,142 @@ public class OpenListClient { requestBody.put("per_page", perPage); 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) */ - public Map getFileDetail(String path) { - String url = baseUrl + "/api/fs/get"; + public Map getFileDetail(Long userId, String path) { + UserOpenlist config = getUserOpenlist(userId); + if (config == null) { + throw new RuntimeException("用户未绑定 OpenList"); + } + + String url = config.getOpenlistUrl() + "/api/fs/get"; Map requestBody = new HashMap<>(); requestBody.put("path", path); - return postWithRetry(url, requestBody); + return postWithRetry(userId, url, requestBody); } /** * 创建文件夹 - * - * @param path 文件夹路径,如 /movies/科幻 + * + * @param userId 用户ID + * @param path 文件夹路径,如 /movies/科幻 * @return 创建结果 */ - public Map createFolder(String path) { - String url = baseUrl + "/api/fs/mkdir"; + public Map createFolder(Long userId, String path) { + UserOpenlist config = getUserOpenlist(userId); + if (config == null) { + throw new RuntimeException("用户未绑定 OpenList"); + } + + String url = config.getOpenlistUrl() + "/api/fs/mkdir"; Map requestBody = new HashMap<>(); requestBody.put("path", path); - return postWithRetry(url, requestBody); + return postWithRetry(userId, url, requestBody); } /** * 删除文件或文件夹 - * - * @param path 文件路径 - * @param names 文件名列表 + * + * @param userId 用户ID + * @param path 文件路径 + * @param names 文件名列表 * @return 删除结果 */ - public Map deleteFiles(String path, String[] names) { - String url = baseUrl + "/api/fs/remove"; + public Map 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 requestBody = new HashMap<>(); requestBody.put("dir", path); requestBody.put("names", names); - return postWithRetry(url, requestBody); + return postWithRetry(userId, url, requestBody); } /** - * 获取 OpenList 服务地址 - * - * @return OpenList base URL + * 获取用户的 OpenList 服务地址 + * + * @param userId 用户ID + * @return OpenList base URL,未绑定时返回 null */ - public String getBaseUrl() { - return baseUrl; + public String getUserBaseUrl(Long userId) { + UserOpenlist config = getUserOpenlist(userId); + return config != null ? config.getOpenlistUrl() : null; + } + + // ==================== 同步相关 API ==================== + + /** + * 获取指定路径下的所有内容(自动处理分页,返回全部结果) + * 用于同步场景,需要获取目录下所有文件和文件夹 + * + * @param userId 用户ID + * @param path OpenList 路径,如 / 或 /movies + * @return 内容列表(包含文件和文件夹信息) + */ + public List> listAll(Long userId, String path) { + UserOpenlist config = getUserOpenlist(userId); + if (config == null) { + throw new RuntimeException("用户未绑定 OpenList"); + } + + String url = config.getOpenlistUrl() + "/api/fs/list"; + List> allContent = new ArrayList<>(); + + int page = 1; + int perPage = 100; // 每页 100 条 + + while (true) { + Map requestBody = new HashMap<>(); + requestBody.put("path", path); + requestBody.put("page", page); + requestBody.put("per_page", perPage); + requestBody.put("refresh", false); + + Map result = postWithRetry(userId, url, requestBody); + + if (result == null || (int) result.get("code") != 200) { + log.warn("用户 {} 列出路径 {} 第 {} 页失败: {}", userId, path, page, result); + break; + } + + Map data = (Map) result.get("data"); + if (data == null) break; + + Map content = (Map) data.get("content"); + if (content == null) break; + + @SuppressWarnings("unchecked") + List> items = (List>) 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; } } diff --git a/wangpan-common/target/classes/com/wangpan/common/dal/dataobject/DriveDO.class b/wangpan-common/target/classes/com/wangpan/common/dal/dataobject/DriveDO.class index e69fd50..23f4241 100644 Binary files a/wangpan-common/target/classes/com/wangpan/common/dal/dataobject/DriveDO.class and b/wangpan-common/target/classes/com/wangpan/common/dal/dataobject/DriveDO.class differ diff --git a/wangpan-common/target/classes/com/wangpan/common/dal/dataobject/DriveMovieDO.class b/wangpan-common/target/classes/com/wangpan/common/dal/dataobject/DriveMovieDO.class index 7e6d7db..098e813 100644 Binary files a/wangpan-common/target/classes/com/wangpan/common/dal/dataobject/DriveMovieDO.class and b/wangpan-common/target/classes/com/wangpan/common/dal/dataobject/DriveMovieDO.class differ diff --git a/wangpan-common/target/classes/com/wangpan/common/dal/dataobject/MovieDO.class b/wangpan-common/target/classes/com/wangpan/common/dal/dataobject/MovieDO.class index cb631d5..8709e46 100644 Binary files a/wangpan-common/target/classes/com/wangpan/common/dal/dataobject/MovieDO.class and b/wangpan-common/target/classes/com/wangpan/common/dal/dataobject/MovieDO.class differ diff --git a/wangpan-common/target/classes/com/wangpan/common/dal/dataobject/UserOpenlistDO.class b/wangpan-common/target/classes/com/wangpan/common/dal/dataobject/UserOpenlistDO.class new file mode 100644 index 0000000..3d08aa5 Binary files /dev/null and b/wangpan-common/target/classes/com/wangpan/common/dal/dataobject/UserOpenlistDO.class differ diff --git a/wangpan-common/target/classes/com/wangpan/common/dal/mapper/DriveMovieMapper.class b/wangpan-common/target/classes/com/wangpan/common/dal/mapper/DriveMovieMapper.class index 4456765..a96c2a1 100644 Binary files a/wangpan-common/target/classes/com/wangpan/common/dal/mapper/DriveMovieMapper.class and b/wangpan-common/target/classes/com/wangpan/common/dal/mapper/DriveMovieMapper.class differ diff --git a/wangpan-common/target/classes/com/wangpan/common/dal/mapper/MovieMapper.class b/wangpan-common/target/classes/com/wangpan/common/dal/mapper/MovieMapper.class index 655b760..a570e88 100644 Binary files a/wangpan-common/target/classes/com/wangpan/common/dal/mapper/MovieMapper.class and b/wangpan-common/target/classes/com/wangpan/common/dal/mapper/MovieMapper.class differ diff --git a/wangpan-common/target/classes/com/wangpan/common/dal/mapper/UserOpenlistMapper.class b/wangpan-common/target/classes/com/wangpan/common/dal/mapper/UserOpenlistMapper.class new file mode 100644 index 0000000..0d567a0 Binary files /dev/null and b/wangpan-common/target/classes/com/wangpan/common/dal/mapper/UserOpenlistMapper.class differ diff --git a/wangpan-common/target/classes/com/wangpan/common/dal/repository/DriveMovieRepository.class b/wangpan-common/target/classes/com/wangpan/common/dal/repository/DriveMovieRepository.class index 236da7e..9ec3d02 100644 Binary files a/wangpan-common/target/classes/com/wangpan/common/dal/repository/DriveMovieRepository.class and b/wangpan-common/target/classes/com/wangpan/common/dal/repository/DriveMovieRepository.class differ diff --git a/wangpan-common/target/classes/com/wangpan/common/dal/repository/DriveRepository.class b/wangpan-common/target/classes/com/wangpan/common/dal/repository/DriveRepository.class index e4eec7d..468b0fc 100644 Binary files a/wangpan-common/target/classes/com/wangpan/common/dal/repository/DriveRepository.class and b/wangpan-common/target/classes/com/wangpan/common/dal/repository/DriveRepository.class differ diff --git a/wangpan-common/target/classes/com/wangpan/common/dal/repository/MovieRepository.class b/wangpan-common/target/classes/com/wangpan/common/dal/repository/MovieRepository.class index e59c768..3d31d78 100644 Binary files a/wangpan-common/target/classes/com/wangpan/common/dal/repository/MovieRepository.class and b/wangpan-common/target/classes/com/wangpan/common/dal/repository/MovieRepository.class differ diff --git a/wangpan-common/target/classes/com/wangpan/common/dal/repository/UserOpenlistRepository.class b/wangpan-common/target/classes/com/wangpan/common/dal/repository/UserOpenlistRepository.class new file mode 100644 index 0000000..c184a06 Binary files /dev/null and b/wangpan-common/target/classes/com/wangpan/common/dal/repository/UserOpenlistRepository.class differ diff --git a/wangpan-common/target/classes/com/wangpan/common/entity/Drive.class b/wangpan-common/target/classes/com/wangpan/common/entity/Drive.class index c0e8edd..3a92936 100644 Binary files a/wangpan-common/target/classes/com/wangpan/common/entity/Drive.class and b/wangpan-common/target/classes/com/wangpan/common/entity/Drive.class differ diff --git a/wangpan-common/target/classes/com/wangpan/common/entity/DriveMovie.class b/wangpan-common/target/classes/com/wangpan/common/entity/DriveMovie.class index a696c61..d5cdac3 100644 Binary files a/wangpan-common/target/classes/com/wangpan/common/entity/DriveMovie.class and b/wangpan-common/target/classes/com/wangpan/common/entity/DriveMovie.class differ diff --git a/wangpan-common/target/classes/com/wangpan/common/entity/Movie.class b/wangpan-common/target/classes/com/wangpan/common/entity/Movie.class index 2a858e4..84a756b 100644 Binary files a/wangpan-common/target/classes/com/wangpan/common/entity/Movie.class and b/wangpan-common/target/classes/com/wangpan/common/entity/Movie.class differ diff --git a/wangpan-common/target/classes/com/wangpan/common/entity/UserOpenlist.class b/wangpan-common/target/classes/com/wangpan/common/entity/UserOpenlist.class new file mode 100644 index 0000000..e66a802 Binary files /dev/null and b/wangpan-common/target/classes/com/wangpan/common/entity/UserOpenlist.class differ diff --git a/wangpan-common/target/classes/com/wangpan/common/util/EncryptUtil.class b/wangpan-common/target/classes/com/wangpan/common/util/EncryptUtil.class index bc7de25..4137640 100644 Binary files a/wangpan-common/target/classes/com/wangpan/common/util/EncryptUtil.class and b/wangpan-common/target/classes/com/wangpan/common/util/EncryptUtil.class differ diff --git a/wangpan-common/target/classes/com/wangpan/common/util/OpenListClient.class b/wangpan-common/target/classes/com/wangpan/common/util/OpenListClient.class index 6f108e9..b5a81e8 100644 Binary files a/wangpan-common/target/classes/com/wangpan/common/util/OpenListClient.class and b/wangpan-common/target/classes/com/wangpan/common/util/OpenListClient.class differ diff --git a/wangpan-common/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/wangpan-common/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst index 2ab9e73..bde4de2 100644 --- a/wangpan-common/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +++ b/wangpan-common/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -1,6 +1,7 @@ com\wangpan\common\dal\repository\IconRepository.class com\wangpan\common\util\EncryptUtil.class com\wangpan\common\dal\dataobject\MovieDO.class +com\wangpan\common\dal\mapper\UserOpenlistMapper.class com\wangpan\common\dal\dataobject\TmdbCacheDO.class com\wangpan\common\interceptor\AuthInterceptor.class com\wangpan\common\dal\repository\TokenRepository.class @@ -29,6 +30,7 @@ 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\entity\UserOpenlist.class com\wangpan\common\dal\dataobject\TokenDO.class com\wangpan\common\dal\mapper\UserMapper.class com\wangpan\common\dal\dataobject\IconDO.class @@ -45,7 +47,9 @@ 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\dataobject\UserOpenlistDO.class com\wangpan\common\dal\repository\TmdbCacheRepository.class +com\wangpan\common\dal\repository\UserOpenlistRepository.class com\wangpan\common\util\IconLogoFetcher.class com\wangpan\common\dal\dataobject\DriveMovieDO.class com\wangpan\common\dal\repository\DailyRecommendRepository.class diff --git a/wangpan-common/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/wangpan-common/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst index db9e63a..d3c433d 100644 --- a/wangpan-common/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +++ b/wangpan-common/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -1,3 +1,4 @@ +D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\mapper\UserOpenlistMapper.java 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 @@ -8,6 +9,7 @@ D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\reposi 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\dal\dataobject\UserOpenlistDO.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 @@ -17,6 +19,7 @@ D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\reposi 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\entity\UserOpenlist.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 @@ -42,6 +45,7 @@ D:\java2\wangpan\java\wangpan-common\src\main\java\com\wangpan\common\dal\mapper 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\repository\UserOpenlistRepository.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\util\OpenListClient.java diff --git a/wangpan-common/target/wangpan-common-1.0-SNAPSHOT.jar b/wangpan-common/target/wangpan-common-1.0-SNAPSHOT.jar index 0699982..db82de2 100644 Binary files a/wangpan-common/target/wangpan-common-1.0-SNAPSHOT.jar and b/wangpan-common/target/wangpan-common-1.0-SNAPSHOT.jar differ diff --git a/wangpan-drive-service/src/main/java/com/wangpan/DriveServiceApplication.java b/wangpan-drive-service/src/main/java/com/wangpan/DriveServiceApplication.java index 9a1140d..d003a58 100644 --- a/wangpan-drive-service/src/main/java/com/wangpan/DriveServiceApplication.java +++ b/wangpan-drive-service/src/main/java/com/wangpan/DriveServiceApplication.java @@ -2,6 +2,7 @@ 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; /** @@ -10,6 +11,7 @@ import com.ulisesbocchio.jasyptspringboot.annotation.EnableEncryptableProperties */ @SpringBootApplication @EnableEncryptableProperties +@EnableScheduling public class DriveServiceApplication { public static void main(String[] args) { diff --git a/wangpan-drive-service/src/main/java/com/wangpan/config/DatabaseInitializer.java b/wangpan-drive-service/src/main/java/com/wangpan/config/DatabaseInitializer.java index 4dba7ec..db8f079 100644 --- a/wangpan-drive-service/src/main/java/com/wangpan/config/DatabaseInitializer.java +++ b/wangpan-drive-service/src/main/java/com/wangpan/config/DatabaseInitializer.java @@ -4,6 +4,7 @@ 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; @@ -64,11 +65,12 @@ 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", @@ -102,10 +104,6 @@ public class DatabaseInitializer { SchemaUtil.ensureColumnExists(conn, "wp_drive_movie", "storage_type", "VARCHAR(20) NOT NULL DEFAULT 'metadata' COMMENT '存储类型:metadata(仅元数据)/ openlist(OpenList 管理)'"); - // 检查 wp_drive_movie 表是否有 overview 字段(电影简介) - SchemaUtil.ensureColumnExists(conn, "wp_drive_movie", "overview", - "TEXT DEFAULT NULL COMMENT '电影简介(TMDB刮削)'"); - // 检查 wp_drive_movie 表是否有 casts 字段(主演) SchemaUtil.ensureColumnExists(conn, "wp_drive_movie", "casts", "VARCHAR(500) DEFAULT NULL COMMENT '主演(逗号分隔,TMDB刮削)'"); @@ -135,6 +133,15 @@ 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'")) { diff --git a/wangpan-drive-service/src/main/java/com/wangpan/controller/DriveController.java b/wangpan-drive-service/src/main/java/com/wangpan/controller/DriveController.java index 89eb306..e928450 100644 --- a/wangpan-drive-service/src/main/java/com/wangpan/controller/DriveController.java +++ b/wangpan-drive-service/src/main/java/com/wangpan/controller/DriveController.java @@ -81,14 +81,6 @@ public class DriveController { Drive drive = new Drive(user.getId(), name.trim(), (String) data.get("description")); - // 处理 OpenList 关联:前端传入 openlistStorageId 和 openlistMountPath - String openlistStorageId = (String) data.get("openlistStorageId"); - String openlistMountPath = (String) data.get("openlistMountPath"); - if (openlistStorageId != null && !openlistStorageId.isBlank()) { - drive.setOpenlistStorageId(openlistStorageId.trim()); - drive.setOpenlistMountPath(openlistMountPath != null ? openlistMountPath.trim() : "/"); - } - // 处理 Logo 图片上传:前端传入 Base64 编码的图片数据,上传到 MinIO 后存储 URL String logoData = (String) data.get("logoData"); if (logoData != null && !logoData.isBlank()) { @@ -399,9 +391,10 @@ public class DriveController { movie.setMimeType(detectMimeType(title)); movie.setExtension(getExtension(title)); - // 如果网盘关联了 OpenList,设置文件路径和存储类型 - if (drive.getOpenlistStorageId() != null && !drive.getOpenlistStorageId().isBlank()) { - String openlistPath = buildOpenlistPath(drive, folderId, title.trim()); + // 如果用户绑定了 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 { @@ -481,18 +474,21 @@ public class DriveController { * 更新电影信息 * PUT /api/drive/{driveId}/movies/{movieId} * - * 请求体(所有字段可选): + * 请求体(所有字段可选,只传需要更新的字段): * { * "title": "新标题", - * "originalName": "新文件名.mp4", - * "subtitle": "副标题", * "description": "描述", * "rating": 85, * "tags": "标签1,标签2", - * "category": "分类" + * "posterUrl": "海报URL", + * "backdropUrl": "背景图URL", + * "casts": "主演列表", + * "directors": "导演列表", + * "resourceLibrary": "资源库来源", + * "maintainer": "维护者" * } * - * 说明:用于 TMDB 刮削有误时,用户自定义修改文件名等信息 + * 说明:用于 TMDB 刮削有误时,用户自定义修改文件名等信息,也支持手动补充刮削字段 */ @PutMapping("/{driveId}/movies/{movieId}") public Result updateMovie(@PathVariable("driveId") Long driveId, @@ -506,19 +502,38 @@ 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"); - boolean success = movieRepository.update(movieId, title, originalName, subtitle, description, rating, tags, 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); + } + if (success) { return Result.success("更新成功"); } else { @@ -643,18 +658,17 @@ public class DriveController { /** * 构建 OpenList 文件路径 - * 例如:/movies/科幻/阿凡达.mp4 + * 例如:/uc/科幻/阿凡达.mp4 * - * @param drive 网盘实体 - * @param folderId 文件夹ID(0 表示根目录) - * @param fileName 文件名 + * @param mountPath 挂载路径(如 /uc) + * @param folderId 文件夹 ID(0 表示根目录) + * @param fileName 文件名 * @return OpenList 文件路径 */ - private String buildOpenlistPath(Drive drive, Long folderId, String fileName) { + private String buildOpenlistPath(String mountPath, Long folderId, String fileName) { StringBuilder path = new StringBuilder(); // 添加挂载路径 - String mountPath = drive.getOpenlistMountPath(); if (mountPath != null && !mountPath.equals("/")) { path.append(mountPath); } @@ -736,6 +750,11 @@ public class DriveController { /** * DriveMovie 实体转 Map + * + * TMDB 刮削字段(casts、directors、posterUrl、backdropUrl、rating) + * 仅在刮削成功(scrapeStatus = "success")时返回实际值,否则返回 null + * + * 已移除字段:subtitle、originalName、category、overview(不再对外暴露) */ private Map movieToMap(DriveMovie movie) { var map = new LinkedHashMap(); @@ -743,8 +762,6 @@ 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()); @@ -752,18 +769,23 @@ 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("minioUrl", movie.getMinioUrl()); - map.put("posterUrl", movie.getPosterUrl()); map.put("openlistFilePath", movie.getOpenlistFilePath()); map.put("storageType", movie.getStorageType()); 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; } @@ -774,6 +796,7 @@ public class DriveController { * GET /api/drive/{driveId}/movies/{movieId}/play-url * * 返回 OpenList 的真实下载链接,用于前端播放 + * 使用当前用户的 OpenList 连接配置 */ @GetMapping("/{driveId}/movies/{movieId}/play-url") public Result getMoviePlayUrl(@PathVariable("driveId") Long driveId, @@ -792,8 +815,8 @@ public class DriveController { } try { - // 调用 OpenList API 获取下载链接 - Map result = openListClient.getFileDetail(movie.getOpenlistFilePath()); + // 调用 OpenList API 获取下载链接(使用用户级连接) + Map result = openListClient.getFileDetail(user.getId(), movie.getOpenlistFilePath()); if (result != null && (int) result.get("code") == 200) { Map data = (Map) result.get("data"); @@ -819,6 +842,7 @@ public class DriveController { * POST /api/drive/{driveId}/folders/{folderId}/sync-files * * 从 OpenList 获取实际文件列表,并同步到数据库 + * 使用当前用户的 OpenList 连接配置 */ @PostMapping("/{driveId}/folders/{folderId}/sync-files") public Result syncFilesFromOpenList(@PathVariable("driveId") Long driveId, @@ -836,17 +860,18 @@ public class DriveController { return Result.error("同步失败:文件夹不存在或无权访问"); } - // 检查网盘是否关联了 OpenList - if (drive.getOpenlistStorageId() == null || drive.getOpenlistStorageId().isBlank()) { - return Result.error("同步失败:网盘未关联 OpenList 存储"); + // 检查用户是否绑定了 OpenList + var userOpenlist = openListClient.getUserOpenlist(user.getId()); + if (userOpenlist == null || userOpenlist.getMountPath() == null) { + return Result.error("同步失败:用户未绑定 OpenList,请先在设置中绑定 OpenList 连接"); } try { // 构建 OpenList 路径 - String openlistPath = buildOpenlistPath(drive, folderId, ""); + String openlistPath = buildOpenlistPath(userOpenlist.getMountPath(), folderId, ""); - // 调用 OpenList API 获取文件列表 - Map result = openListClient.listFiles(openlistPath, 1, 100); + // 调用 OpenList API 获取文件列表(使用用户级连接) + Map result = openListClient.listFiles(user.getId(), openlistPath, 1, 100); if (result != null && (int) result.get("code") == 200) { Map data = (Map) result.get("data"); diff --git a/wangpan-drive-service/src/main/java/com/wangpan/controller/OpenListController.java b/wangpan-drive-service/src/main/java/com/wangpan/controller/OpenListController.java new file mode 100644 index 0000000..d7482a3 --- /dev/null +++ b/wangpan-drive-service/src/main/java/com/wangpan/controller/OpenListController.java @@ -0,0 +1,317 @@ +package com.wangpan.controller; + +import com.wangpan.common.Result; +import com.wangpan.common.annotation.RequireAuth; +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; + + /** + * 从请求属性中获取当前登录用户(由 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 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 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); + } + + // 实际测试 OpenList 连接,验证账号密码是否仍然可用 + UserOpenlist config = openListClient.getUserOpenlist(user.getId()); + if (config != null) { + var testResult = openListClient.testConnection( + config.getOpenlistUrl(), + config.getOpenlistUsername(), + config.getOpenlistPassword() + ); + info.put("connected", testResult.get("success")); + info.put("connectMessage", testResult.get("message")); + } 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()); + var syncResult = openListSyncService.syncFromOpenList(user.getId()); + + if (syncResult.errors.isEmpty()) { + return Result.success("同步完成", syncResult.toMap()); + } else { + return Result.success("同步完成(部分失败)", syncResult.toMap()); + } + } +} diff --git a/wangpan-drive-service/src/main/java/com/wangpan/listener/OpenListHeartbeatScheduler.java b/wangpan-drive-service/src/main/java/com/wangpan/listener/OpenListHeartbeatScheduler.java new file mode 100644 index 0000000..c944a09 --- /dev/null +++ b/wangpan-drive-service/src/main/java/com/wangpan/listener/OpenListHeartbeatScheduler.java @@ -0,0 +1,99 @@ +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 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 { + // 调用 OpenList 登录接口验证连接 + Map 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); + } +} diff --git a/wangpan-drive-service/src/main/java/com/wangpan/service/OpenListSyncService.java b/wangpan-drive-service/src/main/java/com/wangpan/service/OpenListSyncService.java new file mode 100644 index 0000000..12ece1c --- /dev/null +++ b/wangpan-drive-service/src/main/java/com/wangpan/service/OpenListSyncService.java @@ -0,0 +1,286 @@ +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 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 errors = new ArrayList<>(); + + public Map toMap() { + var map = new LinkedHashMap(); + 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. 查找或创建关联网盘 + Drive drive = findOrCreateDrive(userId, config); + if (drive == null) { + result.errors.add("创建网盘失败"); + return result; + } + result.driveCount = 1; + log.info("用户 {} 使用网盘: {} (ID={})", userId, drive.getName(), drive.getId()); + + // 3. 从根目录开始递归同步 + String rootPath = "/"; + syncDirectory(userId, drive.getId(), rootPath, 0L, 0, result); + + // 4. 更新网盘统计信息 + int totalFolders = folderRepository.countByDrive(drive.getId(), userId); + int totalMovies = movieRepository.countByDrive(drive.getId(), userId); + driveRepository.updateStats(drive.getId(), totalFolders, totalMovies, 0L); + + log.info("用户 {} 同步完成: 网盘={}, 文件夹={}, 电影={}, 跳过文件={}, 跳过文件夹={}, 错误={}", + userId, drive.getName(), result.folderCount, result.movieCount, + result.skippedFileCount, result.skippedFolderCount, result.errors.size()); + + return result; + } + + /** + * 查找或创建关联 OpenList 的网盘 + * 如果用户已有 openlistMountPath 不为空的网盘,则复用;否则创建新的 + */ + private Drive findOrCreateDrive(Long userId, UserOpenlist config) { + // 查询用户所有网盘,查找已关联 OpenList 的 + var drives = driveRepository.queryByUserId(userId, 1, 100); + for (var driveMap : drives) { + String mountPath = (String) driveMap.get("openlistMountPath"); + if (mountPath != null && !mountPath.isBlank()) { + // 复用已有网盘 + 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); + 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> items = openListClient.listAll(userId, openlistPath); + if (items.isEmpty()) { + return; + } + + // 分离文件夹和视频文件 + List> folders = new ArrayList<>(); + List> videoFiles = new ArrayList<>(); + + for (var item : items) { + String name = (String) item.get("name"); + boolean isDir = (boolean) item.getOrDefault("is_dir", false); + + 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; + } +} diff --git a/wangpan-drive-service/src/main/resources/application.yml b/wangpan-drive-service/src/main/resources/application.yml index e66edfa..be76a0b 100644 --- a/wangpan-drive-service/src/main/resources/application.yml +++ b/wangpan-drive-service/src/main/resources/application.yml @@ -64,6 +64,6 @@ mybatis-plus: # OpenList 配置 openlist: - base-url: http://ldfun.asia:13000 - username: djl1022 - password: Djl1022! + base-url: ENC(nAus1Dw7kxDsHnaqF+0g1Np+4wsYAcRlIqXPxaag1knMYJlBQsf/U3AKhetuIJywsGvENyNH7cscCtlDsv4oLA==) + username: ENC(s4yi9ARKcY1dY1whS0MKMr7RjM6FMzePB6FYfmCeEilAbV927glsrdP8EUZblGun) + password: ENC(TUkBEpl8iK5kIYcuFE5tLsMeozUDfqdeep1se21GTxE9D+HfcdfsVTJEgNuol2N+) diff --git a/wangpan-drive-service/target/classes/application.yml b/wangpan-drive-service/target/classes/application.yml index e66edfa..be76a0b 100644 --- a/wangpan-drive-service/target/classes/application.yml +++ b/wangpan-drive-service/target/classes/application.yml @@ -64,6 +64,6 @@ mybatis-plus: # OpenList 配置 openlist: - base-url: http://ldfun.asia:13000 - username: djl1022 - password: Djl1022! + base-url: ENC(nAus1Dw7kxDsHnaqF+0g1Np+4wsYAcRlIqXPxaag1knMYJlBQsf/U3AKhetuIJywsGvENyNH7cscCtlDsv4oLA==) + username: ENC(s4yi9ARKcY1dY1whS0MKMr7RjM6FMzePB6FYfmCeEilAbV927glsrdP8EUZblGun) + password: ENC(TUkBEpl8iK5kIYcuFE5tLsMeozUDfqdeep1se21GTxE9D+HfcdfsVTJEgNuol2N+) diff --git a/wangpan-drive-service/target/classes/com/wangpan/DriveServiceApplication.class b/wangpan-drive-service/target/classes/com/wangpan/DriveServiceApplication.class index d5e00b1..f9220bf 100644 Binary files a/wangpan-drive-service/target/classes/com/wangpan/DriveServiceApplication.class and b/wangpan-drive-service/target/classes/com/wangpan/DriveServiceApplication.class differ diff --git a/wangpan-drive-service/target/classes/com/wangpan/config/DatabaseInitializer.class b/wangpan-drive-service/target/classes/com/wangpan/config/DatabaseInitializer.class index beedabd..7d916ab 100644 Binary files a/wangpan-drive-service/target/classes/com/wangpan/config/DatabaseInitializer.class and b/wangpan-drive-service/target/classes/com/wangpan/config/DatabaseInitializer.class differ diff --git a/wangpan-drive-service/target/classes/com/wangpan/controller/DriveController.class b/wangpan-drive-service/target/classes/com/wangpan/controller/DriveController.class index bfc5eef..8b003ca 100644 Binary files a/wangpan-drive-service/target/classes/com/wangpan/controller/DriveController.class and b/wangpan-drive-service/target/classes/com/wangpan/controller/DriveController.class differ diff --git a/wangpan-drive-service/target/classes/com/wangpan/controller/IconController.class b/wangpan-drive-service/target/classes/com/wangpan/controller/IconController.class index 8f228ab..4ddbb15 100644 Binary files a/wangpan-drive-service/target/classes/com/wangpan/controller/IconController.class and b/wangpan-drive-service/target/classes/com/wangpan/controller/IconController.class differ diff --git a/wangpan-drive-service/target/classes/com/wangpan/controller/OpenListController.class b/wangpan-drive-service/target/classes/com/wangpan/controller/OpenListController.class new file mode 100644 index 0000000..c11cfa2 Binary files /dev/null and b/wangpan-drive-service/target/classes/com/wangpan/controller/OpenListController.class differ diff --git a/wangpan-drive-service/target/classes/com/wangpan/listener/IconCleanupScheduler$CleanupTask.class b/wangpan-drive-service/target/classes/com/wangpan/listener/IconCleanupScheduler$CleanupTask.class index f4b2a68..560f717 100644 Binary files a/wangpan-drive-service/target/classes/com/wangpan/listener/IconCleanupScheduler$CleanupTask.class and b/wangpan-drive-service/target/classes/com/wangpan/listener/IconCleanupScheduler$CleanupTask.class differ diff --git a/wangpan-drive-service/target/classes/com/wangpan/listener/IconCleanupScheduler.class b/wangpan-drive-service/target/classes/com/wangpan/listener/IconCleanupScheduler.class index b32368d..cba3a4b 100644 Binary files a/wangpan-drive-service/target/classes/com/wangpan/listener/IconCleanupScheduler.class and b/wangpan-drive-service/target/classes/com/wangpan/listener/IconCleanupScheduler.class differ diff --git a/wangpan-drive-service/target/classes/com/wangpan/listener/OpenListHeartbeatScheduler.class b/wangpan-drive-service/target/classes/com/wangpan/listener/OpenListHeartbeatScheduler.class new file mode 100644 index 0000000..d6d74f6 Binary files /dev/null and b/wangpan-drive-service/target/classes/com/wangpan/listener/OpenListHeartbeatScheduler.class differ diff --git a/wangpan-drive-service/target/classes/com/wangpan/service/OpenListSyncService$SyncResult.class b/wangpan-drive-service/target/classes/com/wangpan/service/OpenListSyncService$SyncResult.class new file mode 100644 index 0000000..417f02f Binary files /dev/null and b/wangpan-drive-service/target/classes/com/wangpan/service/OpenListSyncService$SyncResult.class differ diff --git a/wangpan-drive-service/target/classes/com/wangpan/service/OpenListSyncService.class b/wangpan-drive-service/target/classes/com/wangpan/service/OpenListSyncService.class new file mode 100644 index 0000000..2593ddd Binary files /dev/null and b/wangpan-drive-service/target/classes/com/wangpan/service/OpenListSyncService.class differ diff --git a/wangpan-drive-service/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/wangpan-drive-service/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst index a357e8b..41db8e9 100644 --- a/wangpan-drive-service/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +++ b/wangpan-drive-service/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -1,6 +1,10 @@ com\wangpan\config\DatabaseInitializer.class +com\wangpan\service\OpenListSyncService$SyncResult.class com\wangpan\controller\IconController.class com\wangpan\listener\IconCleanupScheduler.class +com\wangpan\service\OpenListSyncService.class +com\wangpan\controller\OpenListController.class com\wangpan\controller\DriveController.class com\wangpan\DriveServiceApplication.class com\wangpan\listener\IconCleanupScheduler$CleanupTask.class +com\wangpan\listener\OpenListHeartbeatScheduler.class diff --git a/wangpan-drive-service/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/wangpan-drive-service/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst index be3c528..5b9fd04 100644 --- a/wangpan-drive-service/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +++ b/wangpan-drive-service/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -2,4 +2,7 @@ D:\java2\wangpan\java\wangpan-drive-service\src\main\java\com\wangpan\config\Dat D:\java2\wangpan\java\wangpan-drive-service\src\main\java\com\wangpan\controller\DriveController.java D:\java2\wangpan\java\wangpan-drive-service\src\main\java\com\wangpan\listener\IconCleanupScheduler.java D:\java2\wangpan\java\wangpan-drive-service\src\main\java\com\wangpan\DriveServiceApplication.java +D:\java2\wangpan\java\wangpan-drive-service\src\main\java\com\wangpan\service\OpenListSyncService.java D:\java2\wangpan\java\wangpan-drive-service\src\main\java\com\wangpan\controller\IconController.java +D:\java2\wangpan\java\wangpan-drive-service\src\main\java\com\wangpan\controller\OpenListController.java +D:\java2\wangpan\java\wangpan-drive-service\src\main\java\com\wangpan\listener\OpenListHeartbeatScheduler.java diff --git a/wangpan-drive-service/target/wangpan-drive-service-1.0-SNAPSHOT.jar b/wangpan-drive-service/target/wangpan-drive-service-1.0-SNAPSHOT.jar index b62bfe4..4daeeaf 100644 Binary files a/wangpan-drive-service/target/wangpan-drive-service-1.0-SNAPSHOT.jar and b/wangpan-drive-service/target/wangpan-drive-service-1.0-SNAPSHOT.jar differ diff --git a/wangpan-drive-service/target/wangpan-drive-service-1.0-SNAPSHOT.jar.original b/wangpan-drive-service/target/wangpan-drive-service-1.0-SNAPSHOT.jar.original index 35e7606..c2f9179 100644 Binary files a/wangpan-drive-service/target/wangpan-drive-service-1.0-SNAPSHOT.jar.original and b/wangpan-drive-service/target/wangpan-drive-service-1.0-SNAPSHOT.jar.original differ diff --git a/wangpan-gateway/src/main/resources/META-INF/resources/api-doc.html b/wangpan-gateway/src/main/resources/META-INF/resources/api-doc.html index 0bb6822..25a902d 100644 --- a/wangpan-gateway/src/main/resources/META-INF/resources/api-doc.html +++ b/wangpan-gateway/src/main/resources/META-INF/resources/api-doc.html @@ -399,7 +399,7 @@ "message": "登录成功", "data": { "id": 7, - "username": "lrq", + "username": "zhangsan", "userType": 0, // 0=普通用户, 1=VIP, 2=管理员 "status": 1, "email": "xxx@xx.com", @@ -514,7 +514,7 @@ "data": { "valid": true, "userId": 7, - "username": "lrq", + "username": "zhangsan", "expireTime": "2026-06-17T11:20:21" } } @@ -569,7 +569,7 @@ { "id": 1, "userId": 7, - "username": "lrq", + "username": "zhangsan", "loginIp": "127.0.0.1", "loginTime": "2026-06-10T11:20:21", "result": "success", @@ -644,7 +644,7 @@ "data": { "total": 5, "list": [ - { "id": 7, "username": "lrq", "userType": 0, "status": 1, ... } + { "id": 7, "username": "zhangsan", "userType": 0, "status": 1, ... } ], "pageNo": 1, "pageSize": 20 @@ -1012,7 +1012,7 @@ "code": 200, "message": "头像更新成功", "data": { - "avatarUrl": "http://118.178.238.159:39000/wangpan/avatar/1_1719234567890.png?..." + "avatarUrl": "http://your-server:39000/wangpan/avatar/1_1719234567890.png?..." } } @@ -1076,7 +1076,7 @@ "id": 1, "name": "我的网盘", "description": "个人网盘", - "logoUrl": "http://118.178.238.159:39000/wangpan/drive/1/logo.png", + "logoUrl": "http://your-server:39000/wangpan/drive/1/logo.png", "createTime": "2026-06-26T10:00:00" } } @@ -1143,7 +1143,7 @@ "id": 1, "name": "我的网盘", "description": "个人网盘", - "logoUrl": "http://118.178.238.159:39000/wangpan/drive/1/logo.png", + "logoUrl": "http://your-server:39000/wangpan/drive/1/logo.png", "folderCount": 5, "fileCount": 20, "totalSize": 10737418240, @@ -1207,7 +1207,7 @@ "id": 1, "name": "我的网盘", "description": "个人网盘", - "logoUrl": "http://118.178.238.159:39000/wangpan/drive/1/logo.png", + "logoUrl": "http://your-server:39000/wangpan/drive/1/logo.png", "folderCount": 5, "fileCount": 20, "totalSize": 10737418240, @@ -1269,7 +1269,7 @@ "id": 1, "name": "新网盘名称", "description": "新描述", - "logoUrl": "http://118.178.238.159:39000/wangpan/drive/1/logo.png", + "logoUrl": "http://your-server:39000/wangpan/drive/1/logo.png", "updateTime": "2026-06-26T11:00:00" } } @@ -1780,28 +1780,26 @@ driveIdLong所属网盘ID folderIdLong所属文件夹ID titleString电影名称 - originalNameString原始文件名 mimeTypeStringMIME类型 fileSizeLong文件大小(字节) extensionString文件扩展名 descriptionString描述 maintainerString维护者 statusString状态 - categoryString分类 scrapeTypeString刮削类型 - ratingInteger评分 + scrapeStatusString刮削状态 + ratingInteger|null评分(刮削成功时返回) tagsString标签 - overviewString电影简介(TMDB刮削) - castsString主演(逗号分隔,TMDB刮削) - directorsString导演(逗号分隔,TMDB刮削) - minioUrlStringMinIO文件访问URL - posterUrlString海报URL + castsString|null主演(刮削成功时返回) + directorsString|null导演(刮削成功时返回) + posterUrlString|null海报URL(刮削成功时返回) + backdropUrlString|null背景图URL(刮削成功时返回) createTimeString创建时间 updateTimeString更新时间
-

成功响应示例

+

成功响应示例(刮削成功)

{ "code": 200, "message": "查询成功", @@ -1809,19 +1807,50 @@ "id": 1, "driveId": 1, "folderId": 0, - "title": "电影.mp4", - "originalName": "电影.mp4", + "title": "阿凡达.mp4", "mimeType": "video/mp4", "fileSize": 1073741824, "extension": "mp4", "description": "电影描述", + "maintainer": "admin", + "status": "active", + "scrapeType": "tmdb", + "scrapeStatus": "success", "rating": 85, "tags": "动作,科幻", - "overview": "电影简介...", "casts": "主演1,主演2", "directors": "导演1", "posterUrl": "http://...", - "minioUrl": "http://...", + "backdropUrl": "http://...", + "createTime": "2026-06-26T10:00:00", + "updateTime": "2026-06-26T10:00:00" + } +}
+
+
+

成功响应示例(未刮削/刮削失败)

+
{ + "code": 200, + "message": "查询成功", + "data": { + "id": 2, + "driveId": 1, + "folderId": 0, + "title": "未刮削电影.mp4", + "mimeType": "video/mp4", + "fileSize": 536870912, + "extension": "mp4", + "description": null, + "maintainer": null, + "status": "active", + "scrapeType": "none", + "scrapeStatus": "pending", + "rating": null, + "tags": null, + "casts": null, + "directors": null, + "posterUrl": null, + "backdropUrl": null, "createTime": "2026-06-26T10:00:00", "updateTime": "2026-06-26T10:00:00" } @@ -1865,11 +1894,15 @@

请求体(部分更新,所有字段可选)

{ "title": "新电影名称", - "originalName": "新文件名.mp4", "description": "新描述", "rating": 90, "tags": "新标签", - "category": "新分类" + "posterUrl": "海报URL", + "backdropUrl": "背景图URL", + "casts": "主演列表", + "directors": "导演列表", + "resourceLibrary": "资源库来源", + "maintainer": "维护者" }
@@ -1877,11 +1910,15 @@ - - + + + + + +
参数类型说明
titleString电影名称
originalNameString原始文件名(用于 TMDB 刮削有误时修改)
descriptionString描述
ratingInteger评分 0-100
tagsString标签
categoryString分类
posterUrlString海报URL
backdropUrlString背景图URL
castsString主演(逗号分隔)
directorsString导演(逗号分隔)
resourceLibraryString资源库来源
maintainerString维护者
@@ -2080,7 +2117,7 @@

请求示例(仅外网)

{ "name": "百度", - "url": "https://www.baidu.com", + "url": "https://www.example.com", "linkType": "external", "description": "搜索引擎", "sortOrder": 1 @@ -2091,7 +2128,7 @@
{ "name": "内部系统", "url": "https://system.example.com", - "internalUrl": "http://192.168.1.100:8080", + "internalUrl": "http://192.168.x.x:8080", "linkType": "both", "description": "内部管理系统", "sortOrder": 2 @@ -2105,13 +2142,13 @@ "data": { "id": 1, "name": "百度", - "url": "https://www.baidu.com", + "url": "https://www.example.com", "internalUrl": null, "linkType": "external", "description": "搜索引擎", "sortOrder": 1, "status": "active", - "logoUrl": "http://118.178.238.159:39000/wangpan/icons/2/1718265600000.png", + "logoUrl": "http://your-server:39000/wangpan/icons/2/1718265600000.png", "createTime": "2026-06-13T16:00:00" } }
@@ -2141,13 +2178,13 @@ { "id": 1, "name": "百度", - "url": "https://www.baidu.com", + "url": "https://www.example.com", "internalUrl": null, "linkType": "external", "description": "搜索引擎", "sortOrder": 1, "status": "active", - "logoUrl": "http://118.178.238.159:39000/wangpan/icons/2/1718265600000.png", + "logoUrl": "http://your-server:39000/wangpan/icons/2/1718265600000.png", "createTime": "2026-06-13T16:00:00", "updateTime": "2026-06-13T16:00:00" } @@ -2182,13 +2219,13 @@ "data": { "id": 1, "name": "百度", - "url": "https://www.baidu.com", + "url": "https://www.example.com", "internalUrl": null, "linkType": "external", "description": "搜索引擎", "sortOrder": 1, "status": "active", - "logoUrl": "http://118.178.238.159:39000/wangpan/icons/2/1718265600000.png", + "logoUrl": "http://your-server:39000/wangpan/icons/2/1718265600000.png", "createTime": "2026-06-13T16:00:00", "updateTime": "2026-06-13T16:00:00" } @@ -2245,7 +2282,7 @@
{ "name": "新名称", "url": "https://new-url.com", - "internalUrl": "http://192.168.1.100:8080", + "internalUrl": "http://192.168.x.x:8080", "linkType": "both", "sortOrder": 2 }
@@ -2259,12 +2296,12 @@ "id": 1, "name": "新名称", "url": "https://new-url.com", - "internalUrl": "http://192.168.1.100:8080", + "internalUrl": "http://192.168.x.x:8080", "linkType": "both", "description": "搜索引擎", "sortOrder": 2, "status": "active", - "logoUrl": "http://118.178.238.159:39000/wangpan/icons/2/1718265600000.png", + "logoUrl": "http://your-server:39000/wangpan/icons/2/1718265600000.png", "createTime": "2026-06-13T16:00:00", "updateTime": "2026-06-13T17:00:00" } @@ -2356,7 +2393,7 @@ "data": { "id": 1, "name": "百度", - "logoUrl": "http://118.178.238.159:39000/wangpan/icons/2/1718265600000.png", + "logoUrl": "http://your-server:39000/wangpan/icons/2/1718265600000.png", "updateTime": "2026-06-15T10:00:00" } }
@@ -2532,7 +2569,6 @@ folderGroups[].movies[].scrapeStatusString刮削状态 folderGroups[].movies[].ratingInteger评分(0-100) folderGroups[].movies[].tagsString标签 - folderGroups[].movies[].overviewString电影简介(TMDB刮削) folderGroups[].movies[].castsString主演(逗号分隔,TMDB刮削) folderGroups[].movies[].directorsString导演(逗号分隔,TMDB刮削) folderGroups[].movies[].posterUrlString竖版海报URL(2:3 比例) @@ -2591,6 +2627,175 @@
+
+
+ GET + /api/movies/all + 查询所有电影(不分文件夹,直接分页)需Token +
+
+
+

功能说明

+

查询当前用户所有网盘下所有未删除的电影,不进行分类直接分页返回。适用于大数据量场景(1万+),内置5分钟缓存机制,支持关键词搜索和多字段排序。

+

设计特点:

+
    +
  • 不进行分类,直接分页查询所有电影
  • +
  • 支持关键词搜索(标题模糊匹配)
  • +
  • 支持排序(按创建时间、更新时间、评分、标题)
  • +
  • 内置5分钟缓存,适用于1万+数据量场景
  • +
  • 最大每页100条,防止单次查询数据量过大
  • +
  • 每日随机刷新:第1页前20条数据每日随机刷新(无关键词搜索时生效),增加内容变化性,其余数据按排序规则保持不变
  • +
+

每日随机刷新机制:

+
    +
  • 触发条件:查询第1页且无关键词搜索时
  • +
  • 随机数量:前20条数据从所有电影中随机选取
  • +
  • 缓存策略:缓存Key包含日期因子,每日自动刷新
  • +
  • 其余数据:第21条及以后按指定排序规则返回
  • +
  • 关键词搜索时:不触发随机,正常返回搜索结果
  • +
+
+
+

查询参数

+ + + + + + + +
参数类型必填说明
keywordString搜索关键词(电影标题模糊匹配)
sortByString排序字段:createTime(创建时间)/ updateTime(更新时间)/ rating(评分)/ title(标题)
sortOrderString排序方向:asc(升序)/ desc(降序),默认desc
pageNoint页码,默认 1
pageSizeint每页条数,默认 50,最大 100
+
+
+

响应字段说明

+ + + + + + + + + + + + + + + + + + + + + + + + + +
字段类型说明
totalLong电影总数
pagesLong总页数
pageNoLong当前页码
pageSizeLong每页条数
list[].idLong电影ID
list[].driveIdLong所属网盘ID
list[].folderIdLong所属文件夹ID
list[].titleString电影名称
list[].originalNameString原始文件名
list[].mimeTypeStringMIME类型
list[].fileSizeLong文件大小(字节)
list[].extensionString文件扩展名
list[].descriptionString描述
list[].scrapeStatusString刮削状态:pending/scraping/success/failed
list[].ratingInteger评分(0-100)
list[].tagsString标签
list[].castsString主演(逗号分隔,TMDB刮削)
list[].directorsString导演(逗号分隔,TMDB刮削)
list[].posterUrlString竖版海报URL(2:3 比例)
list[].backdropUrlString横版背景图URL(16:9 比例)
list[].minioUrlStringMinIO 文件访问URL
list[].createTimeString创建时间
list[].updateTimeString更新时间
+
+
+

成功响应示例

+
{ + "code": 200, + "message": "查询成功", + "data": { + "total": 12580, + "pages": 252, + "pageNo": 1, + "pageSize": 50, + "list": [ + { + "id": 1, + "driveId": 1, + "folderId": 3, + "title": "肖申克的救赎", + "originalName": "The.Shawshank.Redemption.1994.mkv", + "fileSize": 8589934592, + "description": "一场冤案让银行家安迪入狱,他在肖申克监狱用20年时间挖出一条自由之路...", + "scrapeStatus": "success", + "rating": 93, + "casts": "蒂姆·罗宾斯,摩根·弗里曼", + "directors": "弗兰克·德拉邦特", + "posterUrl": "http://...:39000/wangpan/posters/1/poster_xxx.jpg", + "backdropUrl": "http://...:39000/wangpan/posters/1/backdrop_xxx.jpg", + "createTime": "2026-06-20 10:30:00", + "updateTime": "2026-06-27 14:20:00" + } + ] + } +}
+
+
+

成功响应示例 — 命中缓存

+
{ + "code": 200, + "message": "查询成功(缓存)", + "data": { + "total": 12580, + "pages": 252, + "pageNo": 1, + "pageSize": 50, + "list": [...] + } +}
+
+
+

失败响应示例 — 未登录或 Token 无效

+
{ + "code": 401, + "message": "未登录或 Token 已过期", + "data": null +}
+
+
+

失败响应示例 — 查询失败

+
{ + "code": 500, + "message": "查询失败:数据库连接异常", + "data": null +}
+
+
+

前端调用示例

+
// 查询第1页,每页50条 +fetch('/api/movies/all?pageNo=1&pageSize=50', { + headers: { + 'Authorization': 'Bearer ' + token + } +}) +.then(res => res.json()) +.then(data => { + console.log('总数:', data.data.total); + console.log('电影列表:', data.data.list); +}); + +// 搜索电影 +fetch('/api/movies/all?keyword=肖申克&pageNo=1&pageSize=50', { + headers: { + 'Authorization': 'Bearer ' + token + } +}) +.then(res => res.json()) +.then(data => { + console.log('搜索结果:', data.data.list); +}); + +// 按评分降序排序 +fetch('/api/movies/all?sortBy=rating&sortOrder=desc&pageNo=1&pageSize=50', { + headers: { + 'Authorization': 'Bearer ' + token + } +}) +.then(res => res.json()) +.then(data => { + console.log('高分电影:', data.data.list); +});
+
+
+
+
GET @@ -2624,7 +2829,6 @@ descriptionString电影描述 scrapeStatusString刮削状态:pending/scraping/success/failed ratingInteger评分(0-100) - overviewString电影简介(TMDB刮削) castsString主演(逗号分隔,TMDB刮削) directorsString导演(逗号分隔,TMDB刮削) posterUrlString竖版海报URL(2:3 比例) @@ -2651,7 +2855,6 @@ "description": "电影描述", "scrapeStatus": "success", "rating": 93, - "overview": "电影简介...", "casts": "Tim Robbins, Morgan Freeman", "directors": "Frank Darabont", "posterUrl": "http://...:39000/wangpan/posters/1/poster_xxx.jpg", @@ -2713,7 +2916,6 @@ ratingInteger评分(仅 status=success 时返回) categoryString分类(仅 status=success 时返回) tagsString标签(仅 status=success 时返回) - overviewString电影简介(仅 status=success 时返回) castsString主演(仅 status=success 时返回) directorsString导演(仅 status=success 时返回) @@ -2760,7 +2962,6 @@ "rating": 85, "category": "动作", "tags": "动作,科幻", - "overview": "电影简介...", "casts": "主演1,主演2", "directors": "导演1" } @@ -3097,7 +3298,6 @@ tmdbIdLongTMDB电影ID titleString电影标题(来自TMDB) originalTitleString原始标题(外文) - overviewString电影简介 releaseDateString上映日期 posterPathStringTMDB海报路径 backdropPathStringTMDB背景图路径 @@ -3126,7 +3326,6 @@ "tmdbId": 597, "title": "泰坦尼克号", "originalTitle": "Titanic", - "overview": "1912年4月10日,泰坦尼克号从 Southampton 出发...", "releaseDate": "1997-11-18", "posterPath": "/9xjZS2rlVxm8SFx8kPC3JIGCOY.jpg", "backdropPath": "/rTh4K5uw9HypmpGslcKd4QfHl93.jpg", @@ -3202,7 +3401,6 @@ list[].titleString电影标题 list[].originalTitleString原始标题 list[].releaseDateString上映日期 - list[].overviewString电影简介 list[].posterPathString海报图片URL list[].voteAverageDouble评分 @@ -3222,7 +3420,6 @@ "title": "泰坦尼克号", "originalTitle": "Titanic", "releaseDate": "1997-11-18", - "overview": "电影简介...", "posterPath": "https://image.tmdb.org/t/p/w500/xxx.jpg", "voteAverage": 7.9 } @@ -3260,7 +3457,6 @@ tmdbIdLongTMDB电影ID titleString电影标题(来自TMDB) originalTitleString原始标题(外文) - overviewString电影简介 releaseDateString上映日期 posterPathStringTMDB海报路径 backdropPathStringTMDB背景图路径 @@ -3289,7 +3485,6 @@ "tmdbId": 597, "title": "泰坦尼克号", "originalTitle": "Titanic", - "overview": "1912年4月10日,泰坦尼克号从 Southampton 出发...", "releaseDate": "1997-11-18", "posterPath": "/9xjZS2rlVxm8SFx8kPC3JIGCOY.jpg", "backdropPath": "/rTh4K5uw9HypmpGslcKd4QfHl93.jpg", @@ -3357,6 +3552,7 @@ "total": 2 } }
+
@@ -3430,40 +3626,184 @@
GET /api/tmdb-scraper/scraper-logs - 查询刮削日志需Token + 查询刮削日志(分页)需Token
+
+

功能说明

+

分页查询定时任务执行记录,返回每条记录的汇总信息和详细结果(包含每部电影的刮削结果和失败原因)。

+

查询参数

- - - + + + +
参数默认值说明
pageNo1页码
pageSize20每页条数
参数类型必填说明
pageNoInteger页码,默认 1
pageSizeInteger每页条数,默认 20,最大 50
+
+
+

响应字段说明

+ + + + + + + + + + + + + + + + + + + + + + + + +
字段类型说明
totalLong总记录数
pageNoInteger当前页码
pageSizeInteger每页条数
list[].idLong日志ID
list[].trigger_typeString触发方式:auto(定时自动)/ manual(手动触发)
list[].statusString执行状态:success / partial / failed / skipped
list[].total_processedInteger总处理电影数
list[].total_successInteger成功刮削数
list[].total_skippedInteger跳过数
list[].total_failedInteger失败数
list[].user_countInteger涉及用户数
list[].duration_msLong执行耗时(毫秒)
list[].start_timeString开始时间
list[].end_timeString结束时间
list[].error_messageString错误信息(如有)
list[].detailsArray详细执行结果(解析后的JSON)
list[].details[].usernameString用户名
list[].details[].userIdLong用户ID
list[].details[].processedInteger该用户处理的电影数
list[].details[].successInteger该用户成功刮削数
list[].details[].skippedInteger该用户跳过数
list[].details[].failedInteger该用户失败数
list[].details[].messagesArray失败详情列表(格式:电影名称: 失败原因)

响应示例

{ "code": 200, + "message": "查询成功", "data": { "total": 10, + "pageNo": 1, + "pageSize": 20, "list": [ { "id": 1, - "scanTime": "2026-06-18T10:00:00", - "totalMovies": 5, - "successCount": 4, - "failCount": 1 + "trigger_type": "auto", + "status": "partial", + "total_processed": 5, + "total_success": 4, + "total_skipped": 0, + "total_failed": 1, + "user_count": 1, + "duration_ms": 12500, + "start_time": "2026-06-27T10:00:00", + "end_time": "2026-06-27T10:00:12", + "error_message": null, + "details": [ + { + "username": "testuser", + "userId": 1, + "processed": 5, + "success": 4, + "skipped": 0, + "failed": 1, + "messages": [ + "泰坦尼克号: TMDB未找到匹配的电影" + ] + } + ] } - ], - "pageNo": 1, - "pageSize": 20 + ] } }
+
+
+ GET + /api/tmdb-scraper/scraper-logs/{id} + 查询单条刮削日志详情需Token +
+
+
+

功能说明

+

查询单条刮削日志的详细信息,包括每个用户的刮削结果、失败电影列表及原因。

+
+
+

路径参数

+ + + +
参数类型说明
idLong日志ID
+
+
+

响应字段说明

+ + + + + + + + + + + + + + + + + + + + + + +
字段类型说明
idLong日志ID
trigger_typeString触发方式:auto(定时自动)/ manual(手动触发)
statusString执行状态:success / partial / failed / skipped
total_processedInteger总处理电影数
total_successInteger成功刮削数
total_skippedInteger跳过数
total_failedInteger失败数
user_countInteger涉及用户数
duration_msLong执行耗时(毫秒)
start_timeString开始时间
end_timeString结束时间
error_messageString错误信息(如有)
detailsArray详细执行结果(解析后的JSON)
details[].usernameString用户名
details[].userIdLong用户ID
details[].processedInteger该用户处理的电影数
details[].successInteger该用户成功刮削数
details[].skippedInteger该用户跳过数
details[].failedInteger该用户失败数
details[].messagesArray失败详情列表(格式:电影名称: 失败原因)
+
+
+

响应示例

+
{ + "code": 200, + "message": "查询成功", + "data": { + "id": 1, + "trigger_type": "auto", + "status": "partial", + "total_processed": 5, + "total_success": 4, + "total_skipped": 0, + "total_failed": 1, + "user_count": 1, + "duration_ms": 12500, + "start_time": "2026-06-27T10:00:00", + "end_time": "2026-06-27T10:00:12", + "error_message": null, + "details": [ + { + "username": "testuser", + "userId": 1, + "processed": 5, + "success": 4, + "skipped": 0, + "failed": 1, + "messages": [ + "泰坦尼克号: TMDB未找到匹配的电影", + "阿凡达: TMDB API请求失败,请检查API Key是否有效" + ] + } + ] + } +}
+
+
+

失败响应示例 — 日志不存在

+
{ + "code": 404, + "message": "日志不存在", + "data": null +}
+
+
+
+
DELETE @@ -3497,15 +3837,16 @@
-

OpenList 多网盘聚合服务

- wangpan-drive-service : 8082 · 依赖 OpenList 服务 (http://ldfun.asia:13000) +

OpenList 用户级连接管理

+ wangpan-drive-service : 8082 · OpenListController / DriveController

功能说明

- OpenList 集成模块将 OpenList 作为多网盘聚合后端,支持 80+ 种网盘服务(阿里云盘、百度网盘、UC网盘、OneDrive 等)。 - Java 服务负责业务逻辑(用户、权限、TMDB 刮削),OpenList 负责文件操作(上传、下载、预览)。 + 每个用户可以绑定自己的 OpenList 实例(地址、账号、密码),互不干扰。 + 敏感信息使用 Jasypt 加密存储,Token 缓存到数据库避免重复登录。 + OpenList 负责文件操作(同步、播放),TMDB 刮削使用当前用户的 API Key。

@@ -3515,29 +3856,45 @@
Drive:8082
-
OpenList:13000
+
用户A的OpenList
-
各网盘
+
UC网盘
+
+
+ + + + + + +
用户B的OpenList
+ +
阿里云盘
-

前置配置

+

使用流程

- 1. 在 OpenList 管理后台 (http://ldfun.asia:13000/@manage) 添加网盘存储
- 2. 记录存储的 挂载路径(如 /uc)和 存储 ID(通过 API 获取)
- 3. 在 application.yml 中配置 OpenList 服务地址和账号密码
- 4. 创建网盘时传入 openlistStorageIdopenlistMountPath 关联 OpenList 存储 + 1. 前端调用 POST /api/openlist/test 测试 OpenList 连接(可选)
+ 2. 前端调用 POST /api/openlist/bind 绑定 OpenList(传入地址、账号、密码)
+ 3. 绑定成功后后端自动异步同步:递归遍历 OpenList 目录树,同步文件夹层级和视频文件到数据库
+ 4. 也可手动调用 POST /api/openlist/sync 触发全量同步
+ 5. 同步后电影可进行 TMDB 刮削,播放视频自动使用用户绑定的 OpenList 连接
+ 6. 后端每 24 小时自动检测 OpenList 心跳,记录连接状态
+ 7. TMDB 刮削使用当前用户的 API Key(与 OpenList 连接隔离)

- + + +
POST - /api/drive - 创建网盘(关联 OpenList 存储) + /api/openlist/test + 测试 OpenList 连接(不保存) 需Token
@@ -3549,53 +3906,280 @@ Content-Type: application/json

请求体

{ - "name": "UC网盘电影库", - "description": "UC网盘存储", - "openlistStorageId": "1", - "openlistMountPath": "/uc" + "openlistUrl": "http://www.example.com:3000", + "openlistUsername": "用户名", + "openlistPassword": "密码" }

字段说明

- - - - + + +
字段类型必填说明
nameString网盘名称
descriptionString网盘描述
openlistStorageIdStringOpenList 存储 ID(不传则仅记录元数据)
openlistMountPathStringOpenList 挂载路径(如 "/uc")
openlistUrlStringOpenList 服务地址(如 http://example.com:13000)
openlistUsernameStringOpenList 账号
openlistPasswordStringOpenList 密码

成功响应示例

{ "code": 200, - "message": "创建成功", + "message": "连接成功", + "data": null +}
+
+
+

失败响应示例

+
{ + "code": 500, + "message": "连接失败:Invalid username or password", + "data": null +}
+
+
+
+ + +
+
+ POST + /api/openlist/bind + 绑定 OpenList 连接 + 需Token +
+
+
+

请求头

+
Authorization: Bearer <token> +Content-Type: application/json
+
+
+

请求体

+
{ + "openlistUrl": "http://example.com:13000", + "openlistUsername": "your_username", + "openlistPassword": "your_password" +}
+
+
+

字段说明

+ + + + + +
字段类型必填说明
openlistUrlStringOpenList 服务地址
openlistUsernameStringOpenList 账号
openlistPasswordStringOpenList 密码
+
+
+

功能说明

+

+ 验证连接 → 保存配置(敏感信息 Jasypt 加密) → 缓存 Token(24小时有效期)。
+ 每个用户只能绑定一个 OpenList 实例,重复绑定会更新配置。
+ 绑定成功后自动触发异步同步:递归遍历 OpenList 目录树,同步文件夹层级和视频文件到数据库。
+ 同步只处理视频格式文件(mp4, mkv, avi 等),非视频文件自动跳过。 +

+
+
+

成功响应示例

+
{ + "code": 200, + "message": "绑定成功,正在后台同步文件...", + "data": null +}
+
+
+

失败响应示例

+
{ + "code": 500, + "message": "绑定失败:连接失败:Invalid username or password", + "data": null +}
+
+
+
+ + +
+
+ DELETE + /api/openlist/unbind + 解绑 OpenList 连接 + 需Token +
+
+
+

请求头

+
Authorization: Bearer <token>
+
+
+

功能说明

+

+ 删除当前用户的 OpenList 连接配置(包括加密的 URL、账号、密码和 Token 缓存)。
+ 解绑后同步文件和播放链接接口将不可用。 +

+
+
+

成功响应示例

+
{ + "code": 200, + "message": "解绑成功", + "data": null +}
+
+
+

失败响应示例

+
{ + "code": 500, + "message": "解绑失败:未找到绑定记录", + "data": null +}
+
+
+
+ + +
+
+ GET + /api/openlist/info + 查询当前用户 OpenList 连接信息 + 需Token +
+
+
+

请求头

+
Authorization: Bearer <token>
+
+
+

功能说明

+

+ 返回脱敏后的连接信息(URL、用户名均脱敏显示,不返回明文密码)。
+ 会实际调用 OpenList 登录接口验证当前连接是否可用,返回 connectedconnectMessage 字段。
+ 未绑定时返回 null。 +

+
+
+

成功响应示例(已绑定)

+
{ + "code": 200, + "message": "操作成功", "data": { "id": 1, - "name": "UC网盘电影库", - "description": "UC网盘存储", - "openlistStorageId": "1", - "openlistMountPath": "/uc", - "folderCount": 0, - "fileCount": 0, - "totalSize": 0, + "openlistUrl": "http://ld***:13000", + "openlistUsername": "dj***22", + "storageId": "1", + "mountPath": "/uc", "status": "active", + "tokenValid": true, + "connected": true, + "connectMessage": "连接成功", + "lastHeartbeatTime": "2026-06-27T15:00:00", + "heartbeatStatus": "success", + "heartbeatError": null, "createTime": "2026-06-26T15:00:00", "updateTime": "2026-06-26T15:00:00" } }
-

失败响应示例

+

响应字段说明

+ + + + + + + + + + + + + +
字段说明
openlistUrlOpenList 服务地址(脱敏显示,如 http://ld***:13000)
openlistUsernameOpenList 账号(脱敏显示,如 dj***22)
storageIdOpenList 存储 ID(用户在 OpenList 后台配置)
mountPath挂载路径(如 /uc)
status状态:active / disabled
tokenValid本地缓存的 Token 是否有效(true 表示未过期)
connected实时连接测试结果:true 表示当前账号密码可成功连接 OpenList
connectMessage实时连接测试结果描述,如 "连接成功" 或 "连接失败:Invalid username or password"
lastHeartbeatTime最后一次定时心跳检测时间(每 24 小时自动检测一次)
heartbeatStatus最近一次心跳检测结果:success / failed
heartbeatError心跳检测失败原因(成功时为 null)
+
+
+

成功响应示例(未绑定)

{ - "code": 400, - "message": "创建网盘失败:网盘名称不能为空", + "code": 200, + "message": "操作成功", "data": null }
+ + + +
+
+ POST + /api/openlist/sync + 手动触发 OpenList 文件同步 + 需Token +
+
+
+

请求头

+
Authorization: Bearer <token>
+
+
+

功能说明

+

+ 从 OpenList 根目录开始递归遍历目录树,同步文件夹层级和视频文件到本地数据库。
+ 同步策略
+   1. 自动为用户创建/复用关联 OpenList 的网盘(Drive)
+   2. 递归遍历 OpenList 目录树,在本地创建对应的文件夹(DriveFolder)
+   3. 只同步视频格式文件(mp4, mkv, avi, mov, wmv, flv, webm 等),非视频文件自动跳过
+   4. 视频文件创建为电影记录(DriveMovie),storageType=openlist
+   5. 不含视频文件的空文件夹也会被跳过
+ 前置条件:用户已通过 /api/openlist/bind 绑定 OpenList 连接。
+ 注意:绑定 OpenList 时已自动触发异步同步,此接口用于手动重新同步。 +

+
+
+

成功响应示例

+
{ + "code": 200, + "message": "同步完成", + "data": { + "driveCount": 1, + "folderCount": 10, + "movieCount": 50, + "skippedFileCount": 5, + "skippedFolderCount": 2, + "errorCount": 0, + "errors": [] + } +}
+
+
+

响应字段说明

+ + + + + + + + + +
字段类型说明
driveCountint关联网盘数量(通常为 1)
folderCountint本次同步创建的文件夹数量
movieCountint本次同步创建的视频文件(电影)数量
skippedFileCountint跳过的非视频文件数量
skippedFolderCountint跳过的不含视频的文件夹数量
errorCountint同步过程中的错误数量
errorsArray错误详情列表(字符串数组)
+
+
+

失败响应示例

+
{ + "code": 500, + "message": "同步失败:用户未绑定 OpenList,请先绑定", + "data": null +}
+
+
+
+ + +
@@ -3620,8 +4204,9 @@ Content-Type: application/json

功能说明

- 调用 OpenList API 获取指定路径下的文件列表,自动创建数据库中缺失的电影记录。 - 已存在的文件不会重复创建。同步后文件可进行 TMDB 刮削。 + 使用当前用户绑定的 OpenList 连接,获取指定路径下的文件列表,自动创建数据库中缺失的电影记录。
+ 已存在的文件不会重复创建。同步后文件可进行 TMDB 刮削。
+ 前置条件:用户已通过 /api/openlist/bind 绑定 OpenList 连接。

@@ -3646,8 +4231,8 @@ Content-Type: application/json

失败响应示例

{ - "code": 400, - "message": "同步失败:网盘未关联 OpenList 存储", + "code": 500, + "message": "同步失败:用户未绑定 OpenList,请先在设置中绑定 OpenList 连接", "data": null }
@@ -3678,8 +4263,9 @@ Content-Type: application/json

功能说明

- 调用 OpenList API 获取文件的真实下载地址,返回的 URL 可直接用于视频播放器播放。 - 仅支持 storageTypeopenlist 的电影记录。 + 使用当前用户绑定的 OpenList 连接,获取文件的真实下载地址,返回的 URL 可直接用于视频播放器播放。
+ 仅支持 storageTypeopenlist 的电影记录。
+ Token 过期时自动刷新,对用户透明。

@@ -3706,7 +4292,7 @@ Content-Type: application/json

失败响应示例

{ - "code": 400, + "code": 500, "message": "获取失败:该电影不是 OpenList 管理的文件", "data": null }
@@ -3714,48 +4300,6 @@ Content-Type: application/json
- -
-
- GET - /api/drive - 查看网盘列表(含 OpenList 信息) - 需Token -
-
-
-

请求头

-
Authorization: Bearer <token>
-
-
-

成功响应示例

-
{ - "code": 200, - "data": [ - { - "id": 1, - "name": "UC网盘电影库", - "openlistStorageId": "1", - "openlistMountPath": "/uc", - "folderCount": 2, - "fileCount": 10, - "totalSize": 10737418240, - "status": "active" - } - ] -}
-
-
-

新增字段说明

- - - - -
字段说明
openlistStorageId关联的 OpenList 存储 ID(null 表示未关联)
openlistMountPathOpenList 挂载路径(如 "/uc")
-
-
-
-
@@ -3799,12 +4343,12 @@ Content-Type: application/json
}
-

新增字段说明

+

OpenList 相关字段说明

- + - +
字段说明
openlistFilePathOpenList 文件路径(如 "/uc/阿凡达.mp4")
openlistFilePathOpenList 文件路径(如 "/uc/阿凡达.mp4"),由系统根据用户绑定的挂载路径自动生成
storageType存储类型:metadata(仅元数据)/ openlist(OpenList 管理)
fileSize文件大小(字节)
fileSize文件大小(字节),同步时从 OpenList 获取
@@ -3814,38 +4358,88 @@ Content-Type: application/json

前端调用示例

-

创建网盘

-
const response = await fetch('/api/drive', { +

1. 绑定 OpenList

+
// 先测试连接 +const testResp = await fetch('/api/openlist/test', { method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }, + headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ - name: 'UC网盘电影库', - openlistStorageId: '1', - openlistMountPath: '/uc' + openlistUrl: 'http://example.com:13000', + openlistUsername: 'your_username', + openlistPassword: 'your_password' + }) +}); + +// 测试通过后绑定 +const bindResp = await fetch('/api/openlist/bind', { + method: 'POST', + headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ + openlistUrl: 'http://example.com:13000', + openlistUsername: 'your_username', + openlistPassword: 'your_password' }) });
-

同步文件

-
const response = await fetch(`/api/drive/${driveId}/folders/${folderId}/sync-files`, { +

2. 查询连接信息

+
const resp = await fetch('/api/openlist/info', { + headers: { 'Authorization': `Bearer ${token}` } +}); +const data = await resp.json(); +if (data.data) { + console.log('已绑定:', data.data.openlistUrl); + console.log('Token有效:', data.data.tokenValid); +} else { + console.log('未绑定 OpenList'); +}
+
+
+

3. 手动同步文件

+
// 绑定后已自动触发异步同步,也可手动重新同步 +const resp = await fetch('/api/openlist/sync', { method: 'POST', headers: { 'Authorization': `Bearer ${token}` } }); -const data = await response.json(); -console.log(`同步了 ${data.data.syncCount} 个文件`);
+const data = await resp.json(); +console.log(`同步了 ${data.data.folderCount} 个文件夹, ${data.data.movieCount} 个视频`); +if (data.data.errorCount > 0) { + console.warn('同步错误:', data.data.errors); +}
-

播放视频

-
const response = await fetch(`/api/drive/${driveId}/movies/${movieId}/play-url`, { +

4. 查询连接状态

+
const resp = await fetch('/api/openlist/info', { headers: { 'Authorization': `Bearer ${token}` } }); -const data = await response.json(); +const data = await resp.json(); +if (data.data) { + console.log('地址:', data.data.openlistUrl); + console.log('实时连接:', data.data.connected ? '正常' : '异常'); + console.log('连接信息:', data.data.connectMessage); + console.log('心跳状态:', data.data.heartbeatStatus); +} else { + console.log('未绑定 OpenList'); +}
+
+
+

5. 播放视频

+
const resp = await fetch(`/api/drive/${driveId}/movies/${movieId}/play-url`, { + headers: { 'Authorization': `Bearer ${token}` } +}); +const data = await resp.json(); videoPlayer.src = data.data.playUrl; // 直接设置视频源 videoPlayer.play();
+
+

6. 解绑 OpenList

+
const resp = await fetch('/api/openlist/unbind', { + method: 'DELETE', + headers: { 'Authorization': `Bearer ${token}` } +}); +const data = await resp.json(); +console.log(data.message); // "解绑成功"
+
@@ -3898,10 +4492,24 @@ videoPlayer.play();
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active')); document.querySelectorAll('.section').forEach(s => s.classList.remove('active')); this.classList.add('active'); - document.getElementById(this.dataset.section).classList.add('active'); + const sectionId = this.dataset.section; + const section = document.getElementById(sectionId); + if (section) { + section.classList.add('active'); + } else { + console.error('Section not found:', sectionId); + } }); }); + // 页面加载时默认激活第一个导航项 + document.addEventListener('DOMContentLoaded', function() { + const firstNavItem = document.querySelector('.nav-item'); + if (firstNavItem && !document.querySelector('.section.active')) { + firstNavItem.click(); + } + }); + // API 详情展开/收起 function toggleDetail(header) { const detail = header.nextElementSibling; diff --git a/wangpan-gateway/src/main/resources/application.yml b/wangpan-gateway/src/main/resources/application.yml index 260826b..44c9a5a 100644 --- a/wangpan-gateway/src/main/resources/application.yml +++ b/wangpan-gateway/src/main/resources/application.yml @@ -21,7 +21,7 @@ spring: - id: wangpan-drive-service uri: http://localhost:8082 predicates: - - Path=/api/drive/**,/api/icon/** + - Path=/api/drive/**,/api/icon/**,/api/openlist/** filters: - StripPrefix=0 # TMDB服务路由 diff --git a/wangpan-gateway/target/classes/META-INF/resources/api-doc.html b/wangpan-gateway/target/classes/META-INF/resources/api-doc.html index 0bb6822..25a902d 100644 --- a/wangpan-gateway/target/classes/META-INF/resources/api-doc.html +++ b/wangpan-gateway/target/classes/META-INF/resources/api-doc.html @@ -399,7 +399,7 @@ "message": "登录成功", "data": { "id": 7, - "username": "lrq", + "username": "zhangsan", "userType": 0, // 0=普通用户, 1=VIP, 2=管理员 "status": 1, "email": "xxx@xx.com", @@ -514,7 +514,7 @@ "data": { "valid": true, "userId": 7, - "username": "lrq", + "username": "zhangsan", "expireTime": "2026-06-17T11:20:21" } } @@ -569,7 +569,7 @@ { "id": 1, "userId": 7, - "username": "lrq", + "username": "zhangsan", "loginIp": "127.0.0.1", "loginTime": "2026-06-10T11:20:21", "result": "success", @@ -644,7 +644,7 @@ "data": { "total": 5, "list": [ - { "id": 7, "username": "lrq", "userType": 0, "status": 1, ... } + { "id": 7, "username": "zhangsan", "userType": 0, "status": 1, ... } ], "pageNo": 1, "pageSize": 20 @@ -1012,7 +1012,7 @@ "code": 200, "message": "头像更新成功", "data": { - "avatarUrl": "http://118.178.238.159:39000/wangpan/avatar/1_1719234567890.png?..." + "avatarUrl": "http://your-server:39000/wangpan/avatar/1_1719234567890.png?..." } } @@ -1076,7 +1076,7 @@ "id": 1, "name": "我的网盘", "description": "个人网盘", - "logoUrl": "http://118.178.238.159:39000/wangpan/drive/1/logo.png", + "logoUrl": "http://your-server:39000/wangpan/drive/1/logo.png", "createTime": "2026-06-26T10:00:00" } } @@ -1143,7 +1143,7 @@ "id": 1, "name": "我的网盘", "description": "个人网盘", - "logoUrl": "http://118.178.238.159:39000/wangpan/drive/1/logo.png", + "logoUrl": "http://your-server:39000/wangpan/drive/1/logo.png", "folderCount": 5, "fileCount": 20, "totalSize": 10737418240, @@ -1207,7 +1207,7 @@ "id": 1, "name": "我的网盘", "description": "个人网盘", - "logoUrl": "http://118.178.238.159:39000/wangpan/drive/1/logo.png", + "logoUrl": "http://your-server:39000/wangpan/drive/1/logo.png", "folderCount": 5, "fileCount": 20, "totalSize": 10737418240, @@ -1269,7 +1269,7 @@ "id": 1, "name": "新网盘名称", "description": "新描述", - "logoUrl": "http://118.178.238.159:39000/wangpan/drive/1/logo.png", + "logoUrl": "http://your-server:39000/wangpan/drive/1/logo.png", "updateTime": "2026-06-26T11:00:00" } } @@ -1780,28 +1780,26 @@ driveIdLong所属网盘ID folderIdLong所属文件夹ID titleString电影名称 - originalNameString原始文件名 mimeTypeStringMIME类型 fileSizeLong文件大小(字节) extensionString文件扩展名 descriptionString描述 maintainerString维护者 statusString状态 - categoryString分类 scrapeTypeString刮削类型 - ratingInteger评分 + scrapeStatusString刮削状态 + ratingInteger|null评分(刮削成功时返回) tagsString标签 - overviewString电影简介(TMDB刮削) - castsString主演(逗号分隔,TMDB刮削) - directorsString导演(逗号分隔,TMDB刮削) - minioUrlStringMinIO文件访问URL - posterUrlString海报URL + castsString|null主演(刮削成功时返回) + directorsString|null导演(刮削成功时返回) + posterUrlString|null海报URL(刮削成功时返回) + backdropUrlString|null背景图URL(刮削成功时返回) createTimeString创建时间 updateTimeString更新时间
-

成功响应示例

+

成功响应示例(刮削成功)

{ "code": 200, "message": "查询成功", @@ -1809,19 +1807,50 @@ "id": 1, "driveId": 1, "folderId": 0, - "title": "电影.mp4", - "originalName": "电影.mp4", + "title": "阿凡达.mp4", "mimeType": "video/mp4", "fileSize": 1073741824, "extension": "mp4", "description": "电影描述", + "maintainer": "admin", + "status": "active", + "scrapeType": "tmdb", + "scrapeStatus": "success", "rating": 85, "tags": "动作,科幻", - "overview": "电影简介...", "casts": "主演1,主演2", "directors": "导演1", "posterUrl": "http://...", - "minioUrl": "http://...", + "backdropUrl": "http://...", + "createTime": "2026-06-26T10:00:00", + "updateTime": "2026-06-26T10:00:00" + } +}
+
+
+

成功响应示例(未刮削/刮削失败)

+
{ + "code": 200, + "message": "查询成功", + "data": { + "id": 2, + "driveId": 1, + "folderId": 0, + "title": "未刮削电影.mp4", + "mimeType": "video/mp4", + "fileSize": 536870912, + "extension": "mp4", + "description": null, + "maintainer": null, + "status": "active", + "scrapeType": "none", + "scrapeStatus": "pending", + "rating": null, + "tags": null, + "casts": null, + "directors": null, + "posterUrl": null, + "backdropUrl": null, "createTime": "2026-06-26T10:00:00", "updateTime": "2026-06-26T10:00:00" } @@ -1865,11 +1894,15 @@

请求体(部分更新,所有字段可选)

{ "title": "新电影名称", - "originalName": "新文件名.mp4", "description": "新描述", "rating": 90, "tags": "新标签", - "category": "新分类" + "posterUrl": "海报URL", + "backdropUrl": "背景图URL", + "casts": "主演列表", + "directors": "导演列表", + "resourceLibrary": "资源库来源", + "maintainer": "维护者" }
@@ -1877,11 +1910,15 @@ - - + + + + + +
参数类型说明
titleString电影名称
originalNameString原始文件名(用于 TMDB 刮削有误时修改)
descriptionString描述
ratingInteger评分 0-100
tagsString标签
categoryString分类
posterUrlString海报URL
backdropUrlString背景图URL
castsString主演(逗号分隔)
directorsString导演(逗号分隔)
resourceLibraryString资源库来源
maintainerString维护者
@@ -2080,7 +2117,7 @@

请求示例(仅外网)

{ "name": "百度", - "url": "https://www.baidu.com", + "url": "https://www.example.com", "linkType": "external", "description": "搜索引擎", "sortOrder": 1 @@ -2091,7 +2128,7 @@
{ "name": "内部系统", "url": "https://system.example.com", - "internalUrl": "http://192.168.1.100:8080", + "internalUrl": "http://192.168.x.x:8080", "linkType": "both", "description": "内部管理系统", "sortOrder": 2 @@ -2105,13 +2142,13 @@ "data": { "id": 1, "name": "百度", - "url": "https://www.baidu.com", + "url": "https://www.example.com", "internalUrl": null, "linkType": "external", "description": "搜索引擎", "sortOrder": 1, "status": "active", - "logoUrl": "http://118.178.238.159:39000/wangpan/icons/2/1718265600000.png", + "logoUrl": "http://your-server:39000/wangpan/icons/2/1718265600000.png", "createTime": "2026-06-13T16:00:00" } }
@@ -2141,13 +2178,13 @@ { "id": 1, "name": "百度", - "url": "https://www.baidu.com", + "url": "https://www.example.com", "internalUrl": null, "linkType": "external", "description": "搜索引擎", "sortOrder": 1, "status": "active", - "logoUrl": "http://118.178.238.159:39000/wangpan/icons/2/1718265600000.png", + "logoUrl": "http://your-server:39000/wangpan/icons/2/1718265600000.png", "createTime": "2026-06-13T16:00:00", "updateTime": "2026-06-13T16:00:00" } @@ -2182,13 +2219,13 @@ "data": { "id": 1, "name": "百度", - "url": "https://www.baidu.com", + "url": "https://www.example.com", "internalUrl": null, "linkType": "external", "description": "搜索引擎", "sortOrder": 1, "status": "active", - "logoUrl": "http://118.178.238.159:39000/wangpan/icons/2/1718265600000.png", + "logoUrl": "http://your-server:39000/wangpan/icons/2/1718265600000.png", "createTime": "2026-06-13T16:00:00", "updateTime": "2026-06-13T16:00:00" } @@ -2245,7 +2282,7 @@
{ "name": "新名称", "url": "https://new-url.com", - "internalUrl": "http://192.168.1.100:8080", + "internalUrl": "http://192.168.x.x:8080", "linkType": "both", "sortOrder": 2 }
@@ -2259,12 +2296,12 @@ "id": 1, "name": "新名称", "url": "https://new-url.com", - "internalUrl": "http://192.168.1.100:8080", + "internalUrl": "http://192.168.x.x:8080", "linkType": "both", "description": "搜索引擎", "sortOrder": 2, "status": "active", - "logoUrl": "http://118.178.238.159:39000/wangpan/icons/2/1718265600000.png", + "logoUrl": "http://your-server:39000/wangpan/icons/2/1718265600000.png", "createTime": "2026-06-13T16:00:00", "updateTime": "2026-06-13T17:00:00" } @@ -2356,7 +2393,7 @@ "data": { "id": 1, "name": "百度", - "logoUrl": "http://118.178.238.159:39000/wangpan/icons/2/1718265600000.png", + "logoUrl": "http://your-server:39000/wangpan/icons/2/1718265600000.png", "updateTime": "2026-06-15T10:00:00" } }
@@ -2532,7 +2569,6 @@ folderGroups[].movies[].scrapeStatusString刮削状态 folderGroups[].movies[].ratingInteger评分(0-100) folderGroups[].movies[].tagsString标签 - folderGroups[].movies[].overviewString电影简介(TMDB刮削) folderGroups[].movies[].castsString主演(逗号分隔,TMDB刮削) folderGroups[].movies[].directorsString导演(逗号分隔,TMDB刮削) folderGroups[].movies[].posterUrlString竖版海报URL(2:3 比例) @@ -2591,6 +2627,175 @@
+
+
+ GET + /api/movies/all + 查询所有电影(不分文件夹,直接分页)需Token +
+
+
+

功能说明

+

查询当前用户所有网盘下所有未删除的电影,不进行分类直接分页返回。适用于大数据量场景(1万+),内置5分钟缓存机制,支持关键词搜索和多字段排序。

+

设计特点:

+
    +
  • 不进行分类,直接分页查询所有电影
  • +
  • 支持关键词搜索(标题模糊匹配)
  • +
  • 支持排序(按创建时间、更新时间、评分、标题)
  • +
  • 内置5分钟缓存,适用于1万+数据量场景
  • +
  • 最大每页100条,防止单次查询数据量过大
  • +
  • 每日随机刷新:第1页前20条数据每日随机刷新(无关键词搜索时生效),增加内容变化性,其余数据按排序规则保持不变
  • +
+

每日随机刷新机制:

+
    +
  • 触发条件:查询第1页且无关键词搜索时
  • +
  • 随机数量:前20条数据从所有电影中随机选取
  • +
  • 缓存策略:缓存Key包含日期因子,每日自动刷新
  • +
  • 其余数据:第21条及以后按指定排序规则返回
  • +
  • 关键词搜索时:不触发随机,正常返回搜索结果
  • +
+
+
+

查询参数

+ + + + + + + +
参数类型必填说明
keywordString搜索关键词(电影标题模糊匹配)
sortByString排序字段:createTime(创建时间)/ updateTime(更新时间)/ rating(评分)/ title(标题)
sortOrderString排序方向:asc(升序)/ desc(降序),默认desc
pageNoint页码,默认 1
pageSizeint每页条数,默认 50,最大 100
+
+
+

响应字段说明

+ + + + + + + + + + + + + + + + + + + + + + + + + +
字段类型说明
totalLong电影总数
pagesLong总页数
pageNoLong当前页码
pageSizeLong每页条数
list[].idLong电影ID
list[].driveIdLong所属网盘ID
list[].folderIdLong所属文件夹ID
list[].titleString电影名称
list[].originalNameString原始文件名
list[].mimeTypeStringMIME类型
list[].fileSizeLong文件大小(字节)
list[].extensionString文件扩展名
list[].descriptionString描述
list[].scrapeStatusString刮削状态:pending/scraping/success/failed
list[].ratingInteger评分(0-100)
list[].tagsString标签
list[].castsString主演(逗号分隔,TMDB刮削)
list[].directorsString导演(逗号分隔,TMDB刮削)
list[].posterUrlString竖版海报URL(2:3 比例)
list[].backdropUrlString横版背景图URL(16:9 比例)
list[].minioUrlStringMinIO 文件访问URL
list[].createTimeString创建时间
list[].updateTimeString更新时间
+
+
+

成功响应示例

+
{ + "code": 200, + "message": "查询成功", + "data": { + "total": 12580, + "pages": 252, + "pageNo": 1, + "pageSize": 50, + "list": [ + { + "id": 1, + "driveId": 1, + "folderId": 3, + "title": "肖申克的救赎", + "originalName": "The.Shawshank.Redemption.1994.mkv", + "fileSize": 8589934592, + "description": "一场冤案让银行家安迪入狱,他在肖申克监狱用20年时间挖出一条自由之路...", + "scrapeStatus": "success", + "rating": 93, + "casts": "蒂姆·罗宾斯,摩根·弗里曼", + "directors": "弗兰克·德拉邦特", + "posterUrl": "http://...:39000/wangpan/posters/1/poster_xxx.jpg", + "backdropUrl": "http://...:39000/wangpan/posters/1/backdrop_xxx.jpg", + "createTime": "2026-06-20 10:30:00", + "updateTime": "2026-06-27 14:20:00" + } + ] + } +}
+
+
+

成功响应示例 — 命中缓存

+
{ + "code": 200, + "message": "查询成功(缓存)", + "data": { + "total": 12580, + "pages": 252, + "pageNo": 1, + "pageSize": 50, + "list": [...] + } +}
+
+
+

失败响应示例 — 未登录或 Token 无效

+
{ + "code": 401, + "message": "未登录或 Token 已过期", + "data": null +}
+
+
+

失败响应示例 — 查询失败

+
{ + "code": 500, + "message": "查询失败:数据库连接异常", + "data": null +}
+
+
+

前端调用示例

+
// 查询第1页,每页50条 +fetch('/api/movies/all?pageNo=1&pageSize=50', { + headers: { + 'Authorization': 'Bearer ' + token + } +}) +.then(res => res.json()) +.then(data => { + console.log('总数:', data.data.total); + console.log('电影列表:', data.data.list); +}); + +// 搜索电影 +fetch('/api/movies/all?keyword=肖申克&pageNo=1&pageSize=50', { + headers: { + 'Authorization': 'Bearer ' + token + } +}) +.then(res => res.json()) +.then(data => { + console.log('搜索结果:', data.data.list); +}); + +// 按评分降序排序 +fetch('/api/movies/all?sortBy=rating&sortOrder=desc&pageNo=1&pageSize=50', { + headers: { + 'Authorization': 'Bearer ' + token + } +}) +.then(res => res.json()) +.then(data => { + console.log('高分电影:', data.data.list); +});
+
+
+
+
GET @@ -2624,7 +2829,6 @@ descriptionString电影描述 scrapeStatusString刮削状态:pending/scraping/success/failed ratingInteger评分(0-100) - overviewString电影简介(TMDB刮削) castsString主演(逗号分隔,TMDB刮削) directorsString导演(逗号分隔,TMDB刮削) posterUrlString竖版海报URL(2:3 比例) @@ -2651,7 +2855,6 @@ "description": "电影描述", "scrapeStatus": "success", "rating": 93, - "overview": "电影简介...", "casts": "Tim Robbins, Morgan Freeman", "directors": "Frank Darabont", "posterUrl": "http://...:39000/wangpan/posters/1/poster_xxx.jpg", @@ -2713,7 +2916,6 @@ ratingInteger评分(仅 status=success 时返回) categoryString分类(仅 status=success 时返回) tagsString标签(仅 status=success 时返回) - overviewString电影简介(仅 status=success 时返回) castsString主演(仅 status=success 时返回) directorsString导演(仅 status=success 时返回) @@ -2760,7 +2962,6 @@ "rating": 85, "category": "动作", "tags": "动作,科幻", - "overview": "电影简介...", "casts": "主演1,主演2", "directors": "导演1" } @@ -3097,7 +3298,6 @@ tmdbIdLongTMDB电影ID titleString电影标题(来自TMDB) originalTitleString原始标题(外文) - overviewString电影简介 releaseDateString上映日期 posterPathStringTMDB海报路径 backdropPathStringTMDB背景图路径 @@ -3126,7 +3326,6 @@ "tmdbId": 597, "title": "泰坦尼克号", "originalTitle": "Titanic", - "overview": "1912年4月10日,泰坦尼克号从 Southampton 出发...", "releaseDate": "1997-11-18", "posterPath": "/9xjZS2rlVxm8SFx8kPC3JIGCOY.jpg", "backdropPath": "/rTh4K5uw9HypmpGslcKd4QfHl93.jpg", @@ -3202,7 +3401,6 @@ list[].titleString电影标题 list[].originalTitleString原始标题 list[].releaseDateString上映日期 - list[].overviewString电影简介 list[].posterPathString海报图片URL list[].voteAverageDouble评分 @@ -3222,7 +3420,6 @@ "title": "泰坦尼克号", "originalTitle": "Titanic", "releaseDate": "1997-11-18", - "overview": "电影简介...", "posterPath": "https://image.tmdb.org/t/p/w500/xxx.jpg", "voteAverage": 7.9 } @@ -3260,7 +3457,6 @@ tmdbIdLongTMDB电影ID titleString电影标题(来自TMDB) originalTitleString原始标题(外文) - overviewString电影简介 releaseDateString上映日期 posterPathStringTMDB海报路径 backdropPathStringTMDB背景图路径 @@ -3289,7 +3485,6 @@ "tmdbId": 597, "title": "泰坦尼克号", "originalTitle": "Titanic", - "overview": "1912年4月10日,泰坦尼克号从 Southampton 出发...", "releaseDate": "1997-11-18", "posterPath": "/9xjZS2rlVxm8SFx8kPC3JIGCOY.jpg", "backdropPath": "/rTh4K5uw9HypmpGslcKd4QfHl93.jpg", @@ -3357,6 +3552,7 @@ "total": 2 } }
+
@@ -3430,40 +3626,184 @@
GET /api/tmdb-scraper/scraper-logs - 查询刮削日志需Token + 查询刮削日志(分页)需Token
+
+

功能说明

+

分页查询定时任务执行记录,返回每条记录的汇总信息和详细结果(包含每部电影的刮削结果和失败原因)。

+

查询参数

- - - + + + +
参数默认值说明
pageNo1页码
pageSize20每页条数
参数类型必填说明
pageNoInteger页码,默认 1
pageSizeInteger每页条数,默认 20,最大 50
+
+
+

响应字段说明

+ + + + + + + + + + + + + + + + + + + + + + + + +
字段类型说明
totalLong总记录数
pageNoInteger当前页码
pageSizeInteger每页条数
list[].idLong日志ID
list[].trigger_typeString触发方式:auto(定时自动)/ manual(手动触发)
list[].statusString执行状态:success / partial / failed / skipped
list[].total_processedInteger总处理电影数
list[].total_successInteger成功刮削数
list[].total_skippedInteger跳过数
list[].total_failedInteger失败数
list[].user_countInteger涉及用户数
list[].duration_msLong执行耗时(毫秒)
list[].start_timeString开始时间
list[].end_timeString结束时间
list[].error_messageString错误信息(如有)
list[].detailsArray详细执行结果(解析后的JSON)
list[].details[].usernameString用户名
list[].details[].userIdLong用户ID
list[].details[].processedInteger该用户处理的电影数
list[].details[].successInteger该用户成功刮削数
list[].details[].skippedInteger该用户跳过数
list[].details[].failedInteger该用户失败数
list[].details[].messagesArray失败详情列表(格式:电影名称: 失败原因)

响应示例

{ "code": 200, + "message": "查询成功", "data": { "total": 10, + "pageNo": 1, + "pageSize": 20, "list": [ { "id": 1, - "scanTime": "2026-06-18T10:00:00", - "totalMovies": 5, - "successCount": 4, - "failCount": 1 + "trigger_type": "auto", + "status": "partial", + "total_processed": 5, + "total_success": 4, + "total_skipped": 0, + "total_failed": 1, + "user_count": 1, + "duration_ms": 12500, + "start_time": "2026-06-27T10:00:00", + "end_time": "2026-06-27T10:00:12", + "error_message": null, + "details": [ + { + "username": "testuser", + "userId": 1, + "processed": 5, + "success": 4, + "skipped": 0, + "failed": 1, + "messages": [ + "泰坦尼克号: TMDB未找到匹配的电影" + ] + } + ] } - ], - "pageNo": 1, - "pageSize": 20 + ] } }
+
+
+ GET + /api/tmdb-scraper/scraper-logs/{id} + 查询单条刮削日志详情需Token +
+
+
+

功能说明

+

查询单条刮削日志的详细信息,包括每个用户的刮削结果、失败电影列表及原因。

+
+
+

路径参数

+ + + +
参数类型说明
idLong日志ID
+
+
+

响应字段说明

+ + + + + + + + + + + + + + + + + + + + + + +
字段类型说明
idLong日志ID
trigger_typeString触发方式:auto(定时自动)/ manual(手动触发)
statusString执行状态:success / partial / failed / skipped
total_processedInteger总处理电影数
total_successInteger成功刮削数
total_skippedInteger跳过数
total_failedInteger失败数
user_countInteger涉及用户数
duration_msLong执行耗时(毫秒)
start_timeString开始时间
end_timeString结束时间
error_messageString错误信息(如有)
detailsArray详细执行结果(解析后的JSON)
details[].usernameString用户名
details[].userIdLong用户ID
details[].processedInteger该用户处理的电影数
details[].successInteger该用户成功刮削数
details[].skippedInteger该用户跳过数
details[].failedInteger该用户失败数
details[].messagesArray失败详情列表(格式:电影名称: 失败原因)
+
+
+

响应示例

+
{ + "code": 200, + "message": "查询成功", + "data": { + "id": 1, + "trigger_type": "auto", + "status": "partial", + "total_processed": 5, + "total_success": 4, + "total_skipped": 0, + "total_failed": 1, + "user_count": 1, + "duration_ms": 12500, + "start_time": "2026-06-27T10:00:00", + "end_time": "2026-06-27T10:00:12", + "error_message": null, + "details": [ + { + "username": "testuser", + "userId": 1, + "processed": 5, + "success": 4, + "skipped": 0, + "failed": 1, + "messages": [ + "泰坦尼克号: TMDB未找到匹配的电影", + "阿凡达: TMDB API请求失败,请检查API Key是否有效" + ] + } + ] + } +}
+
+
+

失败响应示例 — 日志不存在

+
{ + "code": 404, + "message": "日志不存在", + "data": null +}
+
+
+
+
DELETE @@ -3497,15 +3837,16 @@
-

OpenList 多网盘聚合服务

- wangpan-drive-service : 8082 · 依赖 OpenList 服务 (http://ldfun.asia:13000) +

OpenList 用户级连接管理

+ wangpan-drive-service : 8082 · OpenListController / DriveController

功能说明

- OpenList 集成模块将 OpenList 作为多网盘聚合后端,支持 80+ 种网盘服务(阿里云盘、百度网盘、UC网盘、OneDrive 等)。 - Java 服务负责业务逻辑(用户、权限、TMDB 刮削),OpenList 负责文件操作(上传、下载、预览)。 + 每个用户可以绑定自己的 OpenList 实例(地址、账号、密码),互不干扰。 + 敏感信息使用 Jasypt 加密存储,Token 缓存到数据库避免重复登录。 + OpenList 负责文件操作(同步、播放),TMDB 刮削使用当前用户的 API Key。

@@ -3515,29 +3856,45 @@
Drive:8082
-
OpenList:13000
+
用户A的OpenList
-
各网盘
+
UC网盘
+
+
+ + + + + + +
用户B的OpenList
+ +
阿里云盘
-

前置配置

+

使用流程

- 1. 在 OpenList 管理后台 (http://ldfun.asia:13000/@manage) 添加网盘存储
- 2. 记录存储的 挂载路径(如 /uc)和 存储 ID(通过 API 获取)
- 3. 在 application.yml 中配置 OpenList 服务地址和账号密码
- 4. 创建网盘时传入 openlistStorageIdopenlistMountPath 关联 OpenList 存储 + 1. 前端调用 POST /api/openlist/test 测试 OpenList 连接(可选)
+ 2. 前端调用 POST /api/openlist/bind 绑定 OpenList(传入地址、账号、密码)
+ 3. 绑定成功后后端自动异步同步:递归遍历 OpenList 目录树,同步文件夹层级和视频文件到数据库
+ 4. 也可手动调用 POST /api/openlist/sync 触发全量同步
+ 5. 同步后电影可进行 TMDB 刮削,播放视频自动使用用户绑定的 OpenList 连接
+ 6. 后端每 24 小时自动检测 OpenList 心跳,记录连接状态
+ 7. TMDB 刮削使用当前用户的 API Key(与 OpenList 连接隔离)

- + + +
POST - /api/drive - 创建网盘(关联 OpenList 存储) + /api/openlist/test + 测试 OpenList 连接(不保存) 需Token
@@ -3549,53 +3906,280 @@ Content-Type: application/json

请求体

{ - "name": "UC网盘电影库", - "description": "UC网盘存储", - "openlistStorageId": "1", - "openlistMountPath": "/uc" + "openlistUrl": "http://www.example.com:3000", + "openlistUsername": "用户名", + "openlistPassword": "密码" }

字段说明

- - - - + + +
字段类型必填说明
nameString网盘名称
descriptionString网盘描述
openlistStorageIdStringOpenList 存储 ID(不传则仅记录元数据)
openlistMountPathStringOpenList 挂载路径(如 "/uc")
openlistUrlStringOpenList 服务地址(如 http://example.com:13000)
openlistUsernameStringOpenList 账号
openlistPasswordStringOpenList 密码

成功响应示例

{ "code": 200, - "message": "创建成功", + "message": "连接成功", + "data": null +}
+
+
+

失败响应示例

+
{ + "code": 500, + "message": "连接失败:Invalid username or password", + "data": null +}
+
+
+
+ + +
+
+ POST + /api/openlist/bind + 绑定 OpenList 连接 + 需Token +
+
+
+

请求头

+
Authorization: Bearer <token> +Content-Type: application/json
+
+
+

请求体

+
{ + "openlistUrl": "http://example.com:13000", + "openlistUsername": "your_username", + "openlistPassword": "your_password" +}
+
+
+

字段说明

+ + + + + +
字段类型必填说明
openlistUrlStringOpenList 服务地址
openlistUsernameStringOpenList 账号
openlistPasswordStringOpenList 密码
+
+
+

功能说明

+

+ 验证连接 → 保存配置(敏感信息 Jasypt 加密) → 缓存 Token(24小时有效期)。
+ 每个用户只能绑定一个 OpenList 实例,重复绑定会更新配置。
+ 绑定成功后自动触发异步同步:递归遍历 OpenList 目录树,同步文件夹层级和视频文件到数据库。
+ 同步只处理视频格式文件(mp4, mkv, avi 等),非视频文件自动跳过。 +

+
+
+

成功响应示例

+
{ + "code": 200, + "message": "绑定成功,正在后台同步文件...", + "data": null +}
+
+
+

失败响应示例

+
{ + "code": 500, + "message": "绑定失败:连接失败:Invalid username or password", + "data": null +}
+
+
+
+ + +
+
+ DELETE + /api/openlist/unbind + 解绑 OpenList 连接 + 需Token +
+
+
+

请求头

+
Authorization: Bearer <token>
+
+
+

功能说明

+

+ 删除当前用户的 OpenList 连接配置(包括加密的 URL、账号、密码和 Token 缓存)。
+ 解绑后同步文件和播放链接接口将不可用。 +

+
+
+

成功响应示例

+
{ + "code": 200, + "message": "解绑成功", + "data": null +}
+
+
+

失败响应示例

+
{ + "code": 500, + "message": "解绑失败:未找到绑定记录", + "data": null +}
+
+
+
+ + +
+
+ GET + /api/openlist/info + 查询当前用户 OpenList 连接信息 + 需Token +
+
+
+

请求头

+
Authorization: Bearer <token>
+
+
+

功能说明

+

+ 返回脱敏后的连接信息(URL、用户名均脱敏显示,不返回明文密码)。
+ 会实际调用 OpenList 登录接口验证当前连接是否可用,返回 connectedconnectMessage 字段。
+ 未绑定时返回 null。 +

+
+
+

成功响应示例(已绑定)

+
{ + "code": 200, + "message": "操作成功", "data": { "id": 1, - "name": "UC网盘电影库", - "description": "UC网盘存储", - "openlistStorageId": "1", - "openlistMountPath": "/uc", - "folderCount": 0, - "fileCount": 0, - "totalSize": 0, + "openlistUrl": "http://ld***:13000", + "openlistUsername": "dj***22", + "storageId": "1", + "mountPath": "/uc", "status": "active", + "tokenValid": true, + "connected": true, + "connectMessage": "连接成功", + "lastHeartbeatTime": "2026-06-27T15:00:00", + "heartbeatStatus": "success", + "heartbeatError": null, "createTime": "2026-06-26T15:00:00", "updateTime": "2026-06-26T15:00:00" } }
-

失败响应示例

+

响应字段说明

+ + + + + + + + + + + + + +
字段说明
openlistUrlOpenList 服务地址(脱敏显示,如 http://ld***:13000)
openlistUsernameOpenList 账号(脱敏显示,如 dj***22)
storageIdOpenList 存储 ID(用户在 OpenList 后台配置)
mountPath挂载路径(如 /uc)
status状态:active / disabled
tokenValid本地缓存的 Token 是否有效(true 表示未过期)
connected实时连接测试结果:true 表示当前账号密码可成功连接 OpenList
connectMessage实时连接测试结果描述,如 "连接成功" 或 "连接失败:Invalid username or password"
lastHeartbeatTime最后一次定时心跳检测时间(每 24 小时自动检测一次)
heartbeatStatus最近一次心跳检测结果:success / failed
heartbeatError心跳检测失败原因(成功时为 null)
+
+
+

成功响应示例(未绑定)

{ - "code": 400, - "message": "创建网盘失败:网盘名称不能为空", + "code": 200, + "message": "操作成功", "data": null }
+ + + +
+
+ POST + /api/openlist/sync + 手动触发 OpenList 文件同步 + 需Token +
+
+
+

请求头

+
Authorization: Bearer <token>
+
+
+

功能说明

+

+ 从 OpenList 根目录开始递归遍历目录树,同步文件夹层级和视频文件到本地数据库。
+ 同步策略
+   1. 自动为用户创建/复用关联 OpenList 的网盘(Drive)
+   2. 递归遍历 OpenList 目录树,在本地创建对应的文件夹(DriveFolder)
+   3. 只同步视频格式文件(mp4, mkv, avi, mov, wmv, flv, webm 等),非视频文件自动跳过
+   4. 视频文件创建为电影记录(DriveMovie),storageType=openlist
+   5. 不含视频文件的空文件夹也会被跳过
+ 前置条件:用户已通过 /api/openlist/bind 绑定 OpenList 连接。
+ 注意:绑定 OpenList 时已自动触发异步同步,此接口用于手动重新同步。 +

+
+
+

成功响应示例

+
{ + "code": 200, + "message": "同步完成", + "data": { + "driveCount": 1, + "folderCount": 10, + "movieCount": 50, + "skippedFileCount": 5, + "skippedFolderCount": 2, + "errorCount": 0, + "errors": [] + } +}
+
+
+

响应字段说明

+ + + + + + + + + +
字段类型说明
driveCountint关联网盘数量(通常为 1)
folderCountint本次同步创建的文件夹数量
movieCountint本次同步创建的视频文件(电影)数量
skippedFileCountint跳过的非视频文件数量
skippedFolderCountint跳过的不含视频的文件夹数量
errorCountint同步过程中的错误数量
errorsArray错误详情列表(字符串数组)
+
+
+

失败响应示例

+
{ + "code": 500, + "message": "同步失败:用户未绑定 OpenList,请先绑定", + "data": null +}
+
+
+
+ + +
@@ -3620,8 +4204,9 @@ Content-Type: application/json

功能说明

- 调用 OpenList API 获取指定路径下的文件列表,自动创建数据库中缺失的电影记录。 - 已存在的文件不会重复创建。同步后文件可进行 TMDB 刮削。 + 使用当前用户绑定的 OpenList 连接,获取指定路径下的文件列表,自动创建数据库中缺失的电影记录。
+ 已存在的文件不会重复创建。同步后文件可进行 TMDB 刮削。
+ 前置条件:用户已通过 /api/openlist/bind 绑定 OpenList 连接。

@@ -3646,8 +4231,8 @@ Content-Type: application/json

失败响应示例

{ - "code": 400, - "message": "同步失败:网盘未关联 OpenList 存储", + "code": 500, + "message": "同步失败:用户未绑定 OpenList,请先在设置中绑定 OpenList 连接", "data": null }
@@ -3678,8 +4263,9 @@ Content-Type: application/json

功能说明

- 调用 OpenList API 获取文件的真实下载地址,返回的 URL 可直接用于视频播放器播放。 - 仅支持 storageTypeopenlist 的电影记录。 + 使用当前用户绑定的 OpenList 连接,获取文件的真实下载地址,返回的 URL 可直接用于视频播放器播放。
+ 仅支持 storageTypeopenlist 的电影记录。
+ Token 过期时自动刷新,对用户透明。

@@ -3706,7 +4292,7 @@ Content-Type: application/json

失败响应示例

{ - "code": 400, + "code": 500, "message": "获取失败:该电影不是 OpenList 管理的文件", "data": null }
@@ -3714,48 +4300,6 @@ Content-Type: application/json
- -
-
- GET - /api/drive - 查看网盘列表(含 OpenList 信息) - 需Token -
-
-
-

请求头

-
Authorization: Bearer <token>
-
-
-

成功响应示例

-
{ - "code": 200, - "data": [ - { - "id": 1, - "name": "UC网盘电影库", - "openlistStorageId": "1", - "openlistMountPath": "/uc", - "folderCount": 2, - "fileCount": 10, - "totalSize": 10737418240, - "status": "active" - } - ] -}
-
-
-

新增字段说明

- - - - -
字段说明
openlistStorageId关联的 OpenList 存储 ID(null 表示未关联)
openlistMountPathOpenList 挂载路径(如 "/uc")
-
-
-
-
@@ -3799,12 +4343,12 @@ Content-Type: application/json
}
-

新增字段说明

+

OpenList 相关字段说明

- + - +
字段说明
openlistFilePathOpenList 文件路径(如 "/uc/阿凡达.mp4")
openlistFilePathOpenList 文件路径(如 "/uc/阿凡达.mp4"),由系统根据用户绑定的挂载路径自动生成
storageType存储类型:metadata(仅元数据)/ openlist(OpenList 管理)
fileSize文件大小(字节)
fileSize文件大小(字节),同步时从 OpenList 获取
@@ -3814,38 +4358,88 @@ Content-Type: application/json

前端调用示例

-

创建网盘

-
const response = await fetch('/api/drive', { +

1. 绑定 OpenList

+
// 先测试连接 +const testResp = await fetch('/api/openlist/test', { method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }, + headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ - name: 'UC网盘电影库', - openlistStorageId: '1', - openlistMountPath: '/uc' + openlistUrl: 'http://example.com:13000', + openlistUsername: 'your_username', + openlistPassword: 'your_password' + }) +}); + +// 测试通过后绑定 +const bindResp = await fetch('/api/openlist/bind', { + method: 'POST', + headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ + openlistUrl: 'http://example.com:13000', + openlistUsername: 'your_username', + openlistPassword: 'your_password' }) });
-

同步文件

-
const response = await fetch(`/api/drive/${driveId}/folders/${folderId}/sync-files`, { +

2. 查询连接信息

+
const resp = await fetch('/api/openlist/info', { + headers: { 'Authorization': `Bearer ${token}` } +}); +const data = await resp.json(); +if (data.data) { + console.log('已绑定:', data.data.openlistUrl); + console.log('Token有效:', data.data.tokenValid); +} else { + console.log('未绑定 OpenList'); +}
+
+
+

3. 手动同步文件

+
// 绑定后已自动触发异步同步,也可手动重新同步 +const resp = await fetch('/api/openlist/sync', { method: 'POST', headers: { 'Authorization': `Bearer ${token}` } }); -const data = await response.json(); -console.log(`同步了 ${data.data.syncCount} 个文件`);
+const data = await resp.json(); +console.log(`同步了 ${data.data.folderCount} 个文件夹, ${data.data.movieCount} 个视频`); +if (data.data.errorCount > 0) { + console.warn('同步错误:', data.data.errors); +}
-

播放视频

-
const response = await fetch(`/api/drive/${driveId}/movies/${movieId}/play-url`, { +

4. 查询连接状态

+
const resp = await fetch('/api/openlist/info', { headers: { 'Authorization': `Bearer ${token}` } }); -const data = await response.json(); +const data = await resp.json(); +if (data.data) { + console.log('地址:', data.data.openlistUrl); + console.log('实时连接:', data.data.connected ? '正常' : '异常'); + console.log('连接信息:', data.data.connectMessage); + console.log('心跳状态:', data.data.heartbeatStatus); +} else { + console.log('未绑定 OpenList'); +}
+
+
+

5. 播放视频

+
const resp = await fetch(`/api/drive/${driveId}/movies/${movieId}/play-url`, { + headers: { 'Authorization': `Bearer ${token}` } +}); +const data = await resp.json(); videoPlayer.src = data.data.playUrl; // 直接设置视频源 videoPlayer.play();
+
+

6. 解绑 OpenList

+
const resp = await fetch('/api/openlist/unbind', { + method: 'DELETE', + headers: { 'Authorization': `Bearer ${token}` } +}); +const data = await resp.json(); +console.log(data.message); // "解绑成功"
+
@@ -3898,10 +4492,24 @@ videoPlayer.play();
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active')); document.querySelectorAll('.section').forEach(s => s.classList.remove('active')); this.classList.add('active'); - document.getElementById(this.dataset.section).classList.add('active'); + const sectionId = this.dataset.section; + const section = document.getElementById(sectionId); + if (section) { + section.classList.add('active'); + } else { + console.error('Section not found:', sectionId); + } }); }); + // 页面加载时默认激活第一个导航项 + document.addEventListener('DOMContentLoaded', function() { + const firstNavItem = document.querySelector('.nav-item'); + if (firstNavItem && !document.querySelector('.section.active')) { + firstNavItem.click(); + } + }); + // API 详情展开/收起 function toggleDetail(header) { const detail = header.nextElementSibling; diff --git a/wangpan-gateway/target/classes/application.yml b/wangpan-gateway/target/classes/application.yml index 260826b..44c9a5a 100644 --- a/wangpan-gateway/target/classes/application.yml +++ b/wangpan-gateway/target/classes/application.yml @@ -21,7 +21,7 @@ spring: - id: wangpan-drive-service uri: http://localhost:8082 predicates: - - Path=/api/drive/**,/api/icon/** + - Path=/api/drive/**,/api/icon/**,/api/openlist/** filters: - StripPrefix=0 # TMDB服务路由 diff --git a/wangpan-gateway/target/classes/com/wangpan/GatewayApplication.class b/wangpan-gateway/target/classes/com/wangpan/GatewayApplication.class index 937b5d1..0e117e0 100644 Binary files a/wangpan-gateway/target/classes/com/wangpan/GatewayApplication.class and b/wangpan-gateway/target/classes/com/wangpan/GatewayApplication.class differ diff --git a/wangpan-gateway/target/wangpan-gateway-1.0-SNAPSHOT.jar b/wangpan-gateway/target/wangpan-gateway-1.0-SNAPSHOT.jar index 70b1de8..3f3cd80 100644 Binary files a/wangpan-gateway/target/wangpan-gateway-1.0-SNAPSHOT.jar and b/wangpan-gateway/target/wangpan-gateway-1.0-SNAPSHOT.jar differ diff --git a/wangpan-gateway/target/wangpan-gateway-1.0-SNAPSHOT.jar.original b/wangpan-gateway/target/wangpan-gateway-1.0-SNAPSHOT.jar.original index c59790e..e61ec30 100644 Binary files a/wangpan-gateway/target/wangpan-gateway-1.0-SNAPSHOT.jar.original and b/wangpan-gateway/target/wangpan-gateway-1.0-SNAPSHOT.jar.original differ diff --git a/wangpan-tmdb-service/src/main/java/com/wangpan/controller/MovieController.java b/wangpan-tmdb-service/src/main/java/com/wangpan/controller/MovieController.java index f31a078..565c0f7 100644 --- a/wangpan-tmdb-service/src/main/java/com/wangpan/controller/MovieController.java +++ b/wangpan-tmdb-service/src/main/java/com/wangpan/controller/MovieController.java @@ -20,6 +20,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; /** @@ -35,6 +36,12 @@ public class MovieController { private static final Logger log = LoggerFactory.getLogger(MovieController.class); + /** 缓存过期时间:5分钟(毫秒) */ + private static final long CACHE_EXPIRE_MS = 5 * 60 * 1000; + + /** 电影列表查询缓存(key: userId_queryParams, value: 缓存结果) */ + private final ConcurrentHashMap movieListCache = new ConcurrentHashMap<>(); + private final MovieRepository movieRepository; private final DriveMovieRepository driveMovieRepository; private final DailyRecommendRepository dailyRecommendRepository; @@ -103,6 +110,181 @@ public class MovieController { return Result.success("查询成功", resultData); } + /** 每日随机推荐数量(第1页前20条) */ + private static final int DAILY_RANDOM_COUNT = 20; + + /** + * 查询当前用户所有网盘下所有未删除的电影(不分文件夹,直接分页返回) + * GET /api/movies/all + * + * 设计说明: + * - 不进行分类,直接分页查询所有电影 + * - 支持关键词搜索(标题模糊匹配) + * - 支持排序(按创建时间、更新时间、评分、标题) + * - 内置5分钟缓存,适用于1万+数据量场景 + * - 最大每页100条,防止单次查询数据量过大 + * - 第1页前20条每日随机刷新,增加变化性(无关键词搜索时生效) + */ + @GetMapping("/all") + public Result listAllMovies(@RequestParam(required = false) String keyword, + @RequestParam(required = false) String sortBy, + @RequestParam(required = false) String sortOrder, + @RequestParam(defaultValue = "1") int pageNo, + @RequestParam(defaultValue = "50") int pageSize, + HttpServletRequest request) { + User user = getCurrentUser(request); + + // 限制每页大小,防止大数据量下内存溢出 + pageSize = Math.min(100, Math.max(1, pageSize)); + + log.info("用户 [{}] 查询所有电影列表 | 关键词={} 排序={} 页码={} 每页={}", + user.getUsername(), keyword, sortBy, pageNo, pageSize); + + // 1. 构建缓存Key(第1页无关键词时,加入日期因子实现每日刷新) + String cacheKey; + if (pageNo == 1 && (keyword == null || keyword.isBlank())) { + // 第1页且无关键词搜索:加入当天日期,实现每日缓存刷新 + String today = java.time.LocalDate.now().toString(); + cacheKey = buildCacheKey(user.getId(), "daily_" + today, sortBy, sortOrder, pageNo, pageSize); + } else { + cacheKey = buildCacheKey(user.getId(), keyword, sortBy, sortOrder, pageNo, pageSize); + } + + // 2. 尝试从缓存获取 + CacheEntry cachedEntry = movieListCache.get(cacheKey); + if (cachedEntry != null && !cachedEntry.isExpired()) { + log.info("用户 [{}] 命中电影列表缓存 | key={}", user.getUsername(), cacheKey); + return Result.success("查询成功(缓存)", cachedEntry.getData()); + } + + // 3. 缓存未命中或已过期,查询数据库 + try { + Map pageInfo; + + // 第1页且无关键词搜索时,前20条随机刷新 + if (pageNo == 1 && (keyword == null || keyword.isBlank())) { + pageInfo = queryFirstPageWithDailyRandom(user.getId(), sortBy, sortOrder, pageSize); + } else { + // 其他页码或有关键词搜索时,正常查询 + pageInfo = driveMovieRepository.queryAllMoviesByUser( + user.getId(), keyword, sortBy, sortOrder, pageNo, pageSize); + } + + // 4. 写入缓存(5分钟过期) + long expireTime = System.currentTimeMillis() + CACHE_EXPIRE_MS; + movieListCache.put(cacheKey, new CacheEntry(pageInfo, expireTime)); + + // 5. 定期清理过期缓存(每100次查询清理一次,避免内存泄漏) + if (movieListCache.size() > 100) { + cleanExpiredCache(); + } + + log.info("用户 [{}] 电影列表查询完成 | 总数={}", user.getUsername(), pageInfo.get("total")); + return Result.success("查询成功", pageInfo); + + } catch (Exception e) { + log.error("查询电影列表失败", e); + return Result.error("查询失败:" + e.getMessage()); + } + } + + /** + * 第1页查询:前20条每日随机,其余按排序 + * + * @param userId 用户ID + * @param sortBy 排序字段 + * @param sortOrder 排序方向 + * @param pageSize 每页大小 + * @return 分页结果 + */ + private Map queryFirstPageWithDailyRandom(Long userId, String sortBy, String sortOrder, int pageSize) { + // 1. 随机查询20部电影 + List randomMovies = driveMovieRepository.queryRandomMovies(userId, DAILY_RANDOM_COUNT); + + if (randomMovies.isEmpty()) { + // 没有电影,返回空结果 + Map emptyResult = new HashMap<>(); + emptyResult.put("total", 0L); + emptyResult.put("pages", 0L); + emptyResult.put("pageNo", 1L); + emptyResult.put("pageSize", (long) pageSize); + emptyResult.put("list", new ArrayList<>()); + return emptyResult; + } + + // 2. 获取随机电影的ID列表 + List randomIds = randomMovies.stream() + .map(DriveMovieDO::getId) + .toList(); + + // 3. 查询剩余电影(排除随机20部),按原排序 + int remainingSize = Math.max(0, pageSize - DAILY_RANDOM_COUNT); + Map remainingResult = null; + if (remainingSize > 0) { + remainingResult = driveMovieRepository.queryAllMoviesExcludeIds( + userId, randomIds, null, sortBy, sortOrder, 1, remainingSize); + } + + // 4. 合并结果:随机20部 + 剩余电影 + List> mergedList = new ArrayList<>(); + + // 添加随机20部 + for (DriveMovieDO movieDO : randomMovies) { + mergedList.add(toMap(movieDO)); + } + + // 添加剩余电影 + if (remainingResult != null && remainingResult.get("list") != null) { + @SuppressWarnings("unchecked") + List> remainingList = (List>) remainingResult.get("list"); + mergedList.addAll(remainingList); + } + + // 5. 构建分页信息 + // 总数需要加上随机20部(因为排除查询的总数不包含这20部) + long totalCount = randomIds.size(); + if (remainingResult != null && remainingResult.get("total") != null) { + totalCount += ((Number) remainingResult.get("total")).longValue(); + } + + Map pageInfo = new HashMap<>(); + pageInfo.put("total", totalCount); + pageInfo.put("pages", remainingResult != null ? remainingResult.get("pages") : 1L); + pageInfo.put("pageNo", 1L); + pageInfo.put("pageSize", (long) pageSize); + pageInfo.put("list", mergedList); + + log.info("第1页每日随机刷新 | 随机数={} 总数={}", randomIds.size(), totalCount); + return pageInfo; + } + + /** + * DriveMovieDO 转 Map(用于每日随机刷新场景) + */ + private Map toMap(DriveMovieDO movieDO) { + Map map = new HashMap<>(); + map.put("id", movieDO.getId()); + map.put("driveId", movieDO.getDriveId()); + map.put("folderId", movieDO.getFolderId()); + map.put("title", movieDO.getTitle()); + 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("scrapeStatus", movieDO.getScrapeStatus()); + 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("createTime", movieDO.getCreateTime() != null ? movieDO.getCreateTime().toString() : null); + map.put("updateTime", movieDO.getUpdateTime() != null ? movieDO.getUpdateTime().toString() : null); + return map; + } + /** * 查询单个电影详情 * GET /api/movies/{id} @@ -176,7 +358,6 @@ public class MovieController { resultData.put("rating", movieDO.getRating()); resultData.put("category", movieDO.getCategory()); resultData.put("tags", movieDO.getTags()); - resultData.put("overview", movieDO.getOverview()); resultData.put("casts", movieDO.getCasts()); resultData.put("directors", movieDO.getDirectors()); } else { @@ -448,7 +629,6 @@ public class MovieController { map.put("posterUrl", movie.getPosterUrl()); map.put("backdropUrl", movie.getBackdropUrl()); map.put("tags", movie.getTags()); - map.put("overview", movie.getOverview()); map.put("casts", movie.getCasts()); map.put("directors", movie.getDirectors()); @@ -484,4 +664,45 @@ public class MovieController { setter.accept(value); } } + + // ==================== 缓存内部类 ==================== + + /** + * 缓存条目(包含数据和过期时间) + */ + private static class CacheEntry { + private final Map data; + private final long expireTime; + + public CacheEntry(Map data, long expireTime) { + this.data = data; + this.expireTime = expireTime; + } + + public boolean isExpired() { + return System.currentTimeMillis() > expireTime; + } + + public Map getData() { + return data; + } + } + + /** + * 生成缓存Key + */ + private String buildCacheKey(Long userId, String keyword, String sortBy, String sortOrder, int pageNo, int pageSize) { + return userId + ":" + + (keyword != null ? keyword : "") + ":" + + (sortBy != null ? sortBy : "") + ":" + + (sortOrder != null ? sortOrder : "") + ":" + + pageNo + ":" + pageSize; + } + + /** + * 清理过期缓存(可选调用) + */ + private void cleanExpiredCache() { + movieListCache.entrySet().removeIf(entry -> entry.getValue().isExpired()); + } } \ No newline at end of file diff --git a/wangpan-tmdb-service/src/main/java/com/wangpan/controller/TmdbScraperController.java b/wangpan-tmdb-service/src/main/java/com/wangpan/controller/TmdbScraperController.java index 881e58a..112cf59 100644 --- a/wangpan-tmdb-service/src/main/java/com/wangpan/controller/TmdbScraperController.java +++ b/wangpan-tmdb-service/src/main/java/com/wangpan/controller/TmdbScraperController.java @@ -320,6 +320,8 @@ public class TmdbScraperController { /** * 分页查询定时任务执行记录 * GET /api/tmdb-scraper/scraper-logs + * + * 返回每条执行记录的汇总信息和详细结果(包含每部电影的刮削结果和失败原因) */ @GetMapping("/scraper-logs") public Result getScraperLogs(@RequestParam(defaultValue = "1") int pageNo, @@ -332,9 +334,28 @@ public class TmdbScraperController { List> logs = scraperLogRepository.queryRecent(pageNo, pageSize); long total = scraperLogRepository.countAll(); + // 解析 detailJson 字段,转换为结构化数据 + List> parsedLogs = new ArrayList<>(); + for (Map logEntry : logs) { + Map parsedLog = new HashMap<>(logEntry); + String detailJson = (String) logEntry.get("detail_json"); + if (detailJson != null && !detailJson.isEmpty()) { + try { + // 使用 Jackson ObjectMapper 解析 JSON + var objectMapper = new com.fasterxml.jackson.databind.ObjectMapper(); + var details = objectMapper.readValue(detailJson, List.class); + parsedLog.put("details", details); + } catch (Exception e) { + log.warn("解析刮削日志详情失败: {}", e.getMessage()); + parsedLog.put("details", null); + } + } + parsedLogs.add(parsedLog); + } + // 构建分页响应 Map result = new HashMap<>(); - result.put("list", logs); + result.put("list", parsedLogs); result.put("pageNo", pageNo); result.put("pageSize", pageSize); result.put("total", total); @@ -342,6 +363,38 @@ public class TmdbScraperController { return Result.success("查询成功", result); } + /** + * 查询单条刮削日志详情 + * GET /api/tmdb-scraper/scraper-logs/{id} + * + * 返回该次执行的具体信息:每个用户的刮削结果、失败电影列表及原因 + */ + @GetMapping("/scraper-logs/{id}") + public Result getScraperLogDetail(@PathVariable("id") Long id, HttpServletRequest request) { + User user = getCurrentUser(request); + + Map logRecord = scraperLogRepository.findById(id); + if (logRecord == null) { + return Result.notFound("日志不存在"); + } + + // 解析 detailJson + Map result = new HashMap<>(logRecord); + String detailJson = (String) logRecord.get("detail_json"); + if (detailJson != null && !detailJson.isEmpty()) { + try { + var objectMapper = new com.fasterxml.jackson.databind.ObjectMapper(); + var details = objectMapper.readValue(detailJson, List.class); + result.put("details", details); + } catch (Exception e) { + log.warn("解析刮削日志详情失败: {}", e.getMessage()); + result.put("details", null); + } + } + + return Result.success("查询成功", result); + } + /** * 删除缓存 * DELETE /api/tmdb-scraper/cache/{movieId} diff --git a/wangpan-tmdb-service/src/main/java/com/wangpan/listener/TmdbScheduler.java b/wangpan-tmdb-service/src/main/java/com/wangpan/listener/TmdbScheduler.java index 35f6c00..d62a9b0 100644 --- a/wangpan-tmdb-service/src/main/java/com/wangpan/listener/TmdbScheduler.java +++ b/wangpan-tmdb-service/src/main/java/com/wangpan/listener/TmdbScheduler.java @@ -7,6 +7,7 @@ import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -228,7 +229,7 @@ public class TmdbScheduler { totalFailed += failed; userCount++; - // 记录该用户的执行结果 + // 记录该用户的执行结果(包含每部电影的具体刮削结果) var userResult = new HashMap(); userResult.put("username", username); userResult.put("userId", userId); @@ -236,6 +237,10 @@ public class TmdbScheduler { userResult.put("success", success); userResult.put("skipped", skipped); userResult.put("failed", failed); + // 保存失败详情列表(格式:电影名称: 失败原因) + @SuppressWarnings("unchecked") + var failMessages = (List) result.get("messages"); + userResult.put("messages", failMessages != null ? failMessages : new ArrayList()); userResults.add(userResult); scanLog.append(" 结果: 成功=").append(success).append(", 跳过=").append(skipped) @@ -363,6 +368,7 @@ public class TmdbScheduler { /** * 构建详细执行结果JSON(简单拼接,不依赖第三方库) + * 包含每部电影的具体刮削结果和失败原因 */ private String buildDetailJson(List> userResults) { if (userResults == null || userResults.isEmpty()) { @@ -379,8 +385,21 @@ public class TmdbScheduler { .append(",\"processed\":").append(ur.get("processed")) .append(",\"success\":").append(ur.get("success")) .append(",\"skipped\":").append(ur.get("skipped")) - .append(",\"failed\":").append(ur.get("failed")) - .append("}"); + .append(",\"failed\":").append(ur.get("failed")); + + // 添加失败详情列表 + @SuppressWarnings("unchecked") + var messages = (List) ur.get("messages"); + if (messages != null && !messages.isEmpty()) { + sb.append(",\"messages\":["); + for (int j = 0; j < messages.size(); j++) { + if (j > 0) sb.append(","); + sb.append("\"").append(escapeJson(messages.get(j))).append("\""); + } + sb.append("]"); + } + + sb.append("}"); } sb.append("]"); return sb.toString(); diff --git a/wangpan-tmdb-service/src/main/java/com/wangpan/service/TmdbScraperService.java b/wangpan-tmdb-service/src/main/java/com/wangpan/service/TmdbScraperService.java index ea109dc..71fce57 100644 --- a/wangpan-tmdb-service/src/main/java/com/wangpan/service/TmdbScraperService.java +++ b/wangpan-tmdb-service/src/main/java/com/wangpan/service/TmdbScraperService.java @@ -135,7 +135,8 @@ public class TmdbScraperService { // ========== 第2步:检查缓存有效性(7天内已刮削则跳过) ========== var existingCache = tmdbCacheRepository.findByMovieId(movieId); - if (existingCache != null && !isCacheExpired(existingCache.getCacheTime().toString())) { + if (existingCache != null && existingCache.getCacheTime() != null + && !isCacheExpired(existingCache.getCacheTime().toString())) { // 使用缓存数据,更新刮削状态为成功 movieRepository.updateScrapeStatus(movieId, "success", null); return successResult("使用缓存数据(" + existingCache.getTitle() + ")", tmdbCacheRepository.toMap(existingCache)); @@ -188,8 +189,16 @@ public class TmdbScraperService { return errorResult("未找到匹配度足够高的结果: " + movie.getTitle()); } - var tmdbId = bestMatch.get("id").asLong(); - LOGGER.info("[TmdbScraper] 匹配成功: " + bestMatch.get("title").asText() + " (TMDB ID=" + tmdbId + ")"); + // 添加 null 检查,防止空指针异常 + var idNode = bestMatch.get("id"); + var titleNode = bestMatch.get("title"); + if (idNode == null || titleNode == null) { + movieRepository.updateScrapeStatus(movieId, "failed", "TMDB返回数据格式异常:缺少必要字段"); + return errorResult("TMDB返回数据格式异常:缺少必要字段"); + } + + var tmdbId = idNode.asLong(); + LOGGER.info("[TmdbScraper] 匹配成功: " + titleNode.asText() + " (TMDB ID=" + tmdbId + ")"); // ========== 第6步:获取电影详情(含完整信息) ========== rateLimitDelay(); // 遵守速率限制 @@ -251,7 +260,8 @@ public class TmdbScraperService { try { // 检查缓存 var existingCache = tmdbCacheRepository.findByMovieId(movie.getId()); - if (existingCache != null && !isCacheExpired(existingCache.getCacheTime().toString())) { + if (existingCache != null && existingCache.getCacheTime() != null + && !isCacheExpired(existingCache.getCacheTime().toString())) { skipCount++; continue; // 已有有效缓存,跳过 } @@ -664,8 +674,11 @@ public class TmdbScraperService { var credits = tmdbData.get("credits"); if (credits.has("crew")) { for (var crew : credits.get("crew")) { - if ("Director".equals(crew.get("job").asText())) { - directors.add(crew.get("name").asText()); + // 添加 null 检查,防止空指针异常 + var jobNode = crew.get("job"); + var nameNode = crew.get("name"); + if (jobNode != null && nameNode != null && "Director".equals(jobNode.asText())) { + directors.add(nameNode.asText()); if (directors.size() >= 2) break; // 最多取2个导演 } } @@ -673,8 +686,12 @@ public class TmdbScraperService { if (credits.has("cast")) { int count = 0; for (var cast : credits.get("cast")) { - casts.add(cast.get("name").asText()); - if (++count >= 5) break; // 最多取5个主演 + // 添加 null 检查,防止空指针异常 + var nameNode = cast.get("name"); + if (nameNode != null) { + casts.add(nameNode.asText()); + if (++count >= 5) break; // 最多取5个主演 + } } } } @@ -764,8 +781,8 @@ public class TmdbScraperService { // 更新电影简介(TMDB overview → description) // 即使为空也更新数据库,确保前端能获取到字段(空字符串而非null) - var overview = cacheData.get("overview") != null ? cacheData.get("overview").toString() : ""; - movieRepository.updateDescription(movie.getId(), overview); + var tmdbOverview = cacheData.get("overview") != null ? cacheData.get("overview").toString() : ""; + movieRepository.updateDescription(movie.getId(), tmdbOverview); // 更新主演(即使为空也更新) var casts = cacheData.get("casts") != null ? cacheData.get("casts").toString() : ""; diff --git a/wangpan-tmdb-service/target/classes/com/wangpan/controller/MovieController$CacheEntry.class b/wangpan-tmdb-service/target/classes/com/wangpan/controller/MovieController$CacheEntry.class new file mode 100644 index 0000000..4c17753 Binary files /dev/null and b/wangpan-tmdb-service/target/classes/com/wangpan/controller/MovieController$CacheEntry.class differ diff --git a/wangpan-tmdb-service/target/classes/com/wangpan/controller/MovieController.class b/wangpan-tmdb-service/target/classes/com/wangpan/controller/MovieController.class index 14356fd..457523e 100644 Binary files a/wangpan-tmdb-service/target/classes/com/wangpan/controller/MovieController.class and b/wangpan-tmdb-service/target/classes/com/wangpan/controller/MovieController.class differ diff --git a/wangpan-tmdb-service/target/classes/com/wangpan/controller/TmdbScraperController.class b/wangpan-tmdb-service/target/classes/com/wangpan/controller/TmdbScraperController.class index 7ff4726..61baf11 100644 Binary files a/wangpan-tmdb-service/target/classes/com/wangpan/controller/TmdbScraperController.class and b/wangpan-tmdb-service/target/classes/com/wangpan/controller/TmdbScraperController.class differ diff --git a/wangpan-tmdb-service/target/classes/com/wangpan/listener/TmdbScheduler$ScrapeTask.class b/wangpan-tmdb-service/target/classes/com/wangpan/listener/TmdbScheduler$ScrapeTask.class index 37d958c..4df1742 100644 Binary files a/wangpan-tmdb-service/target/classes/com/wangpan/listener/TmdbScheduler$ScrapeTask.class and b/wangpan-tmdb-service/target/classes/com/wangpan/listener/TmdbScheduler$ScrapeTask.class differ diff --git a/wangpan-tmdb-service/target/classes/com/wangpan/listener/TmdbScheduler.class b/wangpan-tmdb-service/target/classes/com/wangpan/listener/TmdbScheduler.class index b4e3585..ebb7d00 100644 Binary files a/wangpan-tmdb-service/target/classes/com/wangpan/listener/TmdbScheduler.class and b/wangpan-tmdb-service/target/classes/com/wangpan/listener/TmdbScheduler.class differ diff --git a/wangpan-tmdb-service/target/classes/com/wangpan/service/TmdbScraperService.class b/wangpan-tmdb-service/target/classes/com/wangpan/service/TmdbScraperService.class index 023240c..409d3bf 100644 Binary files a/wangpan-tmdb-service/target/classes/com/wangpan/service/TmdbScraperService.class and b/wangpan-tmdb-service/target/classes/com/wangpan/service/TmdbScraperService.class differ diff --git a/wangpan-tmdb-service/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/wangpan-tmdb-service/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst index 47d809d..3c4c07d 100644 --- a/wangpan-tmdb-service/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +++ b/wangpan-tmdb-service/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -1,4 +1,5 @@ com\wangpan\config\DatabaseInitializer.class +com\wangpan\controller\MovieController$CacheEntry.class com\wangpan\controller\TmdbController.class com\wangpan\controller\MovieController.class com\wangpan\listener\TmdbScheduler$ScrapeTask.class diff --git a/wangpan-tmdb-service/target/wangpan-tmdb-service-1.0-SNAPSHOT.jar b/wangpan-tmdb-service/target/wangpan-tmdb-service-1.0-SNAPSHOT.jar index 31578fd..44dcefb 100644 Binary files a/wangpan-tmdb-service/target/wangpan-tmdb-service-1.0-SNAPSHOT.jar and b/wangpan-tmdb-service/target/wangpan-tmdb-service-1.0-SNAPSHOT.jar differ diff --git a/wangpan-tmdb-service/target/wangpan-tmdb-service-1.0-SNAPSHOT.jar.original b/wangpan-tmdb-service/target/wangpan-tmdb-service-1.0-SNAPSHOT.jar.original index 1ef12ec..f2049fa 100644 Binary files a/wangpan-tmdb-service/target/wangpan-tmdb-service-1.0-SNAPSHOT.jar.original and b/wangpan-tmdb-service/target/wangpan-tmdb-service-1.0-SNAPSHOT.jar.original differ diff --git a/wangpan-user-service/target/classes/com/wangpan/UserServiceApplication.class b/wangpan-user-service/target/classes/com/wangpan/UserServiceApplication.class index f353d5f..ead56b1 100644 Binary files a/wangpan-user-service/target/classes/com/wangpan/UserServiceApplication.class and b/wangpan-user-service/target/classes/com/wangpan/UserServiceApplication.class differ diff --git a/wangpan-user-service/target/classes/com/wangpan/config/DatabaseInitializer.class b/wangpan-user-service/target/classes/com/wangpan/config/DatabaseInitializer.class index 0723f2c..c7ebc97 100644 Binary files a/wangpan-user-service/target/classes/com/wangpan/config/DatabaseInitializer.class and b/wangpan-user-service/target/classes/com/wangpan/config/DatabaseInitializer.class differ diff --git a/wangpan-user-service/target/classes/com/wangpan/controller/AuthController.class b/wangpan-user-service/target/classes/com/wangpan/controller/AuthController.class index f060e1e..e532629 100644 Binary files a/wangpan-user-service/target/classes/com/wangpan/controller/AuthController.class and b/wangpan-user-service/target/classes/com/wangpan/controller/AuthController.class differ diff --git a/wangpan-user-service/target/classes/com/wangpan/controller/LoginLogController.class b/wangpan-user-service/target/classes/com/wangpan/controller/LoginLogController.class index 7ec9fc1..588c438 100644 Binary files a/wangpan-user-service/target/classes/com/wangpan/controller/LoginLogController.class and b/wangpan-user-service/target/classes/com/wangpan/controller/LoginLogController.class differ diff --git a/wangpan-user-service/target/classes/com/wangpan/controller/ProfileController.class b/wangpan-user-service/target/classes/com/wangpan/controller/ProfileController.class index feac2e2..6b77119 100644 Binary files a/wangpan-user-service/target/classes/com/wangpan/controller/ProfileController.class and b/wangpan-user-service/target/classes/com/wangpan/controller/ProfileController.class differ diff --git a/wangpan-user-service/target/classes/com/wangpan/controller/UserController.class b/wangpan-user-service/target/classes/com/wangpan/controller/UserController.class index 9379979..46eba6f 100644 Binary files a/wangpan-user-service/target/classes/com/wangpan/controller/UserController.class and b/wangpan-user-service/target/classes/com/wangpan/controller/UserController.class differ diff --git a/wangpan-user-service/target/classes/com/wangpan/service/UserService.class b/wangpan-user-service/target/classes/com/wangpan/service/UserService.class index 45d4aea..9bff97f 100644 Binary files a/wangpan-user-service/target/classes/com/wangpan/service/UserService.class and b/wangpan-user-service/target/classes/com/wangpan/service/UserService.class differ diff --git a/wangpan-user-service/target/wangpan-user-service-1.0-SNAPSHOT.jar b/wangpan-user-service/target/wangpan-user-service-1.0-SNAPSHOT.jar index 8b0da34..8dfc8cb 100644 Binary files a/wangpan-user-service/target/wangpan-user-service-1.0-SNAPSHOT.jar and b/wangpan-user-service/target/wangpan-user-service-1.0-SNAPSHOT.jar differ diff --git a/wangpan-user-service/target/wangpan-user-service-1.0-SNAPSHOT.jar.original b/wangpan-user-service/target/wangpan-user-service-1.0-SNAPSHOT.jar.original index 441b494..32d4a1e 100644 Binary files a/wangpan-user-service/target/wangpan-user-service-1.0-SNAPSHOT.jar.original and b/wangpan-user-service/target/wangpan-user-service-1.0-SNAPSHOT.jar.original differ