Initial commit
This commit is contained in:
commit
f79c193b09
0
.idea/.gitignore
generated
vendored
Normal file
0
.idea/.gitignore
generated
vendored
Normal file
6
.idea/CoolRequestSetting.xml
generated
Normal file
6
.idea/CoolRequestSetting.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CoolRequestSetting">
|
||||||
|
<option name="projectCachePath" value="project-68d65c10-bc51-4ce1-a353-4b174fc1a7d4" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
30
.idea/compiler.xml
generated
Normal file
30
.idea/compiler.xml
generated
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<excludeFromCompile>
|
||||||
|
<directory url="file://$PROJECT_DIR$/src/main/java" includeSubdirectories="true" />
|
||||||
|
</excludeFromCompile>
|
||||||
|
<annotationProcessing>
|
||||||
|
<profile name="Maven default annotation processors profile" enabled="true">
|
||||||
|
<sourceOutputDir name="target/generated-sources/annotations" />
|
||||||
|
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
|
||||||
|
<outputRelativeToContentRoot value="true" />
|
||||||
|
<module name="wangpan-common" />
|
||||||
|
<module name="wangpan-user-service" />
|
||||||
|
<module name="wangpan-gateway" />
|
||||||
|
<module name="wangpan-tmdb-service" />
|
||||||
|
<module name="wangpan-drive-service" />
|
||||||
|
</profile>
|
||||||
|
</annotationProcessing>
|
||||||
|
</component>
|
||||||
|
<component name="JavacSettings">
|
||||||
|
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
|
||||||
|
<module name="wangpan-common" options="-parameters" />
|
||||||
|
<module name="wangpan-drive-service" options="-parameters" />
|
||||||
|
<module name="wangpan-gateway" options="-parameters" />
|
||||||
|
<module name="wangpan-login" options="-parameters" />
|
||||||
|
<module name="wangpan-tmdb-service" options="-parameters" />
|
||||||
|
<module name="wangpan-user-service" options="-parameters" />
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
12
.idea/encodings.xml
generated
Normal file
12
.idea/encodings.xml
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Encoding">
|
||||||
|
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||||
|
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
|
||||||
|
<file url="file://$PROJECT_DIR$/wangpan-common/src/main/java" charset="UTF-8" />
|
||||||
|
<file url="file://$PROJECT_DIR$/wangpan-drive-service/src/main/java" charset="UTF-8" />
|
||||||
|
<file url="file://$PROJECT_DIR$/wangpan-gateway/src/main/java" charset="UTF-8" />
|
||||||
|
<file url="file://$PROJECT_DIR$/wangpan-tmdb-service/src/main/java" charset="UTF-8" />
|
||||||
|
<file url="file://$PROJECT_DIR$/wangpan-user-service/src/main/java" charset="UTF-8" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
5
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
5
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
20
.idea/jarRepositories.xml
generated
Normal file
20
.idea/jarRepositories.xml
generated
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="RemoteRepositoriesConfiguration">
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="central" />
|
||||||
|
<option name="name" value="Central Repository" />
|
||||||
|
<option name="url" value="http://maven.aliyun.com/nexus/content/groups/public/" />
|
||||||
|
</remote-repository>
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="central" />
|
||||||
|
<option name="name" value="Maven Central repository" />
|
||||||
|
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||||
|
</remote-repository>
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="jboss.community" />
|
||||||
|
<option name="name" value="JBoss Community repository" />
|
||||||
|
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
||||||
|
</remote-repository>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
12
.idea/misc.xml
generated
Normal file
12
.idea/misc.xml
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
|
<component name="MavenProjectsManager">
|
||||||
|
<option name="originalFiles">
|
||||||
|
<list>
|
||||||
|
<option value="$PROJECT_DIR$/pom.xml" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK" />
|
||||||
|
</project>
|
||||||
152
.idea/workspace.xml
generated
Normal file
152
.idea/workspace.xml
generated
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AutoImportSettings">
|
||||||
|
<option name="autoReloadType" value="SELECTIVE" />
|
||||||
|
</component>
|
||||||
|
<component name="ChangeListManager">
|
||||||
|
<list default="true" id="a2d7bfcd-46c4-4e1a-941c-f149d0cd7b56" name="Changes" comment="" />
|
||||||
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
|
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||||
|
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||||
|
</component>
|
||||||
|
<component name="CopilotPersistence">
|
||||||
|
<persistenceIdMap>
|
||||||
|
<entry key="_D:/java2/wangpan/java" value="3Ekulj5UUM57tZGI3EQwJeZxtS7" />
|
||||||
|
</persistenceIdMap>
|
||||||
|
</component>
|
||||||
|
<component name="ProjectColorInfo">{
|
||||||
|
"associatedIndex": 4
|
||||||
|
}</component>
|
||||||
|
<component name="ProjectId" id="3Ekulj5UUM57tZGI3EQwJeZxtS7" />
|
||||||
|
<component name="ProjectViewState">
|
||||||
|
<option name="flattenModules" value="true" />
|
||||||
|
<option name="showLibraryContents" value="true" />
|
||||||
|
<option name="showScratchesAndConsoles" value="false" />
|
||||||
|
</component>
|
||||||
|
<component name="PropertiesComponent">{
|
||||||
|
"keyToString": {
|
||||||
|
"Application.Application.executor": "Run",
|
||||||
|
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||||
|
"RequestMappingsPanelOrder0": "0",
|
||||||
|
"RequestMappingsPanelOrder1": "1",
|
||||||
|
"RequestMappingsPanelWidth0": "75",
|
||||||
|
"RequestMappingsPanelWidth1": "75",
|
||||||
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
|
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||||
|
"Spring Boot.DriveServiceApplication.executor": "Run",
|
||||||
|
"Spring Boot.GatewayApplication.executor": "Run",
|
||||||
|
"Spring Boot.TmdbServiceApplication.executor": "Run",
|
||||||
|
"Spring Boot.UserServiceApplication.executor": "Run",
|
||||||
|
"kotlin-language-version-configured": "true",
|
||||||
|
"last_opened_file_path": "D:/java2/wangpan/java",
|
||||||
|
"nodejs_package_manager_path": "npm",
|
||||||
|
"project.structure.last.edited": "Project",
|
||||||
|
"project.structure.proportion": "0.0",
|
||||||
|
"project.structure.side.proportion": "0.0",
|
||||||
|
"run.configurations.included.in.services": "true",
|
||||||
|
"settings.editor.selected.configurable": "preferences.pluginManager",
|
||||||
|
"vue.rearranger.settings.migration": "true"
|
||||||
|
}
|
||||||
|
}</component>
|
||||||
|
<component name="RunDashboard">
|
||||||
|
<option name="configurationTypes">
|
||||||
|
<set>
|
||||||
|
<option value="KtorApplicationConfigurationType" />
|
||||||
|
<option value="MicronautRunConfigurationType" />
|
||||||
|
<option value="QuarkusRunConfigurationType" />
|
||||||
|
<option value="SpringBootApplicationConfigurationType" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
<option name="configurationStatuses">
|
||||||
|
<map>
|
||||||
|
<entry key="SpringBootApplicationConfigurationType">
|
||||||
|
<value>
|
||||||
|
<map>
|
||||||
|
<entry key="DriveServiceApplication" value="STOPPED" />
|
||||||
|
<entry key="GatewayApplication" value="STOPPED" />
|
||||||
|
<entry key="TmdbServiceApplication" value="STOPPED" />
|
||||||
|
<entry key="UserServiceApplication" value="STOPPED" />
|
||||||
|
</map>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
<component name="RunManager" selected="Application.Application">
|
||||||
|
<configuration name="Application" type="Application" factoryName="Application" temporary="true" nameIsGenerated="true">
|
||||||
|
<option name="MAIN_CLASS_NAME" value="com.wangpan.Application" />
|
||||||
|
<module name="wangpan-login" />
|
||||||
|
<extension name="coverage">
|
||||||
|
<pattern>
|
||||||
|
<option name="PATTERN" value="com.wangpan.*" />
|
||||||
|
<option name="ENABLED" value="true" />
|
||||||
|
</pattern>
|
||||||
|
</extension>
|
||||||
|
<method v="2">
|
||||||
|
<option name="Make" enabled="true" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
<configuration name="DriveServiceApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
|
||||||
|
<module name="wangpan-drive-service" />
|
||||||
|
<option name="SPRING_BOOT_MAIN_CLASS" value="com.wangpan.DriveServiceApplication" />
|
||||||
|
<method v="2">
|
||||||
|
<option name="Make" enabled="true" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
<configuration name="GatewayApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
|
||||||
|
<module name="wangpan-gateway" />
|
||||||
|
<option name="SPRING_BOOT_MAIN_CLASS" value="com.wangpan.GatewayApplication" />
|
||||||
|
<method v="2">
|
||||||
|
<option name="Make" enabled="true" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
<configuration name="TmdbServiceApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
|
||||||
|
<module name="wangpan-tmdb-service" />
|
||||||
|
<option name="SPRING_BOOT_MAIN_CLASS" value="com.wangpan.TmdbServiceApplication" />
|
||||||
|
<method v="2">
|
||||||
|
<option name="Make" enabled="true" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
<configuration name="UserServiceApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
|
||||||
|
<module name="wangpan-user-service" />
|
||||||
|
<option name="SPRING_BOOT_MAIN_CLASS" value="com.wangpan.UserServiceApplication" />
|
||||||
|
<method v="2">
|
||||||
|
<option name="Make" enabled="true" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
<recent_temporary>
|
||||||
|
<list>
|
||||||
|
<item itemvalue="Application.Application" />
|
||||||
|
</list>
|
||||||
|
</recent_temporary>
|
||||||
|
</component>
|
||||||
|
<component name="SharedIndexes">
|
||||||
|
<attachedChunks>
|
||||||
|
<set>
|
||||||
|
<option value="bundled-jdk-30f59d01ecdd-cffe25b9f5b3-intellij.indexing.shared.core-IU-253.28294.334" />
|
||||||
|
<option value="bundled-js-predefined-d6986cc7102b-c7e53b3be11b-JavaScript-IU-253.28294.334" />
|
||||||
|
</set>
|
||||||
|
</attachedChunks>
|
||||||
|
</component>
|
||||||
|
<component name="TaskManager">
|
||||||
|
<task active="true" id="Default" summary="Default task">
|
||||||
|
<changelist id="a2d7bfcd-46c4-4e1a-941c-f149d0cd7b56" name="Changes" comment="" />
|
||||||
|
<created>1780735042508</created>
|
||||||
|
<option name="number" value="Default" />
|
||||||
|
<option name="presentableId" value="Default" />
|
||||||
|
<updated>1780735042508</updated>
|
||||||
|
<workItem from="1780735043932" duration="324000" />
|
||||||
|
<workItem from="1780908974643" duration="53000" />
|
||||||
|
<workItem from="1780982378586" duration="749000" />
|
||||||
|
<workItem from="1781080653000" duration="1636000" />
|
||||||
|
<workItem from="1781082319895" duration="1150000" />
|
||||||
|
<workItem from="1781159460445" duration="603000" />
|
||||||
|
<workItem from="1781337048187" duration="8000" />
|
||||||
|
</task>
|
||||||
|
<servers />
|
||||||
|
</component>
|
||||||
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
|
<option name="version" value="3" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"java.configuration.updateBuildConfiguration": "automatic",
|
||||||
|
"java.compile.nullAnalysis.mode": "automatic"
|
||||||
|
}
|
||||||
316
API.md
Normal file
316
API.md
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
# 网盘系统 API 接口文档
|
||||||
|
|
||||||
|
## 网关入口
|
||||||
|
|
||||||
|
**统一访问地址:** `http://localhost:8888`
|
||||||
|
|
||||||
|
所有请求通过网关路由到对应微服务,前端无需关心后端服务拆分。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 用户认证服务 (wangpan-user-service:8081)
|
||||||
|
|
||||||
|
### 1.1 用户登录
|
||||||
|
- **接口:** `POST /api/login`
|
||||||
|
- **请求体:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"username": "用户名",
|
||||||
|
"password": "密码"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **响应:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "登录成功",
|
||||||
|
"data": {
|
||||||
|
"success": true,
|
||||||
|
"token": "访问令牌",
|
||||||
|
"user": { "id": 1, "username": "xxx", ... }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 用户注册
|
||||||
|
- **接口:** `POST /api/register`
|
||||||
|
- **请求体:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"username": "用户名",
|
||||||
|
"password": "密码"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **响应:** 登录成功相同格式
|
||||||
|
|
||||||
|
### 1.3 令牌验证
|
||||||
|
- **接口:** `GET /api/token/validate`
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
- **响应:** 令牌是否有效
|
||||||
|
|
||||||
|
### 1.4 登录日志查询
|
||||||
|
- **接口:** `GET /api/login-logs`
|
||||||
|
- **参数:**
|
||||||
|
- `username` (可选) - 按用户名筛选
|
||||||
|
- `result` (可选) - 按登录结果筛选 (success/fail)
|
||||||
|
- `pageNo` (默认 1) - 页码
|
||||||
|
- `pageSize` (默认 20) - 每页条数
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 网盘管理服务 (wangpan-drive-service:8082)
|
||||||
|
|
||||||
|
### 2.1 创建网盘
|
||||||
|
- **接口:** `POST /api/drive`
|
||||||
|
- **请求体:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "网盘名称"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
### 2.2 查询网盘列表
|
||||||
|
- **接口:** `GET /api/drive`
|
||||||
|
- **参数:**
|
||||||
|
- `pageNo` (默认 1)
|
||||||
|
- `pageSize` (默认 20)
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
### 2.3 查询网盘详情
|
||||||
|
- **接口:** `GET /api/drive/{id}`
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
### 2.4 更新网盘
|
||||||
|
- **接口:** `PUT /api/drive/{id}`
|
||||||
|
- **请求体:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "新网盘名称"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
### 2.5 删除网盘
|
||||||
|
- **接口:** `DELETE /api/drive/{id}`
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
### 2.6 创建文件夹
|
||||||
|
- **接口:** `POST /api/drive/{driveId}/folders`
|
||||||
|
- **请求体:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "文件夹名称",
|
||||||
|
"parentId": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
### 2.7 查询文件夹列表
|
||||||
|
- **接口:** `GET /api/drive/{driveId}/folders`
|
||||||
|
- **参数:**
|
||||||
|
- `parentId` (默认 0) - 父文件夹 ID
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
### 2.8 查询文件夹详情
|
||||||
|
- **接口:** `GET /api/drive/{driveId}/folders/{folderId}`
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
### 2.9 更新文件夹
|
||||||
|
- **接口:** `PUT /api/drive/{driveId}/folders/{folderId}`
|
||||||
|
- **请求体:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "新文件夹名称"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
### 2.10 删除文件夹
|
||||||
|
- **接口:** `DELETE /api/drive/{driveId}/folders/{folderId}`
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
### 2.11 创建电影文件
|
||||||
|
- **接口:** `POST /api/drive/{driveId}/movies`
|
||||||
|
- **请求体:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"title": "电影名称",
|
||||||
|
"folderId": 0,
|
||||||
|
"posterUrl": "海报URL",
|
||||||
|
"overview": "简介",
|
||||||
|
"releaseDate": "2024-01-01",
|
||||||
|
"voteAverage": 8.5,
|
||||||
|
"tmdbId": 12345,
|
||||||
|
"category": "动作",
|
||||||
|
"scrapeType": "auto"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
### 2.12 查询电影文件列表
|
||||||
|
- **接口:** `GET /api/drive/{driveId}/movies`
|
||||||
|
- **参数:**
|
||||||
|
- `folderId` (默认 0) - 文件夹 ID
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
### 2.13 查询电影文件详情
|
||||||
|
- **接口:** `GET /api/drive/{driveId}/movies/{movieId}`
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
### 2.14 更新电影文件
|
||||||
|
- **接口:** `PUT /api/drive/{driveId}/movies/{movieId}`
|
||||||
|
- **请求体:** 同创建
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
### 2.15 删除电影文件
|
||||||
|
- **接口:** `DELETE /api/drive/{driveId}/movies/{movieId}`
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. TMDB 刮削服务 (wangpan-tmdb-service:8083)
|
||||||
|
|
||||||
|
### 3.1 电影列表查询
|
||||||
|
- **接口:** `GET /api/movies`
|
||||||
|
- **参数:**
|
||||||
|
- `keyword` (可选) - 搜索关键词
|
||||||
|
- `status` (可选) - 状态 (pending/scraped/failed)
|
||||||
|
- `category` (可选) - 分类
|
||||||
|
- `scrapeType` (可选) - 刮削类型
|
||||||
|
- `pageNo` (默认 1)
|
||||||
|
- `pageSize` (默认 20)
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
### 3.2 电影详情查询
|
||||||
|
- **接口:** `GET /api/movies/{id}`
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
### 3.3 创建电影
|
||||||
|
- **接口:** `POST /api/movies`
|
||||||
|
- **请求体:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"title": "电影名称"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
### 3.4 更新电影
|
||||||
|
- **接口:** `PUT /api/movies/{id}`
|
||||||
|
- **请求体:** 电影更新字段
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
### 3.5 删除电影
|
||||||
|
- **接口:** `DELETE /api/movies/{id}`
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
### 3.6 批量删除电影
|
||||||
|
- **接口:** `POST /api/movies/batch-delete`
|
||||||
|
- **请求体:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ids": [1, 2, 3]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
### 3.7 电影统计
|
||||||
|
- **接口:** `GET /api/movies/stats`
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
### 3.8 TMDB API Key 获取
|
||||||
|
- **接口:** `GET /api/tmdb/key`
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
### 3.9 TMDB API Key 设置
|
||||||
|
- **接口:** `POST /api/tmdb/key`
|
||||||
|
- **请求体:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tmdbApiKey": "你的API Key"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
### 3.10 单部电影刮削
|
||||||
|
- **接口:** `POST /api/tmdb-scraper/scrape`
|
||||||
|
- **请求体:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"movieId": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
### 3.11 批量刮削
|
||||||
|
- **接口:** `POST /api/tmdb-scraper/batch-scrape`
|
||||||
|
- **请求体:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"movieIds": [1, 2, 3]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
### 3.12 查询缓存列表
|
||||||
|
- **接口:** `GET /api/tmdb-scraper/caches`
|
||||||
|
- **参数:**
|
||||||
|
- `pageNo` (默认 1)
|
||||||
|
- `pageSize` (默认 20)
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
### 3.13 刮削统计
|
||||||
|
- **接口:** `GET /api/tmdb-scraper/stats`
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
### 3.14 手动触发定时任务
|
||||||
|
- **接口:** `POST /api/tmdb-scraper/trigger`
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
### 3.15 查询刮削日志
|
||||||
|
- **接口:** `GET /api/tmdb-scraper/scraper-logs`
|
||||||
|
- **参数:**
|
||||||
|
- `pageNo` (默认 1)
|
||||||
|
- `pageSize` (默认 20)
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
### 3.16 删除缓存
|
||||||
|
- **接口:** `DELETE /api/tmdb-scraper/cache/{movieId}`
|
||||||
|
- **请求头:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 通用响应格式
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "操作结果描述",
|
||||||
|
"data": { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 状态码说明
|
||||||
|
| 状态码 | 说明 |
|
||||||
|
|--------|------|
|
||||||
|
| 200 | 成功 |
|
||||||
|
| 400 | 请求参数错误 |
|
||||||
|
| 401 | 未授权/令牌无效 |
|
||||||
|
| 403 | 权限不足 |
|
||||||
|
| 404 | 资源不存在 |
|
||||||
|
| 500 | 服务器内部错误 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 服务架构说明
|
||||||
|
|
||||||
|
```
|
||||||
|
前端 → Gateway(8888) → 路由转发 → 微服务
|
||||||
|
├── user-service(8081)
|
||||||
|
├── drive-service(8082)
|
||||||
|
└── tmdb-service(8083)
|
||||||
|
```
|
||||||
|
|
||||||
|
所有服务通过网关统一路由转发,各服务独立运行。
|
||||||
67
pom.xml
Normal file
67
pom.xml
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>com.wangpan</groupId>
|
||||||
|
<artifactId>wangpan-parent</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
<packaging>pom</packaging>
|
||||||
|
|
||||||
|
<name>WangPan Parent</name>
|
||||||
|
<description>网盘系统 - 微服务架构父 POM</description>
|
||||||
|
|
||||||
|
<!-- 子模块 -->
|
||||||
|
<modules>
|
||||||
|
<module>wangpan-common</module>
|
||||||
|
<module>wangpan-user-service</module>
|
||||||
|
<module>wangpan-drive-service</module>
|
||||||
|
<module>wangpan-tmdb-service</module>
|
||||||
|
<module>wangpan-gateway</module>
|
||||||
|
</modules>
|
||||||
|
|
||||||
|
<!-- Spring Boot 父依赖 -->
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>3.2.5</version>
|
||||||
|
<relativePath/>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>17</maven.compiler.source>
|
||||||
|
<maven.compiler.target>17</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||||
|
|
||||||
|
<!-- 依赖版本 -->
|
||||||
|
<mysql.version>8.0.33</mysql.version>
|
||||||
|
<minio.version>8.5.7</minio.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<!-- 公共模块 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.wangpan</groupId>
|
||||||
|
<artifactId>wangpan-common</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<source>17</source>
|
||||||
|
<target>17</target>
|
||||||
|
<encoding>UTF-8</encoding>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
68
wangpan-common/pom.xml
Normal file
68
wangpan-common/pom.xml
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>com.wangpan</groupId>
|
||||||
|
<artifactId>wangpan-parent</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>wangpan-common</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<name>WangPan Common</name>
|
||||||
|
<description>公共模块 - 实体类、工具类、通用响应</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<mybatis-plus.version>3.5.7</mybatis-plus.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- Spring Boot Web (仅用于注解) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- MyBatis-Plus Spring Boot 3 Starter -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.baomidou</groupId>
|
||||||
|
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||||
|
<version>${mybatis-plus.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- MySQL驱动 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.mysql</groupId>
|
||||||
|
<artifactId>mysql-connector-j</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Jasypt加密库 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jasypt</groupId>
|
||||||
|
<artifactId>jasypt</artifactId>
|
||||||
|
<version>1.9.3</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- MinIO对象存储SDK -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.minio</groupId>
|
||||||
|
<artifactId>minio</artifactId>
|
||||||
|
<version>${minio.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Jackson -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||||
|
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
242
wangpan-common/src/main/java/com/wangpan/common/Result.java
Normal file
242
wangpan-common/src/main/java/com/wangpan/common/Result.java
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
package com.wangpan.common;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一API响应结果类
|
||||||
|
* 功能:封装所有API接口的返回数据格式
|
||||||
|
*
|
||||||
|
* 设计原则:
|
||||||
|
* - 统一返回格式,便于前端解析
|
||||||
|
* - 包含状态码、消息、数据和时间戳
|
||||||
|
* - 支持泛型,可返回任意类型的数据
|
||||||
|
*
|
||||||
|
* 前端使用示例:
|
||||||
|
* {
|
||||||
|
* "code": 200,
|
||||||
|
* "message": "操作成功",
|
||||||
|
* "data": {...},
|
||||||
|
* "timestamp": 1686000000000
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public class Result<T> implements Serializable {
|
||||||
|
|
||||||
|
// 序列化版本号
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
// ==================== 响应状态码常量 ====================
|
||||||
|
|
||||||
|
/** 操作成功 */
|
||||||
|
public static final int CODE_SUCCESS = 200;
|
||||||
|
|
||||||
|
/** 参数错误(400)- 请求参数缺失或格式错误 */
|
||||||
|
public static final int CODE_BAD_REQUEST = 400;
|
||||||
|
|
||||||
|
/** 未授权(401)- 用户未登录或token无效 */
|
||||||
|
public static final int CODE_UNAUTHORIZED = 401;
|
||||||
|
|
||||||
|
/** 禁止访问(403)- 无权限或账户被禁用 */
|
||||||
|
public static final int CODE_FORBIDDEN = 403;
|
||||||
|
|
||||||
|
/** 资源不存在(404)- 用户不存在等 */
|
||||||
|
public static final int CODE_NOT_FOUND = 404;
|
||||||
|
|
||||||
|
/** 请求过于频繁(429)- 登录失败次数过多被限流 */
|
||||||
|
public static final int CODE_TOO_MANY_REQUESTS = 429;
|
||||||
|
|
||||||
|
/** 服务器内部错误(500)- 系统异常 */
|
||||||
|
public static final int CODE_SERVER_ERROR = 500;
|
||||||
|
|
||||||
|
// ==================== 属性字段 ====================
|
||||||
|
|
||||||
|
/** 响应状态码:200成功,其他为失败 */
|
||||||
|
private int code;
|
||||||
|
|
||||||
|
/** 响应消息:描述操作结果的文字信息 */
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
/** 响应数据:具体的业务数据对象 */
|
||||||
|
private T data;
|
||||||
|
|
||||||
|
/** 时间戳:服务器处理请求的时间 */
|
||||||
|
private long timestamp;
|
||||||
|
|
||||||
|
// ==================== 构造方法 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 私有构造方法:强制使用静态工厂方法创建实例
|
||||||
|
*/
|
||||||
|
private Result() {
|
||||||
|
this.timestamp = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 带参数的私有构造方法
|
||||||
|
*
|
||||||
|
* @param code 状态码
|
||||||
|
* @param message 消息
|
||||||
|
* @param data 数据
|
||||||
|
*/
|
||||||
|
private Result(int code, String message, T data) {
|
||||||
|
this.code = code;
|
||||||
|
this.message = message;
|
||||||
|
this.data = data;
|
||||||
|
this.timestamp = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 静态工厂方法(推荐使用) ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建成功响应(无数据)
|
||||||
|
*
|
||||||
|
* @return 成功的Result对象
|
||||||
|
*/
|
||||||
|
public static <T> Result<T> success() {
|
||||||
|
return new Result<>(CODE_SUCCESS, "操作成功", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建成功响应(带消息)
|
||||||
|
*
|
||||||
|
* @param message 成功消息
|
||||||
|
* @return 成功的Result对象
|
||||||
|
*/
|
||||||
|
public static <T> Result<T> success(String message) {
|
||||||
|
return new Result<>(CODE_SUCCESS, message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建成功响应(带数据)
|
||||||
|
* 最常用的成功响应方法
|
||||||
|
*
|
||||||
|
* @param data 要返回的数据对象
|
||||||
|
* @return 包含数据的成功Result对象
|
||||||
|
*/
|
||||||
|
public static <T> Result<T> success(T data) {
|
||||||
|
return new Result<>(CODE_SUCCESS, "操作成功", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建成功响应(带消息和数据)
|
||||||
|
*
|
||||||
|
* @param message 成功消息
|
||||||
|
* @param data 返回的数据
|
||||||
|
* @return Result对象
|
||||||
|
*/
|
||||||
|
public static <T> Result<T> success(String message, T data) {
|
||||||
|
return new Result<>(CODE_SUCCESS, message, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建失败响应(仅包含错误消息)
|
||||||
|
*
|
||||||
|
* @param message 错误信息
|
||||||
|
* @return 失败的Result对象
|
||||||
|
*/
|
||||||
|
public static <T> Result<T> error(String message) {
|
||||||
|
return new Result<>(CODE_SERVER_ERROR, message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建失败响应(指定状态码和消息)
|
||||||
|
*
|
||||||
|
* @param code 错误状态码
|
||||||
|
* @param message 错误信息
|
||||||
|
* @return 失败的Result对象
|
||||||
|
*/
|
||||||
|
public static <T> Result<T> error(int code, String message) {
|
||||||
|
return new Result<>(code, message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 快捷失败方法 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参数错误响应(400)
|
||||||
|
* 用于参数校验失败的场景
|
||||||
|
*
|
||||||
|
* @param message 错误详情
|
||||||
|
* @return 400状态码的Result
|
||||||
|
*/
|
||||||
|
public static <T> Result<T> badRequest(String message) {
|
||||||
|
return new Result<>(CODE_BAD_REQUEST, message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 未授权响应(401)
|
||||||
|
* 用于用户未登录或认证失败的场景
|
||||||
|
*
|
||||||
|
* @param message 错误详情
|
||||||
|
* @return 401状态码的Result
|
||||||
|
*/
|
||||||
|
public static <T> Result<T> unauthorized(String message) {
|
||||||
|
return new Result<>(CODE_UNAUTHORIZED, message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 禁止访问响应(403)
|
||||||
|
* 用于权限不足或账户被禁用的场景
|
||||||
|
*
|
||||||
|
* @param message 错误详情
|
||||||
|
* @return 403状态码的Result
|
||||||
|
*/
|
||||||
|
public static <T> Result<T> forbidden(String message) {
|
||||||
|
return new Result<>(CODE_FORBIDDEN, message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 资源不存在响应(404)
|
||||||
|
* 用于查询的用户不存在等场景
|
||||||
|
*
|
||||||
|
* @param message 错误详情
|
||||||
|
* @return 404状态码的Result
|
||||||
|
*/
|
||||||
|
public static <T> Result<T> notFound(String message) {
|
||||||
|
return new Result<>(CODE_NOT_FOUND, message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求频繁响应(429)
|
||||||
|
* 用于登录失败次数过多被限流的场景
|
||||||
|
*
|
||||||
|
* @param message 错误详情
|
||||||
|
* @return 429状态码的Result
|
||||||
|
*/
|
||||||
|
public static <T> Result<T> tooManyRequests(String message) {
|
||||||
|
return new Result<>(CODE_TOO_MANY_REQUESTS, message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Getter和Setter方法 ====================
|
||||||
|
|
||||||
|
public int getCode() { return code; }
|
||||||
|
public void setCode(int code) { this.code = code; }
|
||||||
|
|
||||||
|
public String getMessage() { return message; }
|
||||||
|
public void setMessage(String message) { this.message = message; }
|
||||||
|
|
||||||
|
public T getData() { return data; }
|
||||||
|
public void setData(T data) { this.data = data; }
|
||||||
|
|
||||||
|
public long getTimestamp() { return timestamp; }
|
||||||
|
public void setTimestamp(long timestamp) { this.timestamp = timestamp; }
|
||||||
|
|
||||||
|
// ==================== 判断方法 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否为成功响应
|
||||||
|
*
|
||||||
|
* @return true-成功,false-失败
|
||||||
|
*/
|
||||||
|
public boolean isSuccess() {
|
||||||
|
return code == CODE_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Result{" +
|
||||||
|
"code=" + code +
|
||||||
|
", message='" + message + '\'' +
|
||||||
|
", data=" + data +
|
||||||
|
", timestamp=" + timestamp +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package com.wangpan.common.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记需要管理员权限的接口
|
||||||
|
* 配合 AuthInterceptor 使用,在 RequireAuth 基础上额外校验管理员身份
|
||||||
|
*/
|
||||||
|
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface RequireAdmin {
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package com.wangpan.common.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记需要用户认证的接口
|
||||||
|
* 配合 AuthInterceptor 使用,自动校验 Token 并将用户信息注入请求属性
|
||||||
|
*/
|
||||||
|
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface RequireAuth {
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
package com.wangpan.common.config;
|
||||||
|
|
||||||
|
import com.wangpan.common.interceptor.AuthInterceptor;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Web MVC 配置
|
||||||
|
* 注册 AuthInterceptor 拦截器,统一处理认证和权限校验
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class WebMvcConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
private final AuthInterceptor authInterceptor;
|
||||||
|
|
||||||
|
public WebMvcConfig(AuthInterceptor authInterceptor) {
|
||||||
|
this.authInterceptor = authInterceptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
|
// 注册认证拦截器,拦截所有请求
|
||||||
|
registry.addInterceptor(authInterceptor)
|
||||||
|
.addPathPatterns("/**")
|
||||||
|
.excludePathPatterns(
|
||||||
|
"/api/login", // 登录接口
|
||||||
|
"/api/register" // 注册接口
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,96 @@
|
|||||||
|
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.TableLogic;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网盘数据对象(Data Object)
|
||||||
|
* 对应数据库表:wp_drive
|
||||||
|
*
|
||||||
|
* 设计说明:
|
||||||
|
* - DO 字段名使用 Java 驼峰命名,MyBatis-Plus 自动映射为数据库 snake_case 列
|
||||||
|
* - DO 仅负责数据映射,不包含业务逻辑
|
||||||
|
* - 与 Entity 分离:Entity 面向业务层,DO 面向数据层
|
||||||
|
*/
|
||||||
|
@TableName(value = "wp_drive", autoResultMap = true)
|
||||||
|
public class DriveDO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** 网盘ID:主键,自增长 */
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 所属用户ID */
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/** 网盘名称 */
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/** 网盘描述 */
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
/** 网盘 Logo URL(MinIO 存储链接) */
|
||||||
|
private String logoUrl;
|
||||||
|
|
||||||
|
/** 文件夹数量 */
|
||||||
|
private Integer folderCount;
|
||||||
|
|
||||||
|
/** 文件数量 */
|
||||||
|
private Integer fileCount;
|
||||||
|
|
||||||
|
/** 总大小(字节) */
|
||||||
|
private Long totalSize;
|
||||||
|
|
||||||
|
/** 状态:active / disabled */
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/** 更新时间 */
|
||||||
|
@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 getName() { return name; }
|
||||||
|
public void setName(String name) { this.name = name; }
|
||||||
|
|
||||||
|
public String getDescription() { return description; }
|
||||||
|
public void setDescription(String description) { this.description = description; }
|
||||||
|
|
||||||
|
public String getLogoUrl() { return logoUrl; }
|
||||||
|
public void setLogoUrl(String logoUrl) { this.logoUrl = logoUrl; }
|
||||||
|
|
||||||
|
public Integer getFolderCount() { return folderCount; }
|
||||||
|
public void setFolderCount(Integer folderCount) { this.folderCount = folderCount; }
|
||||||
|
|
||||||
|
public Integer getFileCount() { return fileCount; }
|
||||||
|
public void setFileCount(Integer fileCount) { this.fileCount = fileCount; }
|
||||||
|
|
||||||
|
public Long getTotalSize() { return totalSize; }
|
||||||
|
public void setTotalSize(Long totalSize) { this.totalSize = totalSize; }
|
||||||
|
|
||||||
|
public String getStatus() { return status; }
|
||||||
|
public void setStatus(String status) { this.status = status; }
|
||||||
|
|
||||||
|
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; }
|
||||||
|
}
|
||||||
@ -0,0 +1,106 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网盘文件夹数据对象(Data Object)
|
||||||
|
* 对应数据库表:wp_drive_folder
|
||||||
|
*
|
||||||
|
* 设计说明:
|
||||||
|
* - DO 字段名使用 Java 驼峰命名,MyBatis-Plus 自动映射为数据库 snake_case 列
|
||||||
|
* - DO 仅负责数据映射,不包含业务逻辑
|
||||||
|
*/
|
||||||
|
@TableName(value = "wp_drive_folder", autoResultMap = true)
|
||||||
|
public class DriveFolderDO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** 文件夹ID:主键,自增长 */
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 所属网盘ID */
|
||||||
|
private Long driveId;
|
||||||
|
|
||||||
|
/** 所属用户ID */
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/** 父文件夹ID(0=根目录) */
|
||||||
|
private Long parentId;
|
||||||
|
|
||||||
|
/** 文件夹名称 */
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/** 文件夹描述 */
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
/** 排序权重 */
|
||||||
|
private Integer sortOrder;
|
||||||
|
|
||||||
|
/** 层级深度(根目录为0) */
|
||||||
|
private Integer depth;
|
||||||
|
|
||||||
|
/** 子文件夹数量 */
|
||||||
|
private Integer childFolderCount;
|
||||||
|
|
||||||
|
/** 文件数量 */
|
||||||
|
private Integer fileCount;
|
||||||
|
|
||||||
|
/** 总大小(字节) */
|
||||||
|
private Long totalSize;
|
||||||
|
|
||||||
|
/** 更新时间 */
|
||||||
|
@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 getDriveId() { return driveId; }
|
||||||
|
public void setDriveId(Long driveId) { this.driveId = driveId; }
|
||||||
|
|
||||||
|
public Long getUserId() { return userId; }
|
||||||
|
public void setUserId(Long userId) { this.userId = userId; }
|
||||||
|
|
||||||
|
public Long getParentId() { return parentId; }
|
||||||
|
public void setParentId(Long parentId) { this.parentId = parentId; }
|
||||||
|
|
||||||
|
public String getName() { return name; }
|
||||||
|
public void setName(String name) { this.name = name; }
|
||||||
|
|
||||||
|
public String getDescription() { return description; }
|
||||||
|
public void setDescription(String description) { this.description = description; }
|
||||||
|
|
||||||
|
public Integer getSortOrder() { return sortOrder; }
|
||||||
|
public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; }
|
||||||
|
|
||||||
|
public Integer getDepth() { return depth; }
|
||||||
|
public void setDepth(Integer depth) { this.depth = depth; }
|
||||||
|
|
||||||
|
public Integer getChildFolderCount() { return childFolderCount; }
|
||||||
|
public void setChildFolderCount(Integer childFolderCount) { this.childFolderCount = childFolderCount; }
|
||||||
|
|
||||||
|
public Integer getFileCount() { return fileCount; }
|
||||||
|
public void setFileCount(Integer fileCount) { this.fileCount = fileCount; }
|
||||||
|
|
||||||
|
public Long getTotalSize() { return totalSize; }
|
||||||
|
public void setTotalSize(Long totalSize) { this.totalSize = totalSize; }
|
||||||
|
|
||||||
|
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; }
|
||||||
|
}
|
||||||
@ -0,0 +1,167 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网盘电影数据对象(Data Object)
|
||||||
|
* 对应数据库表:wp_drive_movie
|
||||||
|
*
|
||||||
|
* 设计说明:
|
||||||
|
* - DO 字段名使用 Java 驼峰命名,MyBatis-Plus 自动映射为数据库 snake_case 列
|
||||||
|
* - DO 仅负责数据映射,不包含业务逻辑
|
||||||
|
* - minioUrl 和 posterUrl 在数据库中为加密存储,DO 中存储的是解密后的明文
|
||||||
|
*/
|
||||||
|
@TableName(value = "wp_drive_movie", autoResultMap = true)
|
||||||
|
public class DriveMovieDO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** 电影ID:主键,自增长 */
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 所属网盘ID */
|
||||||
|
private Long driveId;
|
||||||
|
|
||||||
|
/** 所属文件夹ID(0=根目录) */
|
||||||
|
private Long folderId;
|
||||||
|
|
||||||
|
/** 所属用户ID */
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/** 电影标题 */
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
/** 电影副标题 */
|
||||||
|
private String subtitle;
|
||||||
|
|
||||||
|
/** 原始文件名 */
|
||||||
|
private String originalName;
|
||||||
|
|
||||||
|
/** MinIO 对象名称 */
|
||||||
|
private String minioObjectName;
|
||||||
|
|
||||||
|
/** MinIO 访问 URL(加密存储,DO 中为解密后的明文) */
|
||||||
|
private String minioUrl;
|
||||||
|
|
||||||
|
/** MIME 类型 */
|
||||||
|
private String mimeType;
|
||||||
|
|
||||||
|
/** 文件大小(字节) */
|
||||||
|
private Long fileSize;
|
||||||
|
|
||||||
|
/** 文件扩展名 */
|
||||||
|
private String extension;
|
||||||
|
|
||||||
|
/** 电影描述 */
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
/** 资源库来源 */
|
||||||
|
private String resourceLibrary;
|
||||||
|
|
||||||
|
/** 维护者 */
|
||||||
|
private String maintainer;
|
||||||
|
|
||||||
|
/** 状态:active / deleted */
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/** 分类 */
|
||||||
|
private String category;
|
||||||
|
|
||||||
|
/** 刮削类型:tmdb / manual / none */
|
||||||
|
private String scrapeType;
|
||||||
|
|
||||||
|
/** 评分(0-100) */
|
||||||
|
private Integer rating;
|
||||||
|
|
||||||
|
/** 海报 URL(加密存储,DO 中为解密后的明文) */
|
||||||
|
private String posterUrl;
|
||||||
|
|
||||||
|
/** 标签(逗号分隔) */
|
||||||
|
private String tags;
|
||||||
|
|
||||||
|
/** 更新时间 */
|
||||||
|
@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 getDriveId() { return driveId; }
|
||||||
|
public void setDriveId(Long driveId) { this.driveId = driveId; }
|
||||||
|
|
||||||
|
public Long getFolderId() { return folderId; }
|
||||||
|
public void setFolderId(Long folderId) { this.folderId = folderId; }
|
||||||
|
|
||||||
|
public Long getUserId() { return userId; }
|
||||||
|
public void setUserId(Long userId) { this.userId = userId; }
|
||||||
|
|
||||||
|
public String getTitle() { return title; }
|
||||||
|
public void setTitle(String title) { this.title = title; }
|
||||||
|
|
||||||
|
public String getSubtitle() { return subtitle; }
|
||||||
|
public void setSubtitle(String subtitle) { this.subtitle = subtitle; }
|
||||||
|
|
||||||
|
public String getOriginalName() { return originalName; }
|
||||||
|
public void setOriginalName(String originalName) { this.originalName = originalName; }
|
||||||
|
|
||||||
|
public String getMinioObjectName() { return minioObjectName; }
|
||||||
|
public void setMinioObjectName(String minioObjectName) { this.minioObjectName = minioObjectName; }
|
||||||
|
|
||||||
|
public String getMinioUrl() { return minioUrl; }
|
||||||
|
public void setMinioUrl(String minioUrl) { this.minioUrl = minioUrl; }
|
||||||
|
|
||||||
|
public String getMimeType() { return mimeType; }
|
||||||
|
public void setMimeType(String mimeType) { this.mimeType = mimeType; }
|
||||||
|
|
||||||
|
public Long getFileSize() { return fileSize; }
|
||||||
|
public void setFileSize(Long fileSize) { this.fileSize = fileSize; }
|
||||||
|
|
||||||
|
public String getExtension() { return extension; }
|
||||||
|
public void setExtension(String extension) { this.extension = extension; }
|
||||||
|
|
||||||
|
public String getDescription() { return description; }
|
||||||
|
public void setDescription(String description) { this.description = description; }
|
||||||
|
|
||||||
|
public String getResourceLibrary() { return resourceLibrary; }
|
||||||
|
public void setResourceLibrary(String resourceLibrary) { this.resourceLibrary = resourceLibrary; }
|
||||||
|
|
||||||
|
public String getMaintainer() { return maintainer; }
|
||||||
|
public void setMaintainer(String maintainer) { this.maintainer = maintainer; }
|
||||||
|
|
||||||
|
public String getStatus() { return status; }
|
||||||
|
public void setStatus(String status) { this.status = status; }
|
||||||
|
|
||||||
|
public String getCategory() { return category; }
|
||||||
|
public void setCategory(String category) { this.category = category; }
|
||||||
|
|
||||||
|
public String getScrapeType() { return scrapeType; }
|
||||||
|
public void setScrapeType(String scrapeType) { this.scrapeType = scrapeType; }
|
||||||
|
|
||||||
|
public Integer getRating() { return rating; }
|
||||||
|
public void setRating(Integer rating) { this.rating = rating; }
|
||||||
|
|
||||||
|
public String getPosterUrl() { return posterUrl; }
|
||||||
|
public void setPosterUrl(String posterUrl) { this.posterUrl = posterUrl; }
|
||||||
|
|
||||||
|
public String getTags() { return tags; }
|
||||||
|
public void setTags(String tags) { this.tags = tags; }
|
||||||
|
|
||||||
|
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; }
|
||||||
|
}
|
||||||
@ -0,0 +1,159 @@
|
|||||||
|
package com.wangpan.common.dal.dataobject;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon 数据对象(Data Object)
|
||||||
|
* 对应数据库表:wp_icon
|
||||||
|
*
|
||||||
|
* 设计说明:
|
||||||
|
* - DO 字段名使用 Java 驼峰命名,MyBatis-Plus 自动映射为数据库 snake_case 列
|
||||||
|
* - DO 仅负责数据映射,不包含业务逻辑
|
||||||
|
* - 与 Entity 分离:Entity 面向业务层,DO 面向数据层
|
||||||
|
*/
|
||||||
|
@TableName("wp_icon")
|
||||||
|
public class IconDO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** Icon ID:主键,自增长 */
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 所属用户ID */
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/** Icon 名称 */
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/** Icon 链接(外网 URL) */
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
/** 内网链接 URL */
|
||||||
|
private String internalUrl;
|
||||||
|
|
||||||
|
/** 链接类型:external(仅外网)/ internal(仅内网)/ both(内外网) */
|
||||||
|
private String linkType;
|
||||||
|
|
||||||
|
/** Icon 描述 */
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
/** 排序值(数字越小越靠前) */
|
||||||
|
private Integer sortOrder;
|
||||||
|
|
||||||
|
/** 状态:active / disabled */
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/** Logo 图片 URL(MinIO 存储) */
|
||||||
|
private String logoUrl;
|
||||||
|
|
||||||
|
/** 创建时间 */
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
/** 更新时间 */
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
|
||||||
|
// ==================== 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 getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUrl() {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUrl(String url) {
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getInternalUrl() {
|
||||||
|
return internalUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInternalUrl(String internalUrl) {
|
||||||
|
this.internalUrl = internalUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLinkType() {
|
||||||
|
return linkType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLinkType(String linkType) {
|
||||||
|
this.linkType = linkType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDescription(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getSortOrder() {
|
||||||
|
return sortOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSortOrder(Integer sortOrder) {
|
||||||
|
this.sortOrder = sortOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(String status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLogoUrl() {
|
||||||
|
return logoUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLogoUrl(String logoUrl) {
|
||||||
|
this.logoUrl = logoUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录日志数据对象(Data Object)
|
||||||
|
* 对应数据库表:wp_login_log
|
||||||
|
*
|
||||||
|
* 设计说明:
|
||||||
|
* - DO 字段名使用 Java 驼峰命名,MyBatis-Plus 自动映射为数据库 snake_case 列
|
||||||
|
* - DO 仅负责数据映射,不包含业务逻辑
|
||||||
|
* - 日志是只追加的审计记录,不提供 UPDATE/DELETE 操作
|
||||||
|
*/
|
||||||
|
@TableName(value = "wp_login_log", autoResultMap = true)
|
||||||
|
public class LoginLogDO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** 记录ID */
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 用户ID(可能为null,如用户名不存在时) */
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/** 登录输入的用户名 */
|
||||||
|
private String loginUsername;
|
||||||
|
|
||||||
|
/** 客户端IP */
|
||||||
|
private String loginIp;
|
||||||
|
|
||||||
|
/** 登录结果:success / fail */
|
||||||
|
private String loginResult;
|
||||||
|
|
||||||
|
/** 失败原因(成功时为null) */
|
||||||
|
private String failReason;
|
||||||
|
|
||||||
|
/** 浏览器UA信息 */
|
||||||
|
private String userAgent;
|
||||||
|
|
||||||
|
/** 登录时间 */
|
||||||
|
@TableField(fill = com.baomidou.mybatisplus.annotation.FieldFill.INSERT)
|
||||||
|
private LocalDateTime loginTime;
|
||||||
|
|
||||||
|
// ==================== 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 getLoginUsername() { return loginUsername; }
|
||||||
|
public void setLoginUsername(String loginUsername) { this.loginUsername = loginUsername; }
|
||||||
|
|
||||||
|
public String getLoginIp() { return loginIp; }
|
||||||
|
public void setLoginIp(String loginIp) { this.loginIp = loginIp; }
|
||||||
|
|
||||||
|
public String getLoginResult() { return loginResult; }
|
||||||
|
public void setLoginResult(String loginResult) { this.loginResult = loginResult; }
|
||||||
|
|
||||||
|
public String getFailReason() { return failReason; }
|
||||||
|
public void setFailReason(String failReason) { this.failReason = failReason; }
|
||||||
|
|
||||||
|
public String getUserAgent() { return userAgent; }
|
||||||
|
public void setUserAgent(String userAgent) { this.userAgent = userAgent; }
|
||||||
|
|
||||||
|
public LocalDateTime getLoginTime() { return loginTime; }
|
||||||
|
public void setLoginTime(LocalDateTime loginTime) { this.loginTime = loginTime; }
|
||||||
|
}
|
||||||
@ -0,0 +1,112 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 电影数据对象(Data Object)
|
||||||
|
* 对应数据库表:wp_movie
|
||||||
|
*
|
||||||
|
* 设计说明:
|
||||||
|
* - DO 字段名使用 Java 驼峰命名,MyBatis-Plus 自动映射为数据库 snake_case 列
|
||||||
|
* - DO 仅负责数据映射,不包含业务逻辑
|
||||||
|
*/
|
||||||
|
@TableName(value = "wp_movie", autoResultMap = true)
|
||||||
|
public class MovieDO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** 电影ID:主键,自增长 */
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 所属用户ID */
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/** 电影标题 */
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
/** 副标题 */
|
||||||
|
private String subtitle;
|
||||||
|
|
||||||
|
/** 资源库名称 */
|
||||||
|
private String resourceLibrary;
|
||||||
|
|
||||||
|
/** 维护人/来源 */
|
||||||
|
private String maintainer;
|
||||||
|
|
||||||
|
/** 状态:pending_review / reviewed / published / archived */
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/** 分类 */
|
||||||
|
private String category;
|
||||||
|
|
||||||
|
/** 刮削类型:auto / manual / none */
|
||||||
|
private String scrapeType;
|
||||||
|
|
||||||
|
/** 评分(0-100) */
|
||||||
|
private Integer rating;
|
||||||
|
|
||||||
|
/** 海报图片URL */
|
||||||
|
private String posterUrl;
|
||||||
|
|
||||||
|
/** 标签(逗号分隔) */
|
||||||
|
private String tags;
|
||||||
|
|
||||||
|
/** 更新时间 */
|
||||||
|
@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 getTitle() { return title; }
|
||||||
|
public void setTitle(String title) { this.title = title; }
|
||||||
|
|
||||||
|
public String getSubtitle() { return subtitle; }
|
||||||
|
public void setSubtitle(String subtitle) { this.subtitle = subtitle; }
|
||||||
|
|
||||||
|
public String getResourceLibrary() { return resourceLibrary; }
|
||||||
|
public void setResourceLibrary(String resourceLibrary) { this.resourceLibrary = resourceLibrary; }
|
||||||
|
|
||||||
|
public String getMaintainer() { return maintainer; }
|
||||||
|
public void setMaintainer(String maintainer) { this.maintainer = maintainer; }
|
||||||
|
|
||||||
|
public String getStatus() { return status; }
|
||||||
|
public void setStatus(String status) { this.status = status; }
|
||||||
|
|
||||||
|
public String getCategory() { return category; }
|
||||||
|
public void setCategory(String category) { this.category = category; }
|
||||||
|
|
||||||
|
public String getScrapeType() { return scrapeType; }
|
||||||
|
public void setScrapeType(String scrapeType) { this.scrapeType = scrapeType; }
|
||||||
|
|
||||||
|
public Integer getRating() { return rating; }
|
||||||
|
public void setRating(Integer rating) { this.rating = rating; }
|
||||||
|
|
||||||
|
public String getPosterUrl() { return posterUrl; }
|
||||||
|
public void setPosterUrl(String posterUrl) { this.posterUrl = posterUrl; }
|
||||||
|
|
||||||
|
public String getTags() { return tags; }
|
||||||
|
public void setTags(String tags) { this.tags = tags; }
|
||||||
|
|
||||||
|
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; }
|
||||||
|
}
|
||||||
@ -0,0 +1,160 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TMDB 刮削缓存数据对象(Data Object)
|
||||||
|
* 对应数据库表:wp_tmdb_cache
|
||||||
|
*
|
||||||
|
* 设计说明:
|
||||||
|
* - DO 字段名使用 Java 驼峰命名,MyBatis-Plus 自动映射为数据库 snake_case 列
|
||||||
|
* - DO 仅负责数据映射,不包含业务逻辑
|
||||||
|
* - localPosterUrl 在数据库中为加密存储,DO 中存储的是解密后的明文
|
||||||
|
*/
|
||||||
|
@TableName(value = "wp_tmdb_cache", autoResultMap = true)
|
||||||
|
public class TmdbCacheDO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** 记录ID */
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 关联电影ID */
|
||||||
|
private Long movieId;
|
||||||
|
|
||||||
|
/** 关联用户ID */
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/** TMDB 电影ID */
|
||||||
|
private Integer tmdbId;
|
||||||
|
|
||||||
|
/** 电影标题(来自TMDB) */
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
/** 原始标题(外文) */
|
||||||
|
private String originalTitle;
|
||||||
|
|
||||||
|
/** 剧情简介 */
|
||||||
|
private String overview;
|
||||||
|
|
||||||
|
/** 上映日期 */
|
||||||
|
private String releaseDate;
|
||||||
|
|
||||||
|
/** TMDB 海报路径 */
|
||||||
|
private String posterPath;
|
||||||
|
|
||||||
|
/** TMDB 背景图路径 */
|
||||||
|
private String backdropPath;
|
||||||
|
|
||||||
|
/** 本地海报URL(加密存储,DO 中为解密后的明文) */
|
||||||
|
private String localPosterUrl;
|
||||||
|
|
||||||
|
/** TMDB 评分(0-100分制) */
|
||||||
|
private Integer tmdbRating;
|
||||||
|
|
||||||
|
/** TMDB 投票数 */
|
||||||
|
private Integer voteCount;
|
||||||
|
|
||||||
|
/** 片长(分钟) */
|
||||||
|
private Integer runtime;
|
||||||
|
|
||||||
|
/** 类型(逗号分隔) */
|
||||||
|
private String genres;
|
||||||
|
|
||||||
|
/** 导演(逗号分隔) */
|
||||||
|
private String directors;
|
||||||
|
|
||||||
|
/** 主演(逗号分隔) */
|
||||||
|
private String casts;
|
||||||
|
|
||||||
|
/** 原始语言 */
|
||||||
|
private String originalLanguage;
|
||||||
|
|
||||||
|
/** 制片国家 */
|
||||||
|
private String productionCountries;
|
||||||
|
|
||||||
|
/** 缓存状态:active / expired */
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/** 缓存时间 */
|
||||||
|
private LocalDateTime cacheTime;
|
||||||
|
|
||||||
|
/** 更新时间 */
|
||||||
|
@TableField(update = "NOW()")
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
|
||||||
|
// ==================== Getter 和 Setter ====================
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
|
public Long getMovieId() { return movieId; }
|
||||||
|
public void setMovieId(Long movieId) { this.movieId = movieId; }
|
||||||
|
|
||||||
|
public Long getUserId() { return userId; }
|
||||||
|
public void setUserId(Long userId) { this.userId = userId; }
|
||||||
|
|
||||||
|
public Integer getTmdbId() { return tmdbId; }
|
||||||
|
public void setTmdbId(Integer tmdbId) { this.tmdbId = tmdbId; }
|
||||||
|
|
||||||
|
public String getTitle() { return title; }
|
||||||
|
public void setTitle(String title) { this.title = title; }
|
||||||
|
|
||||||
|
public String getOriginalTitle() { return originalTitle; }
|
||||||
|
public void setOriginalTitle(String originalTitle) { this.originalTitle = originalTitle; }
|
||||||
|
|
||||||
|
public String getOverview() { return overview; }
|
||||||
|
public void setOverview(String overview) { this.overview = overview; }
|
||||||
|
|
||||||
|
public String getReleaseDate() { return releaseDate; }
|
||||||
|
public void setReleaseDate(String releaseDate) { this.releaseDate = releaseDate; }
|
||||||
|
|
||||||
|
public String getPosterPath() { return posterPath; }
|
||||||
|
public void setPosterPath(String posterPath) { this.posterPath = posterPath; }
|
||||||
|
|
||||||
|
public String getBackdropPath() { return backdropPath; }
|
||||||
|
public void setBackdropPath(String backdropPath) { this.backdropPath = backdropPath; }
|
||||||
|
|
||||||
|
public String getLocalPosterUrl() { return localPosterUrl; }
|
||||||
|
public void setLocalPosterUrl(String localPosterUrl) { this.localPosterUrl = localPosterUrl; }
|
||||||
|
|
||||||
|
public Integer getTmdbRating() { return tmdbRating; }
|
||||||
|
public void setTmdbRating(Integer tmdbRating) { this.tmdbRating = tmdbRating; }
|
||||||
|
|
||||||
|
public Integer getVoteCount() { return voteCount; }
|
||||||
|
public void setVoteCount(Integer voteCount) { this.voteCount = voteCount; }
|
||||||
|
|
||||||
|
public Integer getRuntime() { return runtime; }
|
||||||
|
public void setRuntime(Integer runtime) { this.runtime = runtime; }
|
||||||
|
|
||||||
|
public String getGenres() { return genres; }
|
||||||
|
public void setGenres(String genres) { this.genres = genres; }
|
||||||
|
|
||||||
|
public String getDirectors() { return directors; }
|
||||||
|
public void setDirectors(String directors) { this.directors = directors; }
|
||||||
|
|
||||||
|
public String getCasts() { return casts; }
|
||||||
|
public void setCasts(String casts) { this.casts = casts; }
|
||||||
|
|
||||||
|
public String getOriginalLanguage() { return originalLanguage; }
|
||||||
|
public void setOriginalLanguage(String originalLanguage) { this.originalLanguage = originalLanguage; }
|
||||||
|
|
||||||
|
public String getProductionCountries() { return productionCountries; }
|
||||||
|
public void setProductionCountries(String productionCountries) { this.productionCountries = productionCountries; }
|
||||||
|
|
||||||
|
public String getStatus() { return status; }
|
||||||
|
public void setStatus(String status) { this.status = status; }
|
||||||
|
|
||||||
|
public LocalDateTime getCacheTime() { return cacheTime; }
|
||||||
|
public void setCacheTime(LocalDateTime cacheTime) { this.cacheTime = cacheTime; }
|
||||||
|
|
||||||
|
public LocalDateTime getUpdateTime() { return updateTime; }
|
||||||
|
public void setUpdateTime(LocalDateTime updateTime) { this.updateTime = updateTime; }
|
||||||
|
}
|
||||||
@ -0,0 +1,111 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TMDB 定时刮削执行记录数据对象(Data Object)
|
||||||
|
* 对应数据库表:wp_tmdb_scraper_log
|
||||||
|
*
|
||||||
|
* 设计说明:
|
||||||
|
* - DO 字段名使用 Java 驼峰命名,MyBatis-Plus 自动映射为数据库 snake_case 列
|
||||||
|
* - DO 仅负责数据映射,不包含业务逻辑
|
||||||
|
*/
|
||||||
|
@TableName(value = "wp_tmdb_scraper_log", autoResultMap = true)
|
||||||
|
public class TmdbScraperLogDO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** 记录ID */
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 触发方式:auto=定时自动 / manual=手动触发 */
|
||||||
|
private String triggerType;
|
||||||
|
|
||||||
|
/** 执行状态:success / partial / failed / skipped */
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/** 总处理电影数 */
|
||||||
|
private Integer totalProcessed;
|
||||||
|
|
||||||
|
/** 成功刮削数 */
|
||||||
|
private Integer totalSuccess;
|
||||||
|
|
||||||
|
/** 跳过数 */
|
||||||
|
private Integer totalSkipped;
|
||||||
|
|
||||||
|
/** 失败数 */
|
||||||
|
private Integer totalFailed;
|
||||||
|
|
||||||
|
/** 涉及用户数 */
|
||||||
|
private Integer userCount;
|
||||||
|
|
||||||
|
/** 详细执行结果JSON */
|
||||||
|
private String detailJson;
|
||||||
|
|
||||||
|
/** 错误信息 */
|
||||||
|
private String errorMessage;
|
||||||
|
|
||||||
|
/** 执行耗时(毫秒) */
|
||||||
|
private Long durationMs;
|
||||||
|
|
||||||
|
/** 开始时间 */
|
||||||
|
private LocalDateTime startTime;
|
||||||
|
|
||||||
|
/** 结束时间 */
|
||||||
|
private LocalDateTime endTime;
|
||||||
|
|
||||||
|
/** 创建时间 */
|
||||||
|
@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 String getTriggerType() { return triggerType; }
|
||||||
|
public void setTriggerType(String triggerType) { this.triggerType = triggerType; }
|
||||||
|
|
||||||
|
public String getStatus() { return status; }
|
||||||
|
public void setStatus(String status) { this.status = status; }
|
||||||
|
|
||||||
|
public Integer getTotalProcessed() { return totalProcessed; }
|
||||||
|
public void setTotalProcessed(Integer totalProcessed) { this.totalProcessed = totalProcessed; }
|
||||||
|
|
||||||
|
public Integer getTotalSuccess() { return totalSuccess; }
|
||||||
|
public void setTotalSuccess(Integer totalSuccess) { this.totalSuccess = totalSuccess; }
|
||||||
|
|
||||||
|
public Integer getTotalSkipped() { return totalSkipped; }
|
||||||
|
public void setTotalSkipped(Integer totalSkipped) { this.totalSkipped = totalSkipped; }
|
||||||
|
|
||||||
|
public Integer getTotalFailed() { return totalFailed; }
|
||||||
|
public void setTotalFailed(Integer totalFailed) { this.totalFailed = totalFailed; }
|
||||||
|
|
||||||
|
public Integer getUserCount() { return userCount; }
|
||||||
|
public void setUserCount(Integer userCount) { this.userCount = userCount; }
|
||||||
|
|
||||||
|
public String getDetailJson() { return detailJson; }
|
||||||
|
public void setDetailJson(String detailJson) { this.detailJson = detailJson; }
|
||||||
|
|
||||||
|
public String getErrorMessage() { return errorMessage; }
|
||||||
|
public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; }
|
||||||
|
|
||||||
|
public Long getDurationMs() { return durationMs; }
|
||||||
|
public void setDurationMs(Long durationMs) { this.durationMs = durationMs; }
|
||||||
|
|
||||||
|
public LocalDateTime getStartTime() { return startTime; }
|
||||||
|
public void setStartTime(LocalDateTime startTime) { this.startTime = startTime; }
|
||||||
|
|
||||||
|
public LocalDateTime getEndTime() { return endTime; }
|
||||||
|
public void setEndTime(LocalDateTime endTime) { this.endTime = endTime; }
|
||||||
|
|
||||||
|
public LocalDateTime getCreateTime() { return createTime; }
|
||||||
|
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
|
||||||
|
}
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户令牌数据对象(Data Object)
|
||||||
|
* 对应数据库表:wp_user_token
|
||||||
|
*
|
||||||
|
* 设计说明:
|
||||||
|
* - DO 字段名使用 Java 驼峰命名,MyBatis-Plus 自动映射为数据库 snake_case 列
|
||||||
|
* - DO 仅负责数据映射,不包含业务逻辑
|
||||||
|
* - token 字段在数据库中为加密存储
|
||||||
|
*/
|
||||||
|
@TableName(value = "wp_user_token", autoResultMap = true)
|
||||||
|
public class TokenDO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** 记录ID */
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 关联用户ID */
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/** 令牌值(加密存储) */
|
||||||
|
private String token;
|
||||||
|
|
||||||
|
/** 设备信息 */
|
||||||
|
private String deviceInfo;
|
||||||
|
|
||||||
|
/** IP地址 */
|
||||||
|
private String ipAddress;
|
||||||
|
|
||||||
|
/** 过期时间 */
|
||||||
|
private LocalDateTime expireTime;
|
||||||
|
|
||||||
|
/** 状态:1=有效, 0=已撤销 */
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
/** 创建时间 */
|
||||||
|
@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 getToken() { return token; }
|
||||||
|
public void setToken(String token) { this.token = token; }
|
||||||
|
|
||||||
|
public String getDeviceInfo() { return deviceInfo; }
|
||||||
|
public void setDeviceInfo(String deviceInfo) { this.deviceInfo = deviceInfo; }
|
||||||
|
|
||||||
|
public String getIpAddress() { return ipAddress; }
|
||||||
|
public void setIpAddress(String ipAddress) { this.ipAddress = ipAddress; }
|
||||||
|
|
||||||
|
public LocalDateTime getExpireTime() { return expireTime; }
|
||||||
|
public void setExpireTime(LocalDateTime expireTime) { this.expireTime = expireTime; }
|
||||||
|
|
||||||
|
public Integer getStatus() { return status; }
|
||||||
|
public void setStatus(Integer status) { this.status = status; }
|
||||||
|
|
||||||
|
public LocalDateTime getCreateTime() { return createTime; }
|
||||||
|
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
|
||||||
|
}
|
||||||
@ -0,0 +1,139 @@
|
|||||||
|
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.TableLogic;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户数据对象(Data Object)
|
||||||
|
* 对应数据库表:wp_user
|
||||||
|
*
|
||||||
|
* 设计说明:
|
||||||
|
* - DO 字段名使用 Java 驼峰命名,MyBatis-Plus 自动映射为数据库 snake_case 列
|
||||||
|
* - DO 仅负责数据映射,不包含业务逻辑
|
||||||
|
* - tmdbApiKey 在数据库中为加密存储,DO 中存储的是加密后的密文
|
||||||
|
*/
|
||||||
|
@TableName(value = "wp_user", autoResultMap = true)
|
||||||
|
public class UserDO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** 用户ID:主键,自增长 */
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 用户名 */
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
/** 密码(加密存储) */
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
/** 邮箱地址 */
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
/** 手机号码 */
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
/** 真实姓名 */
|
||||||
|
private String realName;
|
||||||
|
|
||||||
|
/** 头像URL */
|
||||||
|
private String avatarUrl;
|
||||||
|
|
||||||
|
/** 状态:0=禁用, 1=正常, 2=锁定 */
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
/** 用户类型:0=普通, 1=VIP, 2=管理员 */
|
||||||
|
private Integer userType;
|
||||||
|
|
||||||
|
/** 最后登录IP */
|
||||||
|
private String lastLoginIp;
|
||||||
|
|
||||||
|
/** 最后登录时间 */
|
||||||
|
private LocalDateTime lastLoginTime;
|
||||||
|
|
||||||
|
/** 锁定截止时间(NULL=未锁定) */
|
||||||
|
private LocalDateTime lockUntil;
|
||||||
|
|
||||||
|
/** 连续登录失败次数 */
|
||||||
|
private Integer failCount;
|
||||||
|
|
||||||
|
/** TMDB API Key(加密存储) */
|
||||||
|
private String tmdbApiKey;
|
||||||
|
|
||||||
|
/** 创建时间 */
|
||||||
|
@TableField(fill = com.baomidou.mybatisplus.annotation.FieldFill.INSERT)
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
/** 更新时间 */
|
||||||
|
@TableField(update = "NOW()")
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
|
||||||
|
/** 逻辑删除标记:0=未删除, 1=已删除 */
|
||||||
|
@TableLogic
|
||||||
|
private Integer isDeleted;
|
||||||
|
|
||||||
|
/** 备注信息 */
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
// ==================== Getter 和 Setter ====================
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
|
public String getUsername() { return username; }
|
||||||
|
public void setUsername(String username) { this.username = username; }
|
||||||
|
|
||||||
|
public String getPassword() { return password; }
|
||||||
|
public void setPassword(String password) { this.password = password; }
|
||||||
|
|
||||||
|
public String getEmail() { return email; }
|
||||||
|
public void setEmail(String email) { this.email = email; }
|
||||||
|
|
||||||
|
public String getPhone() { return phone; }
|
||||||
|
public void setPhone(String phone) { this.phone = phone; }
|
||||||
|
|
||||||
|
public String getRealName() { return realName; }
|
||||||
|
public void setRealName(String realName) { this.realName = realName; }
|
||||||
|
|
||||||
|
public String getAvatarUrl() { return avatarUrl; }
|
||||||
|
public void setAvatarUrl(String avatarUrl) { this.avatarUrl = avatarUrl; }
|
||||||
|
|
||||||
|
public Integer getStatus() { return status; }
|
||||||
|
public void setStatus(Integer status) { this.status = status; }
|
||||||
|
|
||||||
|
public Integer getUserType() { return userType; }
|
||||||
|
public void setUserType(Integer userType) { this.userType = userType; }
|
||||||
|
|
||||||
|
public String getLastLoginIp() { return lastLoginIp; }
|
||||||
|
public void setLastLoginIp(String lastLoginIp) { this.lastLoginIp = lastLoginIp; }
|
||||||
|
|
||||||
|
public LocalDateTime getLastLoginTime() { return lastLoginTime; }
|
||||||
|
public void setLastLoginTime(LocalDateTime lastLoginTime) { this.lastLoginTime = lastLoginTime; }
|
||||||
|
|
||||||
|
public LocalDateTime getLockUntil() { return lockUntil; }
|
||||||
|
public void setLockUntil(LocalDateTime lockUntil) { this.lockUntil = lockUntil; }
|
||||||
|
|
||||||
|
public Integer getFailCount() { return failCount; }
|
||||||
|
public void setFailCount(Integer failCount) { this.failCount = failCount; }
|
||||||
|
|
||||||
|
public String getTmdbApiKey() { return tmdbApiKey; }
|
||||||
|
public void setTmdbApiKey(String tmdbApiKey) { this.tmdbApiKey = tmdbApiKey; }
|
||||||
|
|
||||||
|
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; }
|
||||||
|
|
||||||
|
public Integer getIsDeleted() { return isDeleted; }
|
||||||
|
public void setIsDeleted(Integer isDeleted) { this.isDeleted = isDeleted; }
|
||||||
|
|
||||||
|
public String getRemark() { return remark; }
|
||||||
|
public void setRemark(String remark) { this.remark = remark; }
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
package com.wangpan.common.dal.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.wangpan.common.dal.dataobject.DriveFolderDO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网盘文件夹 Mapper 接口
|
||||||
|
*
|
||||||
|
* 继承 MyBatis-Plus BaseMapper,自动获得 CRUD 能力
|
||||||
|
* 复杂查询方法在 XML 中实现
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface DriveFolderMapper extends BaseMapper<DriveFolderDO> {
|
||||||
|
|
||||||
|
/** 建表SQL(保留供 DatabaseInitializer 使用) */
|
||||||
|
String CREATE_TABLE_SQL = """
|
||||||
|
CREATE TABLE IF NOT EXISTS wp_drive_folder (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '文件夹ID',
|
||||||
|
drive_id BIGINT NOT NULL COMMENT '所属网盘ID',
|
||||||
|
user_id BIGINT NOT NULL COMMENT '所属用户ID',
|
||||||
|
parent_id BIGINT NOT NULL DEFAULT 0 COMMENT '父文件夹ID(0=根目录)',
|
||||||
|
name VARCHAR(200) NOT NULL COMMENT '文件夹名称',
|
||||||
|
description VARCHAR(500) DEFAULT NULL COMMENT '文件夹描述',
|
||||||
|
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重',
|
||||||
|
depth INT NOT NULL DEFAULT 0 COMMENT '层级深度',
|
||||||
|
child_folder_count INT NOT NULL DEFAULT 0 COMMENT '子文件夹数量',
|
||||||
|
file_count INT NOT NULL DEFAULT 0 COMMENT '文件数量',
|
||||||
|
total_size BIGINT NOT NULL DEFAULT 0 COMMENT '总大小(字节)',
|
||||||
|
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
INDEX idx_drive_id (drive_id),
|
||||||
|
INDEX idx_user_id (user_id),
|
||||||
|
INDEX idx_parent_id (drive_id, parent_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='网盘文件夹表'
|
||||||
|
""";
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
package com.wangpan.common.dal.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.wangpan.common.dal.dataobject.DriveDO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网盘 Mapper 接口
|
||||||
|
*
|
||||||
|
* 继承 MyBatis-Plus BaseMapper,自动获得 CRUD 能力
|
||||||
|
* 复杂查询方法在 XML 中实现
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface DriveMapper extends BaseMapper<DriveDO> {
|
||||||
|
|
||||||
|
/** 建表SQL(保留供 DatabaseInitializer 使用) */
|
||||||
|
String CREATE_TABLE_SQL = """
|
||||||
|
CREATE TABLE IF NOT EXISTS wp_drive (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '网盘ID',
|
||||||
|
user_id BIGINT NOT NULL COMMENT '所属用户ID',
|
||||||
|
name VARCHAR(100) NOT NULL COMMENT '网盘名称',
|
||||||
|
description VARCHAR(500) DEFAULT NULL COMMENT '网盘描述',
|
||||||
|
logo_url TEXT DEFAULT NULL COMMENT '网盘Logo URL(MinIO存储链接)',
|
||||||
|
folder_count INT NOT NULL DEFAULT 0 COMMENT '文件夹数量',
|
||||||
|
file_count INT NOT NULL DEFAULT 0 COMMENT '文件数量',
|
||||||
|
total_size BIGINT NOT NULL DEFAULT 0 COMMENT '总大小(字节)',
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态:active/disabled',
|
||||||
|
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
INDEX idx_user_id (user_id),
|
||||||
|
INDEX idx_status (status)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='网盘表'
|
||||||
|
""";
|
||||||
|
}
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
package com.wangpan.common.dal.mapper;
|
||||||
|
|
||||||
|
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 java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网盘电影 Mapper 接口
|
||||||
|
*
|
||||||
|
* 继承 MyBatis-Plus BaseMapper,自动获得 CRUD 能力
|
||||||
|
* 复杂查询方法在 XML 中实现
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface DriveMovieMapper extends BaseMapper<DriveMovieDO> {
|
||||||
|
|
||||||
|
/** 建表SQL(保留供 DatabaseInitializer 使用) */
|
||||||
|
String CREATE_TABLE_SQL = """
|
||||||
|
CREATE TABLE IF NOT EXISTS wp_drive_movie (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '电影ID',
|
||||||
|
drive_id BIGINT NOT NULL COMMENT '所属网盘ID',
|
||||||
|
folder_id BIGINT NOT NULL DEFAULT 0 COMMENT '所属文件夹ID(0=根目录)',
|
||||||
|
user_id BIGINT NOT NULL COMMENT '所属用户ID',
|
||||||
|
title VARCHAR(300) NOT NULL COMMENT '电影标题',
|
||||||
|
subtitle VARCHAR(300) DEFAULT NULL COMMENT '电影副标题',
|
||||||
|
original_name VARCHAR(300) DEFAULT NULL COMMENT '原始文件名',
|
||||||
|
minio_object_name VARCHAR(500) DEFAULT NULL COMMENT 'MinIO对象名称',
|
||||||
|
minio_url TEXT DEFAULT NULL COMMENT 'MinIO访问URL(加密存储)',
|
||||||
|
mime_type VARCHAR(100) DEFAULT NULL COMMENT 'MIME类型',
|
||||||
|
file_size BIGINT NOT NULL DEFAULT 0 COMMENT '文件大小(字节)',
|
||||||
|
extension VARCHAR(20) DEFAULT NULL COMMENT '文件扩展名',
|
||||||
|
description TEXT DEFAULT NULL COMMENT '电影描述',
|
||||||
|
resource_library VARCHAR(100) DEFAULT NULL COMMENT '资源库来源',
|
||||||
|
maintainer VARCHAR(100) DEFAULT NULL COMMENT '维护者',
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态:active/deleted',
|
||||||
|
category VARCHAR(50) DEFAULT NULL COMMENT '分类',
|
||||||
|
scrape_type VARCHAR(20) DEFAULT 'none' COMMENT '刮削类型:tmdb/manual/none',
|
||||||
|
rating INT DEFAULT NULL COMMENT '评分(0-100)',
|
||||||
|
poster_url TEXT DEFAULT NULL COMMENT '海报URL(加密存储)',
|
||||||
|
tags VARCHAR(500) DEFAULT NULL COMMENT '标签(逗号分隔)',
|
||||||
|
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
INDEX idx_drive_id (drive_id),
|
||||||
|
INDEX idx_user_id (user_id),
|
||||||
|
INDEX idx_folder_id (drive_id, folder_id),
|
||||||
|
INDEX idx_status (status),
|
||||||
|
INDEX idx_rating (rating),
|
||||||
|
INDEX idx_scrape_type (scrape_type)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='网盘电影表(文件与电影合并)'
|
||||||
|
""";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找用户所有网盘下的重复电影(按 title 分组)
|
||||||
|
* 复杂SQL,在XML中实现
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> findDuplicateMovies(@Param("userId") Long userId);
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
package com.wangpan.common.dal.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.wangpan.common.dal.dataobject.IconDO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon Mapper 接口
|
||||||
|
*
|
||||||
|
* 继承 MyBatis-Plus BaseMapper,自动获得 CRUD 能力
|
||||||
|
* 复杂查询方法在 XML 中实现
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface IconMapper extends BaseMapper<IconDO> {
|
||||||
|
|
||||||
|
/** 建表 SQL(保留供 DatabaseInitializer 使用) */
|
||||||
|
String CREATE_TABLE_SQL = """
|
||||||
|
CREATE TABLE IF NOT EXISTS wp_icon (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 'Icon ID',
|
||||||
|
user_id BIGINT NOT NULL COMMENT '所属用户ID',
|
||||||
|
name VARCHAR(100) NOT NULL COMMENT 'Icon 名称',
|
||||||
|
url VARCHAR(500) DEFAULT NULL COMMENT '外网链接 URL',
|
||||||
|
internal_url VARCHAR(500) DEFAULT NULL COMMENT '内网链接 URL',
|
||||||
|
link_type VARCHAR(20) NOT NULL DEFAULT 'external' COMMENT '链接类型:external(仅外网)/ internal(仅内网)/ both(内外网)',
|
||||||
|
description VARCHAR(500) DEFAULT NULL COMMENT 'Icon 描述',
|
||||||
|
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序值(数字越小越靠前)',
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态:active/disabled',
|
||||||
|
logo_url VARCHAR(500) DEFAULT NULL COMMENT 'Logo 图片 URL(MinIO 存储)',
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
INDEX idx_user_id (user_id),
|
||||||
|
INDEX idx_status (status),
|
||||||
|
INDEX idx_sort_order (sort_order)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Icon 表'
|
||||||
|
""";
|
||||||
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
package com.wangpan.common.dal.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.wangpan.common.dal.dataobject.LoginLogDO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录日志 Mapper 接口
|
||||||
|
*
|
||||||
|
* 继承 MyBatis-Plus BaseMapper,自动获得 CRUD 能力
|
||||||
|
* 复杂查询方法在 XML 中实现
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface LoginLogMapper extends BaseMapper<LoginLogDO> {
|
||||||
|
|
||||||
|
/** 建表SQL(保留供 DatabaseInitializer 使用)
|
||||||
|
* 列名与 LoginLogDO 字段名通过 MyBatis-Plus 驼峰转下划线自动映射:
|
||||||
|
* loginUsername -> login_username, loginIp -> login_ip, loginResult -> login_result
|
||||||
|
*/
|
||||||
|
String CREATE_TABLE_SQL = """
|
||||||
|
CREATE TABLE IF NOT EXISTS wp_login_log (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '记录ID',
|
||||||
|
user_id BIGINT NOT NULL COMMENT '关联用户ID',
|
||||||
|
login_username VARCHAR(50) NOT NULL COMMENT '登录用户名',
|
||||||
|
login_ip VARCHAR(50) DEFAULT NULL COMMENT '登录IP地址',
|
||||||
|
user_agent VARCHAR(500) DEFAULT NULL COMMENT '浏览器User-Agent',
|
||||||
|
login_result VARCHAR(20) NOT NULL DEFAULT 'success' COMMENT '登录结果:success/fail',
|
||||||
|
fail_reason VARCHAR(200) DEFAULT NULL COMMENT '失败原因',
|
||||||
|
login_time DATETIME NOT NULL COMMENT '登录时间',
|
||||||
|
INDEX idx_user_id (user_id),
|
||||||
|
INDEX idx_login_time (login_time),
|
||||||
|
INDEX idx_login_username (login_username)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='登录日志表'
|
||||||
|
""";
|
||||||
|
}
|
||||||
@ -0,0 +1,97 @@
|
|||||||
|
package com.wangpan.common.dal.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.wangpan.common.dal.dataobject.MovieDO;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 电影 Mapper 接口
|
||||||
|
*
|
||||||
|
* 继承 MyBatis-Plus BaseMapper,自动获得 CRUD 能力
|
||||||
|
* 复杂查询方法在 XML 中实现
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface MovieMapper extends BaseMapper<MovieDO> {
|
||||||
|
|
||||||
|
/** 建表SQL(保留供 DatabaseInitializer 使用) */
|
||||||
|
String CREATE_TABLE_SQL = """
|
||||||
|
CREATE TABLE IF NOT EXISTS wp_movie (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '电影ID',
|
||||||
|
drive_id BIGINT NOT NULL COMMENT '所属网盘ID',
|
||||||
|
folder_id BIGINT NOT NULL DEFAULT 0 COMMENT '所属文件夹ID(0=根目录)',
|
||||||
|
user_id BIGINT NOT NULL COMMENT '所属用户ID',
|
||||||
|
title VARCHAR(300) NOT NULL COMMENT '电影标题',
|
||||||
|
subtitle VARCHAR(300) DEFAULT NULL COMMENT '电影副标题',
|
||||||
|
original_name VARCHAR(300) DEFAULT NULL COMMENT '原始文件名',
|
||||||
|
minio_object_name VARCHAR(500) DEFAULT NULL COMMENT 'MinIO对象名称',
|
||||||
|
minio_url TEXT DEFAULT NULL COMMENT 'MinIO访问URL(加密存储)',
|
||||||
|
mime_type VARCHAR(100) DEFAULT NULL COMMENT 'MIME类型',
|
||||||
|
file_size BIGINT NOT NULL DEFAULT 0 COMMENT '文件大小(字节)',
|
||||||
|
extension VARCHAR(20) DEFAULT NULL COMMENT '文件扩展名',
|
||||||
|
description TEXT DEFAULT NULL COMMENT '电影描述',
|
||||||
|
resource_library VARCHAR(100) DEFAULT NULL COMMENT '资源库来源',
|
||||||
|
maintainer VARCHAR(100) DEFAULT NULL COMMENT '维护者',
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态:active/deleted',
|
||||||
|
category VARCHAR(50) DEFAULT NULL COMMENT '分类',
|
||||||
|
scrape_type VARCHAR(20) DEFAULT 'none' COMMENT '刮削类型:tmdb/manual/none',
|
||||||
|
rating INT DEFAULT NULL COMMENT '评分(0-100)',
|
||||||
|
poster_url TEXT DEFAULT NULL COMMENT '海报URL(加密存储)',
|
||||||
|
tags VARCHAR(500) DEFAULT NULL COMMENT '标签(逗号分隔)',
|
||||||
|
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
INDEX idx_drive_id (drive_id),
|
||||||
|
INDEX idx_user_id (user_id),
|
||||||
|
INDEX idx_folder_id (drive_id, folder_id),
|
||||||
|
INDEX idx_status (status),
|
||||||
|
INDEX idx_rating (rating),
|
||||||
|
INDEX idx_scrape_type (scrape_type)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='电影表'
|
||||||
|
""";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找用户所有网盘下的重复电影(按 title 分组)
|
||||||
|
* 复杂SQL,在XML中实现
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> findDuplicateMovies(@Param("userId") Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找所有配置了TMDB API Key的用户(供定时刮削使用)
|
||||||
|
* 复杂SQL,在XML中实现
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> findUsersWithTmdbKey();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找指定用户需要刮削的电影
|
||||||
|
* 复杂SQL,在XML中实现
|
||||||
|
*/
|
||||||
|
List<MovieDO> findMoviesNeedingScrape(@Param("userId") Long userId, @Param("expireDays") int expireDays);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新电影评分
|
||||||
|
*/
|
||||||
|
@Update("UPDATE wp_movie SET rating = #{rating}, update_time = NOW() WHERE id = #{id}")
|
||||||
|
int updateRating(@Param("id") Long id, @Param("rating") Integer rating);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新电影海报URL
|
||||||
|
*/
|
||||||
|
@Update("UPDATE wp_movie SET poster_url = #{posterUrl}, update_time = NOW() WHERE id = #{id}")
|
||||||
|
int updatePosterUrl(@Param("id") Long id, @Param("posterUrl") String posterUrl);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新电影标签
|
||||||
|
*/
|
||||||
|
@Update("UPDATE wp_movie SET tags = #{tags}, update_time = NOW() WHERE id = #{id}")
|
||||||
|
int updateTags(@Param("id") Long id, @Param("tags") String tags);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新刮削类型标记
|
||||||
|
*/
|
||||||
|
@Update("UPDATE wp_movie SET scrape_type = #{scrapeType}, update_time = NOW() WHERE id = #{id}")
|
||||||
|
int updateScrapeType(@Param("id") Long id, @Param("scrapeType") String scrapeType);
|
||||||
|
}
|
||||||
@ -0,0 +1,96 @@
|
|||||||
|
package com.wangpan.common.dal.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.wangpan.common.dal.dataobject.TmdbCacheDO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TMDB 刮削缓存 Mapper 接口
|
||||||
|
*
|
||||||
|
* 继承 MyBatis-Plus BaseMapper,自动获得 CRUD 能力
|
||||||
|
* 复杂查询方法在 XML 中实现
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface TmdbCacheMapper extends BaseMapper<TmdbCacheDO> {
|
||||||
|
|
||||||
|
/** 建表SQL(保留供 DatabaseInitializer 使用) */
|
||||||
|
String CREATE_TABLE_SQL = """
|
||||||
|
CREATE TABLE IF NOT EXISTS wp_tmdb_cache (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '记录ID',
|
||||||
|
movie_id BIGINT NOT NULL COMMENT '关联wp_movie表的电影ID',
|
||||||
|
user_id BIGINT NOT NULL COMMENT '关联用户ID',
|
||||||
|
tmdb_id INT DEFAULT NULL COMMENT 'TMDB电影ID',
|
||||||
|
title VARCHAR(200) NOT NULL COMMENT '电影标题(来自TMDB)',
|
||||||
|
original_title VARCHAR(200) DEFAULT NULL COMMENT '原始标题(外文)',
|
||||||
|
overview TEXT DEFAULT NULL COMMENT '剧情简介',
|
||||||
|
release_date VARCHAR(20) DEFAULT NULL COMMENT '上映日期',
|
||||||
|
poster_path VARCHAR(300) DEFAULT NULL COMMENT 'TMDB海报路径',
|
||||||
|
backdrop_path VARCHAR(300) DEFAULT NULL COMMENT 'TMDB背景图路径',
|
||||||
|
local_poster_url TEXT DEFAULT NULL COMMENT '本地海报URL(加密存储)',
|
||||||
|
tmdb_rating INT DEFAULT NULL COMMENT 'TMDB评分(0-100分制)',
|
||||||
|
vote_count INT DEFAULT NULL COMMENT 'TMDB投票数',
|
||||||
|
runtime INT DEFAULT NULL COMMENT '片长(分钟)',
|
||||||
|
genres VARCHAR(300) DEFAULT NULL COMMENT '类型(逗号分隔)',
|
||||||
|
directors VARCHAR(200) DEFAULT NULL COMMENT '导演(逗号分隔)',
|
||||||
|
casts VARCHAR(500) DEFAULT NULL COMMENT '主演(逗号分隔)',
|
||||||
|
original_language VARCHAR(10) DEFAULT NULL COMMENT '原始语言',
|
||||||
|
production_countries VARCHAR(200) DEFAULT NULL COMMENT '制片国家',
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '缓存状态:active/expired',
|
||||||
|
cache_time DATETIME NOT NULL COMMENT '缓存时间',
|
||||||
|
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
UNIQUE KEY uk_movie_user (movie_id, user_id),
|
||||||
|
INDEX idx_tmdb_id (tmdb_id),
|
||||||
|
INDEX idx_user_cache (user_id, status),
|
||||||
|
INDEX idx_cache_time (cache_time),
|
||||||
|
INDEX idx_status (status)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='TMDB刮削缓存表'
|
||||||
|
""";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据电影ID查询有效缓存
|
||||||
|
*/
|
||||||
|
TmdbCacheDO findByMovieId(@Param("movieId") Long movieId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据TMDB ID查询有效缓存列表
|
||||||
|
*/
|
||||||
|
List<TmdbCacheDO> findByTmdbId(@Param("tmdbId") Integer tmdbId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询用户缓存(含关联电影标题)
|
||||||
|
* 复杂SQL,在XML中实现
|
||||||
|
*/
|
||||||
|
Map<String, Object> queryByUserId(@Param("userId") Long userId, @Param("pageNo") int pageNo, @Param("pageSize") int pageSize);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询所有过期的缓存ID列表
|
||||||
|
*/
|
||||||
|
List<Long> findExpiredCaches(@Param("expireBefore") LocalDateTime expireBefore);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增或更新缓存记录(UPSERT)
|
||||||
|
* 复杂SQL,在XML中实现
|
||||||
|
*/
|
||||||
|
int saveOrUpdate(@Param("data") Map<String, Object> data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量将缓存标记为过期
|
||||||
|
*/
|
||||||
|
int markAsExpired(@Param("cacheIds") List<Long> cacheIds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定电影的缓存
|
||||||
|
*/
|
||||||
|
int deleteByMovieId(@Param("movieId") Long movieId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户的缓存统计信息
|
||||||
|
* 复杂SQL,在XML中实现
|
||||||
|
*/
|
||||||
|
Map<String, Object> getStatistics(@Param("userId") Long userId);
|
||||||
|
}
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
package com.wangpan.common.dal.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.wangpan.common.dal.dataobject.TmdbScraperLogDO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TMDB 定时刮削执行记录 Mapper 接口
|
||||||
|
*
|
||||||
|
* 继承 MyBatis-Plus BaseMapper,自动获得 CRUD 能力
|
||||||
|
* 复杂查询方法在 XML 中实现
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface TmdbScraperLogMapper extends BaseMapper<TmdbScraperLogDO> {
|
||||||
|
|
||||||
|
/** 建表SQL(保留供 DatabaseInitializer 使用) */
|
||||||
|
String CREATE_TABLE_SQL = """
|
||||||
|
CREATE TABLE IF NOT EXISTS wp_tmdb_scraper_log (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '记录ID',
|
||||||
|
trigger_type VARCHAR(20) NOT NULL DEFAULT 'auto' COMMENT '触发方式:auto=定时自动 / manual=手动触发',
|
||||||
|
status VARCHAR(20) NOT NULL COMMENT '执行状态:success/partial/failed/skipped',
|
||||||
|
total_processed INT NOT NULL DEFAULT 0 COMMENT '总处理电影数',
|
||||||
|
total_success INT NOT NULL DEFAULT 0 COMMENT '成功刮削数',
|
||||||
|
total_skipped INT NOT NULL DEFAULT 0 COMMENT '跳过数',
|
||||||
|
total_failed INT NOT NULL DEFAULT 0 COMMENT '失败数',
|
||||||
|
user_count INT NOT NULL DEFAULT 0 COMMENT '涉及用户数',
|
||||||
|
detail_json TEXT DEFAULT NULL COMMENT '详细执行结果JSON',
|
||||||
|
error_message TEXT DEFAULT NULL COMMENT '错误信息(如有)',
|
||||||
|
duration_ms BIGINT NOT NULL DEFAULT 0 COMMENT '执行耗时(毫秒)',
|
||||||
|
start_time DATETIME NOT NULL COMMENT '开始时间',
|
||||||
|
end_time DATETIME NOT NULL COMMENT '结束时间',
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
INDEX idx_trigger_type (trigger_type),
|
||||||
|
INDEX idx_status (status),
|
||||||
|
INDEX idx_start_time (start_time)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='TMDB定时刮削执行记录表'
|
||||||
|
""";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入一条执行记录
|
||||||
|
* 复杂SQL,在XML中实现
|
||||||
|
*/
|
||||||
|
int insertLog(@Param("log") Map<String, Object> log);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询最近的执行记录
|
||||||
|
* 复杂SQL,在XML中实现
|
||||||
|
*/
|
||||||
|
Map<String, Object> queryRecent(@Param("pageNo") int pageNo, @Param("pageSize") int pageSize);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询最近的N条执行记录
|
||||||
|
* 复杂SQL,在XML中实现
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> queryRecentByLimit(@Param("limit") int limit);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID查询单条记录
|
||||||
|
* 复杂SQL,在XML中实现
|
||||||
|
*/
|
||||||
|
Map<String, Object> findById(@Param("id") Long id);
|
||||||
|
}
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
package com.wangpan.common.dal.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.wangpan.common.dal.dataobject.TokenDO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
import org.apache.ibatis.annotations.Update;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户令牌 Mapper 接口
|
||||||
|
*
|
||||||
|
* 继承 MyBatis-Plus BaseMapper,自动获得 CRUD 能力
|
||||||
|
* 复杂查询方法在 XML 中实现
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface TokenMapper extends BaseMapper<TokenDO> {
|
||||||
|
|
||||||
|
/** 建表SQL(保留供 DatabaseInitializer 使用) */
|
||||||
|
String CREATE_TABLE_SQL = """
|
||||||
|
CREATE TABLE IF NOT EXISTS wp_user_token (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '记录ID',
|
||||||
|
user_id BIGINT NOT NULL COMMENT '关联用户ID',
|
||||||
|
token VARCHAR(500) NOT NULL COMMENT '令牌值(Jasypt加密存储)',
|
||||||
|
device_info VARCHAR(200) DEFAULT NULL COMMENT '设备信息(User-Agent摘要)',
|
||||||
|
ip_address VARCHAR(50) DEFAULT NULL COMMENT '创建时的IP地址',
|
||||||
|
expire_time DATETIME NOT NULL COMMENT '过期时间',
|
||||||
|
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1-有效,0-已撤销',
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
INDEX idx_user_id (user_id),
|
||||||
|
INDEX idx_user_status (user_id, status),
|
||||||
|
INDEX idx_token (token(100)),
|
||||||
|
INDEX idx_expire_time (expire_time)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||||
|
COMMENT='用户令牌表'""";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询用户的有效令牌数量
|
||||||
|
*/
|
||||||
|
@Select("SELECT COUNT(*) FROM wp_user_token WHERE user_id = #{userId} AND status = 1 AND expire_time > NOW()")
|
||||||
|
int countActiveTokens(@Param("userId") Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证令牌是否有效
|
||||||
|
*/
|
||||||
|
@Select("SELECT COUNT(*) FROM wp_user_token WHERE token = #{token} AND status = 1 AND expire_time > NOW()")
|
||||||
|
boolean isValid(@Param("token") String token);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 撤销指定令牌
|
||||||
|
*/
|
||||||
|
@Update("UPDATE wp_user_token SET status = 0 WHERE id = #{tokenId} AND user_id = #{userId}")
|
||||||
|
int revokeById(@Param("tokenId") Long tokenId, @Param("userId") Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 撤销用户的所有令牌
|
||||||
|
*/
|
||||||
|
@Update("UPDATE wp_user_token SET status = 0 WHERE user_id = #{userId} AND status = 1")
|
||||||
|
int revokeAllByUserId(@Param("userId") Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理过期的已撤销令牌
|
||||||
|
*/
|
||||||
|
@Update("DELETE FROM wp_user_token WHERE status = 0 AND expire_time < DATE_SUB(NOW(), INTERVAL 30 DAY)")
|
||||||
|
int cleanExpiredRevokedTokens();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除用户的所有令牌记录
|
||||||
|
*/
|
||||||
|
@Update("DELETE FROM wp_user_token WHERE user_id = #{userId}")
|
||||||
|
int deleteByUserId(@Param("userId") Long userId);
|
||||||
|
}
|
||||||
@ -0,0 +1,101 @@
|
|||||||
|
package com.wangpan.common.dal.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.wangpan.common.dal.dataobject.UserDO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Update;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户 Mapper 接口
|
||||||
|
*
|
||||||
|
* 继承 MyBatis-Plus BaseMapper,自动获得 CRUD 能力
|
||||||
|
* 复杂查询方法在 XML 中实现
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface UserMapper extends BaseMapper<UserDO> {
|
||||||
|
|
||||||
|
/** 建表SQL(保留供 DatabaseInitializer 使用) */
|
||||||
|
String CREATE_TABLE_SQL = """
|
||||||
|
CREATE TABLE IF NOT EXISTS wp_user (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID',
|
||||||
|
username VARCHAR(50) NOT NULL COMMENT '用户名',
|
||||||
|
password VARCHAR(100) NOT NULL COMMENT '密码(MD5加密)',
|
||||||
|
email VARCHAR(100) DEFAULT NULL COMMENT '邮箱地址',
|
||||||
|
phone VARCHAR(20) DEFAULT NULL COMMENT '手机号码',
|
||||||
|
real_name VARCHAR(50) DEFAULT NULL COMMENT '真实姓名',
|
||||||
|
avatar_url TEXT DEFAULT NULL COMMENT '头像URL',
|
||||||
|
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用, 1=正常, 2=锁定',
|
||||||
|
user_type TINYINT NOT NULL DEFAULT 0 COMMENT '用户类型:0=普通, 1=VIP, 2=管理员',
|
||||||
|
last_login_ip VARCHAR(50) DEFAULT NULL COMMENT '最后登录IP',
|
||||||
|
last_login_time DATETIME DEFAULT NULL COMMENT '最后登录时间',
|
||||||
|
lock_until DATETIME DEFAULT NULL COMMENT '锁定截止时间(NULL=未锁定)',
|
||||||
|
fail_count INT NOT NULL DEFAULT 0 COMMENT '连续登录失败次数',
|
||||||
|
tmdb_api_key VARCHAR(500) DEFAULT NULL COMMENT 'TMDB API Key(Jasypt加密存储)',
|
||||||
|
is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除:0=未删除, 1=已删除',
|
||||||
|
remark VARCHAR(500) DEFAULT NULL COMMENT '备注信息',
|
||||||
|
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
UNIQUE KEY uk_username (username),
|
||||||
|
INDEX idx_email (email),
|
||||||
|
INDEX idx_status (status),
|
||||||
|
INDEX idx_user_type (user_type)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'
|
||||||
|
""";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户的最后登录信息
|
||||||
|
*/
|
||||||
|
@Update("UPDATE wp_user SET last_login_ip = #{loginIp}, last_login_time = #{loginTime}, update_time = NOW() WHERE id = #{userId}")
|
||||||
|
int updateLastLoginInfo(@Param("userId") Long userId, @Param("loginIp") String loginIp, @Param("loginTime") LocalDateTime loginTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 原子性增加连续失败次数
|
||||||
|
*/
|
||||||
|
@Update("UPDATE wp_user SET fail_count = fail_count + 1, update_time = NOW() WHERE id = #{userId}")
|
||||||
|
int incrementFailCount(@Param("userId") Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置连续失败次数
|
||||||
|
*/
|
||||||
|
@Update("UPDATE wp_user SET fail_count = 0, update_time = NOW() WHERE id = #{userId}")
|
||||||
|
int resetFailCount(@Param("userId") Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置用户的锁定截止时间
|
||||||
|
*/
|
||||||
|
@Update("UPDATE wp_user SET lock_until = #{lockUntil}, status = 2, update_time = NOW() WHERE id = #{userId}")
|
||||||
|
int updateLockUntil(@Param("userId") Long userId, @Param("lockUntil") LocalDateTime lockUntil);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除用户的锁定状态
|
||||||
|
*/
|
||||||
|
@Update("UPDATE wp_user SET lock_until = NULL, status = 1, update_time = NOW() WHERE id = #{userId}")
|
||||||
|
int clearLockUntil(@Param("userId") Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户密码
|
||||||
|
*/
|
||||||
|
@Update("UPDATE wp_user SET password = #{password}, update_time = NOW() WHERE id = #{userId}")
|
||||||
|
int updatePassword(@Param("userId") Long userId, @Param("password") String password);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户状态
|
||||||
|
*/
|
||||||
|
@Update("UPDATE wp_user SET status = #{status}, update_time = NOW() WHERE id = #{userId}")
|
||||||
|
int updateStatus(@Param("userId") Long userId, @Param("status") int status);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户的TMDB API Key
|
||||||
|
*/
|
||||||
|
@Update("UPDATE wp_user SET tmdb_api_key = #{tmdbApiKey}, update_time = NOW() WHERE id = #{userId}")
|
||||||
|
int updateTmdbApiKey(@Param("userId") Long userId, @Param("tmdbApiKey") String tmdbApiKey);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 软删除用户
|
||||||
|
*/
|
||||||
|
@Update("UPDATE wp_user SET is_deleted = 1, update_time = NOW() WHERE id = #{userId} AND is_deleted = 0")
|
||||||
|
int softDelete(@Param("userId") Long userId);
|
||||||
|
}
|
||||||
@ -0,0 +1,223 @@
|
|||||||
|
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.DriveFolderDO;
|
||||||
|
import com.wangpan.common.dal.mapper.DriveFolderMapper;
|
||||||
|
import com.wangpan.common.entity.DriveFolder;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网盘文件夹 Repository 层(Dao)
|
||||||
|
*
|
||||||
|
* 继承 MyBatis-Plus ServiceImpl<DriveFolderMapper, DriveFolderDO>,自动获得 CRUD 能力
|
||||||
|
* 职责:
|
||||||
|
* 1. 对外提供业务维度的数据访问接口(使用 Entity/Map)
|
||||||
|
* 2. 使用 MyBatis-Plus 的 LambdaQueryWrapper/LambdaUpdateWrapper 构建查询
|
||||||
|
* 3. 处理 Entity ↔ DriveFolderDO 的转换
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public class DriveFolderRepository extends ServiceImpl<DriveFolderMapper, DriveFolderDO> {
|
||||||
|
|
||||||
|
// ==================== 插入 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建文件夹
|
||||||
|
* 将业务 Entity 转换为 DO 后调用 MyBatis-Plus save 方法插入
|
||||||
|
*/
|
||||||
|
public Long insert(DriveFolder folder) {
|
||||||
|
var folderDO = toDriveFolderDO(folder);
|
||||||
|
save(folderDO);
|
||||||
|
return folderDO.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 查询 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID查询文件夹(返回 Entity)
|
||||||
|
* 使用 MyBatis-Plus getById 方法
|
||||||
|
*/
|
||||||
|
public DriveFolder findById(Long id) {
|
||||||
|
var folderDO = getById(id);
|
||||||
|
return folderDO != null ? toDriveFolder(folderDO) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询指定网盘下指定父文件夹的子文件夹列表
|
||||||
|
* 使用 MyBatis-Plus LambdaQueryWrapper 构建查询条件
|
||||||
|
*/
|
||||||
|
public List<Map<String, Object>> queryByDriveAndParent(Long driveId, Long parentId, Long userId) {
|
||||||
|
var wrapper = new LambdaQueryWrapper<DriveFolderDO>()
|
||||||
|
.eq(DriveFolderDO::getDriveId, driveId)
|
||||||
|
.eq(DriveFolderDO::getParentId, parentId)
|
||||||
|
.eq(DriveFolderDO::getUserId, userId)
|
||||||
|
.orderByAsc(DriveFolderDO::getSortOrder)
|
||||||
|
.orderByAsc(DriveFolderDO::getId);
|
||||||
|
|
||||||
|
var folderDOs = list(wrapper);
|
||||||
|
var list = new ArrayList<Map<String, Object>>();
|
||||||
|
for (var folderDO : folderDOs) {
|
||||||
|
list.add(toMap(folderDO));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计指定网盘下指定父文件夹的子文件夹数量
|
||||||
|
* 使用 MyBatis-Plus count 方法
|
||||||
|
*/
|
||||||
|
public int countByDriveAndParent(Long driveId, Long parentId, Long userId) {
|
||||||
|
return (int) count(new LambdaQueryWrapper<DriveFolderDO>()
|
||||||
|
.eq(DriveFolderDO::getDriveId, driveId)
|
||||||
|
.eq(DriveFolderDO::getParentId, parentId)
|
||||||
|
.eq(DriveFolderDO::getUserId, userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计指定网盘下所有层级的文件夹总数
|
||||||
|
*/
|
||||||
|
public int countByDrive(Long driveId, Long userId) {
|
||||||
|
return (int) count(new LambdaQueryWrapper<DriveFolderDO>()
|
||||||
|
.eq(DriveFolderDO::getDriveId, driveId)
|
||||||
|
.eq(DriveFolderDO::getUserId, userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询指定网盘下的所有文件夹(用于树形结构构建)
|
||||||
|
* 使用 MyBatis-Plus LambdaQueryWrapper 构建查询条件
|
||||||
|
*/
|
||||||
|
public List<Map<String, Object>> queryAllByDrive(Long driveId, Long userId) {
|
||||||
|
var wrapper = new LambdaQueryWrapper<DriveFolderDO>()
|
||||||
|
.eq(DriveFolderDO::getDriveId, driveId)
|
||||||
|
.eq(DriveFolderDO::getUserId, userId)
|
||||||
|
.orderByAsc(DriveFolderDO::getDepth)
|
||||||
|
.orderByAsc(DriveFolderDO::getSortOrder);
|
||||||
|
|
||||||
|
var folderDOs = list(wrapper);
|
||||||
|
var list = new ArrayList<Map<String, Object>>();
|
||||||
|
for (var folderDO : folderDOs) {
|
||||||
|
list.add(toMap(folderDO));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 更新 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新文件夹基本信息
|
||||||
|
* 使用 MyBatis-Plus LambdaUpdateWrapper 构建更新条件
|
||||||
|
*/
|
||||||
|
public boolean update(Long id, String name, String description, Long parentId) {
|
||||||
|
var wrapper = new LambdaUpdateWrapper<DriveFolderDO>()
|
||||||
|
.eq(DriveFolderDO::getId, id)
|
||||||
|
.set(DriveFolderDO::getName, name)
|
||||||
|
.set(DriveFolderDO::getDescription, description)
|
||||||
|
.set(DriveFolderDO::getParentId, parentId)
|
||||||
|
.set(DriveFolderDO::getUpdateTime, LocalDateTime.now());
|
||||||
|
return update(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新文件夹统计信息
|
||||||
|
* 使用 MyBatis-Plus LambdaUpdateWrapper 构建更新条件
|
||||||
|
*/
|
||||||
|
public boolean updateStats(Long id, Integer childFolderCount, Integer fileCount, Long totalSize) {
|
||||||
|
var wrapper = new LambdaUpdateWrapper<DriveFolderDO>()
|
||||||
|
.eq(DriveFolderDO::getId, id)
|
||||||
|
.set(DriveFolderDO::getChildFolderCount, childFolderCount)
|
||||||
|
.set(DriveFolderDO::getFileCount, fileCount)
|
||||||
|
.set(DriveFolderDO::getTotalSize, totalSize)
|
||||||
|
.set(DriveFolderDO::getUpdateTime, LocalDateTime.now());
|
||||||
|
return update(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 删除 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除文件夹(需确保文件夹为空)
|
||||||
|
* 删除前校验:子文件夹数量和文件数量必须都为0
|
||||||
|
*/
|
||||||
|
public boolean delete(Long id, Long userId) {
|
||||||
|
// 先查询文件夹是否存在且属于该用户
|
||||||
|
var wrapper = new LambdaQueryWrapper<DriveFolderDO>()
|
||||||
|
.eq(DriveFolderDO::getId, id)
|
||||||
|
.eq(DriveFolderDO::getUserId, userId);
|
||||||
|
var folderDO = getOne(wrapper, false);
|
||||||
|
if (folderDO == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 校验文件夹是否为空
|
||||||
|
if (folderDO.getChildFolderCount() > 0 || folderDO.getFileCount() > 0) {
|
||||||
|
throw new IllegalStateException("文件夹不为空,无法删除(子文件夹:" + folderDO.getChildFolderCount()
|
||||||
|
+ ",文件:" + folderDO.getFileCount() + ")");
|
||||||
|
}
|
||||||
|
return remove(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Entity ↔ DO 转换 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entity → DO 转换
|
||||||
|
*/
|
||||||
|
private DriveFolderDO toDriveFolderDO(DriveFolder folder) {
|
||||||
|
var folderDO = new DriveFolderDO();
|
||||||
|
folderDO.setId(folder.getId());
|
||||||
|
folderDO.setDriveId(folder.getDriveId());
|
||||||
|
folderDO.setUserId(folder.getUserId());
|
||||||
|
folderDO.setParentId(folder.getParentId());
|
||||||
|
folderDO.setName(folder.getName());
|
||||||
|
folderDO.setDescription(folder.getDescription());
|
||||||
|
folderDO.setSortOrder(folder.getSortOrder());
|
||||||
|
folderDO.setDepth(folder.getDepth());
|
||||||
|
folderDO.setChildFolderCount(folder.getChildFolderCount());
|
||||||
|
folderDO.setFileCount(folder.getFileCount());
|
||||||
|
folderDO.setTotalSize(folder.getTotalSize());
|
||||||
|
folderDO.setUpdateTime(folder.getUpdateTime());
|
||||||
|
return folderDO;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DO → Entity 转换
|
||||||
|
*/
|
||||||
|
private DriveFolder toDriveFolder(DriveFolderDO folderDO) {
|
||||||
|
var folder = new DriveFolder();
|
||||||
|
folder.setId(folderDO.getId());
|
||||||
|
folder.setDriveId(folderDO.getDriveId());
|
||||||
|
folder.setUserId(folderDO.getUserId());
|
||||||
|
folder.setParentId(folderDO.getParentId());
|
||||||
|
folder.setName(folderDO.getName());
|
||||||
|
folder.setDescription(folderDO.getDescription());
|
||||||
|
folder.setSortOrder(folderDO.getSortOrder());
|
||||||
|
folder.setDepth(folderDO.getDepth());
|
||||||
|
folder.setChildFolderCount(folderDO.getChildFolderCount());
|
||||||
|
folder.setFileCount(folderDO.getFileCount());
|
||||||
|
folder.setTotalSize(folderDO.getTotalSize());
|
||||||
|
folder.setUpdateTime(folderDO.getUpdateTime());
|
||||||
|
folder.setCreateTime(folderDO.getCreateTime());
|
||||||
|
return folder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DO → Map 转换(用于 API 返回)
|
||||||
|
*/
|
||||||
|
private Map<String, Object> toMap(DriveFolderDO folderDO) {
|
||||||
|
var map = new LinkedHashMap<String, Object>();
|
||||||
|
map.put("id", folderDO.getId());
|
||||||
|
map.put("driveId", folderDO.getDriveId());
|
||||||
|
map.put("parentId", folderDO.getParentId());
|
||||||
|
map.put("name", folderDO.getName());
|
||||||
|
map.put("description", folderDO.getDescription());
|
||||||
|
map.put("sortOrder", folderDO.getSortOrder());
|
||||||
|
map.put("depth", folderDO.getDepth());
|
||||||
|
map.put("childFolderCount", folderDO.getChildFolderCount());
|
||||||
|
map.put("fileCount", folderDO.getFileCount());
|
||||||
|
map.put("totalSize", folderDO.getTotalSize());
|
||||||
|
map.put("updateTime", folderDO.getUpdateTime() != null ? folderDO.getUpdateTime().toString() : null);
|
||||||
|
map.put("createTime", folderDO.getCreateTime() != null ? folderDO.getCreateTime().toString() : null);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,315 @@
|
|||||||
|
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.plugins.pagination.Page;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.wangpan.common.dal.dataobject.DriveMovieDO;
|
||||||
|
import com.wangpan.common.dal.mapper.DriveMovieMapper;
|
||||||
|
import com.wangpan.common.entity.DriveMovie;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网盘电影 Repository 层(Dao)
|
||||||
|
*
|
||||||
|
* 继承 MyBatis-Plus ServiceImpl<DriveMovieMapper, DriveMovieDO>,自动获得 CRUD 能力
|
||||||
|
* 职责:
|
||||||
|
* 1. 对外提供业务维度的数据访问接口(使用 Entity/Map)
|
||||||
|
* 2. 使用 MyBatis-Plus 的 LambdaQueryWrapper/LambdaUpdateWrapper 构建查询
|
||||||
|
* 3. 处理 Entity ↔ DriveMovieDO 的转换
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public class DriveMovieRepository extends ServiceImpl<DriveMovieMapper, DriveMovieDO> {
|
||||||
|
|
||||||
|
// ==================== 插入 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建电影记录
|
||||||
|
* 将业务 Entity 转换为 DO 后调用 MyBatis-Plus save 方法插入
|
||||||
|
*/
|
||||||
|
public Long insert(DriveMovie movie) {
|
||||||
|
var movieDO = toDriveMovieDO(movie);
|
||||||
|
save(movieDO);
|
||||||
|
return movieDO.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 查询 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID查询电影(返回 Entity)
|
||||||
|
* 使用 MyBatis-Plus getById 方法
|
||||||
|
*/
|
||||||
|
public DriveMovie findById(Long id) {
|
||||||
|
var movieDO = getById(id);
|
||||||
|
return movieDO != null ? toDriveMovie(movieDO) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询指定网盘下指定文件夹的电影列表(分页)
|
||||||
|
* 使用 MyBatis-Plus LambdaQueryWrapper 构建分页查询
|
||||||
|
*/
|
||||||
|
public List<Map<String, Object>> queryByDriveAndFolder(Long driveId, Long folderId, Long userId,
|
||||||
|
int pageNo, int pageSize) {
|
||||||
|
var page = new Page<DriveMovieDO>(pageNo, pageSize);
|
||||||
|
var wrapper = new LambdaQueryWrapper<DriveMovieDO>()
|
||||||
|
.eq(DriveMovieDO::getDriveId, driveId)
|
||||||
|
.eq(DriveMovieDO::getFolderId, folderId)
|
||||||
|
.eq(DriveMovieDO::getUserId, userId)
|
||||||
|
.eq(DriveMovieDO::getStatus, "active")
|
||||||
|
.orderByDesc(DriveMovieDO::getCreateTime);
|
||||||
|
|
||||||
|
var result = page(page, wrapper);
|
||||||
|
var list = new ArrayList<Map<String, Object>>();
|
||||||
|
for (var movieDO : result.getRecords()) {
|
||||||
|
list.add(toMap(movieDO));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计指定网盘下指定文件夹的电影数量
|
||||||
|
* 使用 MyBatis-Plus count 方法
|
||||||
|
*/
|
||||||
|
public int countByDriveAndFolder(Long driveId, Long folderId, Long userId) {
|
||||||
|
return (int) count(new LambdaQueryWrapper<DriveMovieDO>()
|
||||||
|
.eq(DriveMovieDO::getDriveId, driveId)
|
||||||
|
.eq(DriveMovieDO::getFolderId, folderId)
|
||||||
|
.eq(DriveMovieDO::getUserId, userId)
|
||||||
|
.eq(DriveMovieDO::getStatus, "active"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计指定网盘下所有层级的电影总数
|
||||||
|
*/
|
||||||
|
public int countByDrive(Long driveId, Long userId) {
|
||||||
|
return (int) count(new LambdaQueryWrapper<DriveMovieDO>()
|
||||||
|
.eq(DriveMovieDO::getDriveId, driveId)
|
||||||
|
.eq(DriveMovieDO::getUserId, userId)
|
||||||
|
.eq(DriveMovieDO::getStatus, "active"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询用户所有网盘下的电影列表(分页)
|
||||||
|
* 使用 MyBatis-Plus LambdaQueryWrapper 构建分页查询
|
||||||
|
*/
|
||||||
|
public List<Map<String, Object>> queryByUser(Long userId, int pageNo, int pageSize) {
|
||||||
|
var page = new Page<DriveMovieDO>(pageNo, pageSize);
|
||||||
|
var wrapper = new LambdaQueryWrapper<DriveMovieDO>()
|
||||||
|
.eq(DriveMovieDO::getUserId, userId)
|
||||||
|
.eq(DriveMovieDO::getStatus, "active")
|
||||||
|
.orderByDesc(DriveMovieDO::getCreateTime);
|
||||||
|
|
||||||
|
var result = page(page, wrapper);
|
||||||
|
var list = new ArrayList<Map<String, Object>>();
|
||||||
|
for (var movieDO : result.getRecords()) {
|
||||||
|
list.add(toMap(movieDO));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计用户所有网盘下的电影数量
|
||||||
|
* 使用 MyBatis-Plus count 方法
|
||||||
|
*/
|
||||||
|
public int countByUser(Long userId) {
|
||||||
|
return (int) count(new LambdaQueryWrapper<DriveMovieDO>()
|
||||||
|
.eq(DriveMovieDO::getUserId, userId)
|
||||||
|
.eq(DriveMovieDO::getStatus, "active"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按评分排序查询电影列表(分页)
|
||||||
|
* 使用 MyBatis-Plus LambdaQueryWrapper 构建分页查询
|
||||||
|
*/
|
||||||
|
public List<Map<String, Object>> queryByRating(Long driveId, Long userId, int pageNo, int pageSize) {
|
||||||
|
var page = new Page<DriveMovieDO>(pageNo, pageSize);
|
||||||
|
var wrapper = new LambdaQueryWrapper<DriveMovieDO>()
|
||||||
|
.eq(DriveMovieDO::getDriveId, driveId)
|
||||||
|
.eq(DriveMovieDO::getUserId, userId)
|
||||||
|
.eq(DriveMovieDO::getStatus, "active")
|
||||||
|
.orderByDesc(DriveMovieDO::getRating);
|
||||||
|
|
||||||
|
var result = page(page, wrapper);
|
||||||
|
var list = new ArrayList<Map<String, Object>>();
|
||||||
|
for (var movieDO : result.getRecords()) {
|
||||||
|
list.add(toMap(movieDO));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 更新 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新电影基本信息
|
||||||
|
* 使用 MyBatis-Plus LambdaUpdateWrapper 构建更新条件
|
||||||
|
*/
|
||||||
|
public boolean update(Long id, String title, String subtitle, String description,
|
||||||
|
Integer rating, String tags, String category) {
|
||||||
|
var wrapper = new LambdaUpdateWrapper<DriveMovieDO>()
|
||||||
|
.eq(DriveMovieDO::getId, id)
|
||||||
|
.set(DriveMovieDO::getTitle, title)
|
||||||
|
.set(DriveMovieDO::getSubtitle, subtitle)
|
||||||
|
.set(DriveMovieDO::getDescription, description)
|
||||||
|
.set(DriveMovieDO::getRating, rating)
|
||||||
|
.set(DriveMovieDO::getTags, tags)
|
||||||
|
.set(DriveMovieDO::getCategory, category)
|
||||||
|
.set(DriveMovieDO::getUpdateTime, LocalDateTime.now());
|
||||||
|
return update(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新电影刮削信息
|
||||||
|
* 使用 MyBatis-Plus LambdaUpdateWrapper 构建更新条件
|
||||||
|
*/
|
||||||
|
public boolean updateScrapeInfo(Long id, String scrapeType, String resourceLibrary,
|
||||||
|
String maintainer, String posterUrl) {
|
||||||
|
var wrapper = new LambdaUpdateWrapper<DriveMovieDO>()
|
||||||
|
.eq(DriveMovieDO::getId, id)
|
||||||
|
.set(DriveMovieDO::getScrapeType, scrapeType)
|
||||||
|
.set(DriveMovieDO::getResourceLibrary, resourceLibrary)
|
||||||
|
.set(DriveMovieDO::getMaintainer, maintainer)
|
||||||
|
.set(DriveMovieDO::getPosterUrl, posterUrl)
|
||||||
|
.set(DriveMovieDO::getUpdateTime, LocalDateTime.now());
|
||||||
|
return update(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移动电影到另一个文件夹
|
||||||
|
* 使用 MyBatis-Plus LambdaUpdateWrapper 构建更新条件
|
||||||
|
*/
|
||||||
|
public boolean moveToFolder(Long movieId, Long newFolderId, Long userId) {
|
||||||
|
var wrapper = new LambdaUpdateWrapper<DriveMovieDO>()
|
||||||
|
.eq(DriveMovieDO::getId, movieId)
|
||||||
|
.eq(DriveMovieDO::getUserId, userId)
|
||||||
|
.set(DriveMovieDO::getFolderId, newFolderId)
|
||||||
|
.set(DriveMovieDO::getUpdateTime, LocalDateTime.now());
|
||||||
|
return update(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 删除 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 逻辑删除电影
|
||||||
|
* 使用 MyBatis-Plus LambdaUpdateWrapper 将状态设为 deleted
|
||||||
|
*/
|
||||||
|
public boolean logicalDelete(Long id, Long userId) {
|
||||||
|
var wrapper = new LambdaUpdateWrapper<DriveMovieDO>()
|
||||||
|
.eq(DriveMovieDO::getId, id)
|
||||||
|
.eq(DriveMovieDO::getUserId, userId)
|
||||||
|
.set(DriveMovieDO::getStatus, "deleted")
|
||||||
|
.set(DriveMovieDO::getUpdateTime, LocalDateTime.now());
|
||||||
|
return update(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 物理删除电影
|
||||||
|
* 使用 MyBatis-Plus remove 方法配合条件构造器
|
||||||
|
*/
|
||||||
|
public boolean delete(Long id, Long userId) {
|
||||||
|
var wrapper = new LambdaQueryWrapper<DriveMovieDO>()
|
||||||
|
.eq(DriveMovieDO::getId, id)
|
||||||
|
.eq(DriveMovieDO::getUserId, userId);
|
||||||
|
return remove(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找用户所有网盘下的重复电影(按 title 分组)
|
||||||
|
* 委托给 Mapper 接口中定义的 XML 查询
|
||||||
|
*/
|
||||||
|
public List<Map<String, Object>> findDuplicateMovies(Long userId) {
|
||||||
|
return baseMapper.findDuplicateMovies(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Entity ↔ DO 转换 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entity → DO 转换
|
||||||
|
*/
|
||||||
|
private DriveMovieDO toDriveMovieDO(DriveMovie movie) {
|
||||||
|
var movieDO = new DriveMovieDO();
|
||||||
|
movieDO.setId(movie.getId());
|
||||||
|
movieDO.setDriveId(movie.getDriveId());
|
||||||
|
movieDO.setFolderId(movie.getFolderId());
|
||||||
|
movieDO.setUserId(movie.getUserId());
|
||||||
|
movieDO.setTitle(movie.getTitle());
|
||||||
|
movieDO.setSubtitle(movie.getSubtitle());
|
||||||
|
movieDO.setOriginalName(movie.getOriginalName());
|
||||||
|
movieDO.setMinioObjectName(movie.getMinioObjectName());
|
||||||
|
movieDO.setMinioUrl(movie.getMinioUrl());
|
||||||
|
movieDO.setMimeType(movie.getMimeType());
|
||||||
|
movieDO.setFileSize(movie.getFileSize());
|
||||||
|
movieDO.setExtension(movie.getExtension());
|
||||||
|
movieDO.setDescription(movie.getDescription());
|
||||||
|
movieDO.setResourceLibrary(movie.getResourceLibrary());
|
||||||
|
movieDO.setMaintainer(movie.getMaintainer());
|
||||||
|
movieDO.setStatus(movie.getStatus());
|
||||||
|
movieDO.setCategory(movie.getCategory());
|
||||||
|
movieDO.setScrapeType(movie.getScrapeType());
|
||||||
|
movieDO.setRating(movie.getRating());
|
||||||
|
movieDO.setPosterUrl(movie.getPosterUrl());
|
||||||
|
movieDO.setTags(movie.getTags());
|
||||||
|
movieDO.setUpdateTime(movie.getUpdateTime());
|
||||||
|
return movieDO;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DO → Entity 转换
|
||||||
|
*/
|
||||||
|
private DriveMovie toDriveMovie(DriveMovieDO movieDO) {
|
||||||
|
var movie = new DriveMovie();
|
||||||
|
movie.setId(movieDO.getId());
|
||||||
|
movie.setDriveId(movieDO.getDriveId());
|
||||||
|
movie.setFolderId(movieDO.getFolderId());
|
||||||
|
movie.setUserId(movieDO.getUserId());
|
||||||
|
movie.setTitle(movieDO.getTitle());
|
||||||
|
movie.setSubtitle(movieDO.getSubtitle());
|
||||||
|
movie.setOriginalName(movieDO.getOriginalName());
|
||||||
|
movie.setMinioObjectName(movieDO.getMinioObjectName());
|
||||||
|
movie.setMinioUrl(movieDO.getMinioUrl());
|
||||||
|
movie.setMimeType(movieDO.getMimeType());
|
||||||
|
movie.setFileSize(movieDO.getFileSize());
|
||||||
|
movie.setExtension(movieDO.getExtension());
|
||||||
|
movie.setDescription(movieDO.getDescription());
|
||||||
|
movie.setResourceLibrary(movieDO.getResourceLibrary());
|
||||||
|
movie.setMaintainer(movieDO.getMaintainer());
|
||||||
|
movie.setStatus(movieDO.getStatus());
|
||||||
|
movie.setCategory(movieDO.getCategory());
|
||||||
|
movie.setScrapeType(movieDO.getScrapeType());
|
||||||
|
movie.setRating(movieDO.getRating());
|
||||||
|
movie.setPosterUrl(movieDO.getPosterUrl());
|
||||||
|
movie.setTags(movieDO.getTags());
|
||||||
|
movie.setUpdateTime(movieDO.getUpdateTime());
|
||||||
|
movie.setCreateTime(movieDO.getCreateTime());
|
||||||
|
return movie;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DO → Map 转换(用于 API 返回)
|
||||||
|
*/
|
||||||
|
private Map<String, Object> toMap(DriveMovieDO movieDO) {
|
||||||
|
var map = new LinkedHashMap<String, Object>();
|
||||||
|
map.put("id", movieDO.getId());
|
||||||
|
map.put("driveId", movieDO.getDriveId());
|
||||||
|
map.put("folderId", movieDO.getFolderId());
|
||||||
|
map.put("title", movieDO.getTitle());
|
||||||
|
map.put("subtitle", movieDO.getSubtitle());
|
||||||
|
map.put("originalName", movieDO.getOriginalName());
|
||||||
|
map.put("mimeType", movieDO.getMimeType());
|
||||||
|
map.put("fileSize", movieDO.getFileSize());
|
||||||
|
map.put("extension", movieDO.getExtension());
|
||||||
|
map.put("description", movieDO.getDescription());
|
||||||
|
map.put("resourceLibrary", movieDO.getResourceLibrary());
|
||||||
|
map.put("maintainer", movieDO.getMaintainer());
|
||||||
|
map.put("status", movieDO.getStatus());
|
||||||
|
map.put("category", movieDO.getCategory());
|
||||||
|
map.put("scrapeType", movieDO.getScrapeType());
|
||||||
|
map.put("rating", movieDO.getRating());
|
||||||
|
map.put("tags", movieDO.getTags());
|
||||||
|
map.put("updateTime", movieDO.getUpdateTime() != null ? movieDO.getUpdateTime().toString() : null);
|
||||||
|
map.put("createTime", movieDO.getCreateTime() != null ? movieDO.getCreateTime().toString() : null);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,192 @@
|
|||||||
|
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.plugins.pagination.Page;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.wangpan.common.dal.dataobject.DriveDO;
|
||||||
|
import com.wangpan.common.dal.mapper.DriveMapper;
|
||||||
|
import com.wangpan.common.entity.Drive;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网盘 Repository 层(Dao)
|
||||||
|
*
|
||||||
|
* 继承 MyBatis-Plus ServiceImpl<DriveMapper, DriveDO>,自动获得 CRUD 能力
|
||||||
|
* 职责:
|
||||||
|
* 1. 对外提供业务维度的数据访问接口(使用 Entity/Map)
|
||||||
|
* 2. 使用 MyBatis-Plus 的 LambdaQueryWrapper/LambdaUpdateWrapper 构建查询
|
||||||
|
* 3. 处理 Entity ↔ DriveDO 的转换
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public class DriveRepository extends ServiceImpl<DriveMapper, DriveDO> {
|
||||||
|
|
||||||
|
// ==================== 插入 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建网盘
|
||||||
|
* 将业务 Entity 转换为 DO 后调用 MyBatis-Plus save 方法插入
|
||||||
|
*/
|
||||||
|
public Long insert(Drive drive) {
|
||||||
|
var driveDO = toDriveDO(drive);
|
||||||
|
save(driveDO);
|
||||||
|
return driveDO.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 查询 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID查询网盘(返回 Entity)
|
||||||
|
* 使用 MyBatis-Plus getById 方法
|
||||||
|
*/
|
||||||
|
public Drive findById(Long id) {
|
||||||
|
var driveDO = getById(id);
|
||||||
|
return driveDO != null ? toDrive(driveDO) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询用户的网盘列表(分页,返回 Map 列表供 API 使用)
|
||||||
|
* 使用 MyBatis-Plus LambdaQueryWrapper 构建分页查询
|
||||||
|
*/
|
||||||
|
public List<Map<String, Object>> queryByUserId(Long userId, int pageNo, int pageSize) {
|
||||||
|
// 构建分页查询条件:按用户ID过滤,按创建时间降序排列
|
||||||
|
var page = new Page<DriveDO>(pageNo, pageSize);
|
||||||
|
var wrapper = new LambdaQueryWrapper<DriveDO>()
|
||||||
|
.eq(DriveDO::getUserId, userId)
|
||||||
|
.orderByDesc(DriveDO::getCreateTime);
|
||||||
|
|
||||||
|
var result = page(page, wrapper);
|
||||||
|
var list = new ArrayList<Map<String, Object>>();
|
||||||
|
for (var driveDO : result.getRecords()) {
|
||||||
|
list.add(toMap(driveDO));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计用户的网盘数量
|
||||||
|
* 使用 MyBatis-Plus count 方法
|
||||||
|
*/
|
||||||
|
public int countByUserId(Long userId) {
|
||||||
|
return (int) count(new LambdaQueryWrapper<DriveDO>().eq(DriveDO::getUserId, userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 更新 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新网盘基本信息
|
||||||
|
* 使用 MyBatis-Plus LambdaUpdateWrapper 构建更新条件
|
||||||
|
*/
|
||||||
|
public boolean update(Long id, String name, String description, String logoUrl) {
|
||||||
|
var wrapper = new LambdaUpdateWrapper<DriveDO>()
|
||||||
|
.eq(DriveDO::getId, id)
|
||||||
|
.set(DriveDO::getName, name)
|
||||||
|
.set(DriveDO::getDescription, description)
|
||||||
|
.set(DriveDO::getLogoUrl, logoUrl)
|
||||||
|
.set(DriveDO::getUpdateTime, LocalDateTime.now());
|
||||||
|
return update(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新网盘统计信息
|
||||||
|
* 使用 MyBatis-Plus LambdaUpdateWrapper 构建更新条件
|
||||||
|
*/
|
||||||
|
public boolean updateStats(Long id, Integer folderCount, Integer fileCount, Long totalSize) {
|
||||||
|
var wrapper = new LambdaUpdateWrapper<DriveDO>()
|
||||||
|
.eq(DriveDO::getId, id)
|
||||||
|
.set(DriveDO::getFolderCount, folderCount)
|
||||||
|
.set(DriveDO::getFileCount, fileCount)
|
||||||
|
.set(DriveDO::getTotalSize, totalSize)
|
||||||
|
.set(DriveDO::getUpdateTime, LocalDateTime.now());
|
||||||
|
return update(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新网盘状态
|
||||||
|
* 使用 MyBatis-Plus LambdaUpdateWrapper 构建更新条件
|
||||||
|
*/
|
||||||
|
public boolean updateStatus(Long id, String status) {
|
||||||
|
var wrapper = new LambdaUpdateWrapper<DriveDO>()
|
||||||
|
.eq(DriveDO::getId, id)
|
||||||
|
.set(DriveDO::getStatus, status)
|
||||||
|
.set(DriveDO::getUpdateTime, LocalDateTime.now());
|
||||||
|
return update(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 删除 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除网盘
|
||||||
|
* 使用 MyBatis-Plus removeById 方法
|
||||||
|
*/
|
||||||
|
public boolean delete(Long id, Long userId) {
|
||||||
|
// 先校验网盘属于该用户,再删除
|
||||||
|
var wrapper = new LambdaQueryWrapper<DriveDO>()
|
||||||
|
.eq(DriveDO::getId, id)
|
||||||
|
.eq(DriveDO::getUserId, userId);
|
||||||
|
return remove(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Entity ↔ DO 转换 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entity → DO 转换
|
||||||
|
* 将业务层的 Drive 实体转换为数据层的 DriveDO
|
||||||
|
*/
|
||||||
|
private DriveDO toDriveDO(Drive drive) {
|
||||||
|
var driveDO = new DriveDO();
|
||||||
|
driveDO.setId(drive.getId());
|
||||||
|
driveDO.setUserId(drive.getUserId());
|
||||||
|
driveDO.setName(drive.getName());
|
||||||
|
driveDO.setDescription(drive.getDescription());
|
||||||
|
driveDO.setLogoUrl(drive.getLogoUrl());
|
||||||
|
driveDO.setFolderCount(drive.getFolderCount());
|
||||||
|
driveDO.setFileCount(drive.getFileCount());
|
||||||
|
driveDO.setTotalSize(drive.getTotalSize());
|
||||||
|
driveDO.setStatus(drive.getStatus());
|
||||||
|
driveDO.setUpdateTime(drive.getUpdateTime());
|
||||||
|
return driveDO;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DO → Entity 转换
|
||||||
|
* 将数据层的 DriveDO 转换为业务层的 Drive 实体
|
||||||
|
*/
|
||||||
|
private Drive toDrive(DriveDO driveDO) {
|
||||||
|
var drive = new Drive();
|
||||||
|
drive.setId(driveDO.getId());
|
||||||
|
drive.setUserId(driveDO.getUserId());
|
||||||
|
drive.setName(driveDO.getName());
|
||||||
|
drive.setDescription(driveDO.getDescription());
|
||||||
|
drive.setLogoUrl(driveDO.getLogoUrl());
|
||||||
|
drive.setFolderCount(driveDO.getFolderCount());
|
||||||
|
drive.setFileCount(driveDO.getFileCount());
|
||||||
|
drive.setTotalSize(driveDO.getTotalSize());
|
||||||
|
drive.setStatus(driveDO.getStatus());
|
||||||
|
drive.setUpdateTime(driveDO.getUpdateTime());
|
||||||
|
drive.setCreateTime(driveDO.getCreateTime());
|
||||||
|
return drive;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DO → Map 转换
|
||||||
|
* 将 DriveDO 转换为 API 返回用的 Map
|
||||||
|
*/
|
||||||
|
private Map<String, Object> toMap(DriveDO driveDO) {
|
||||||
|
var map = new LinkedHashMap<String, Object>();
|
||||||
|
map.put("id", driveDO.getId());
|
||||||
|
map.put("name", driveDO.getName());
|
||||||
|
map.put("description", driveDO.getDescription());
|
||||||
|
map.put("logoUrl", driveDO.getLogoUrl());
|
||||||
|
map.put("folderCount", driveDO.getFolderCount());
|
||||||
|
map.put("fileCount", driveDO.getFileCount());
|
||||||
|
map.put("totalSize", driveDO.getTotalSize());
|
||||||
|
map.put("status", driveDO.getStatus());
|
||||||
|
map.put("updateTime", driveDO.getUpdateTime() != null ? driveDO.getUpdateTime().toString() : null);
|
||||||
|
map.put("createTime", driveDO.getCreateTime() != null ? driveDO.getCreateTime().toString() : null);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,172 @@
|
|||||||
|
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.IconDO;
|
||||||
|
import com.wangpan.common.dal.mapper.IconMapper;
|
||||||
|
import com.wangpan.common.entity.Icon;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon Repository 层(Dao)
|
||||||
|
*
|
||||||
|
* 继承 MyBatis-Plus ServiceImpl<IconMapper, IconDO>,自动获得 CRUD 能力
|
||||||
|
* 职责:
|
||||||
|
* 1. 对外提供业务维度的数据访问接口(使用 Entity/Map)
|
||||||
|
* 2. 使用 MyBatis-Plus 的 LambdaQueryWrapper/LambdaUpdateWrapper 构建查询
|
||||||
|
* 3. 处理 Entity ↔ IconDO 的转换
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public class IconRepository extends ServiceImpl<IconMapper, IconDO> {
|
||||||
|
|
||||||
|
// ==================== 插入 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 Icon
|
||||||
|
* 将业务 Entity 转换为 DO 后调用 MyBatis-Plus save 方法插入
|
||||||
|
*/
|
||||||
|
public Long insert(Icon icon) {
|
||||||
|
var iconDO = toIconDO(icon);
|
||||||
|
save(iconDO);
|
||||||
|
return iconDO.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 查询 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 ID 查询 Icon(返回 Entity)
|
||||||
|
*/
|
||||||
|
public Icon findById(Long id) {
|
||||||
|
var iconDO = getById(id);
|
||||||
|
return iconDO != null ? toIcon(iconDO) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询用户的 Icon 列表(返回 Map 列表供 API 使用)
|
||||||
|
* 按 sort_order 升序、create_time 降序排列
|
||||||
|
*/
|
||||||
|
public List<Map<String, Object>> queryByUserId(Long userId) {
|
||||||
|
var wrapper = new LambdaQueryWrapper<IconDO>()
|
||||||
|
.eq(IconDO::getUserId, userId)
|
||||||
|
.orderByAsc(IconDO::getSortOrder)
|
||||||
|
.orderByDesc(IconDO::getCreateTime);
|
||||||
|
|
||||||
|
var result = list(wrapper);
|
||||||
|
var list = new ArrayList<Map<String, Object>>();
|
||||||
|
for (var iconDO : result) {
|
||||||
|
list.add(toMap(iconDO));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 ID 和用户 ID 查询 Icon(确保只能访问自己的数据)
|
||||||
|
*/
|
||||||
|
public Icon findByIdAndUserId(Long id, Long userId) {
|
||||||
|
var wrapper = new LambdaQueryWrapper<IconDO>()
|
||||||
|
.eq(IconDO::getId, id)
|
||||||
|
.eq(IconDO::getUserId, userId);
|
||||||
|
var iconDO = getOne(wrapper);
|
||||||
|
return iconDO != null ? toIcon(iconDO) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 更新 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 Icon
|
||||||
|
*/
|
||||||
|
public boolean update(Long id, Long userId, String name, String url, String internalUrl, String linkType, String description, Integer sortOrder) {
|
||||||
|
var wrapper = new LambdaUpdateWrapper<IconDO>()
|
||||||
|
.eq(IconDO::getId, id)
|
||||||
|
.eq(IconDO::getUserId, userId)
|
||||||
|
.set(name != null, IconDO::getName, name)
|
||||||
|
.set(url != null, IconDO::getUrl, url)
|
||||||
|
.set(internalUrl != null, IconDO::getInternalUrl, internalUrl)
|
||||||
|
.set(linkType != null, IconDO::getLinkType, linkType)
|
||||||
|
.set(description != null, IconDO::getDescription, description)
|
||||||
|
.set(sortOrder != null, IconDO::getSortOrder, sortOrder)
|
||||||
|
.set(IconDO::getUpdateTime, LocalDateTime.now());
|
||||||
|
return update(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 Icon 的 logoUrl
|
||||||
|
*/
|
||||||
|
public boolean updateLogoUrl(Long id, String logoUrl) {
|
||||||
|
var wrapper = new LambdaUpdateWrapper<IconDO>()
|
||||||
|
.eq(IconDO::getId, id)
|
||||||
|
.set(IconDO::getLogoUrl, logoUrl)
|
||||||
|
.set(IconDO::getUpdateTime, LocalDateTime.now());
|
||||||
|
return update(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 删除 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除 Icon(校验用户归属)
|
||||||
|
*/
|
||||||
|
public boolean delete(Long id, Long userId) {
|
||||||
|
var wrapper = new LambdaQueryWrapper<IconDO>()
|
||||||
|
.eq(IconDO::getId, id)
|
||||||
|
.eq(IconDO::getUserId, userId);
|
||||||
|
return remove(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Entity ↔ DO 转换 ====================
|
||||||
|
|
||||||
|
private IconDO toIconDO(Icon icon) {
|
||||||
|
var iconDO = new IconDO();
|
||||||
|
iconDO.setId(icon.getId());
|
||||||
|
iconDO.setUserId(icon.getUserId());
|
||||||
|
iconDO.setName(icon.getName());
|
||||||
|
iconDO.setUrl(icon.getUrl());
|
||||||
|
iconDO.setInternalUrl(icon.getInternalUrl());
|
||||||
|
iconDO.setLinkType(icon.getLinkType() != null ? icon.getLinkType() : "external");
|
||||||
|
iconDO.setDescription(icon.getDescription());
|
||||||
|
iconDO.setSortOrder(icon.getSortOrder());
|
||||||
|
iconDO.setStatus(icon.getStatus());
|
||||||
|
iconDO.setLogoUrl(icon.getLogoUrl());
|
||||||
|
// 项目未配置 MetaObjectHandler,需手动设置时间字段
|
||||||
|
var now = java.time.LocalDateTime.now();
|
||||||
|
iconDO.setCreateTime(icon.getCreateTime() != null ? icon.getCreateTime() : now);
|
||||||
|
iconDO.setUpdateTime(icon.getUpdateTime() != null ? icon.getUpdateTime() : now);
|
||||||
|
return iconDO;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Icon toIcon(IconDO iconDO) {
|
||||||
|
var icon = new Icon();
|
||||||
|
icon.setId(iconDO.getId());
|
||||||
|
icon.setUserId(iconDO.getUserId());
|
||||||
|
icon.setName(iconDO.getName());
|
||||||
|
icon.setUrl(iconDO.getUrl());
|
||||||
|
icon.setInternalUrl(iconDO.getInternalUrl());
|
||||||
|
icon.setLinkType(iconDO.getLinkType());
|
||||||
|
icon.setDescription(iconDO.getDescription());
|
||||||
|
icon.setSortOrder(iconDO.getSortOrder());
|
||||||
|
icon.setStatus(iconDO.getStatus());
|
||||||
|
icon.setLogoUrl(iconDO.getLogoUrl());
|
||||||
|
icon.setCreateTime(iconDO.getCreateTime());
|
||||||
|
icon.setUpdateTime(iconDO.getUpdateTime());
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> toMap(IconDO iconDO) {
|
||||||
|
var map = new LinkedHashMap<String, Object>();
|
||||||
|
map.put("id", iconDO.getId());
|
||||||
|
map.put("name", iconDO.getName());
|
||||||
|
map.put("url", iconDO.getUrl());
|
||||||
|
map.put("internalUrl", iconDO.getInternalUrl());
|
||||||
|
map.put("linkType", iconDO.getLinkType());
|
||||||
|
map.put("description", iconDO.getDescription());
|
||||||
|
map.put("sortOrder", iconDO.getSortOrder());
|
||||||
|
map.put("status", iconDO.getStatus());
|
||||||
|
map.put("logoUrl", iconDO.getLogoUrl());
|
||||||
|
map.put("createTime", iconDO.getCreateTime() != null ? iconDO.getCreateTime().toString() : null);
|
||||||
|
map.put("updateTime", iconDO.getUpdateTime() != null ? iconDO.getUpdateTime().toString() : null);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,140 @@
|
|||||||
|
package com.wangpan.common.dal.repository;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.wangpan.common.dal.dataobject.LoginLogDO;
|
||||||
|
import com.wangpan.common.dal.mapper.LoginLogMapper;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录日志 Repository 层(Dao)
|
||||||
|
*
|
||||||
|
* 继承 MyBatis-Plus ServiceImpl<LoginLogMapper, LoginLogDO>,自动获得 CRUD 能力
|
||||||
|
* 职责:
|
||||||
|
* 1. 对外提供业务维度的数据访问接口
|
||||||
|
* 2. 使用 MyBatis-Plus 的 LambdaQueryWrapper 构建查询
|
||||||
|
* 3. LoginLog 模块没有 Entity,直接使用 DO 和 Map
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public class LoginLogRepository extends ServiceImpl<LoginLogMapper, LoginLogDO> {
|
||||||
|
|
||||||
|
// ==================== 插入 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入一条登录日志
|
||||||
|
* 构建 LoginLogDO 后调用 MyBatis-Plus save 方法插入
|
||||||
|
*
|
||||||
|
* @param userId 用户ID(可能为null,如用户名不存在时)
|
||||||
|
* @param loginUsername 登录输入的用户名
|
||||||
|
* @param loginIp 客户端IP
|
||||||
|
* @param loginResult 登录结果:success / fail
|
||||||
|
* @param failReason 失败原因(成功时为null)
|
||||||
|
* @param userAgent 浏览器UA信息
|
||||||
|
*/
|
||||||
|
public int insert(Long userId, String loginUsername, String loginIp,
|
||||||
|
String loginResult, String failReason, String userAgent) {
|
||||||
|
var logDO = new LoginLogDO();
|
||||||
|
logDO.setUserId(userId);
|
||||||
|
logDO.setLoginUsername(loginUsername);
|
||||||
|
logDO.setLoginIp(loginIp);
|
||||||
|
logDO.setLoginResult(loginResult);
|
||||||
|
logDO.setFailReason(failReason);
|
||||||
|
logDO.setUserAgent(userAgent);
|
||||||
|
logDO.setLoginTime(LocalDateTime.now());
|
||||||
|
save(logDO);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 查询 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计指定时间范围内的失败次数
|
||||||
|
* 使用 MyBatis-Plus LambdaQueryWrapper 构建查询条件
|
||||||
|
*/
|
||||||
|
public int countRecentFailures(String loginUsername, int minutes) {
|
||||||
|
var since = LocalDateTime.now().minusMinutes(minutes);
|
||||||
|
return (int) count(new LambdaQueryWrapper<LoginLogDO>()
|
||||||
|
.eq(LoginLogDO::getLoginUsername, loginUsername)
|
||||||
|
.eq(LoginLogDO::getLoginResult, "fail")
|
||||||
|
.ge(LoginLogDO::getLoginTime, since));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询登录日志列表(分页 + 条件筛选 + 用户权限过滤)
|
||||||
|
* 使用 MyBatis-Plus LambdaQueryWrapper 构建分页查询
|
||||||
|
*
|
||||||
|
* @param loginUsername 用户名模糊搜索(可选)
|
||||||
|
* @param loginResult 登录结果过滤:success/fail(可选)
|
||||||
|
* @param startDate 开始日期(可选)
|
||||||
|
* @param endDate 结束日期(可选)
|
||||||
|
* @param userId 用户ID权限过滤(普通用户只能看自己的日志,管理员传null)
|
||||||
|
* @param pageNo 页码
|
||||||
|
* @param pageSize 每页条数
|
||||||
|
*/
|
||||||
|
public Map<String, Object> queryLoginLogs(String loginUsername, String loginResult,
|
||||||
|
String startDate, String endDate,
|
||||||
|
Long userId,
|
||||||
|
int pageNo, int pageSize) {
|
||||||
|
var page = new Page<LoginLogDO>(pageNo, pageSize);
|
||||||
|
var wrapper = new LambdaQueryWrapper<LoginLogDO>();
|
||||||
|
|
||||||
|
// 动态条件:用户权限过滤(普通用户只能看自己的日志)
|
||||||
|
if (userId != null) {
|
||||||
|
wrapper.eq(LoginLogDO::getUserId, userId);
|
||||||
|
}
|
||||||
|
// 动态条件:用户名模糊查询
|
||||||
|
if (loginUsername != null && !loginUsername.isBlank()) {
|
||||||
|
wrapper.like(LoginLogDO::getLoginUsername, loginUsername);
|
||||||
|
}
|
||||||
|
// 动态条件:登录结果过滤
|
||||||
|
if (loginResult != null && !loginResult.isBlank()) {
|
||||||
|
wrapper.eq(LoginLogDO::getLoginResult, loginResult);
|
||||||
|
}
|
||||||
|
// 动态条件:开始日期过滤
|
||||||
|
if (startDate != null && !startDate.isBlank()) {
|
||||||
|
wrapper.ge(LoginLogDO::getLoginTime, startDate + " 00:00:00");
|
||||||
|
}
|
||||||
|
// 动态条件:结束日期过滤
|
||||||
|
if (endDate != null && !endDate.isBlank()) {
|
||||||
|
wrapper.le(LoginLogDO::getLoginTime, endDate + " 23:59:59");
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.orderByDesc(LoginLogDO::getLoginTime);
|
||||||
|
|
||||||
|
var result = page(page, wrapper);
|
||||||
|
var list = new ArrayList<Map<String, Object>>();
|
||||||
|
for (var logDO : result.getRecords()) {
|
||||||
|
list.add(toMap(logDO));
|
||||||
|
}
|
||||||
|
|
||||||
|
var resultMap = new LinkedHashMap<String, Object>();
|
||||||
|
resultMap.put("total", result.getTotal());
|
||||||
|
resultMap.put("list", list);
|
||||||
|
return resultMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== DO → Map 转换 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DO → Map 转换(用于 API 返回)
|
||||||
|
* 保持与前端兼容的字段名:username, ipAddress, status(success/fail)
|
||||||
|
*/
|
||||||
|
private Map<String, Object> toMap(LoginLogDO logDO) {
|
||||||
|
var map = new LinkedHashMap<String, Object>();
|
||||||
|
map.put("id", logDO.getId());
|
||||||
|
map.put("userId", logDO.getUserId());
|
||||||
|
map.put("username", logDO.getLoginUsername());
|
||||||
|
map.put("ipAddress", logDO.getLoginIp());
|
||||||
|
map.put("userAgent", logDO.getUserAgent());
|
||||||
|
map.put("status", logDO.getLoginResult());
|
||||||
|
map.put("failReason", logDO.getFailReason());
|
||||||
|
map.put("loginTime", logDO.getLoginTime() != null ? logDO.getLoginTime().toString() : null);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,340 @@
|
|||||||
|
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.plugins.pagination.Page;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.wangpan.common.dal.dataobject.MovieDO;
|
||||||
|
import com.wangpan.common.dal.mapper.MovieMapper;
|
||||||
|
import com.wangpan.common.entity.Movie;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 电影 Repository 层(Dao)
|
||||||
|
*
|
||||||
|
* 继承 MyBatis-Plus ServiceImpl<MovieMapper, MovieDO>,自动获得 CRUD 能力
|
||||||
|
* 职责:
|
||||||
|
* 1. 对外提供业务维度的数据访问接口(使用 Entity/Map)
|
||||||
|
* 2. 使用 MyBatis-Plus 的 LambdaQueryWrapper/LambdaUpdateWrapper 构建查询
|
||||||
|
* 3. 处理 Entity ↔ MovieDO 的转换
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public class MovieRepository extends ServiceImpl<MovieMapper, MovieDO> {
|
||||||
|
|
||||||
|
// ==================== 插入 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增电影记录
|
||||||
|
* 将业务 Entity 转换为 DO 后调用 MyBatis-Plus save 方法插入
|
||||||
|
*/
|
||||||
|
public long insert(Movie movie) {
|
||||||
|
var movieDO = toMovieDO(movie);
|
||||||
|
save(movieDO);
|
||||||
|
return movieDO.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量插入电影记录
|
||||||
|
* 使用 MyBatis-Plus saveBatch 方法批量插入
|
||||||
|
*/
|
||||||
|
public int batchInsert(List<Movie> movies) {
|
||||||
|
var movieDOs = movies.stream().map(this::toMovieDO).toList();
|
||||||
|
saveBatch(movieDOs);
|
||||||
|
return movieDOs.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 查询 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID查询单个电影(不带用户ID校验,供内部刮削使用)
|
||||||
|
* 使用 MyBatis-Plus getById 方法
|
||||||
|
*/
|
||||||
|
public Movie findById(Long id) {
|
||||||
|
var movieDO = getById(id);
|
||||||
|
return movieDO != null ? toMovie(movieDO) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID查询单个电影(带用户ID校验)
|
||||||
|
* 使用 MyBatis-Plus LambdaQueryWrapper 构建查询条件
|
||||||
|
*/
|
||||||
|
public Movie findById(Long id, Long userId) {
|
||||||
|
var wrapper = new LambdaQueryWrapper<MovieDO>()
|
||||||
|
.eq(MovieDO::getId, id)
|
||||||
|
.eq(MovieDO::getUserId, userId);
|
||||||
|
var movieDO = getOne(wrapper, false);
|
||||||
|
return movieDO != null ? toMovie(movieDO) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询用户的电影列表
|
||||||
|
* 使用 MyBatis-Plus LambdaQueryWrapper 构建分页查询,支持动态条件
|
||||||
|
*/
|
||||||
|
public Map<String, Object> queryByPage(Long userId, String keyword, String status, String category,
|
||||||
|
String scrapeType, String sortBy, String sortOrder,
|
||||||
|
int pageNo, int pageSize) {
|
||||||
|
var page = new Page<MovieDO>(pageNo, pageSize);
|
||||||
|
var wrapper = new LambdaQueryWrapper<MovieDO>()
|
||||||
|
.eq(MovieDO::getUserId, userId);
|
||||||
|
|
||||||
|
// 动态条件:关键词模糊查询(标题或副标题)
|
||||||
|
if (keyword != null && !keyword.isBlank()) {
|
||||||
|
wrapper.and(w -> w.like(MovieDO::getTitle, keyword).or().like(MovieDO::getSubtitle, keyword));
|
||||||
|
}
|
||||||
|
// 动态条件:状态过滤
|
||||||
|
if (status != null && !status.isBlank()) {
|
||||||
|
wrapper.eq(MovieDO::getStatus, status);
|
||||||
|
}
|
||||||
|
// 动态条件:分类过滤
|
||||||
|
if (category != null && !category.isBlank()) {
|
||||||
|
wrapper.eq(MovieDO::getCategory, category);
|
||||||
|
}
|
||||||
|
// 动态条件:刮削类型过滤
|
||||||
|
if (scrapeType != null && !scrapeType.isBlank()) {
|
||||||
|
wrapper.eq(MovieDO::getScrapeType, scrapeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排序:默认按创建时间降序
|
||||||
|
if ("rating".equals(sortBy)) {
|
||||||
|
wrapper.orderBy(true, "asc".equals(sortOrder), MovieDO::getRating);
|
||||||
|
} else if ("title".equals(sortBy)) {
|
||||||
|
wrapper.orderBy(true, "asc".equals(sortOrder), MovieDO::getTitle);
|
||||||
|
} else {
|
||||||
|
wrapper.orderByDesc(MovieDO::getCreateTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = page(page, wrapper);
|
||||||
|
var list = new ArrayList<Map<String, Object>>();
|
||||||
|
for (var movieDO : result.getRecords()) {
|
||||||
|
list.add(toMap(movieDO));
|
||||||
|
}
|
||||||
|
|
||||||
|
var resultMap = new LinkedHashMap<String, Object>();
|
||||||
|
resultMap.put("total", result.getTotal());
|
||||||
|
resultMap.put("list", list);
|
||||||
|
return resultMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户的所有分类列表
|
||||||
|
* 直接查询 category 字段后在内存中去重,避免数据库层面的 groupBy 冗余
|
||||||
|
*/
|
||||||
|
public List<String> findCategories(Long userId) {
|
||||||
|
var wrapper = new LambdaQueryWrapper<MovieDO>()
|
||||||
|
.select(MovieDO::getCategory)
|
||||||
|
.eq(MovieDO::getUserId, userId)
|
||||||
|
.isNotNull(MovieDO::getCategory)
|
||||||
|
.ne(MovieDO::getCategory, "");
|
||||||
|
|
||||||
|
return list(wrapper).stream()
|
||||||
|
.map(MovieDO::getCategory)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户的电影总数统计
|
||||||
|
* 使用 MyBatis-Plus count 方法构建统计查询
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getStatistics(Long userId) {
|
||||||
|
var total = count(new LambdaQueryWrapper<MovieDO>().eq(MovieDO::getUserId, userId));
|
||||||
|
var active = count(new LambdaQueryWrapper<MovieDO>()
|
||||||
|
.eq(MovieDO::getUserId, userId).eq(MovieDO::getStatus, "active"));
|
||||||
|
var scraped = count(new LambdaQueryWrapper<MovieDO>()
|
||||||
|
.eq(MovieDO::getUserId, userId).ne(MovieDO::getScrapeType, "none"));
|
||||||
|
|
||||||
|
var stats = new LinkedHashMap<String, Object>();
|
||||||
|
stats.put("total", total);
|
||||||
|
stats.put("active", active);
|
||||||
|
stats.put("scraped", scraped);
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 更新 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新电影信息
|
||||||
|
* 使用 MyBatis-Plus updateById 方法
|
||||||
|
*/
|
||||||
|
public int update(Movie movie) {
|
||||||
|
var movieDO = toMovieDO(movie);
|
||||||
|
return updateById(movieDO) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新电影状态
|
||||||
|
* 使用 MyBatis-Plus LambdaUpdateWrapper 构建更新条件
|
||||||
|
*/
|
||||||
|
public int updateStatus(Long id, Long userId, String status) {
|
||||||
|
var wrapper = new LambdaUpdateWrapper<MovieDO>()
|
||||||
|
.eq(MovieDO::getId, id)
|
||||||
|
.eq(MovieDO::getUserId, userId)
|
||||||
|
.set(MovieDO::getStatus, status)
|
||||||
|
.set(MovieDO::getUpdateTime, LocalDateTime.now());
|
||||||
|
return update(wrapper) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 删除 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除电影
|
||||||
|
* 使用 MyBatis-Plus remove 方法配合条件构造器
|
||||||
|
*/
|
||||||
|
public int deleteById(Long id, Long userId) {
|
||||||
|
var wrapper = new LambdaQueryWrapper<MovieDO>()
|
||||||
|
.eq(MovieDO::getId, id)
|
||||||
|
.eq(MovieDO::getUserId, userId);
|
||||||
|
return remove(wrapper) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除电影
|
||||||
|
* 使用 MyBatis-Plus removeByIds 方法配合条件构造器
|
||||||
|
*/
|
||||||
|
public int batchDelete(List<Long> ids, Long userId) {
|
||||||
|
var wrapper = new LambdaQueryWrapper<MovieDO>()
|
||||||
|
.in(MovieDO::getId, ids)
|
||||||
|
.eq(MovieDO::getUserId, userId);
|
||||||
|
return remove(wrapper) ? ids.size() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 随机查询指定数量的电影(用于今日推荐)
|
||||||
|
* 使用 MySQL 的 RAND() 函数实现随机排序
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param limit 返回数量,默认3
|
||||||
|
* @return 随机电影列表
|
||||||
|
*/
|
||||||
|
public List<Map<String, Object>> findRandomMovies(Long userId, int limit) {
|
||||||
|
var wrapper = new LambdaQueryWrapper<MovieDO>()
|
||||||
|
.eq(MovieDO::getUserId, userId)
|
||||||
|
.last("ORDER BY RAND() LIMIT " + limit);
|
||||||
|
|
||||||
|
var movieDOs = list(wrapper);
|
||||||
|
return movieDOs.stream().map(this::toMap).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 刮削辅助方法 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找所有配置了TMDB API Key的用户(供定时刮削使用)
|
||||||
|
* 委托给 MovieMapper 中定义的 XML 查询
|
||||||
|
*/
|
||||||
|
public List<Map<String, Object>> findUsersWithTmdbKey() {
|
||||||
|
return baseMapper.findUsersWithTmdbKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找指定用户需要刮削的电影
|
||||||
|
* 委托给 MovieMapper 中定义的 XML 查询
|
||||||
|
*/
|
||||||
|
public List<Movie> findMoviesNeedingScrape(Long userId) {
|
||||||
|
var movieDOs = baseMapper.findMoviesNeedingScrape(userId, 7);
|
||||||
|
return movieDOs.stream().map(this::toMovie).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新电影评分
|
||||||
|
* 委托给 MovieMapper 中定义的 @Update 注解方法
|
||||||
|
*/
|
||||||
|
public int updateRating(Long id, Integer rating) {
|
||||||
|
return baseMapper.updateRating(id, rating);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新电影海报URL
|
||||||
|
* 委托给 MovieMapper 中定义的 @Update 注解方法
|
||||||
|
*/
|
||||||
|
public int updatePosterUrl(Long id, String posterUrl) {
|
||||||
|
return baseMapper.updatePosterUrl(id, posterUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新电影标签
|
||||||
|
* 委托给 MovieMapper 中定义的 @Update 注解方法
|
||||||
|
*/
|
||||||
|
public int updateTags(Long id, String tags) {
|
||||||
|
return baseMapper.updateTags(id, tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新刮削类型标记
|
||||||
|
* 委托给 MovieMapper 中定义的 @Update 注解方法
|
||||||
|
*/
|
||||||
|
public int updateScrapeType(Long id, String scrapeType) {
|
||||||
|
return baseMapper.updateScrapeType(id, scrapeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Entity ↔ DO 转换 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entity → DO 转换
|
||||||
|
*/
|
||||||
|
private MovieDO toMovieDO(Movie movie) {
|
||||||
|
var movieDO = new MovieDO();
|
||||||
|
movieDO.setId(movie.getId());
|
||||||
|
movieDO.setUserId(movie.getUserId());
|
||||||
|
movieDO.setTitle(movie.getTitle());
|
||||||
|
movieDO.setSubtitle(movie.getSubtitle());
|
||||||
|
movieDO.setResourceLibrary(movie.getResourceLibrary());
|
||||||
|
movieDO.setMaintainer(movie.getMaintainer());
|
||||||
|
movieDO.setStatus(movie.getStatus());
|
||||||
|
movieDO.setCategory(movie.getCategory());
|
||||||
|
movieDO.setScrapeType(movie.getScrapeType());
|
||||||
|
movieDO.setRating(movie.getRating());
|
||||||
|
movieDO.setPosterUrl(movie.getPosterUrl());
|
||||||
|
movieDO.setTags(movie.getTags());
|
||||||
|
movieDO.setUpdateTime(movie.getUpdateTime());
|
||||||
|
movieDO.setCreateTime(movie.getCreateTime());
|
||||||
|
return movieDO;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DO → Entity 转换
|
||||||
|
*/
|
||||||
|
private Movie toMovie(MovieDO movieDO) {
|
||||||
|
var movie = new Movie();
|
||||||
|
movie.setId(movieDO.getId());
|
||||||
|
movie.setUserId(movieDO.getUserId());
|
||||||
|
movie.setTitle(movieDO.getTitle());
|
||||||
|
movie.setSubtitle(movieDO.getSubtitle());
|
||||||
|
movie.setResourceLibrary(movieDO.getResourceLibrary());
|
||||||
|
movie.setMaintainer(movieDO.getMaintainer());
|
||||||
|
movie.setStatus(movieDO.getStatus());
|
||||||
|
movie.setCategory(movieDO.getCategory());
|
||||||
|
movie.setScrapeType(movieDO.getScrapeType());
|
||||||
|
movie.setRating(movieDO.getRating());
|
||||||
|
movie.setPosterUrl(movieDO.getPosterUrl());
|
||||||
|
movie.setTags(movieDO.getTags());
|
||||||
|
movie.setUpdateTime(movieDO.getUpdateTime());
|
||||||
|
movie.setCreateTime(movieDO.getCreateTime());
|
||||||
|
return movie;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DO → Map 转换(用于 API 返回)
|
||||||
|
*/
|
||||||
|
private Map<String, Object> toMap(MovieDO movieDO) {
|
||||||
|
var map = new LinkedHashMap<String, Object>();
|
||||||
|
map.put("id", movieDO.getId());
|
||||||
|
map.put("userId", movieDO.getUserId());
|
||||||
|
map.put("title", movieDO.getTitle());
|
||||||
|
map.put("subtitle", movieDO.getSubtitle());
|
||||||
|
map.put("resourceLibrary", movieDO.getResourceLibrary());
|
||||||
|
map.put("maintainer", movieDO.getMaintainer());
|
||||||
|
map.put("status", movieDO.getStatus());
|
||||||
|
map.put("category", movieDO.getCategory());
|
||||||
|
map.put("scrapeType", movieDO.getScrapeType());
|
||||||
|
map.put("rating", movieDO.getRating());
|
||||||
|
map.put("posterUrl", movieDO.getPosterUrl());
|
||||||
|
map.put("tags", movieDO.getTags());
|
||||||
|
map.put("updateTime", movieDO.getUpdateTime() != null ? movieDO.getUpdateTime().toString() : null);
|
||||||
|
map.put("createTime", movieDO.getCreateTime() != null ? movieDO.getCreateTime().toString() : null);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,126 @@
|
|||||||
|
package com.wangpan.common.dal.repository;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.wangpan.common.dal.dataobject.TmdbCacheDO;
|
||||||
|
import com.wangpan.common.dal.mapper.TmdbCacheMapper;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TMDB 刮削缓存 Repository 层(Dao)
|
||||||
|
*
|
||||||
|
* 继承 MyBatis-Plus ServiceImpl<TmdbCacheMapper, TmdbCacheDO>,自动获得 CRUD 能力
|
||||||
|
* 职责:
|
||||||
|
* 1. 对外提供业务维度的数据访问接口
|
||||||
|
* 2. 使用 MyBatis-Plus 的 LambdaQueryWrapper 构建查询
|
||||||
|
* 3. TmdbCache 模块没有 Entity,直接使用 DO 和 Map
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public class TmdbCacheRepository extends ServiceImpl<TmdbCacheMapper, TmdbCacheDO> {
|
||||||
|
|
||||||
|
// ==================== 查询 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据电影ID查询缓存
|
||||||
|
* 委托给 TmdbCacheMapper 中定义的 XML 查询
|
||||||
|
*/
|
||||||
|
public TmdbCacheDO findByMovieId(Long movieId) {
|
||||||
|
return baseMapper.findByMovieId(movieId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据TMDB ID查询缓存
|
||||||
|
* 委托给 TmdbCacheMapper 中定义的 XML 查询
|
||||||
|
*/
|
||||||
|
public List<TmdbCacheDO> findByTmdbId(Integer tmdbId) {
|
||||||
|
return baseMapper.findByTmdbId(tmdbId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询用户的所有缓存记录(分页)
|
||||||
|
* 委托给 TmdbCacheMapper 中定义的 XML 查询
|
||||||
|
*/
|
||||||
|
public Map<String, Object> queryByUserId(Long userId, int pageNo, int pageSize) {
|
||||||
|
return baseMapper.queryByUserId(userId, pageNo, pageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询所有过期的缓存记录
|
||||||
|
* 委托给 TmdbCacheMapper 中定义的 XML 查询
|
||||||
|
*/
|
||||||
|
public List<Long> findExpiredCaches(LocalDateTime expireBefore) {
|
||||||
|
return baseMapper.findExpiredCaches(expireBefore);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 写入/更新 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增或更新缓存记录
|
||||||
|
* 委托给 TmdbCacheMapper 中定义的 XML 查询(UPSERT)
|
||||||
|
*/
|
||||||
|
public int saveOrUpdate(Map<String, Object> data) {
|
||||||
|
return baseMapper.saveOrUpdate(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将缓存标记为过期
|
||||||
|
* 委托给 TmdbCacheMapper 中定义的 XML 查询
|
||||||
|
*/
|
||||||
|
public int markAsExpired(List<Long> cacheIds) {
|
||||||
|
return baseMapper.markAsExpired(cacheIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定电影的缓存
|
||||||
|
* 委托给 TmdbCacheMapper 中定义的 XML 查询
|
||||||
|
*/
|
||||||
|
public int deleteByMovieId(Long movieId) {
|
||||||
|
return baseMapper.deleteByMovieId(movieId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户的缓存统计信息
|
||||||
|
* 委托给 TmdbCacheMapper 中定义的 XML 查询
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getStatistics(Long userId) {
|
||||||
|
return baseMapper.getStatistics(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== DO → Map 转换 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 TmdbCacheDO 转换为 Map(供业务层使用,兼容旧接口返回格式)
|
||||||
|
*/
|
||||||
|
public Map<String, Object> toMap(TmdbCacheDO cacheDO) {
|
||||||
|
if (cacheDO == null) return null;
|
||||||
|
|
||||||
|
var map = new LinkedHashMap<String, Object>();
|
||||||
|
map.put("id", cacheDO.getId());
|
||||||
|
map.put("movieId", cacheDO.getMovieId());
|
||||||
|
map.put("userId", cacheDO.getUserId());
|
||||||
|
map.put("tmdbId", cacheDO.getTmdbId());
|
||||||
|
map.put("title", cacheDO.getTitle());
|
||||||
|
map.put("originalTitle", cacheDO.getOriginalTitle());
|
||||||
|
map.put("overview", cacheDO.getOverview());
|
||||||
|
map.put("releaseDate", cacheDO.getReleaseDate());
|
||||||
|
map.put("posterPath", cacheDO.getPosterPath());
|
||||||
|
map.put("backdropPath", cacheDO.getBackdropPath());
|
||||||
|
map.put("localPosterUrl", cacheDO.getLocalPosterUrl());
|
||||||
|
map.put("tmdbRating", cacheDO.getTmdbRating());
|
||||||
|
map.put("voteCount", cacheDO.getVoteCount());
|
||||||
|
map.put("runtime", cacheDO.getRuntime());
|
||||||
|
map.put("genres", cacheDO.getGenres());
|
||||||
|
map.put("directors", cacheDO.getDirectors());
|
||||||
|
map.put("casts", cacheDO.getCasts());
|
||||||
|
map.put("originalLanguage", cacheDO.getOriginalLanguage());
|
||||||
|
map.put("productionCountries", cacheDO.getProductionCountries());
|
||||||
|
map.put("status", cacheDO.getStatus());
|
||||||
|
map.put("cacheTime", cacheDO.getCacheTime() != null ? cacheDO.getCacheTime().toString() : null);
|
||||||
|
map.put("updateTime", cacheDO.getUpdateTime() != null ? cacheDO.getUpdateTime().toString() : null);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
package com.wangpan.common.dal.repository;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.wangpan.common.dal.dataobject.TmdbScraperLogDO;
|
||||||
|
import com.wangpan.common.dal.mapper.TmdbScraperLogMapper;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TMDB 定时刮削执行记录 Repository 层(Dao)
|
||||||
|
*
|
||||||
|
* 继承 MyBatis-Plus ServiceImpl<TmdbScraperLogMapper, TmdbScraperLogDO>,自动获得 CRUD 能力
|
||||||
|
* 职责:
|
||||||
|
* 1. 对外提供业务维度的数据访问接口
|
||||||
|
* 2. TmdbScraperLog 模块没有 Entity,直接透传 Mapper 返回的 Map
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public class TmdbScraperLogRepository extends ServiceImpl<TmdbScraperLogMapper, TmdbScraperLogDO> {
|
||||||
|
|
||||||
|
// ==================== 插入 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入一条执行记录
|
||||||
|
* 委托给 TmdbScraperLogMapper 中定义的 XML 查询
|
||||||
|
*/
|
||||||
|
public int insert(Map<String, Object> log) {
|
||||||
|
return baseMapper.insertLog(log);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 查询 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询最近的执行记录(分页)
|
||||||
|
* 委托给 TmdbScraperLogMapper 中定义的 XML 查询
|
||||||
|
*/
|
||||||
|
public Map<String, Object> queryRecent(int pageNo, int pageSize) {
|
||||||
|
return baseMapper.queryRecent(pageNo, pageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询最近的N条执行记录
|
||||||
|
* 委托给 TmdbScraperLogMapper 中定义的 XML 查询
|
||||||
|
*/
|
||||||
|
public List<Map<String, Object>> queryRecent(int limit) {
|
||||||
|
return baseMapper.queryRecentByLimit(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID查询单条记录
|
||||||
|
* 委托给 TmdbScraperLogMapper 中定义的 XML 查询
|
||||||
|
*/
|
||||||
|
public Map<String, Object> findById(Long id) {
|
||||||
|
return baseMapper.findById(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,138 @@
|
|||||||
|
package com.wangpan.common.dal.repository;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.wangpan.common.dal.dataobject.TokenDO;
|
||||||
|
import com.wangpan.common.dal.mapper.TokenMapper;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户令牌 Repository 层(Dao)
|
||||||
|
*
|
||||||
|
* 继承 MyBatis-Plus ServiceImpl<TokenMapper, TokenDO>,自动获得 CRUD 能力
|
||||||
|
* 职责:
|
||||||
|
* 1. 对外提供业务维度的数据访问接口
|
||||||
|
* 2. 使用 MyBatis-Plus 的 LambdaQueryWrapper 构建查询
|
||||||
|
* 3. Token 模块没有 Entity,直接使用 DO 和 Map
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public class TokenRepository extends ServiceImpl<TokenMapper, TokenDO> {
|
||||||
|
|
||||||
|
// ==================== 插入 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存新令牌
|
||||||
|
* 构建 TokenDO 后调用 MyBatis-Plus save 方法插入
|
||||||
|
*/
|
||||||
|
public long save(Long userId, String token, String deviceInfo,
|
||||||
|
String ipAddress, LocalDateTime expireTime) {
|
||||||
|
var tokenDO = new TokenDO();
|
||||||
|
tokenDO.setUserId(userId);
|
||||||
|
tokenDO.setToken(token);
|
||||||
|
tokenDO.setDeviceInfo(deviceInfo);
|
||||||
|
tokenDO.setIpAddress(ipAddress);
|
||||||
|
tokenDO.setExpireTime(expireTime);
|
||||||
|
tokenDO.setStatus(1); // 1-有效
|
||||||
|
save(tokenDO);
|
||||||
|
return tokenDO.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 查询 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询用户的有效令牌数量
|
||||||
|
* 委托给 TokenMapper 中定义的 @Select 注解方法
|
||||||
|
*/
|
||||||
|
public int countActiveTokens(Long userId) {
|
||||||
|
return baseMapper.countActiveTokens(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询用户的令牌列表
|
||||||
|
* 使用 MyBatis-Plus LambdaQueryWrapper 构建查询条件
|
||||||
|
*/
|
||||||
|
public List<Map<String, Object>> findByUserId(Long userId, int maxCount) {
|
||||||
|
// 使用 Page 分页替代 .last("LIMIT ..."),避免 SQL 注入风险
|
||||||
|
var page = new Page<TokenDO>(1, maxCount);
|
||||||
|
var wrapper = new LambdaQueryWrapper<TokenDO>()
|
||||||
|
.eq(TokenDO::getUserId, userId)
|
||||||
|
.eq(TokenDO::getStatus, 1)
|
||||||
|
.gt(TokenDO::getExpireTime, LocalDateTime.now())
|
||||||
|
.orderByDesc(TokenDO::getCreateTime);
|
||||||
|
|
||||||
|
var result = page(page, wrapper);
|
||||||
|
var list = new ArrayList<Map<String, Object>>();
|
||||||
|
for (var tokenDO : result.getRecords()) {
|
||||||
|
list.add(toMap(tokenDO));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证令牌是否有效
|
||||||
|
* 委托给 TokenMapper 中定义的 @Select 注解方法
|
||||||
|
*/
|
||||||
|
public boolean isValid(String token) {
|
||||||
|
return baseMapper.isValid(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 更新 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 撤销指定令牌
|
||||||
|
* 委托给 TokenMapper 中定义的 @Update 注解方法
|
||||||
|
*/
|
||||||
|
public int revokeById(Long tokenId, Long userId) {
|
||||||
|
return baseMapper.revokeById(tokenId, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 撤销用户的所有令牌
|
||||||
|
* 委托给 TokenMapper 中定义的 @Update 注解方法
|
||||||
|
*/
|
||||||
|
public int revokeAllByUserId(Long userId) {
|
||||||
|
return baseMapper.revokeAllByUserId(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 删除 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理过期的已撤销令牌
|
||||||
|
* 委托给 TokenMapper 中定义的 @Update 注解方法
|
||||||
|
*/
|
||||||
|
public int cleanExpiredRevokedTokens() {
|
||||||
|
return baseMapper.cleanExpiredRevokedTokens();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除用户的所有令牌记录
|
||||||
|
* 委托给 TokenMapper 中定义的 @Update 注解方法
|
||||||
|
*/
|
||||||
|
public int deleteByUserId(Long userId) {
|
||||||
|
return baseMapper.deleteByUserId(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== DO → Map 转换 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DO → Map 转换(用于 API 返回)
|
||||||
|
*/
|
||||||
|
private Map<String, Object> toMap(TokenDO tokenDO) {
|
||||||
|
var map = new LinkedHashMap<String, Object>();
|
||||||
|
map.put("id", tokenDO.getId());
|
||||||
|
map.put("userId", tokenDO.getUserId());
|
||||||
|
map.put("deviceInfo", tokenDO.getDeviceInfo());
|
||||||
|
map.put("ipAddress", tokenDO.getIpAddress());
|
||||||
|
map.put("expireTime", tokenDO.getExpireTime() != null ? tokenDO.getExpireTime().toString() : null);
|
||||||
|
map.put("status", tokenDO.getStatus());
|
||||||
|
map.put("createTime", tokenDO.getCreateTime() != null ? tokenDO.getCreateTime().toString() : null);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,277 @@
|
|||||||
|
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.plugins.pagination.Page;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.wangpan.common.dal.dataobject.UserDO;
|
||||||
|
import com.wangpan.common.dal.mapper.UserMapper;
|
||||||
|
import com.wangpan.common.entity.User;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户 Repository 层(Dao)
|
||||||
|
*
|
||||||
|
* 继承 MyBatis-Plus ServiceImpl<UserMapper, UserDO>,自动获得 CRUD 能力
|
||||||
|
* 职责:
|
||||||
|
* 1. 对外提供业务维度的数据访问接口(使用 Entity/Map)
|
||||||
|
* 2. 使用 MyBatis-Plus 的 LambdaQueryWrapper 构建查询
|
||||||
|
* 3. 处理 Entity ↔ UserDO 的转换
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public class UserRepository extends ServiceImpl<UserMapper, UserDO> {
|
||||||
|
|
||||||
|
// ==================== 查询 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户名查询用户
|
||||||
|
* 使用 MyBatis-Plus LambdaQueryWrapper 构建查询条件
|
||||||
|
*/
|
||||||
|
public User findByUsername(String username) {
|
||||||
|
var wrapper = new LambdaQueryWrapper<UserDO>()
|
||||||
|
.eq(UserDO::getUsername, username)
|
||||||
|
.eq(UserDO::getIsDeleted, 0);
|
||||||
|
var userDO = getOne(wrapper, false);
|
||||||
|
return userDO != null ? toUser(userDO) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户ID查询用户
|
||||||
|
* 使用 MyBatis-Plus getById 方法
|
||||||
|
*/
|
||||||
|
public User findById(Long userId) {
|
||||||
|
var userDO = getById(userId);
|
||||||
|
// 检查逻辑删除
|
||||||
|
if (userDO != null && userDO.getIsDeleted() != null && userDO.getIsDeleted() == 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return userDO != null ? toUser(userDO) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据邮箱查询用户
|
||||||
|
* 使用 MyBatis-Plus LambdaQueryWrapper 构建查询条件
|
||||||
|
*/
|
||||||
|
public User findByEmail(String email) {
|
||||||
|
var wrapper = new LambdaQueryWrapper<UserDO>()
|
||||||
|
.eq(UserDO::getEmail, email)
|
||||||
|
.eq(UserDO::getIsDeleted, 0);
|
||||||
|
var userDO = getOne(wrapper, false);
|
||||||
|
return userDO != null ? toUser(userDO) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 插入 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入新用户
|
||||||
|
* 将业务 Entity 转换为 DO 后调用 MyBatis-Plus save 方法插入
|
||||||
|
*/
|
||||||
|
public int insert(User user) {
|
||||||
|
var userDO = toUserDO(user);
|
||||||
|
save(userDO);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 更新 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户的最后登录信息
|
||||||
|
* 委托给 UserMapper 中定义的 @Update 注解方法
|
||||||
|
*/
|
||||||
|
public int updateLastLoginInfo(Long userId, String loginIp, LocalDateTime loginTime) {
|
||||||
|
return baseMapper.updateLastLoginInfo(userId, loginIp, loginTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户密码
|
||||||
|
* 委托给 UserMapper 中定义的 @Update 注解方法
|
||||||
|
*/
|
||||||
|
public int updatePassword(Long userId, String newPassword) {
|
||||||
|
return baseMapper.updatePassword(userId, newPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户状态
|
||||||
|
* 委托给 UserMapper 中定义的 @Update 注解方法
|
||||||
|
*/
|
||||||
|
public int updateStatus(Long userId, int status) {
|
||||||
|
return baseMapper.updateStatus(userId, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置用户的锁定截止时间
|
||||||
|
* 委托给 UserMapper 中定义的 @Update 注解方法
|
||||||
|
*/
|
||||||
|
public int updateLockUntil(Long userId, LocalDateTime lockUntil) {
|
||||||
|
return baseMapper.updateLockUntil(userId, lockUntil);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除用户的锁定状态
|
||||||
|
* 委托给 UserMapper 中定义的 @Update 注解方法
|
||||||
|
*/
|
||||||
|
public int clearLockUntil(Long userId) {
|
||||||
|
return baseMapper.clearLockUntil(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 原子性增加连续失败次数
|
||||||
|
* 委托给 UserMapper 中定义的 @Update 注解方法
|
||||||
|
*/
|
||||||
|
public int incrementFailCount(Long userId) {
|
||||||
|
return baseMapper.incrementFailCount(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置连续失败次数
|
||||||
|
* 委托给 UserMapper 中定义的 @Update 注解方法
|
||||||
|
*/
|
||||||
|
public int resetFailCount(Long userId) {
|
||||||
|
return baseMapper.resetFailCount(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户的TMDB API Key
|
||||||
|
* 委托给 UserMapper 中定义的 @Update 注解方法
|
||||||
|
*/
|
||||||
|
public int updateTmdbApiKey(Long userId, String tmdbApiKey) {
|
||||||
|
return baseMapper.updateTmdbApiKey(userId, tmdbApiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员分页查询用户列表
|
||||||
|
* 使用 MyBatis-Plus LambdaQueryWrapper 构建分页查询
|
||||||
|
*/
|
||||||
|
public Map<String, Object> queryUsers(String username, Integer status, Integer userType,
|
||||||
|
int pageNo, int pageSize) {
|
||||||
|
var page = new Page<UserDO>(pageNo, pageSize);
|
||||||
|
var wrapper = new LambdaQueryWrapper<UserDO>()
|
||||||
|
.eq(UserDO::getIsDeleted, 0);
|
||||||
|
|
||||||
|
// 动态条件:用户名模糊查询
|
||||||
|
if (username != null && !username.isBlank()) {
|
||||||
|
wrapper.like(UserDO::getUsername, username);
|
||||||
|
}
|
||||||
|
// 动态条件:状态过滤
|
||||||
|
if (status != null) {
|
||||||
|
wrapper.eq(UserDO::getStatus, status);
|
||||||
|
}
|
||||||
|
// 动态条件:用户类型过滤
|
||||||
|
if (userType != null) {
|
||||||
|
wrapper.eq(UserDO::getUserType, userType);
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.orderByDesc(UserDO::getCreateTime);
|
||||||
|
|
||||||
|
var result = page(page, wrapper);
|
||||||
|
var list = new java.util.ArrayList<java.util.Map<String, Object>>();
|
||||||
|
for (var userDO : result.getRecords()) {
|
||||||
|
list.add(toUserMap(userDO));
|
||||||
|
}
|
||||||
|
|
||||||
|
var resultMap = new java.util.LinkedHashMap<String, Object>();
|
||||||
|
resultMap.put("total", result.getTotal());
|
||||||
|
resultMap.put("list", list);
|
||||||
|
return resultMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员更新用户信息
|
||||||
|
* 使用 MyBatis-Plus LambdaUpdateWrapper 构建更新条件
|
||||||
|
*/
|
||||||
|
public int updateUser(Long userId, String email, String phone, String realName,
|
||||||
|
Integer status, Integer userType) {
|
||||||
|
var wrapper = new LambdaUpdateWrapper<UserDO>()
|
||||||
|
.eq(UserDO::getId, userId)
|
||||||
|
.set(UserDO::getEmail, email)
|
||||||
|
.set(UserDO::getPhone, phone)
|
||||||
|
.set(UserDO::getRealName, realName)
|
||||||
|
.set(UserDO::getStatus, status)
|
||||||
|
.set(UserDO::getUserType, userType)
|
||||||
|
.set(UserDO::getUpdateTime, LocalDateTime.now());
|
||||||
|
return update(wrapper) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 软删除用户
|
||||||
|
* 委托给 UserMapper 中定义的 @Update 注解方法
|
||||||
|
*/
|
||||||
|
public int softDelete(Long userId) {
|
||||||
|
return baseMapper.softDelete(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Entity ↔ DO 转换 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entity → DO 转换
|
||||||
|
*/
|
||||||
|
private UserDO toUserDO(User user) {
|
||||||
|
var userDO = new UserDO();
|
||||||
|
userDO.setId(user.getId());
|
||||||
|
userDO.setUsername(user.getUsername());
|
||||||
|
userDO.setPassword(user.getPassword());
|
||||||
|
userDO.setEmail(user.getEmail());
|
||||||
|
userDO.setPhone(user.getPhone());
|
||||||
|
userDO.setRealName(user.getRealName());
|
||||||
|
userDO.setAvatarUrl(user.getAvatarUrl());
|
||||||
|
userDO.setStatus(user.getStatus());
|
||||||
|
userDO.setUserType(user.getUserType());
|
||||||
|
userDO.setLastLoginIp(user.getLastLoginIp());
|
||||||
|
userDO.setLastLoginTime(user.getLastLoginTime());
|
||||||
|
userDO.setLockUntil(user.getLockUntil());
|
||||||
|
userDO.setFailCount(user.getFailCount());
|
||||||
|
userDO.setTmdbApiKey(user.getTmdbApiKey());
|
||||||
|
userDO.setCreateTime(user.getCreateTime());
|
||||||
|
userDO.setUpdateTime(user.getUpdateTime());
|
||||||
|
userDO.setIsDeleted(user.getIsDeleted());
|
||||||
|
userDO.setRemark(user.getRemark());
|
||||||
|
return userDO;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DO → Entity 转换
|
||||||
|
*/
|
||||||
|
private User toUser(UserDO userDO) {
|
||||||
|
var user = new User();
|
||||||
|
user.setId(userDO.getId());
|
||||||
|
user.setUsername(userDO.getUsername());
|
||||||
|
user.setPassword(userDO.getPassword());
|
||||||
|
user.setEmail(userDO.getEmail());
|
||||||
|
user.setPhone(userDO.getPhone());
|
||||||
|
user.setRealName(userDO.getRealName());
|
||||||
|
user.setAvatarUrl(userDO.getAvatarUrl());
|
||||||
|
user.setStatus(userDO.getStatus());
|
||||||
|
user.setUserType(userDO.getUserType());
|
||||||
|
user.setLastLoginIp(userDO.getLastLoginIp());
|
||||||
|
user.setLastLoginTime(userDO.getLastLoginTime());
|
||||||
|
user.setLockUntil(userDO.getLockUntil());
|
||||||
|
user.setFailCount(userDO.getFailCount());
|
||||||
|
user.setTmdbApiKey(userDO.getTmdbApiKey());
|
||||||
|
user.setCreateTime(userDO.getCreateTime());
|
||||||
|
user.setUpdateTime(userDO.getUpdateTime());
|
||||||
|
user.setIsDeleted(userDO.getIsDeleted());
|
||||||
|
user.setRemark(userDO.getRemark());
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DO → Map 转换(用于管理员查询列表返回)
|
||||||
|
*/
|
||||||
|
private Map<String, Object> toUserMap(UserDO userDO) {
|
||||||
|
var map = new java.util.LinkedHashMap<String, Object>();
|
||||||
|
map.put("id", userDO.getId());
|
||||||
|
map.put("username", userDO.getUsername());
|
||||||
|
map.put("email", userDO.getEmail());
|
||||||
|
map.put("phone", userDO.getPhone());
|
||||||
|
map.put("realName", userDO.getRealName());
|
||||||
|
map.put("status", userDO.getStatus());
|
||||||
|
map.put("userType", userDO.getUserType());
|
||||||
|
map.put("lastLoginIp", userDO.getLastLoginIp());
|
||||||
|
map.put("lastLoginTime", userDO.getLastLoginTime() != null ? userDO.getLastLoginTime().toString() : null);
|
||||||
|
map.put("createTime", userDO.getCreateTime() != null ? userDO.getCreateTime().toString() : null);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,154 @@
|
|||||||
|
package com.wangpan.common.entity;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网盘(资源集合)实体类
|
||||||
|
* 对应数据库表:wp_drive
|
||||||
|
*
|
||||||
|
* 功能:
|
||||||
|
* 1. 用户创建的资源集合(类似"我的电影库"、"我的音乐库")
|
||||||
|
* 2. 每个网盘下可以包含多个文件夹和文件
|
||||||
|
* 3. 私有模式:仅创建者可见和操作
|
||||||
|
*
|
||||||
|
* 设计说明:
|
||||||
|
* - user_id 隔离:每个用户拥有独立的网盘
|
||||||
|
* - 支持网盘名称、描述、图标等基本信息
|
||||||
|
* - 统计字段记录文件夹和文件数量
|
||||||
|
*/
|
||||||
|
public class Drive implements Serializable {
|
||||||
|
|
||||||
|
/** 序列化版本号 */
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
// ==================== 基础属性 ====================
|
||||||
|
|
||||||
|
/** 网盘ID:主键,自增长 */
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 所属用户ID:关联 wp_user 表 */
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/** 网盘名称:如"我的电影库"、"学习资料" */
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/** 网盘描述:详细说明 */
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
/** 网盘 Logo URL:存储在 MinIO 中的图片链接 */
|
||||||
|
private String logoUrl;
|
||||||
|
|
||||||
|
// ==================== 统计属性 ====================
|
||||||
|
|
||||||
|
/** 文件夹数量 */
|
||||||
|
private Integer folderCount;
|
||||||
|
|
||||||
|
/** 文件数量 */
|
||||||
|
private Integer fileCount;
|
||||||
|
|
||||||
|
/** 总大小(字节) */
|
||||||
|
private Long totalSize;
|
||||||
|
|
||||||
|
// ==================== 状态属性 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态:
|
||||||
|
* - active - 正常(默认)
|
||||||
|
* - disabled - 已禁用
|
||||||
|
*/
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
// ==================== 时间戳属性 ====================
|
||||||
|
|
||||||
|
/** 更新时间 */
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
|
||||||
|
/** 创建时间 */
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
// ==================== 构造方法 ====================
|
||||||
|
|
||||||
|
/** 无参构造方法 */
|
||||||
|
public Drive() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基础构造方法
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param name 网盘名称
|
||||||
|
* @param description 网盘描述
|
||||||
|
*/
|
||||||
|
public Drive(Long userId, String name, String description) {
|
||||||
|
this.userId = userId;
|
||||||
|
this.name = name;
|
||||||
|
this.description = description;
|
||||||
|
this.folderCount = 0;
|
||||||
|
this.fileCount = 0;
|
||||||
|
this.totalSize = 0L;
|
||||||
|
this.status = "active";
|
||||||
|
this.createTime = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 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 getName() { return name; }
|
||||||
|
public void setName(String name) { this.name = name; }
|
||||||
|
|
||||||
|
public String getDescription() { return description; }
|
||||||
|
public void setDescription(String description) { this.description = description; }
|
||||||
|
|
||||||
|
public String getLogoUrl() { return logoUrl; }
|
||||||
|
public void setLogoUrl(String logoUrl) { this.logoUrl = logoUrl; }
|
||||||
|
|
||||||
|
public Integer getFolderCount() { return folderCount; }
|
||||||
|
public void setFolderCount(Integer folderCount) { this.folderCount = folderCount; }
|
||||||
|
|
||||||
|
public Integer getFileCount() { return fileCount; }
|
||||||
|
public void setFileCount(Integer fileCount) { this.fileCount = fileCount; }
|
||||||
|
|
||||||
|
public Long getTotalSize() { return totalSize; }
|
||||||
|
public void setTotalSize(Long totalSize) { this.totalSize = totalSize; }
|
||||||
|
|
||||||
|
public String getStatus() { return status; }
|
||||||
|
public void setStatus(String status) { this.status = status; }
|
||||||
|
|
||||||
|
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; }
|
||||||
|
|
||||||
|
// ==================== Object方法重写 ====================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
Drive drive = (Drive) o;
|
||||||
|
return Objects.equals(id, drive.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Drive{" +
|
||||||
|
"id=" + id +
|
||||||
|
", userId=" + userId +
|
||||||
|
", name='" + name + '\'' +
|
||||||
|
", status='" + status + '\'' +
|
||||||
|
", createTime=" + createTime +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,167 @@
|
|||||||
|
package com.wangpan.common.entity;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网盘文件夹实体类
|
||||||
|
* 对应数据库表:wp_drive_folder
|
||||||
|
*
|
||||||
|
* 功能:
|
||||||
|
* 1. 网盘内的文件夹管理
|
||||||
|
* 2. 支持多级嵌套(通过 parent_id 实现)
|
||||||
|
* 3. 私有模式:仅创建者可见和操作
|
||||||
|
*
|
||||||
|
* 设计说明:
|
||||||
|
* - drive_id 关联所属网盘
|
||||||
|
* - parent_id 实现树形结构(0 表示根目录)
|
||||||
|
* - sort_order 控制同级文件夹排序
|
||||||
|
*/
|
||||||
|
public class DriveFolder implements Serializable {
|
||||||
|
|
||||||
|
/** 序列化版本号 */
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
// ==================== 基础属性 ====================
|
||||||
|
|
||||||
|
/** 文件夹ID:主键,自增长 */
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 所属网盘ID:关联 wp_drive 表 */
|
||||||
|
private Long driveId;
|
||||||
|
|
||||||
|
/** 所属用户ID:关联 wp_user 表(冗余字段,便于查询) */
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/** 父文件夹ID:0 表示根目录 */
|
||||||
|
private Long parentId;
|
||||||
|
|
||||||
|
/** 文件夹名称 */
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/** 文件夹描述 */
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
// ==================== 排序与层级属性 ====================
|
||||||
|
|
||||||
|
/** 排序权重:数值越小越靠前 */
|
||||||
|
private Integer sortOrder;
|
||||||
|
|
||||||
|
/** 层级深度:根目录为 0 */
|
||||||
|
private Integer depth;
|
||||||
|
|
||||||
|
// ==================== 统计属性 ====================
|
||||||
|
|
||||||
|
/** 子文件夹数量 */
|
||||||
|
private Integer childFolderCount;
|
||||||
|
|
||||||
|
/** 文件数量 */
|
||||||
|
private Integer fileCount;
|
||||||
|
|
||||||
|
/** 总大小(字节) */
|
||||||
|
private Long totalSize;
|
||||||
|
|
||||||
|
// ==================== 时间戳属性 ====================
|
||||||
|
|
||||||
|
/** 更新时间 */
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
|
||||||
|
/** 创建时间 */
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
// ==================== 构造方法 ====================
|
||||||
|
|
||||||
|
/** 无参构造方法 */
|
||||||
|
public DriveFolder() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基础构造方法
|
||||||
|
*
|
||||||
|
* @param driveId 网盘ID
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param parentId 父文件夹ID(0 表示根目录)
|
||||||
|
* @param name 文件夹名称
|
||||||
|
* @param description 文件夹描述
|
||||||
|
*/
|
||||||
|
public DriveFolder(Long driveId, Long userId, Long parentId, String name, String description) {
|
||||||
|
this.driveId = driveId;
|
||||||
|
this.userId = userId;
|
||||||
|
this.parentId = parentId;
|
||||||
|
this.name = name;
|
||||||
|
this.description = description;
|
||||||
|
this.sortOrder = 0;
|
||||||
|
this.depth = 0;
|
||||||
|
this.childFolderCount = 0;
|
||||||
|
this.fileCount = 0;
|
||||||
|
this.totalSize = 0L;
|
||||||
|
this.createTime = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Getter和Setter方法 ====================
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
|
public Long getDriveId() { return driveId; }
|
||||||
|
public void setDriveId(Long driveId) { this.driveId = driveId; }
|
||||||
|
|
||||||
|
public Long getUserId() { return userId; }
|
||||||
|
public void setUserId(Long userId) { this.userId = userId; }
|
||||||
|
|
||||||
|
public Long getParentId() { return parentId; }
|
||||||
|
public void setParentId(Long parentId) { this.parentId = parentId; }
|
||||||
|
|
||||||
|
public String getName() { return name; }
|
||||||
|
public void setName(String name) { this.name = name; }
|
||||||
|
|
||||||
|
public String getDescription() { return description; }
|
||||||
|
public void setDescription(String description) { this.description = description; }
|
||||||
|
|
||||||
|
public Integer getSortOrder() { return sortOrder; }
|
||||||
|
public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; }
|
||||||
|
|
||||||
|
public Integer getDepth() { return depth; }
|
||||||
|
public void setDepth(Integer depth) { this.depth = depth; }
|
||||||
|
|
||||||
|
public Integer getChildFolderCount() { return childFolderCount; }
|
||||||
|
public void setChildFolderCount(Integer childFolderCount) { this.childFolderCount = childFolderCount; }
|
||||||
|
|
||||||
|
public Integer getFileCount() { return fileCount; }
|
||||||
|
public void setFileCount(Integer fileCount) { this.fileCount = fileCount; }
|
||||||
|
|
||||||
|
public Long getTotalSize() { return totalSize; }
|
||||||
|
public void setTotalSize(Long totalSize) { this.totalSize = totalSize; }
|
||||||
|
|
||||||
|
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; }
|
||||||
|
|
||||||
|
// ==================== Object方法重写 ====================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
DriveFolder that = (DriveFolder) o;
|
||||||
|
return Objects.equals(id, that.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "DriveFolder{" +
|
||||||
|
"id=" + id +
|
||||||
|
", driveId=" + driveId +
|
||||||
|
", parentId=" + parentId +
|
||||||
|
", name='" + name + '\'' +
|
||||||
|
", createTime=" + createTime +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,239 @@
|
|||||||
|
package com.wangpan.common.entity;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网盘电影实体类(文件与电影合并)
|
||||||
|
* 对应数据库表:wp_drive_movie
|
||||||
|
*
|
||||||
|
* 功能:
|
||||||
|
* 1. 网盘内的电影管理(每个文件就是一个电影)
|
||||||
|
* 2. 电影文件存储在 MinIO 对象存储中
|
||||||
|
* 3. 支持 TMDB 刮削信息存储
|
||||||
|
* 4. 私有模式:仅创建者可见和操作
|
||||||
|
*
|
||||||
|
* 设计说明:
|
||||||
|
* - drive_id 关联所属网盘
|
||||||
|
* - folder_id 关联所属文件夹(0 表示根目录)
|
||||||
|
* - minio_object_name 存储 MinIO 中的对象名称
|
||||||
|
* - minio_url 存储加密后的访问 URL
|
||||||
|
* - 每个电影就是一个文件,文件夹下直接创建电影
|
||||||
|
*/
|
||||||
|
public class DriveMovie implements Serializable {
|
||||||
|
|
||||||
|
/** 序列化版本号 */
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
// ==================== 基础属性 ====================
|
||||||
|
|
||||||
|
/** 电影ID:主键,自增长 */
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 所属网盘ID:关联 wp_drive 表 */
|
||||||
|
private Long driveId;
|
||||||
|
|
||||||
|
/** 所属文件夹ID:关联 wp_drive_folder 表,0 表示根目录 */
|
||||||
|
private Long folderId;
|
||||||
|
|
||||||
|
/** 所属用户ID:关联 wp_user 表(冗余字段,便于查询) */
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/** 电影标题:显示名称 */
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
/** 电影副标题:如 "蓝光/科幻" */
|
||||||
|
private String subtitle;
|
||||||
|
|
||||||
|
/** 原始文件名:上传时的文件名 */
|
||||||
|
private String originalName;
|
||||||
|
|
||||||
|
// ==================== MinIO 存储属性 ====================
|
||||||
|
|
||||||
|
/** MinIO 对象名称:如 drive/1/2/filename.mp4 */
|
||||||
|
private String minioObjectName;
|
||||||
|
|
||||||
|
/** MinIO 访问 URL(加密存储) */
|
||||||
|
private String minioUrl;
|
||||||
|
|
||||||
|
// ==================== 文件属性 ====================
|
||||||
|
|
||||||
|
/** MIME 类型:如 video/mp4, image/jpeg */
|
||||||
|
private String mimeType;
|
||||||
|
|
||||||
|
/** 文件大小(字节) */
|
||||||
|
private Long fileSize;
|
||||||
|
|
||||||
|
/** 文件扩展名:如 mp4, mkv, avi */
|
||||||
|
private String extension;
|
||||||
|
|
||||||
|
/** 电影描述 */
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
// ==================== 电影相关属性 ====================
|
||||||
|
|
||||||
|
/** 资源库来源:如 TMDB、本地等 */
|
||||||
|
private String resourceLibrary;
|
||||||
|
|
||||||
|
/** 维护者:资源维护人员 */
|
||||||
|
private String maintainer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态:
|
||||||
|
* - active - 正常(默认)
|
||||||
|
* - deleted - 已删除(逻辑删除)
|
||||||
|
*/
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/** 分类:如 action/sci-fi/drama 等 */
|
||||||
|
private String category;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刮削类型:
|
||||||
|
* - tmdb - TMDB 刮削
|
||||||
|
* - manual - 手动录入
|
||||||
|
* - none - 未刮削
|
||||||
|
*/
|
||||||
|
private String scrapeType;
|
||||||
|
|
||||||
|
/** 评分:0-100 */
|
||||||
|
private Integer rating;
|
||||||
|
|
||||||
|
/** 海报 URL(加密存储) */
|
||||||
|
private String posterUrl;
|
||||||
|
|
||||||
|
/** 标签:逗号分隔,如 "科幻,IMAX,杜比视界" */
|
||||||
|
private String tags;
|
||||||
|
|
||||||
|
// ==================== 时间戳属性 ====================
|
||||||
|
|
||||||
|
/** 更新时间 */
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
|
||||||
|
/** 创建时间 */
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
// ==================== 构造方法 ====================
|
||||||
|
|
||||||
|
/** 无参构造方法 */
|
||||||
|
public DriveMovie() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基础构造方法
|
||||||
|
*
|
||||||
|
* @param driveId 网盘ID
|
||||||
|
* @param folderId 文件夹ID(0 表示根目录)
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param title 电影标题
|
||||||
|
* @param originalName 原始文件名
|
||||||
|
*/
|
||||||
|
public DriveMovie(Long driveId, Long folderId, Long userId, String title, String originalName) {
|
||||||
|
this.driveId = driveId;
|
||||||
|
this.folderId = folderId;
|
||||||
|
this.userId = userId;
|
||||||
|
this.title = title;
|
||||||
|
this.originalName = originalName;
|
||||||
|
this.status = "active";
|
||||||
|
this.scrapeType = "none";
|
||||||
|
this.createTime = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Getter和Setter方法 ====================
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
|
public Long getDriveId() { return driveId; }
|
||||||
|
public void setDriveId(Long driveId) { this.driveId = driveId; }
|
||||||
|
|
||||||
|
public Long getFolderId() { return folderId; }
|
||||||
|
public void setFolderId(Long folderId) { this.folderId = folderId; }
|
||||||
|
|
||||||
|
public Long getUserId() { return userId; }
|
||||||
|
public void setUserId(Long userId) { this.userId = userId; }
|
||||||
|
|
||||||
|
public String getTitle() { return title; }
|
||||||
|
public void setTitle(String title) { this.title = title; }
|
||||||
|
|
||||||
|
public String getSubtitle() { return subtitle; }
|
||||||
|
public void setSubtitle(String subtitle) { this.subtitle = subtitle; }
|
||||||
|
|
||||||
|
public String getOriginalName() { return originalName; }
|
||||||
|
public void setOriginalName(String originalName) { this.originalName = originalName; }
|
||||||
|
|
||||||
|
public String getMinioObjectName() { return minioObjectName; }
|
||||||
|
public void setMinioObjectName(String minioObjectName) { this.minioObjectName = minioObjectName; }
|
||||||
|
|
||||||
|
public String getMinioUrl() { return minioUrl; }
|
||||||
|
public void setMinioUrl(String minioUrl) { this.minioUrl = minioUrl; }
|
||||||
|
|
||||||
|
public String getMimeType() { return mimeType; }
|
||||||
|
public void setMimeType(String mimeType) { this.mimeType = mimeType; }
|
||||||
|
|
||||||
|
public Long getFileSize() { return fileSize; }
|
||||||
|
public void setFileSize(Long fileSize) { this.fileSize = fileSize; }
|
||||||
|
|
||||||
|
public String getExtension() { return extension; }
|
||||||
|
public void setExtension(String extension) { this.extension = extension; }
|
||||||
|
|
||||||
|
public String getDescription() { return description; }
|
||||||
|
public void setDescription(String description) { this.description = description; }
|
||||||
|
|
||||||
|
public String getResourceLibrary() { return resourceLibrary; }
|
||||||
|
public void setResourceLibrary(String resourceLibrary) { this.resourceLibrary = resourceLibrary; }
|
||||||
|
|
||||||
|
public String getMaintainer() { return maintainer; }
|
||||||
|
public void setMaintainer(String maintainer) { this.maintainer = maintainer; }
|
||||||
|
|
||||||
|
public String getStatus() { return status; }
|
||||||
|
public void setStatus(String status) { this.status = status; }
|
||||||
|
|
||||||
|
public String getCategory() { return category; }
|
||||||
|
public void setCategory(String category) { this.category = category; }
|
||||||
|
|
||||||
|
public String getScrapeType() { return scrapeType; }
|
||||||
|
public void setScrapeType(String scrapeType) { this.scrapeType = scrapeType; }
|
||||||
|
|
||||||
|
public Integer getRating() { return rating; }
|
||||||
|
public void setRating(Integer rating) { this.rating = rating; }
|
||||||
|
|
||||||
|
public String getPosterUrl() { return posterUrl; }
|
||||||
|
public void setPosterUrl(String posterUrl) { this.posterUrl = posterUrl; }
|
||||||
|
|
||||||
|
public String getTags() { return tags; }
|
||||||
|
public void setTags(String tags) { this.tags = tags; }
|
||||||
|
|
||||||
|
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; }
|
||||||
|
|
||||||
|
// ==================== Object方法重写 ====================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
DriveMovie movie = (DriveMovie) o;
|
||||||
|
return Objects.equals(id, movie.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "DriveMovie{" +
|
||||||
|
"id=" + id +
|
||||||
|
", driveId=" + driveId +
|
||||||
|
", title='" + title + '\'' +
|
||||||
|
", rating=" + rating +
|
||||||
|
", fileSize=" + fileSize +
|
||||||
|
", createTime=" + createTime +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
148
wangpan-common/src/main/java/com/wangpan/common/entity/Icon.java
Normal file
148
wangpan-common/src/main/java/com/wangpan/common/entity/Icon.java
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
package com.wangpan.common.entity;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon 实体类
|
||||||
|
* 对应数据库表:wp_icon
|
||||||
|
*
|
||||||
|
* 功能:
|
||||||
|
* 1. 用户创建的快捷入口图标(类似书签/快捷方式)
|
||||||
|
* 2. 每个用户拥有独立的 icon 列表,互不影响
|
||||||
|
* 3. 支持名称、链接、描述、排序等信息
|
||||||
|
*
|
||||||
|
* 设计说明:
|
||||||
|
* - user_id 隔离:每个用户拥有独立的 icon 集合
|
||||||
|
* - 与网盘模块完全独立,不共享数据
|
||||||
|
*/
|
||||||
|
public class Icon implements Serializable {
|
||||||
|
|
||||||
|
/** 序列化版本号 */
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** Icon ID:主键,自增长 */
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 所属用户ID:关联 wp_user 表 */
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/** Icon 名称:如"百度"、"GitHub" */
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/** Icon 链接:外网 URL */
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
/** 内网链接 URL */
|
||||||
|
private String internalUrl;
|
||||||
|
|
||||||
|
/** 链接类型:external(仅外网)/ internal(仅内网)/ both(内外网) */
|
||||||
|
private String linkType;
|
||||||
|
|
||||||
|
/** Icon 描述:详细说明 */
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
/** 排序值(数字越小越靠前) */
|
||||||
|
private Integer sortOrder;
|
||||||
|
|
||||||
|
/** 状态:active / disabled */
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/** Logo 图片 URL(MinIO 存储) */
|
||||||
|
private String logoUrl;
|
||||||
|
|
||||||
|
/** 创建时间 */
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
/** 更新时间 */
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
|
||||||
|
// ==================== 构造方法 ====================
|
||||||
|
|
||||||
|
/** 无参构造方法 */
|
||||||
|
public Icon() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基础构造方法
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param name Icon 名称
|
||||||
|
* @param url Icon 链接
|
||||||
|
* @param description Icon 描述
|
||||||
|
*/
|
||||||
|
public Icon(Long userId, String name, String url, String description) {
|
||||||
|
this.userId = userId;
|
||||||
|
this.name = name;
|
||||||
|
this.url = url;
|
||||||
|
this.description = description;
|
||||||
|
this.sortOrder = 0;
|
||||||
|
this.status = "active";
|
||||||
|
this.createTime = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 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 getName() { return name; }
|
||||||
|
public void setName(String name) { this.name = name; }
|
||||||
|
|
||||||
|
public String getUrl() { return url; }
|
||||||
|
public void setUrl(String url) { this.url = url; }
|
||||||
|
|
||||||
|
public String getInternalUrl() { return internalUrl; }
|
||||||
|
public void setInternalUrl(String internalUrl) { this.internalUrl = internalUrl; }
|
||||||
|
|
||||||
|
public String getLinkType() { return linkType; }
|
||||||
|
public void setLinkType(String linkType) { this.linkType = linkType; }
|
||||||
|
|
||||||
|
public String getDescription() { return description; }
|
||||||
|
public void setDescription(String description) { this.description = description; }
|
||||||
|
|
||||||
|
public Integer getSortOrder() { return sortOrder; }
|
||||||
|
public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; }
|
||||||
|
|
||||||
|
public String getStatus() { return status; }
|
||||||
|
public void setStatus(String status) { this.status = status; }
|
||||||
|
|
||||||
|
public String getLogoUrl() { return logoUrl; }
|
||||||
|
public void setLogoUrl(String logoUrl) { this.logoUrl = logoUrl; }
|
||||||
|
|
||||||
|
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;
|
||||||
|
Icon icon = (Icon) o;
|
||||||
|
return Objects.equals(id, icon.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Icon{" +
|
||||||
|
"id=" + id +
|
||||||
|
", userId=" + userId +
|
||||||
|
", name='" + name + '\'' +
|
||||||
|
", url='" + url + '\'' +
|
||||||
|
", status='" + status + '\'' +
|
||||||
|
", createTime=" + createTime +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,178 @@
|
|||||||
|
package com.wangpan.common.entity;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 电影实体类
|
||||||
|
* 对应数据库表:wp_movie(用户电影库表)
|
||||||
|
*
|
||||||
|
* 功能:
|
||||||
|
* 1. 封装电影的基本信息(标题、评分、海报等)
|
||||||
|
* 2. 每个用户拥有独立的电影库(通过 user_id 关联)
|
||||||
|
* 3. 支持状态管理、分类、标签等功能
|
||||||
|
*
|
||||||
|
* 设计说明:
|
||||||
|
* - 数据量可能超过1万条,字段设计考虑查询性能
|
||||||
|
* - 使用合理的索引策略优化查询速度
|
||||||
|
*/
|
||||||
|
public class Movie implements Serializable {
|
||||||
|
|
||||||
|
/** 序列化版本号 */
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
// ==================== 基础属性 ====================
|
||||||
|
|
||||||
|
/** 电影ID:主键,自增长 */
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 所属用户ID:关联 wp_user 表,用于用户隔离 */
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/** 电影标题:主名称,如"沙丘" */
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
/** 副标题:补充信息,如"蓝光/双语/冒险" */
|
||||||
|
private String subtitle;
|
||||||
|
|
||||||
|
// ==================== 资源相关属性 ====================
|
||||||
|
|
||||||
|
/** 资源库名称:如"夸克资源池"、"阿里云盘" */
|
||||||
|
private String resourceLibrary;
|
||||||
|
|
||||||
|
/** 维护人/来源:如"极光共享"、"手动添加" */
|
||||||
|
private String maintainer;
|
||||||
|
|
||||||
|
// ==================== 状态与分类属性 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态枚举:
|
||||||
|
* - pending_review - 待复核(默认)
|
||||||
|
* - reviewed - 已复核
|
||||||
|
* - published - 已发布
|
||||||
|
* - archived - 已归档
|
||||||
|
* - deleted - 已删除(逻辑删除)
|
||||||
|
*/
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/** 分类:如"电影资源"、"电视剧"、"纪录片" */
|
||||||
|
private String category;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刮削类型:
|
||||||
|
* - auto - 自动刮削(从TMDB获取)
|
||||||
|
* - manual - 手动录入
|
||||||
|
* - none - 未刮削
|
||||||
|
*/
|
||||||
|
private String scrapeType;
|
||||||
|
|
||||||
|
// ==================== 评分与展示属性 ====================
|
||||||
|
|
||||||
|
/** 评分:0-100分制 */
|
||||||
|
private Integer rating;
|
||||||
|
|
||||||
|
/** 海报图片URL地址 */
|
||||||
|
private String posterUrl;
|
||||||
|
|
||||||
|
/** 标签:逗号分隔,如"科幻, IMAX, 杜比视界" */
|
||||||
|
private String tags;
|
||||||
|
|
||||||
|
// ==================== 时间戳属性 ====================
|
||||||
|
|
||||||
|
/** 更新时间:资源最后更新时间 */
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
|
||||||
|
/** 创建时间:记录创建时刻 */
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
// ==================== 构造方法 ====================
|
||||||
|
|
||||||
|
/** 无参构造方法:用于框架反射和JSON反序列化 */
|
||||||
|
public Movie() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基础构造方法:创建新电影时使用
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param title 电影标题
|
||||||
|
*/
|
||||||
|
public Movie(Long userId, String title) {
|
||||||
|
this.userId = userId;
|
||||||
|
this.title = title;
|
||||||
|
this.status = "pending_review"; // 默认待复核状态
|
||||||
|
this.scrapeType = "manual"; // 默认手动录入
|
||||||
|
this.createTime = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 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 getTitle() { return title; }
|
||||||
|
public void setTitle(String title) { this.title = title; }
|
||||||
|
|
||||||
|
public String getSubtitle() { return subtitle; }
|
||||||
|
public void setSubtitle(String subtitle) { this.subtitle = subtitle; }
|
||||||
|
|
||||||
|
public String getResourceLibrary() { return resourceLibrary; }
|
||||||
|
public void setResourceLibrary(String resourceLibrary) { this.resourceLibrary = resourceLibrary; }
|
||||||
|
|
||||||
|
public String getMaintainer() { return maintainer; }
|
||||||
|
public void setMaintainer(String maintainer) { this.maintainer = maintainer; }
|
||||||
|
|
||||||
|
public String getStatus() { return status; }
|
||||||
|
public void setStatus(String status) { this.status = status; }
|
||||||
|
|
||||||
|
public String getCategory() { return category; }
|
||||||
|
public void setCategory(String category) { this.category = category; }
|
||||||
|
|
||||||
|
public String getScrapeType() { return scrapeType; }
|
||||||
|
public void setScrapeType(String scrapeType) { this.scrapeType = scrapeType; }
|
||||||
|
|
||||||
|
public Integer getRating() { return rating; }
|
||||||
|
public void setRating(Integer rating) { this.rating = rating; }
|
||||||
|
|
||||||
|
public String getPosterUrl() { return posterUrl; }
|
||||||
|
public void setPosterUrl(String posterUrl) { this.posterUrl = posterUrl; }
|
||||||
|
|
||||||
|
public String getTags() { return tags; }
|
||||||
|
public void setTags(String tags) { this.tags = tags; }
|
||||||
|
|
||||||
|
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; }
|
||||||
|
|
||||||
|
// ==================== Object方法重写 ====================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
Movie movie = (Movie) o;
|
||||||
|
return Objects.equals(id, movie.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Movie{" +
|
||||||
|
"id=" + id +
|
||||||
|
", userId=" + userId +
|
||||||
|
", title='" + title + '\'' +
|
||||||
|
", status='" + status + '\'' +
|
||||||
|
", rating=" + rating +
|
||||||
|
", createTime=" + createTime +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
299
wangpan-common/src/main/java/com/wangpan/common/entity/User.java
Normal file
299
wangpan-common/src/main/java/com/wangpan/common/entity/User.java
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
package com.wangpan.common.entity;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户实体类
|
||||||
|
* 对应数据库表:wp_user(网盘用户表)
|
||||||
|
*
|
||||||
|
* 功能:
|
||||||
|
* 1. 封装用户的基本信息和登录凭证
|
||||||
|
* 2. 作为数据传输对象在各个层之间传递
|
||||||
|
* 3. 实现Serializable接口支持序列化操作
|
||||||
|
*
|
||||||
|
* 设计原则:
|
||||||
|
* - 使用Java Bean规范,提供getter/setter方法
|
||||||
|
* - 字段与数据库表字段一一对应
|
||||||
|
* - 提供构造方法便于创建对象
|
||||||
|
*/
|
||||||
|
public class User implements Serializable {
|
||||||
|
|
||||||
|
// 序列化版本号,用于反序列化时验证类的兼容性
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
// ==================== 基本属性 ====================
|
||||||
|
|
||||||
|
/** 用户ID:主键,自增长 */
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 用户名:登录标识符,唯一不可重复 */
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
/** 密码:加密存储的密码字符串 */
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
/** 邮箱地址:用于找回密码和通知 */
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
/** 手机号码:用于短信验证和二次认证 */
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
/** 真实姓名:用户的真实姓名 */
|
||||||
|
private String realName;
|
||||||
|
|
||||||
|
/** 头像URL:头像图片的网络地址 */
|
||||||
|
private String avatarUrl;
|
||||||
|
|
||||||
|
// ==================== 状态相关属性 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户状态:
|
||||||
|
* 0 - 禁用(账户被冻结)
|
||||||
|
* 1 - 正常(可正常使用)
|
||||||
|
* 2 - 锁定(因多次失败被锁定)
|
||||||
|
*/
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户类型/角色:
|
||||||
|
* 0 - 普通用户
|
||||||
|
* 1 - VIP用户(享有更多存储空间)
|
||||||
|
* 2 - 管理员(拥有系统管理权限)
|
||||||
|
*/
|
||||||
|
private Integer userType;
|
||||||
|
|
||||||
|
// ==================== 登录记录属性 ====================
|
||||||
|
|
||||||
|
/** 最后登录IP地址 */
|
||||||
|
private String lastLoginIp;
|
||||||
|
|
||||||
|
/** 最后登录时间 */
|
||||||
|
private LocalDateTime lastLoginTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 锁定截止时间(lock_until)
|
||||||
|
*
|
||||||
|
* 设计说明:
|
||||||
|
* - 当用户因多次登录失败被锁定时,此字段记录具体的解锁时刻
|
||||||
|
* - null 表示未锁定或已解锁
|
||||||
|
* - 有值表示账户处于锁定状态,直到该时间点才能登录
|
||||||
|
*
|
||||||
|
* 使用场景:
|
||||||
|
* 1. 首次达到失败次数上限时,设置 lock_until = now + 30分钟
|
||||||
|
* 2. 锁定期内再次尝试登录,直接返回剩余等待时间
|
||||||
|
* 3. 锁定期间再次触发锁定(如管理员手动延长),更新为新的时间
|
||||||
|
* 4. 当前时间超过 lock_until 时,自动清除该字段并恢复正常状态
|
||||||
|
*/
|
||||||
|
private LocalDateTime lockUntil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 连续失败登录次数(fail_count)
|
||||||
|
*
|
||||||
|
* 设计说明:
|
||||||
|
* - 记录用户当前连续失败的次数(从上次成功/重置后开始累计)
|
||||||
|
* - 登录成功或解锁后重置为 0
|
||||||
|
* - 达到阈值(如5次)时触发账户锁定
|
||||||
|
*
|
||||||
|
* 为什么不用查日志表?
|
||||||
|
* - 登录日志是审计记录,不可篡改、不可删除
|
||||||
|
* - 用此字段管理"当前状态",日志只做历史追溯
|
||||||
|
*
|
||||||
|
* @see com.wangpan.service.UserService#MAX_LOGIN_FAILURES 阈值常量
|
||||||
|
*/
|
||||||
|
private Integer failCount;
|
||||||
|
|
||||||
|
// ==================== TMDB相关属性 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户的TMDB API Key(加密存储)
|
||||||
|
*
|
||||||
|
* 设计说明:
|
||||||
|
* - 每个用户可以绑定自己的TMDB API Key
|
||||||
|
* - 使用Jasypt加密后存入数据库,保证安全性
|
||||||
|
* - 登录后可根据当前用户身份操作对应的TMDB数据
|
||||||
|
* - null表示尚未绑定TMDB API Key
|
||||||
|
*/
|
||||||
|
private String tmdbApiKey;
|
||||||
|
|
||||||
|
// ==================== 时间戳属性 ====================
|
||||||
|
|
||||||
|
/** 账户创建时间 */
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
/** 最后更新时间 */
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
|
||||||
|
/** 逻辑删除标记:0-未删除,1-已删除 */
|
||||||
|
private Integer isDeleted;
|
||||||
|
|
||||||
|
/** 备注信息 */
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
// ==================== 构造方法 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 无参构造方法
|
||||||
|
* 用于框架反射创建对象、JSON反序列化等场景
|
||||||
|
*/
|
||||||
|
public User() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基础构造方法:用于创建新用户时使用
|
||||||
|
*
|
||||||
|
* @param username 用户名
|
||||||
|
* @param password 密码(应该是加密后的)
|
||||||
|
*/
|
||||||
|
public User(String username, String password) {
|
||||||
|
this.username = username;
|
||||||
|
this.password = password;
|
||||||
|
// 默认设置状态为正常
|
||||||
|
this.status = 1;
|
||||||
|
// 默认设置为普通用户
|
||||||
|
this.userType = 0;
|
||||||
|
// 默认未删除
|
||||||
|
this.isDeleted = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完整构造方法:包含所有必要信息
|
||||||
|
*
|
||||||
|
* @param username 用户名
|
||||||
|
* @param password 密码
|
||||||
|
* @param email 邮箱
|
||||||
|
* @param phone 手机号
|
||||||
|
* @param realName 真实姓名
|
||||||
|
*/
|
||||||
|
public User(String username, String password, String email, String phone, String realName) {
|
||||||
|
this(username, password);
|
||||||
|
this.email = email;
|
||||||
|
this.phone = phone;
|
||||||
|
this.realName = realName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Getter和Setter方法 ====================
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
|
public String getUsername() { return username; }
|
||||||
|
public void setUsername(String username) { this.username = username; }
|
||||||
|
|
||||||
|
public String getPassword() { return password; }
|
||||||
|
public void setPassword(String password) { this.password = password; }
|
||||||
|
|
||||||
|
public String getEmail() { return email; }
|
||||||
|
public void setEmail(String email) { this.email = email; }
|
||||||
|
|
||||||
|
public String getPhone() { return phone; }
|
||||||
|
public void setPhone(String phone) { this.phone = phone; }
|
||||||
|
|
||||||
|
public String getRealName() { return realName; }
|
||||||
|
public void setRealName(String realName) { this.realName = realName; }
|
||||||
|
|
||||||
|
public String getAvatarUrl() { return avatarUrl; }
|
||||||
|
public void setAvatarUrl(String avatarUrl) { this.avatarUrl = avatarUrl; }
|
||||||
|
|
||||||
|
public Integer getStatus() { return status; }
|
||||||
|
public void setStatus(Integer status) { this.status = status; }
|
||||||
|
|
||||||
|
public Integer getUserType() { return userType; }
|
||||||
|
public void setUserType(Integer userType) { this.userType = userType; }
|
||||||
|
|
||||||
|
public String getLastLoginIp() { return lastLoginIp; }
|
||||||
|
public void setLastLoginIp(String lastLoginIp) { this.lastLoginIp = lastLoginIp; }
|
||||||
|
|
||||||
|
public LocalDateTime getLastLoginTime() { return lastLoginTime; }
|
||||||
|
public void setLastLoginTime(LocalDateTime lastLoginTime) { this.lastLoginTime = lastLoginTime; }
|
||||||
|
|
||||||
|
/** 获取锁定截止时间 */
|
||||||
|
public LocalDateTime getLockUntil() { return lockUntil; }
|
||||||
|
/** 设置锁定截止时间 */
|
||||||
|
public void setLockUntil(LocalDateTime lockUntil) { this.lockUntil = lockUntil; }
|
||||||
|
|
||||||
|
/** 获取当前连续失败次数 */
|
||||||
|
public Integer getFailCount() { return failCount; }
|
||||||
|
/** 设置连续失败次数 */
|
||||||
|
public void setFailCount(Integer failCount) { this.failCount = failCount; }
|
||||||
|
|
||||||
|
/** 获取TMDB API Key(加密存储) */
|
||||||
|
public String getTmdbApiKey() { return tmdbApiKey; }
|
||||||
|
/** 设置TMDB API Key(加密后存储) */
|
||||||
|
public void setTmdbApiKey(String tmdbApiKey) { this.tmdbApiKey = tmdbApiKey; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断账户当前是否处于锁定状态
|
||||||
|
*
|
||||||
|
* @return true-已锁定(lock_until不为null且未过期),false-未锁定
|
||||||
|
*/
|
||||||
|
public boolean isLocked() {
|
||||||
|
if (lockUntil == null) {
|
||||||
|
return false; // 没有锁定时间 = 未锁定
|
||||||
|
}
|
||||||
|
// 有锁定时间,检查是否已过期
|
||||||
|
return LocalDateTime.now().isBefore(lockUntil);
|
||||||
|
}
|
||||||
|
|
||||||
|
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; }
|
||||||
|
|
||||||
|
public Integer getIsDeleted() { return isDeleted; }
|
||||||
|
public void setIsDeleted(Integer isDeleted) { this.isDeleted = isDeleted; }
|
||||||
|
|
||||||
|
public String getRemark() { return remark; }
|
||||||
|
public void setRemark(String remark) { this.remark = remark; }
|
||||||
|
|
||||||
|
// ==================== 业务判断方法 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断用户是否处于正常状态
|
||||||
|
*
|
||||||
|
* @return true-账户正常可用,false-账户异常
|
||||||
|
*/
|
||||||
|
public boolean isActive() {
|
||||||
|
return status != null && status == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断用户是否为管理员
|
||||||
|
*
|
||||||
|
* @return true-是管理员,false-非管理员
|
||||||
|
*/
|
||||||
|
public boolean isAdmin() {
|
||||||
|
return userType != null && userType == 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Object方法重写 ====================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
User user = (User) o;
|
||||||
|
// 主要通过ID判断是否为同一用户
|
||||||
|
return Objects.equals(id, user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "User{" +
|
||||||
|
"id=" + id +
|
||||||
|
", username='" + username + '\'' +
|
||||||
|
", email='" + email + '\'' +
|
||||||
|
", status=" + status +
|
||||||
|
", userType=" + userType +
|
||||||
|
", createTime=" + createTime +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
package com.wangpan.common.interceptor;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.wangpan.common.Result;
|
||||||
|
import com.wangpan.common.annotation.RequireAdmin;
|
||||||
|
import com.wangpan.common.annotation.RequireAuth;
|
||||||
|
import com.wangpan.common.util.AuthUtil;
|
||||||
|
import com.wangpan.common.entity.User;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.method.HandlerMethod;
|
||||||
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证拦截器
|
||||||
|
*
|
||||||
|
* 统一处理 @RequireAuth 和 @RequireAdmin 注解:
|
||||||
|
* - @RequireAuth:验证 Token 有效性,将用户信息注入请求属性
|
||||||
|
* - @RequireAdmin:在 RequireAuth 基础上额外校验管理员身份
|
||||||
|
*
|
||||||
|
* 使用方式:在 Controller 方法上添加 @RequireAuth 或 @RequireAdmin 注解
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class AuthInterceptor implements HandlerInterceptor {
|
||||||
|
|
||||||
|
private final AuthUtil authUtil;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
public AuthInterceptor(AuthUtil authUtil, ObjectMapper objectMapper) {
|
||||||
|
this.authUtil = authUtil;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 请求属性名:当前登录用户 */
|
||||||
|
public static final String CURRENT_USER_ATTR = "currentUser";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
|
||||||
|
Object handler) throws Exception {
|
||||||
|
// 仅处理方法级别的请求
|
||||||
|
if (!(handler instanceof HandlerMethod handlerMethod)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查类级别和方法级别的注解
|
||||||
|
boolean requireAuth = handlerMethod.getMethodAnnotation(RequireAuth.class) != null
|
||||||
|
|| handlerMethod.getBeanType().getAnnotation(RequireAuth.class) != null;
|
||||||
|
boolean requireAdmin = handlerMethod.getMethodAnnotation(RequireAdmin.class) != null
|
||||||
|
|| handlerMethod.getBeanType().getAnnotation(RequireAdmin.class) != null;
|
||||||
|
|
||||||
|
// 无需认证的接口直接放行
|
||||||
|
if (!requireAuth && !requireAdmin) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行认证
|
||||||
|
User user = authUtil.authenticate(request);
|
||||||
|
if (user == null) {
|
||||||
|
response.setStatus(401);
|
||||||
|
response.setContentType("application/json; charset=utf-8");
|
||||||
|
Result<?> result = Result.unauthorized("登录已过期,请重新登录");
|
||||||
|
response.getWriter().write(objectMapper.writeValueAsString(result));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 需要管理员权限时额外校验
|
||||||
|
if (requireAdmin && !user.isAdmin()) {
|
||||||
|
response.setStatus(403);
|
||||||
|
response.setContentType("application/json; charset=utf-8");
|
||||||
|
Result<?> result = Result.forbidden("权限不足,仅管理员可进行此操作");
|
||||||
|
response.getWriter().write(objectMapper.writeValueAsString(result));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将用户信息注入请求属性,Controller 可通过 request.getAttribute("currentUser") 获取
|
||||||
|
request.setAttribute(CURRENT_USER_ATTR, user);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
package com.wangpan.common.util;
|
||||||
|
|
||||||
|
import com.wangpan.common.dal.repository.TokenRepository;
|
||||||
|
import com.wangpan.common.dal.repository.UserRepository;
|
||||||
|
import com.wangpan.common.entity.User;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证工具类(Spring组件,实例方法)
|
||||||
|
*
|
||||||
|
* 从 HTTP 请求中提取并验证用户身份。
|
||||||
|
* 所有 Controller 注入此 Bean 后调用实例方法 authenticate()。
|
||||||
|
*
|
||||||
|
* 认证流程:提取 Bearer Token → 验证有效性 → 查询用户信息
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class AuthUtil {
|
||||||
|
|
||||||
|
private final TokenRepository tokenRepository;
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数注入:Spring 容器初始化时自动注入 Repository 实例
|
||||||
|
*/
|
||||||
|
public AuthUtil(TokenRepository tokenRepository, UserRepository userRepository) {
|
||||||
|
this.tokenRepository = tokenRepository;
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 HTTP 请求中认证用户
|
||||||
|
*
|
||||||
|
* 流程:
|
||||||
|
* 1. 从请求头提取 Authorization: Bearer <token>
|
||||||
|
* 2. 解析 Token 获取用户ID
|
||||||
|
* 3. 验证 Token 是否有效(未过期、未注销)
|
||||||
|
* 4. 查询用户信息并返回
|
||||||
|
*
|
||||||
|
* @param request HTTP 请求对象
|
||||||
|
* @return 认证成功的用户对象,认证失败返回 null
|
||||||
|
*/
|
||||||
|
public User authenticate(HttpServletRequest request) {
|
||||||
|
String authHeader = request.getHeader("Authorization");
|
||||||
|
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String token = authHeader.substring(7);
|
||||||
|
Long userId = TokenUtil.getUserId(token);
|
||||||
|
if (userId == null || !tokenRepository.isValid(token)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return userRepository.findById(userId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
package com.wangpan.common.util;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.DriverManager;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据库连接工具类(Spring Bean)
|
||||||
|
* 功能:
|
||||||
|
* 1. 从Spring配置中读取数据库配置(支持Jasypt自动解密)
|
||||||
|
* 2. 管理数据库连接的获取和释放
|
||||||
|
* 3. 提供统一的资源关闭方法
|
||||||
|
*
|
||||||
|
* 支持两种使用方式:
|
||||||
|
* 1. 静态方法调用(向后兼容):DBUtil.getConnection()
|
||||||
|
* 2. Spring Bean注入:@Resource private DBUtil dbUtil;
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class DBUtil {
|
||||||
|
|
||||||
|
/** 静态实例引用,用于静态方法访问 */
|
||||||
|
private static DBUtil instance;
|
||||||
|
|
||||||
|
/** 数据库连接URL */
|
||||||
|
@Value("${spring.datasource.url}")
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
/** 数据库用户名 */
|
||||||
|
@Value("${spring.datasource.username}")
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
/** 数据库密码(Jasypt自动解密) */
|
||||||
|
@Value("${spring.datasource.password}")
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
/** JDBC驱动类名 */
|
||||||
|
@Value("${spring.datasource.driver-class-name:com.mysql.cj.jdbc.Driver}")
|
||||||
|
private String driverClassName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring容器初始化时设置静态实例引用并加载驱动
|
||||||
|
*/
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
instance = this;
|
||||||
|
try {
|
||||||
|
Class.forName(driverClassName);
|
||||||
|
System.out.println("✓ 数据库配置加载成功: " + url);
|
||||||
|
} catch (ClassNotFoundException e) {
|
||||||
|
System.err.println("✗ JDBC驱动加载失败:" + driverClassName);
|
||||||
|
throw new RuntimeException("MySQL驱动未找到,请检查依赖配置", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 静态方法(向后兼容) ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取数据库连接(静态方法)
|
||||||
|
*/
|
||||||
|
public static Connection getConnection() throws SQLException {
|
||||||
|
if (instance == null) {
|
||||||
|
throw new IllegalStateException("DBUtil未初始化,请确保Spring容器已启动");
|
||||||
|
}
|
||||||
|
return instance.doGetConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 内部实现方法 ====================
|
||||||
|
|
||||||
|
private Connection doGetConnection() throws SQLException {
|
||||||
|
try {
|
||||||
|
return DriverManager.getConnection(url, username, password);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
System.err.println("✗ 获取数据库连接失败:" + e.getMessage());
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,299 @@
|
|||||||
|
package com.wangpan.common.util;
|
||||||
|
|
||||||
|
import org.jasypt.encryption.pbe.StandardPBEStringEncryptor;
|
||||||
|
import org.jasypt.encryption.pbe.config.SimplePBEConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jasypt 加解密工具类
|
||||||
|
* 功能:
|
||||||
|
* 1. 对配置文件中的敏感信息进行加密(密码、用户名等)
|
||||||
|
* 2. 在运行时自动识别 enc() 格式并解密为明文
|
||||||
|
* 3. 提供命令行工具生成加密后的密文
|
||||||
|
*
|
||||||
|
* 加密格式:enc(密文)
|
||||||
|
*
|
||||||
|
* 安全说明:
|
||||||
|
* - 加密密钥通过环境变量 JASYPT_ENCRYPTOR_PASSWORD 传入
|
||||||
|
* - 生产环境建议通过 JVM 参数或环境变量传递密钥
|
||||||
|
*/
|
||||||
|
public class EncryptUtil {
|
||||||
|
|
||||||
|
// ==================== 常量定义 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加密值的前缀标识
|
||||||
|
* 当属性值以 "enc(" 开头时,表示该值是加密的,需要解密
|
||||||
|
*/
|
||||||
|
private static final String ENCRYPT_PREFIX = "enc(";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加密值的后缀标识
|
||||||
|
* 与前缀配对使用,完整格式为 enc(密文)
|
||||||
|
*/
|
||||||
|
private static final String ENCRYPT_SUFFIX = ")";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认加密算法 - PBEWithMD5AndDES
|
||||||
|
* PBE = Password Based Encryption(基于密码的加密)
|
||||||
|
* MD5 + DES 是经典组合,兼容性好
|
||||||
|
* 如需更高安全性可改为 PBEWITHSHA256AND256BITAES-CBC-BC
|
||||||
|
*/
|
||||||
|
private static final String DEFAULT_ALGORITHM = "PBEWithMD5AndDES";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认加密密钥(仅用于开发环境!)
|
||||||
|
* 生产环境必须通过环境变量覆盖此值:
|
||||||
|
* -Djasypt.encryptor.password=你的密钥
|
||||||
|
* 或设置环境变量:JASYPT_ENCRYPTOR_PASSWORD=你的密钥
|
||||||
|
*/
|
||||||
|
private static final String DEFAULT_PASSWORD = "wangpan2024";
|
||||||
|
|
||||||
|
// ==================== 单例实例 ====================
|
||||||
|
|
||||||
|
/** 加密器实例(线程安全,全局复用) */
|
||||||
|
private static volatile StandardPBEStringEncryptor encryptor;
|
||||||
|
|
||||||
|
/** 是否已初始化标志 */
|
||||||
|
private static volatile boolean initialized = false;
|
||||||
|
|
||||||
|
// ==================== 公开方法 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解密属性值
|
||||||
|
*
|
||||||
|
* 自动判断是否为加密格式:
|
||||||
|
* - 如果是 enc(密文) 格式 → 解密返回明文
|
||||||
|
* - 如果是普通明文 → 直接返回原值
|
||||||
|
*
|
||||||
|
* 使用示例:
|
||||||
|
* String password = decryptValue("enc(密文...)");
|
||||||
|
* // 返回: 解密后的明文
|
||||||
|
*
|
||||||
|
* @param value 属性值(可能是加密的 enc(...) 格式,也可能是普通明文)
|
||||||
|
* @return 解密后的明文,如果输入为null则返回null
|
||||||
|
*/
|
||||||
|
public static String decryptValue(String value) {
|
||||||
|
// 空值直接返回
|
||||||
|
if (value == null || value.trim().isEmpty()) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为加密格式:以 enc( 开头且以 ) 结尾
|
||||||
|
if (value.startsWith(ENCRYPT_PREFIX) && value.endsWith(ENCRYPT_SUFFIX)) {
|
||||||
|
// 提取括号内的密文部分
|
||||||
|
String encryptedText = value.substring(
|
||||||
|
ENCRYPT_PREFIX.length(),
|
||||||
|
value.length() - ENCRYPT_SUFFIX.length()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 调用解密方法
|
||||||
|
return decrypt(encryptedText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 非加密格式,直接返回原值
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加密明文字符串
|
||||||
|
*
|
||||||
|
* 用于生成加密后的密文,写入配置文件
|
||||||
|
*
|
||||||
|
* 使用示例:
|
||||||
|
* String encrypted = encrypt("明文");
|
||||||
|
* // 返回: 加密后的密文
|
||||||
|
*
|
||||||
|
* @param plainText 需要加密的明文
|
||||||
|
* @return Base64编码的密文字符串
|
||||||
|
*/
|
||||||
|
public static String encrypt(String plainText) {
|
||||||
|
return getEncryptor().encrypt(plainText);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解密密文字符串
|
||||||
|
*
|
||||||
|
* 将Base64编码的密文还原为原始明文
|
||||||
|
*
|
||||||
|
* @param encryptedText Base64编码的密文
|
||||||
|
* @return 解密后的明文
|
||||||
|
*/
|
||||||
|
public static String decrypt(String encryptedText) {
|
||||||
|
return getEncryptor().decrypt(encryptedText);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断一个值是否为加密格式
|
||||||
|
*
|
||||||
|
* @param value 待检查的字符串
|
||||||
|
* @return true-是 enc(...) 格式,false-不是
|
||||||
|
*/
|
||||||
|
public static boolean isEncrypted(String value) {
|
||||||
|
return value != null
|
||||||
|
&& value.startsWith(ENCRYPT_PREFIX)
|
||||||
|
&& value.endsWith(ENCRYPT_SUFFIX);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 私有方法 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取加密器单例(懒加载 + 双重检查锁)
|
||||||
|
*
|
||||||
|
* 设计说明:
|
||||||
|
* 1. 使用双重检查锁确保线程安全
|
||||||
|
* 2. 懒加载模式,首次调用时才初始化
|
||||||
|
* 3. 加密器创建后全局复用,避免重复创建开销
|
||||||
|
*
|
||||||
|
* 密钥优先级(从高到低):
|
||||||
|
* 1. 系统属性 jasypt.encryptor.password(JVM参数)
|
||||||
|
* 2. 环境变量 JASYPT_ENCRYPTOR_PASSWORD
|
||||||
|
* 3. 代码中的默认值(仅开发用)
|
||||||
|
*
|
||||||
|
* @return StandardPBEStringEncryptor 加密器实例
|
||||||
|
*/
|
||||||
|
private static StandardPBEStringEncryptor getEncryptor() {
|
||||||
|
// 第一次检查(无锁)
|
||||||
|
if (!initialized || encryptor == null) {
|
||||||
|
synchronized (EncryptUtil.class) {
|
||||||
|
// 第二次检查(加锁后再次验证)
|
||||||
|
if (!initialized || encryptor == null) {
|
||||||
|
// 创建新的加密器实例
|
||||||
|
encryptor = createEncryptor();
|
||||||
|
initialized = true;
|
||||||
|
|
||||||
|
System.out.println("✓ Jasypt加密器初始化完成");
|
||||||
|
System.out.println(" 算法:" + DEFAULT_ALGORITHM);
|
||||||
|
System.out.println(" 密钥来源:" + getPasswordSource());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return encryptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建并配置加密器实例
|
||||||
|
*
|
||||||
|
* 配置项:
|
||||||
|
* - algorithm: 加密算法
|
||||||
|
* - password: 加密密钥(从环境变量或系统属性获取)
|
||||||
|
* - keyObtentionIterations: 密钥派生迭代次数(增加安全性)
|
||||||
|
*
|
||||||
|
* @return 已配置好的 StandardPBEStringEncryptor 实例
|
||||||
|
*/
|
||||||
|
private static StandardPBEStringEncryptor createEncryptor() {
|
||||||
|
StandardPBEStringEncryptor newEncryptor = new StandardPBEStringEncryptor();
|
||||||
|
|
||||||
|
// 创建配置对象(使用SimplePBEConfig实现类)
|
||||||
|
SimplePBEConfig config = new SimplePBEConfig();
|
||||||
|
config.setAlgorithm(DEFAULT_ALGORITHM);
|
||||||
|
config.setPassword(getEncryptionPassword());
|
||||||
|
config.setKeyObtentionIterations(1000); // 密钥派生迭代次数
|
||||||
|
|
||||||
|
newEncryptor.setConfig(config);
|
||||||
|
return newEncryptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取加密密钥
|
||||||
|
*
|
||||||
|
* 优先级(从高到低):
|
||||||
|
* 1. JVM系统属性:-Djasypt.encryptor.password=xxx
|
||||||
|
* 2. 环境变量:JASYPT_ENCRYPTOR_PASSWORD
|
||||||
|
* 3. 默认值:dev_key(仅用于开发环境)
|
||||||
|
*
|
||||||
|
* @return 加密密钥字符串
|
||||||
|
*/
|
||||||
|
private static String getEncryptionPassword() {
|
||||||
|
// 优先从JVM系统属性获取(最高优先级)
|
||||||
|
String password = System.getProperty("jasypt.encryptor.password");
|
||||||
|
if (password != null && !password.isEmpty()) {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其次从环境变量获取
|
||||||
|
password = System.getenv("JASYPT_ENCRYPTOR_PASSWORD");
|
||||||
|
if (password != null && !password.isEmpty()) {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最后使用默认值(开发环境)
|
||||||
|
System.out.println("⚠ [安全警告] 使用默认加密密钥(开发模式)");
|
||||||
|
System.out.println(" 生产环境请通过 -Djasypt.encryptor.password=xxx 设置密钥");
|
||||||
|
return DEFAULT_PASSWORD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前使用的密钥来源描述(用于日志输出)
|
||||||
|
*
|
||||||
|
* @return 密钥来源说明字符串
|
||||||
|
*/
|
||||||
|
private static String getPasswordSource() {
|
||||||
|
if (System.getProperty("jasypt.encryptor.password") != null) {
|
||||||
|
return "JVM参数 (-Djasypt.encryptor.password)";
|
||||||
|
} else if (System.getenv("JASYPT_ENCRYPTOR_PASSWORD") != null) {
|
||||||
|
return "环境变量 (JASYPT_ENCRYPTOR_PASSWORD)";
|
||||||
|
} else {
|
||||||
|
return "默认值(开发模式)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 主方法(命令行工具) ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 命令行工具入口
|
||||||
|
*
|
||||||
|
* 用法示例:
|
||||||
|
* # 加密(生成密文)
|
||||||
|
* mvn exec:java -Dexec.mainClass="com.wangpan.util.EncryptUtil" -Dexec.args="encrypt 明文"
|
||||||
|
*
|
||||||
|
* # 解密(验证密文)
|
||||||
|
* mvn exec:java -Dexec.mainClass="com.wangpan.util.EncryptUtil" -Dexec.args="decrypt 密文"
|
||||||
|
*
|
||||||
|
* @param args 命令行参数:[encrypt|decrypt] [文本]
|
||||||
|
*/
|
||||||
|
public static void main(String[] args) {
|
||||||
|
if (args.length < 2) {
|
||||||
|
System.out.println("========================================");
|
||||||
|
System.out.println(" Jasypt 加解密工具");
|
||||||
|
System.out.println("========================================\n");
|
||||||
|
System.out.println("用法:");
|
||||||
|
System.out.println(" java EncryptUtil encrypt <明文> # 加密,生成密文");
|
||||||
|
System.out.println(" java EncryptUtil decrypt <密文> # 解密,还原明文\n");
|
||||||
|
System.out.println("示例:");
|
||||||
|
System.out.println(" java EncryptUtil encrypt your_password");
|
||||||
|
System.out.println(" java EncryptUtil decrypt encrypted_text\n");
|
||||||
|
System.out.println("自定义密钥:");
|
||||||
|
System.out.println(" -Djasypt.encryptor.password=你的密钥\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String operation = args[0];
|
||||||
|
String text = args[1];
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ("encrypt".equalsIgnoreCase(operation)) {
|
||||||
|
// 执行加密
|
||||||
|
String encrypted = encrypt(text);
|
||||||
|
System.out.println("\n✓ 加密结果:");
|
||||||
|
System.out.println(" 明文:" + text);
|
||||||
|
System.out.println(" 密文:" + encrypted);
|
||||||
|
System.out.println("\n配置文件中使用方式:");
|
||||||
|
System.out.println(" db.password=enc(" + encrypted + ")");
|
||||||
|
|
||||||
|
} else if ("decrypt".equalsIgnoreCase(operation)) {
|
||||||
|
// 执行解密
|
||||||
|
String decrypted = decrypt(text);
|
||||||
|
System.out.println("\n✓ 解密结果:");
|
||||||
|
System.out.println(" 密文:" + text);
|
||||||
|
System.out.println(" 明文:" + decrypted);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
System.err.println("未知操作:" + operation + ",支持的操作:encrypt / decrypt");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("操作失败:" + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,380 @@
|
|||||||
|
package com.wangpan.common.util;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon Logo 抓取工具类
|
||||||
|
*
|
||||||
|
* 功能:
|
||||||
|
* 1. 根据网址自动抓取网站的 logo 图标
|
||||||
|
* 2. 将图标上传到 MinIO 存储
|
||||||
|
* 3. 返回 MinIO 中的访问 URL
|
||||||
|
*
|
||||||
|
* 抓取策略(按优先级):
|
||||||
|
* 1. og:image(Open Graph 图片,通常是高质量 logo)
|
||||||
|
* 2. apple-touch-icon(苹果设备图标,质量较好)
|
||||||
|
* 3. link rel="icon"(标准 favicon)
|
||||||
|
* 4. 默认 /favicon.ico
|
||||||
|
*/
|
||||||
|
public class IconLogoFetcher {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(IconLogoFetcher.class);
|
||||||
|
|
||||||
|
/** 连接超时时间(毫秒) */
|
||||||
|
private static final int CONNECT_TIMEOUT = 5000;
|
||||||
|
|
||||||
|
/** 读取超时时间(毫秒) */
|
||||||
|
private static final int READ_TIMEOUT = 10000;
|
||||||
|
|
||||||
|
/** 最大下载大小(1MB) */
|
||||||
|
private static final int MAX_DOWNLOAD_SIZE = 1024 * 1024;
|
||||||
|
|
||||||
|
// ==================== 正则匹配模式 ====================
|
||||||
|
|
||||||
|
/** 匹配 og:image meta 标签 */
|
||||||
|
private static final Pattern OG_IMAGE_PATTERN = Pattern.compile(
|
||||||
|
"<meta[^>]+property=[\"']og:image[\"'][^>]+content=[\"']([^\"']+)[\"']",
|
||||||
|
Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
private static final Pattern OG_IMAGE_CONTENT_PATTERN = Pattern.compile(
|
||||||
|
"<meta[^>]+content=[\"']([^\"']+)[\"'][^>]+property=[\"']og:image[\"']",
|
||||||
|
Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
/** 匹配 apple-touch-icon */
|
||||||
|
private static final Pattern APPLE_TOUCH_ICON_PATTERN = Pattern.compile(
|
||||||
|
"<link[^>]+rel=[\"']apple-touch-icon[^\"']*[\"'][^>]+href=[\"']([^\"']+)[\"']",
|
||||||
|
Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
private static final Pattern APPLE_ICON_HREF_PATTERN = Pattern.compile(
|
||||||
|
"<link[^>]+href=[\"']([^\"']+)[\"'][^>]+rel=[\"']apple-touch-icon[^\"']*[\"']",
|
||||||
|
Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
/** 匹配标准 favicon */
|
||||||
|
private static final Pattern LINK_ICON_PATTERN = Pattern.compile(
|
||||||
|
"<link[^>]+rel=[\"'](?:shortcut )?icon[\"'][^>]+href=[\"']([^\"']+)[\"']",
|
||||||
|
Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
private static final Pattern LINK_HREF_REL_PATTERN = Pattern.compile(
|
||||||
|
"<link[^>]+href=[\"']([^\"']+)[\"'][^>]+rel=[\"'](?:shortcut )?icon[\"']",
|
||||||
|
Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据网址抓取 logo 并上传到 MinIO
|
||||||
|
*
|
||||||
|
* @param websiteUrl 网站地址(如 https://www.baidu.com)
|
||||||
|
* @param userId 用户ID(用于 MinIO 路径隔离)
|
||||||
|
* @return 上传成功返回 MinIO 访问 URL,失败返回 null
|
||||||
|
*/
|
||||||
|
public static String fetchAndUploadLogo(String websiteUrl, Long userId) {
|
||||||
|
try {
|
||||||
|
// 按优先级尝试获取高质量 logo
|
||||||
|
String logoUrl = extractLogoFromHtml(websiteUrl);
|
||||||
|
|
||||||
|
// 如果 HTML 中没找到任何 logo,使用默认 /favicon.ico
|
||||||
|
if (logoUrl == null) {
|
||||||
|
logoUrl = resolveUrl(websiteUrl, "/favicon.ico");
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("[IconLogoFetcher] 准备下载 logo: {}", logoUrl);
|
||||||
|
|
||||||
|
// 下载图片
|
||||||
|
byte[] imageBytes = downloadImage(logoUrl);
|
||||||
|
if (imageBytes == null || imageBytes.length == 0) {
|
||||||
|
log.warn("[IconLogoFetcher] logo 下载失败: {}", logoUrl);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确定文件扩展名
|
||||||
|
String ext = guessExtension(imageBytes, logoUrl);
|
||||||
|
|
||||||
|
// 上传到 MinIO
|
||||||
|
String objectName = "icons/" + userId + "/" + System.currentTimeMillis() + "." + ext;
|
||||||
|
String contentType = guessContentType(ext);
|
||||||
|
String uploaded = MinioUtil.uploadFile(objectName, imageBytes, contentType);
|
||||||
|
|
||||||
|
if (uploaded != null) {
|
||||||
|
String fileUrl = MinioUtil.getFileUrl(uploaded);
|
||||||
|
log.info("[IconLogoFetcher] logo 上传成功: {} -> {}", websiteUrl, fileUrl);
|
||||||
|
return fileUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[IconLogoFetcher] 抓取 logo 失败: {}", websiteUrl, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从网页 HTML 中提取高质量 logo 地址
|
||||||
|
* 按优先级尝试:og:image > apple-touch-icon > favicon
|
||||||
|
*/
|
||||||
|
private static String extractLogoFromHtml(String websiteUrl) {
|
||||||
|
try {
|
||||||
|
String html = fetchHtml(websiteUrl);
|
||||||
|
if (html == null || html.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只取前 50KB 内容来查找 logo(通常在 head 区域)
|
||||||
|
String headHtml = html.length() > 51200 ? html.substring(0, 51200) : html;
|
||||||
|
|
||||||
|
// 1. 优先尝试 og:image(通常是高质量 logo)
|
||||||
|
String ogImage = extractOgImage(headHtml);
|
||||||
|
if (ogImage != null) {
|
||||||
|
String resolved = resolveUrl(websiteUrl, ogImage);
|
||||||
|
if (resolved != null) {
|
||||||
|
log.info("[IconLogoFetcher] 找到 og:image: {}", resolved);
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 尝试 apple-touch-icon(质量较好)
|
||||||
|
String appleIcon = extractAppleTouchIcon(headHtml);
|
||||||
|
if (appleIcon != null) {
|
||||||
|
String resolved = resolveUrl(websiteUrl, appleIcon);
|
||||||
|
if (resolved != null) {
|
||||||
|
log.info("[IconLogoFetcher] 找到 apple-touch-icon: {}", resolved);
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 尝试标准 favicon
|
||||||
|
String favicon = extractFavicon(headHtml);
|
||||||
|
if (favicon != null) {
|
||||||
|
String resolved = resolveUrl(websiteUrl, favicon);
|
||||||
|
if (resolved != null) {
|
||||||
|
log.info("[IconLogoFetcher] 找到 favicon: {}", resolved);
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[IconLogoFetcher] 解析 HTML 提取 logo 失败: {}", websiteUrl, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取 og:image(Open Graph 图片)
|
||||||
|
*/
|
||||||
|
private static String extractOgImage(String html) {
|
||||||
|
String match = extractMatch(html, OG_IMAGE_PATTERN);
|
||||||
|
if (match != null) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
return extractMatch(html, OG_IMAGE_CONTENT_PATTERN);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取 apple-touch-icon
|
||||||
|
*/
|
||||||
|
private static String extractAppleTouchIcon(String html) {
|
||||||
|
String match = extractMatch(html, APPLE_TOUCH_ICON_PATTERN);
|
||||||
|
if (match != null) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
return extractMatch(html, APPLE_ICON_HREF_PATTERN);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取标准 favicon
|
||||||
|
*/
|
||||||
|
private static String extractFavicon(String html) {
|
||||||
|
String match = extractMatch(html, LINK_ICON_PATTERN);
|
||||||
|
if (match != null) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
return extractMatch(html, LINK_HREF_REL_PATTERN);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String extractMatch(String html, Pattern pattern) {
|
||||||
|
Matcher matcher = pattern.matcher(html);
|
||||||
|
if (matcher.find()) {
|
||||||
|
return matcher.group(1);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取网页 HTML 内容(仅获取前 30KB)
|
||||||
|
*/
|
||||||
|
private static String fetchHtml(String url) {
|
||||||
|
HttpURLConnection conn = null;
|
||||||
|
try {
|
||||||
|
conn = (HttpURLConnection) new URI(url).toURL().openConnection();
|
||||||
|
conn.setRequestMethod("GET");
|
||||||
|
conn.setConnectTimeout(CONNECT_TIMEOUT);
|
||||||
|
conn.setReadTimeout(READ_TIMEOUT);
|
||||||
|
conn.setRequestProperty("User-Agent",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
|
||||||
|
conn.setInstanceFollowRedirects(true);
|
||||||
|
|
||||||
|
int code = conn.getResponseCode();
|
||||||
|
if (code != 200) {
|
||||||
|
log.debug("[IconLogoFetcher] 网页返回非200状态: {} -> {}", url, code);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String contentType = conn.getContentType();
|
||||||
|
if (contentType != null && !contentType.contains("text/html")) {
|
||||||
|
log.debug("[IconLogoFetcher] 非HTML内容,跳过: {} -> {}", url, contentType);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try (InputStream is = conn.getInputStream()) {
|
||||||
|
byte[] bytes = readLimited(is, 30 * 1024);
|
||||||
|
return new String(bytes, "UTF-8");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("[IconLogoFetcher] 获取网页失败: {} -> {}", url, e.getMessage());
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
if (conn != null) {
|
||||||
|
conn.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载图片数据
|
||||||
|
*/
|
||||||
|
private static byte[] downloadImage(String imageUrl) {
|
||||||
|
HttpURLConnection conn = null;
|
||||||
|
try {
|
||||||
|
conn = (HttpURLConnection) new URI(imageUrl).toURL().openConnection();
|
||||||
|
conn.setRequestMethod("GET");
|
||||||
|
conn.setConnectTimeout(CONNECT_TIMEOUT);
|
||||||
|
conn.setReadTimeout(READ_TIMEOUT);
|
||||||
|
conn.setRequestProperty("User-Agent",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
|
||||||
|
conn.setInstanceFollowRedirects(true);
|
||||||
|
|
||||||
|
int code = conn.getResponseCode();
|
||||||
|
if (code != 200) {
|
||||||
|
log.debug("[IconLogoFetcher] 图片下载返回非200: {} -> {}", imageUrl, code);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try (InputStream is = conn.getInputStream()) {
|
||||||
|
return readLimited(is, MAX_DOWNLOAD_SIZE);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("[IconLogoFetcher] 下载图片失败: {} -> {}", imageUrl, e.getMessage());
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
if (conn != null) {
|
||||||
|
conn.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从输入流中读取有限大小的数据
|
||||||
|
*/
|
||||||
|
private static byte[] readLimited(InputStream is, int maxBytes) throws Exception {
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
byte[] buffer = new byte[4096];
|
||||||
|
int totalRead = 0;
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = is.read(buffer)) != -1) {
|
||||||
|
int toWrite = Math.min(bytesRead, maxBytes - totalRead);
|
||||||
|
baos.write(buffer, 0, toWrite);
|
||||||
|
totalRead += toWrite;
|
||||||
|
if (totalRead >= maxBytes) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return baos.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将相对 URL 解析为绝对 URL
|
||||||
|
*/
|
||||||
|
private static String resolveUrl(String baseUrl, String relativeUrl) {
|
||||||
|
try {
|
||||||
|
if (relativeUrl == null || relativeUrl.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// 已经是完整 URL
|
||||||
|
if (relativeUrl.startsWith("http://") || relativeUrl.startsWith("https://")) {
|
||||||
|
return relativeUrl;
|
||||||
|
}
|
||||||
|
// 协议相对 URL(如 //example.com/favicon.ico)
|
||||||
|
if (relativeUrl.startsWith("//")) {
|
||||||
|
String scheme = baseUrl.startsWith("https://") ? "https:" : "http:";
|
||||||
|
return scheme + relativeUrl;
|
||||||
|
}
|
||||||
|
// 使用 URI.resolve 解析相对路径
|
||||||
|
URI base = new URI(baseUrl);
|
||||||
|
return base.resolve(relativeUrl).toString();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("[IconLogoFetcher] URL 解析失败: base={}, relative={}", baseUrl, relativeUrl);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据文件头魔术字节猜测图片扩展名
|
||||||
|
*/
|
||||||
|
private static String guessExtension(byte[] data, String url) {
|
||||||
|
if (data.length >= 8) {
|
||||||
|
// PNG: 89 50 4E 47
|
||||||
|
if (data[0] == (byte) 0x89 && data[1] == (byte) 0x50) {
|
||||||
|
return "png";
|
||||||
|
}
|
||||||
|
// JPEG: FF D8 FF
|
||||||
|
if (data[0] == (byte) 0xFF && data[1] == (byte) 0xD8) {
|
||||||
|
return "jpg";
|
||||||
|
}
|
||||||
|
// GIF: 47 49 46
|
||||||
|
if (data[0] == (byte) 0x47 && data[1] == (byte) 0x49) {
|
||||||
|
return "gif";
|
||||||
|
}
|
||||||
|
// WEBP: 52 49 46 46 ... 57 45 42 50
|
||||||
|
if (data[0] == (byte) 0x52 && data.length >= 12
|
||||||
|
&& data[8] == (byte) 0x57 && data[9] == (byte) 0x45) {
|
||||||
|
return "webp";
|
||||||
|
}
|
||||||
|
// SVG: 以 <?xml 或 <svg 开头
|
||||||
|
if (data[0] == (byte) 0x3C) {
|
||||||
|
String head = new String(data, 0, Math.min(100, data.length));
|
||||||
|
if (head.contains("<svg")) {
|
||||||
|
return "svg";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 从 URL 推断
|
||||||
|
String lower = url.toLowerCase();
|
||||||
|
if (lower.contains(".png")) return "png";
|
||||||
|
if (lower.contains(".jpg") || lower.contains(".jpeg")) return "jpg";
|
||||||
|
if (lower.contains(".gif")) return "gif";
|
||||||
|
if (lower.contains(".webp")) return "webp";
|
||||||
|
if (lower.contains(".svg")) return "svg";
|
||||||
|
// 默认 ico
|
||||||
|
return "ico";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据扩展名猜测 Content-Type
|
||||||
|
*/
|
||||||
|
private static String guessContentType(String ext) {
|
||||||
|
return switch (ext) {
|
||||||
|
case "png" -> "image/png";
|
||||||
|
case "jpg", "jpeg" -> "image/jpeg";
|
||||||
|
case "gif" -> "image/gif";
|
||||||
|
case "webp" -> "image/webp";
|
||||||
|
case "svg" -> "image/svg+xml";
|
||||||
|
default -> "image/x-icon";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,241 @@
|
|||||||
|
package com.wangpan.common.util;
|
||||||
|
|
||||||
|
import io.minio.*;
|
||||||
|
import io.minio.http.Method;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MinIO 对象存储工具类(Spring Bean)
|
||||||
|
*
|
||||||
|
* 功能:
|
||||||
|
* 1. 从Spring配置中读取MinIO配置(支持Jasypt自动解密)
|
||||||
|
* 2. 初始化 MinIO 客户端连接
|
||||||
|
* 3. 上传文件到指定桶(Bucket)
|
||||||
|
* 4. 获取文件的访问 URL(预签名 URL 或公开 URL)
|
||||||
|
*
|
||||||
|
* 支持两种使用方式:
|
||||||
|
* 1. 静态方法调用(向后兼容):MinioUtil.upload(...)
|
||||||
|
* 2. Spring Bean注入:@Resource private MinioUtil minioUtil;
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@ConditionalOnProperty(prefix = "minio", name = "endpoint")
|
||||||
|
public class MinioUtil {
|
||||||
|
|
||||||
|
/** 日志记录器 */
|
||||||
|
private static final Logger LOGGER = Logger.getLogger(MinioUtil.class.getName());
|
||||||
|
|
||||||
|
/** 静态实例引用,用于静态方法访问 */
|
||||||
|
private static MinioUtil instance;
|
||||||
|
|
||||||
|
/** MinIO 服务端点地址(Jasypt自动解密) */
|
||||||
|
@Value("${minio.endpoint}")
|
||||||
|
private String endpoint;
|
||||||
|
|
||||||
|
/** 访问密钥(Access Key,Jasypt自动解密) */
|
||||||
|
@Value("${minio.accessKey}")
|
||||||
|
private String accessKey;
|
||||||
|
|
||||||
|
/** 秘密密钥(Secret Key,Jasypt自动解密) */
|
||||||
|
@Value("${minio.secretKey}")
|
||||||
|
private String secretKey;
|
||||||
|
|
||||||
|
/** 存储桶名称 */
|
||||||
|
@Value("${minio.bucketName}")
|
||||||
|
private String bucketName;
|
||||||
|
|
||||||
|
/** 预签名 URL 有效期(7天) */
|
||||||
|
private static final int PRESIGNED_URL_EXPIRY_DAYS = 7;
|
||||||
|
|
||||||
|
/** MinIO 客户端实例(单例模式) */
|
||||||
|
private static volatile MinioClient client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring容器初始化时设置静态实例引用并初始化MinIO客户端
|
||||||
|
*/
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
instance = this;
|
||||||
|
initClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 MinIO 客户端
|
||||||
|
*/
|
||||||
|
private void initClient() {
|
||||||
|
try {
|
||||||
|
client = MinioClient.builder()
|
||||||
|
.endpoint(endpoint)
|
||||||
|
.credentials(accessKey, secretKey)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 确保存储桶存在
|
||||||
|
ensureBucketExists();
|
||||||
|
|
||||||
|
LOGGER.info("[MinioUtil] MinIO 客户端初始化成功: " + endpoint);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.log(Level.SEVERE, "[MinioUtil] MinIO 客户端初始化失败", e);
|
||||||
|
throw new RuntimeException("MinIO 初始化失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 MinIO 客户端实例(懒加载 + 双重检查锁)
|
||||||
|
*
|
||||||
|
* @return MinioClient 实例
|
||||||
|
*/
|
||||||
|
private static MinioClient getClient() {
|
||||||
|
if (client == null) {
|
||||||
|
synchronized (MinioUtil.class) {
|
||||||
|
if (client == null && instance != null) {
|
||||||
|
instance.initClient();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保存储桶存在,不存在则创建
|
||||||
|
*/
|
||||||
|
private void ensureBucketExists() {
|
||||||
|
try {
|
||||||
|
boolean exists = client.bucketExists(
|
||||||
|
BucketExistsArgs.builder().bucket(bucketName).build()
|
||||||
|
);
|
||||||
|
if (!exists) {
|
||||||
|
client.makeBucket(
|
||||||
|
MakeBucketArgs.builder().bucket(bucketName).build()
|
||||||
|
);
|
||||||
|
LOGGER.info("[MinioUtil] 创建存储桶成功: " + bucketName);
|
||||||
|
} else {
|
||||||
|
LOGGER.info("[MinioUtil] 存储桶已存在: " + bucketName);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.log(Level.WARNING, "[MinioUtil] 检查/创建存储桶失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 静态方法(向后兼容) ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文件到 MinIO(静态方法)
|
||||||
|
*
|
||||||
|
* @param inputStream 文件输入流
|
||||||
|
* @param objectName 对象名称(文件路径,如 posters/123_456.jpg)
|
||||||
|
* @param contentType 文件 MIME 类型(如 image/jpeg)
|
||||||
|
* @return 上传成功返回 MinIO 对象名称,失败返回 null
|
||||||
|
*/
|
||||||
|
public static String upload(InputStream inputStream, String objectName, String contentType) {
|
||||||
|
if (instance == null) {
|
||||||
|
throw new IllegalStateException("MinioUtil未初始化,请确保Spring容器已启动");
|
||||||
|
}
|
||||||
|
return instance.doUpload(inputStream, objectName, contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传字节数组到 MinIO(静态方法)
|
||||||
|
*
|
||||||
|
* @param objectName 对象名称
|
||||||
|
* @param fileBytes 文件字节数组
|
||||||
|
* @param contentType 文件 MIME 类型
|
||||||
|
* @return 上传成功返回 MinIO 对象名称,失败返回 null
|
||||||
|
*/
|
||||||
|
public static String uploadFile(String objectName, byte[] fileBytes, String contentType) {
|
||||||
|
if (instance == null) {
|
||||||
|
throw new IllegalStateException("MinioUtil未初始化,请确保Spring容器已启动");
|
||||||
|
}
|
||||||
|
return instance.doUploadFile(objectName, fileBytes, contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件的访问 URL(静态方法)
|
||||||
|
*
|
||||||
|
* @param objectName 对象名称
|
||||||
|
* @return 文件访问 URL,失败返回 null
|
||||||
|
*/
|
||||||
|
public static String getFileUrl(String objectName) {
|
||||||
|
if (instance == null) {
|
||||||
|
throw new IllegalStateException("MinioUtil未初始化,请确保Spring容器已启动");
|
||||||
|
}
|
||||||
|
return instance.doGetFileUrl(objectName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 MinIO 服务端点地址(静态方法)
|
||||||
|
*
|
||||||
|
* @return 端点地址
|
||||||
|
*/
|
||||||
|
public static String getEndpoint() {
|
||||||
|
return instance != null ? instance.endpoint : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取存储桶名称(静态方法)
|
||||||
|
*
|
||||||
|
* @return 桶名称
|
||||||
|
*/
|
||||||
|
public static String getBucketName() {
|
||||||
|
return instance != null ? instance.bucketName : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 内部实现方法 ====================
|
||||||
|
|
||||||
|
private String doUpload(InputStream inputStream, String objectName, String contentType) {
|
||||||
|
try {
|
||||||
|
getClient().putObject(
|
||||||
|
PutObjectArgs.builder()
|
||||||
|
.bucket(bucketName)
|
||||||
|
.object(objectName)
|
||||||
|
.stream(inputStream, -1, 10 * 1024 * 1024) // 最大 10MB
|
||||||
|
.contentType(contentType)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
LOGGER.info("[MinioUtil] 文件上传成功: " + objectName);
|
||||||
|
return objectName;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.log(Level.WARNING, "[MinioUtil] 文件上传失败: " + objectName, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String doUploadFile(String objectName, byte[] fileBytes, String contentType) {
|
||||||
|
try (var inputStream = new java.io.ByteArrayInputStream(fileBytes)) {
|
||||||
|
return doUpload(inputStream, objectName, contentType);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.log(Level.WARNING, "[MinioUtil] 文件上传失败: " + objectName, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String doGetFileUrl(String objectName) {
|
||||||
|
try {
|
||||||
|
// 尝试生成预签名 URL(适用于私有桶)
|
||||||
|
String presignedUrl = getClient().getPresignedObjectUrl(
|
||||||
|
GetPresignedObjectUrlArgs.builder()
|
||||||
|
.method(Method.GET)
|
||||||
|
.bucket(bucketName)
|
||||||
|
.object(objectName)
|
||||||
|
.expiry(PRESIGNED_URL_EXPIRY_DAYS, TimeUnit.DAYS)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
LOGGER.info("[MinioUtil] 生成预签名 URL 成功: " + objectName);
|
||||||
|
return presignedUrl;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.log(Level.WARNING, "[MinioUtil] 生成预签名 URL 失败: " + objectName, e);
|
||||||
|
// 如果预签名失败,尝试返回直接访问 URL
|
||||||
|
return endpoint + "/" + bucketName + "/" + objectName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
package com.wangpan.common.util;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据库表结构工具类
|
||||||
|
*
|
||||||
|
* 提供数据库表和字段的检查与创建功能,消除各服务 DatabaseInitializer 中的重复代码。
|
||||||
|
* 包含两个核心方法:
|
||||||
|
* - createTableIfNotExists:检查表是否存在,不存在则创建
|
||||||
|
* - ensureColumnExists:检查字段是否存在,不存在则添加
|
||||||
|
*/
|
||||||
|
public class SchemaUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 如果表不存在则创建
|
||||||
|
*
|
||||||
|
* @param conn 数据库连接
|
||||||
|
* @param tableName 表名
|
||||||
|
* @param createSql 建表 SQL 语句
|
||||||
|
*/
|
||||||
|
public static void createTableIfNotExists(Connection conn, String tableName, String createSql) {
|
||||||
|
try (Statement stmt = conn.createStatement();
|
||||||
|
ResultSet rs = stmt.executeQuery("SHOW TABLES LIKE '" + tableName + "'")) {
|
||||||
|
|
||||||
|
if (!rs.next()) {
|
||||||
|
stmt.executeUpdate(createSql);
|
||||||
|
System.out.println(" ✓ 创建表:" + tableName);
|
||||||
|
} else {
|
||||||
|
System.out.println(" ✓ 表已存在:" + tableName);
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
System.err.println(" ⚠ 创建表失败 [" + tableName + "]:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查列是否存在,不存在则添加
|
||||||
|
*
|
||||||
|
* @param conn 数据库连接
|
||||||
|
* @param tableName 表名
|
||||||
|
* @param columnName 列名
|
||||||
|
* @param columnDef 列定义(类型、默认值、注释等)
|
||||||
|
*/
|
||||||
|
public static void ensureColumnExists(Connection conn, String tableName, String columnName, String columnDef) {
|
||||||
|
try (Statement stmt = conn.createStatement();
|
||||||
|
ResultSet rs = stmt.executeQuery("SHOW COLUMNS FROM " + tableName + " LIKE '" + columnName + "'")) {
|
||||||
|
|
||||||
|
if (!rs.next()) {
|
||||||
|
stmt.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + columnName + " " + columnDef);
|
||||||
|
System.out.println(" ✓ [" + tableName + "] 添加字段:" + columnName);
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
System.err.println(" 检查字段失败 [" + tableName + "." + columnName + "]:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,300 @@
|
|||||||
|
package com.wangpan.common.util;
|
||||||
|
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户令牌(Token)管理工具类 - Java 17 版本
|
||||||
|
*
|
||||||
|
* 功能:
|
||||||
|
* 1. 登录成功后生成唯一令牌(有效期7天)
|
||||||
|
* 2. 验证令牌有效性(解密 + 过期检查)
|
||||||
|
* 3. 令牌刷新/撤销功能
|
||||||
|
*
|
||||||
|
* 设计说明:
|
||||||
|
* - Token = Jasypt加密(userId + UUID + expireTime + secret)
|
||||||
|
* - 存储在数据库 wp_user_token 表中(服务重启后仍有效)
|
||||||
|
* - 内存缓存活跃令牌(减少数据库查询)
|
||||||
|
* - 7天自动过期机制
|
||||||
|
*
|
||||||
|
* 安全特性:
|
||||||
|
* - 使用Jasypt加密存储(与数据库密码同一套加密体系)
|
||||||
|
* - 包含用户ID + UUID + 时间戳防伪造
|
||||||
|
* - IP绑定可选(防止令牌被盗用)
|
||||||
|
*/
|
||||||
|
public class TokenUtil {
|
||||||
|
|
||||||
|
// ==================== 常量定义 ====================
|
||||||
|
|
||||||
|
/** 令牌有效期:7天(单位:小时) */
|
||||||
|
public static final long TOKEN_EXPIRE_HOURS = 7 * 24; // 168小时
|
||||||
|
|
||||||
|
/** 令牌分隔符(用于拼接原始数据) */
|
||||||
|
private static final String SEPARATOR = "|";
|
||||||
|
|
||||||
|
/** 时间格式化器 */
|
||||||
|
private static final DateTimeFormatter TIME_FORMATTER =
|
||||||
|
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
/** 令牌前缀标识(用于快速识别是否为加密令牌) */
|
||||||
|
private static final String TOKEN_PREFIX = "tk_";
|
||||||
|
|
||||||
|
// ==================== 内存缓存(性能优化) ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活跃令牌缓存
|
||||||
|
* Key: 加密后的Token值
|
||||||
|
* Value: TokenInfo对象(包含userId和过期时间)
|
||||||
|
*
|
||||||
|
* 设计说明:
|
||||||
|
* - 使用 ConcurrentHashMap 保证线程安全
|
||||||
|
* - 缓存减少对数据库的频繁查询
|
||||||
|
* - 服务重启后从数据库重新加载
|
||||||
|
*/
|
||||||
|
private static final java.util.concurrent.ConcurrentHashMap<String, TokenInfo> tokenCache =
|
||||||
|
new java.util.concurrent.ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/** 最大缓存数量(防止内存泄漏) */
|
||||||
|
private static final int MAX_CACHE_SIZE = 10000;
|
||||||
|
|
||||||
|
// ==================== 核心方法 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成新的用户令牌
|
||||||
|
*
|
||||||
|
* 流程:
|
||||||
|
* 1. 生成UUID作为唯一标识
|
||||||
|
* 2. 计算过期时间(当前时间 + 7天)
|
||||||
|
* 3. 拼接原始字符串:userId|uuid|expireTime|secret
|
||||||
|
* 4. 使用Jasypt加密得到最终Token
|
||||||
|
* 5. 存入内存缓存
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @return 加密后的令牌字符串(如 tk_abc123...)
|
||||||
|
*/
|
||||||
|
public static String generateToken(Long userId) {
|
||||||
|
// 生成唯一UUID
|
||||||
|
var uuid = UUID.randomUUID().toString().replace("-", "");
|
||||||
|
|
||||||
|
// 计算过期时间
|
||||||
|
var expireTime = LocalDateTime.now().plusHours(TOKEN_EXPIRE_HOURS);
|
||||||
|
var expireTimeStr = expireTime.format(TIME_FORMATTER);
|
||||||
|
|
||||||
|
// 拼接原始数据(明文):userId|uuid|expireTime|secret
|
||||||
|
var rawToken = userId + SEPARATOR + uuid + SEPARATOR + expireTimeStr + SEPARATOR + getSecret();
|
||||||
|
|
||||||
|
// 使用Jasypt加密
|
||||||
|
var encryptedToken = EncryptUtil.encrypt(rawToken);
|
||||||
|
|
||||||
|
// 添加前缀便于识别
|
||||||
|
var finalToken = TOKEN_PREFIX + encryptedToken;
|
||||||
|
|
||||||
|
// 存入内存缓存
|
||||||
|
cacheToken(finalToken, new TokenInfo(userId, expireTime));
|
||||||
|
|
||||||
|
System.out.println("[TokenUtil] 为用户 [" + userId + "] 生成新令牌,有效期至:" + expireTimeStr);
|
||||||
|
|
||||||
|
return finalToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从令牌中快速获取用户ID(简化版validateToken)
|
||||||
|
*
|
||||||
|
* @param token 客户端传入的令牌
|
||||||
|
* @return 用户ID,无效返回null
|
||||||
|
*/
|
||||||
|
public static Long getUserId(String token) {
|
||||||
|
var info = validateToken(token);
|
||||||
|
return info != null ? info.userId : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证令牌有效性并获取用户ID
|
||||||
|
*
|
||||||
|
* 验证流程:
|
||||||
|
* 1. 先查内存缓存(快速路径)
|
||||||
|
* 2. 缓存未命中则解密验证(慢速路径)
|
||||||
|
* 3. 检查是否过期
|
||||||
|
* 4. 返回用户ID或null
|
||||||
|
*
|
||||||
|
* @param token 客户端传入的令牌
|
||||||
|
* @return TokenInfo对象(含userId),无效返回null
|
||||||
|
*/
|
||||||
|
public static TokenInfo validateToken(String token) {
|
||||||
|
// 空值校验
|
||||||
|
if (token == null || token.isBlank()) {
|
||||||
|
System.out.println("[TokenUtil] 令牌为空");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式校验(必须以 tk_ 开头)
|
||||||
|
if (!token.startsWith(TOKEN_PREFIX)) {
|
||||||
|
System.out.println("[TokenUtil] 令牌格式错误(缺少前缀)");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 第1步:先查内存缓存(快速路径) ==========
|
||||||
|
var cachedInfo = tokenCache.get(token);
|
||||||
|
if (cachedInfo != null) {
|
||||||
|
// 检查缓存中的令牌是否已过期
|
||||||
|
if (LocalDateTime.now().isBefore(cachedInfo.expireTime)) {
|
||||||
|
return cachedInfo; // 缓存命中且未过期 → 直接返回
|
||||||
|
} else {
|
||||||
|
// 已过期 → 从缓存移除
|
||||||
|
tokenCache.remove(token);
|
||||||
|
System.out.println("[TokenUtil] 缓存中的令牌已过期");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 第2步:解密验证(慢速路径) ==========
|
||||||
|
try {
|
||||||
|
// 去掉前缀,提取加密部分
|
||||||
|
var encryptedPart = token.substring(TOKEN_PREFIX.length());
|
||||||
|
|
||||||
|
// 解密得到原始字符串
|
||||||
|
var rawToken = EncryptUtil.decrypt(encryptedPart);
|
||||||
|
|
||||||
|
// 解析各字段
|
||||||
|
var parts = rawToken.split("\\|", -1); // -1保留空字段
|
||||||
|
if (parts.length < 4) {
|
||||||
|
System.err.println("[TokenUtil] 令牌解析失败(字段数不足)");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var userId = Long.parseLong(parts[0]);
|
||||||
|
var expireTimeStr = parts[2];
|
||||||
|
var expireTime = LocalDateTime.parse(expireTimeStr, TIME_FORMATTER);
|
||||||
|
|
||||||
|
// ========== 第3步:检查是否过期 ==========
|
||||||
|
var now = LocalDateTime.now();
|
||||||
|
if (now.isAfter(expireTime)) {
|
||||||
|
System.out.println("[TokenUtil] 令牌已过期(过期时间:" + expireTimeStr + ")");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 第4步:重新缓存(下次走快速路径) ==========
|
||||||
|
var info = new TokenInfo(userId, expireTime);
|
||||||
|
cacheToken(token, info);
|
||||||
|
|
||||||
|
return info;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("[TokenUtil] 令牌验证失败:" + e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 撤销令牌(登出时调用)
|
||||||
|
* 从缓存和数据库中移除
|
||||||
|
*
|
||||||
|
* @param token 要撤销的令牌
|
||||||
|
*/
|
||||||
|
public static void revokeToken(String token) {
|
||||||
|
if (token != null) {
|
||||||
|
// 从内存缓存移除
|
||||||
|
tokenCache.remove(token);
|
||||||
|
System.out.println("[TokenUtil] 令牌已撤销");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理所有过期的缓存令牌
|
||||||
|
* 可由定时任务调用,防止内存泄漏
|
||||||
|
*/
|
||||||
|
public static void cleanExpiredTokens() {
|
||||||
|
var now = LocalDateTime.now();
|
||||||
|
var iterator = tokenCache.entrySet().iterator();
|
||||||
|
var cleanedCount = 0;
|
||||||
|
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
var entry = iterator.next();
|
||||||
|
if (now.isAfter(entry.getValue().expireTime)) {
|
||||||
|
iterator.remove();
|
||||||
|
cleanedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleanedCount > 0) {
|
||||||
|
System.out.println("[TokenUtil] 清理了 " + cleanedCount + " 个过期令牌缓存");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 私有辅助方法 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将令牌存入内存缓存(带容量限制)
|
||||||
|
*/
|
||||||
|
private static void cacheToken(String token, TokenInfo info) {
|
||||||
|
// 超过最大容量时清理旧条目
|
||||||
|
if (tokenCache.size() >= MAX_CACHE_SIZE) {
|
||||||
|
cleanExpiredTokens();
|
||||||
|
}
|
||||||
|
tokenCache.put(token, info);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取签名密钥(用于防止令牌被伪造)
|
||||||
|
* 结合固定密钥 + 系统属性,增强安全性
|
||||||
|
*/
|
||||||
|
private static String getSecret() {
|
||||||
|
// 使用固定的签名密钥(生产环境应改为配置项)
|
||||||
|
var baseSecret = "wp-token-secret";
|
||||||
|
// 加入系统属性增加随机性
|
||||||
|
var systemProp = System.getProperty("os.name", "") + System.getProperty("user.name", "");
|
||||||
|
return md5(baseSecret + systemProp).substring(0, 16); // 取前16位作为secret
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MD5哈希计算
|
||||||
|
*/
|
||||||
|
private static String md5(String input) {
|
||||||
|
try {
|
||||||
|
var md = MessageDigest.getInstance("MD5");
|
||||||
|
var digest = md.digest(input.getBytes(StandardCharsets.UTF_8));
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
for (var b : digest) {
|
||||||
|
sb.append(String.format("%02x", b & 0xff));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("MD5计算失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 内部类 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 令牌信息内部类
|
||||||
|
* 用于封装令牌关联的用户信息和过期时间
|
||||||
|
*/
|
||||||
|
public static class TokenInfo {
|
||||||
|
/** 关联的用户ID */
|
||||||
|
public final Long userId;
|
||||||
|
/** 令牌过期时间 */
|
||||||
|
public final LocalDateTime expireTime;
|
||||||
|
|
||||||
|
public TokenInfo(Long userId, LocalDateTime expireTime) {
|
||||||
|
this.userId = userId;
|
||||||
|
this.expireTime = expireTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断令牌是否即将过期(剩余不足1天)
|
||||||
|
*/
|
||||||
|
public boolean isExpiringSoon() {
|
||||||
|
return LocalDateTime.now().plusDays(1).isAfter(expireTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取剩余有效时间(秒)
|
||||||
|
*/
|
||||||
|
public long getRemainingSeconds() {
|
||||||
|
return java.time.Duration.between(LocalDateTime.now(), expireTime).getSeconds();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
|
||||||
|
<!-- 网盘文件夹 Mapper XML:存放复杂SQL,简单CRUD由MyBatis-Plus BaseMapper自动提供 -->
|
||||||
|
<mapper namespace="com.wangpan.common.dal.mapper.DriveFolderMapper">
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
|
||||||
|
<!-- 网盘 Mapper XML:存放复杂SQL,简单CRUD由MyBatis-Plus BaseMapper自动提供 -->
|
||||||
|
<mapper namespace="com.wangpan.common.dal.mapper.DriveMapper">
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
|
||||||
|
<!-- 网盘电影 Mapper XML:存放复杂SQL,简单CRUD由MyBatis-Plus BaseMapper自动提供 -->
|
||||||
|
<mapper namespace="com.wangpan.common.dal.mapper.DriveMovieMapper">
|
||||||
|
|
||||||
|
<!-- 查找用户所有网盘下的重复电影(按 title 分组,出现次数 > 1) -->
|
||||||
|
<select id="findDuplicateMovies" resultType="java.util.Map">
|
||||||
|
SELECT title, COUNT(*) AS cnt
|
||||||
|
FROM wp_drive_movie
|
||||||
|
WHERE user_id = #{userId} AND status = 'active'
|
||||||
|
GROUP BY title
|
||||||
|
HAVING COUNT(*) > 1
|
||||||
|
ORDER BY cnt DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
|
||||||
|
<!-- 登录日志 Mapper XML:存放复杂SQL,简单CRUD由MyBatis-Plus BaseMapper自动提供 -->
|
||||||
|
<mapper namespace="com.wangpan.common.dal.mapper.LoginLogMapper">
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
|
||||||
|
<!-- 电影 Mapper XML:存放复杂SQL,简单CRUD由MyBatis-Plus BaseMapper自动提供 -->
|
||||||
|
<mapper namespace="com.wangpan.common.dal.mapper.MovieMapper">
|
||||||
|
|
||||||
|
<!-- 查找用户所有网盘下的重复电影(按 title 分组,出现次数 > 1) -->
|
||||||
|
<select id="findDuplicateMovies" resultType="java.util.Map">
|
||||||
|
SELECT title, COUNT(*) AS cnt
|
||||||
|
FROM wp_movie
|
||||||
|
WHERE user_id = #{userId} AND status = 'active'
|
||||||
|
GROUP BY title
|
||||||
|
HAVING COUNT(*) > 1
|
||||||
|
ORDER BY cnt DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 查找所有配置了TMDB API Key的用户(供定时刮削使用) -->
|
||||||
|
<select id="findUsersWithTmdbKey" resultType="java.util.Map">
|
||||||
|
SELECT u.id, u.username
|
||||||
|
FROM wp_user u
|
||||||
|
WHERE u.tmdb_api_key IS NOT NULL
|
||||||
|
AND u.tmdb_api_key != ''
|
||||||
|
AND u.status = 1
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 查找指定用户需要刮削的电影 -->
|
||||||
|
<select id="findMoviesNeedingScrape" resultType="com.wangpan.common.dal.dataobject.MovieDO">
|
||||||
|
SELECT m.* FROM wp_movie m
|
||||||
|
LEFT JOIN wp_tmdb_cache c ON m.id = c.movie_id AND c.status = 'active'
|
||||||
|
WHERE m.user_id = #{userId}
|
||||||
|
AND m.title IS NOT NULL AND m.title != ''
|
||||||
|
AND m.scrape_type != 'none'
|
||||||
|
AND (c.id IS NULL OR c.cache_time < DATE_SUB(NOW(), INTERVAL #{expireDays} DAY))
|
||||||
|
ORDER BY m.update_time DESC
|
||||||
|
LIMIT 100
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@ -0,0 +1,90 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
|
||||||
|
<!-- TMDB刮削缓存 Mapper XML:存放复杂SQL,简单CRUD由MyBatis-Plus BaseMapper自动提供 -->
|
||||||
|
<mapper namespace="com.wangpan.common.dal.mapper.TmdbCacheMapper">
|
||||||
|
|
||||||
|
<!-- 根据电影ID查询有效缓存 -->
|
||||||
|
<select id="findByMovieId" resultType="com.wangpan.common.dal.dataobject.TmdbCacheDO">
|
||||||
|
SELECT * FROM wp_tmdb_cache WHERE movie_id = #{movieId} AND status = 'active'
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 根据TMDB ID查询有效缓存列表 -->
|
||||||
|
<select id="findByTmdbId" resultType="com.wangpan.common.dal.dataobject.TmdbCacheDO">
|
||||||
|
SELECT * FROM wp_tmdb_cache WHERE tmdb_id = #{tmdbId} AND status = 'active'
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 分页查询用户缓存(含关联电影标题) -->
|
||||||
|
<select id="queryByUserId" resultType="java.util.Map">
|
||||||
|
SELECT c.*, m.title AS movie_title
|
||||||
|
FROM wp_tmdb_cache c
|
||||||
|
LEFT JOIN wp_movie m ON c.movie_id = m.id
|
||||||
|
WHERE c.user_id = #{userId} AND c.status = 'active'
|
||||||
|
ORDER BY c.cache_time DESC
|
||||||
|
LIMIT #{pageSize} OFFSET #{pageNo}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 查询所有过期的缓存ID列表 -->
|
||||||
|
<select id="findExpiredCaches" resultType="java.lang.Long">
|
||||||
|
SELECT id FROM wp_tmdb_cache WHERE cache_time < #{expireBefore} AND status = 'active'
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 新增或更新缓存记录(UPSERT) -->
|
||||||
|
<insert id="saveOrUpdate" parameterType="map">
|
||||||
|
INSERT INTO wp_tmdb_cache (
|
||||||
|
movie_id, user_id, tmdb_id, title, original_title, overview,
|
||||||
|
release_date, poster_path, backdrop_path, local_poster_url,
|
||||||
|
tmdb_rating, vote_count, runtime, genres, directors, casts,
|
||||||
|
original_language, production_countries, status, cache_time
|
||||||
|
) VALUES (
|
||||||
|
#{data.movieId}, #{data.userId}, #{data.tmdbId}, #{data.title}, #{data.originalTitle}, #{data.overview},
|
||||||
|
#{data.releaseDate}, #{data.posterPath}, #{data.backdropPath}, #{data.localPosterUrl},
|
||||||
|
#{data.tmdbRating}, #{data.voteCount}, #{data.runtime}, #{data.genres}, #{data.directors}, #{data.casts},
|
||||||
|
#{data.originalLanguage}, #{data.productionCountries}, 'active', NOW()
|
||||||
|
)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
tmdb_id = VALUES(tmdb_id),
|
||||||
|
title = VALUES(title),
|
||||||
|
original_title = VALUES(original_title),
|
||||||
|
overview = VALUES(overview),
|
||||||
|
release_date = VALUES(release_date),
|
||||||
|
poster_path = VALUES(poster_path),
|
||||||
|
backdrop_path = VALUES(backdrop_path),
|
||||||
|
local_poster_url = VALUES(local_poster_url),
|
||||||
|
tmdb_rating = VALUES(tmdb_rating),
|
||||||
|
vote_count = VALUES(vote_count),
|
||||||
|
runtime = VALUES(runtime),
|
||||||
|
genres = VALUES(genres),
|
||||||
|
directors = VALUES(directors),
|
||||||
|
casts = VALUES(casts),
|
||||||
|
original_language = VALUES(original_language),
|
||||||
|
production_countries = VALUES(production_countries),
|
||||||
|
status = 'active',
|
||||||
|
cache_time = NOW()
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<!-- 批量将缓存标记为过期 -->
|
||||||
|
<update id="markAsExpired">
|
||||||
|
UPDATE wp_tmdb_cache SET status = 'expired' WHERE id IN
|
||||||
|
<foreach collection="cacheIds" item="id" open="(" separator="," close=")">
|
||||||
|
#{id}
|
||||||
|
</foreach>
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<!-- 删除指定电影的缓存 -->
|
||||||
|
<delete id="deleteByMovieId">
|
||||||
|
DELETE FROM wp_tmdb_cache WHERE movie_id = #{movieId}
|
||||||
|
</delete>
|
||||||
|
|
||||||
|
<!-- 获取用户的缓存统计信息 -->
|
||||||
|
<select id="getStatistics" resultType="java.util.Map">
|
||||||
|
SELECT
|
||||||
|
COUNT(*) AS totalCached,
|
||||||
|
SUM(CASE WHEN cache_time >= DATE_SUB(NOW(), INTERVAL 1 DAY) THEN 1 ELSE 0 END) AS todayCached,
|
||||||
|
SUM(CASE WHEN cache_time < DATE_SUB(NOW(), INTERVAL 7 DAY) THEN 1 ELSE 0 END) AS expiredCount
|
||||||
|
FROM wp_tmdb_cache
|
||||||
|
WHERE user_id = #{userId}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
|
||||||
|
<!-- TMDB定时刮削执行记录 Mapper XML:存放复杂SQL,简单CRUD由MyBatis-Plus BaseMapper自动提供 -->
|
||||||
|
<mapper namespace="com.wangpan.common.dal.mapper.TmdbScraperLogMapper">
|
||||||
|
|
||||||
|
<!-- 插入一条执行记录 -->
|
||||||
|
<insert id="insertLog" parameterType="map">
|
||||||
|
INSERT INTO wp_tmdb_scraper_log (
|
||||||
|
trigger_type, status, total_processed, total_success, total_skipped,
|
||||||
|
total_failed, user_count, detail_json, error_message, duration_ms,
|
||||||
|
start_time, end_time
|
||||||
|
) VALUES (
|
||||||
|
#{log.triggerType}, #{log.status}, #{log.totalProcessed}, #{log.totalSuccess}, #{log.totalSkipped},
|
||||||
|
#{log.totalFailed}, #{log.userCount}, #{log.detailJson}, #{log.errorMessage}, #{log.durationMs},
|
||||||
|
#{log.startTime}, #{log.endTime}
|
||||||
|
)
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<!-- 分页查询最近的执行记录 -->
|
||||||
|
<select id="queryRecent" resultType="java.util.Map">
|
||||||
|
SELECT * FROM wp_tmdb_scraper_log ORDER BY start_time DESC
|
||||||
|
LIMIT #{pageSize} OFFSET #{pageNo}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 查询最近的N条执行记录 -->
|
||||||
|
<select id="queryRecentByLimit" resultType="java.util.Map">
|
||||||
|
SELECT * FROM wp_tmdb_scraper_log ORDER BY start_time DESC LIMIT #{limit}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 根据ID查询单条记录 -->
|
||||||
|
<select id="findById" resultType="java.util.Map">
|
||||||
|
SELECT * FROM wp_tmdb_scraper_log WHERE id = #{id}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
|
||||||
|
<!-- 用户令牌 Mapper XML:存放复杂SQL,简单CRUD由MyBatis-Plus BaseMapper自动提供 -->
|
||||||
|
<mapper namespace="com.wangpan.common.dal.mapper.TokenMapper">
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
|
||||||
|
<!-- 用户 Mapper XML:存放复杂SQL,简单CRUD由MyBatis-Plus BaseMapper自动提供 -->
|
||||||
|
<mapper namespace="com.wangpan.common.dal.mapper.UserMapper">
|
||||||
|
|
||||||
|
</mapper>
|
||||||
BIN
wangpan-common/target/classes/com/wangpan/common/Result.class
Normal file
BIN
wangpan-common/target/classes/com/wangpan/common/Result.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
|
||||||
|
<!-- 网盘文件夹 Mapper XML:存放复杂SQL,简单CRUD由MyBatis-Plus BaseMapper自动提供 -->
|
||||||
|
<mapper namespace="com.wangpan.common.dal.mapper.DriveFolderMapper">
|
||||||
|
|
||||||
|
</mapper>
|
||||||
Binary file not shown.
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
|
||||||
|
<!-- 网盘 Mapper XML:存放复杂SQL,简单CRUD由MyBatis-Plus BaseMapper自动提供 -->
|
||||||
|
<mapper namespace="com.wangpan.common.dal.mapper.DriveMapper">
|
||||||
|
|
||||||
|
</mapper>
|
||||||
Binary file not shown.
@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
|
||||||
|
<!-- 网盘电影 Mapper XML:存放复杂SQL,简单CRUD由MyBatis-Plus BaseMapper自动提供 -->
|
||||||
|
<mapper namespace="com.wangpan.common.dal.mapper.DriveMovieMapper">
|
||||||
|
|
||||||
|
<!-- 查找用户所有网盘下的重复电影(按 title 分组,出现次数 > 1) -->
|
||||||
|
<select id="findDuplicateMovies" resultType="java.util.Map">
|
||||||
|
SELECT title, COUNT(*) AS cnt
|
||||||
|
FROM wp_drive_movie
|
||||||
|
WHERE user_id = #{userId} AND status = 'active'
|
||||||
|
GROUP BY title
|
||||||
|
HAVING COUNT(*) > 1
|
||||||
|
ORDER BY cnt DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
Binary file not shown.
Binary file not shown.
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
|
||||||
|
<!-- 登录日志 Mapper XML:存放复杂SQL,简单CRUD由MyBatis-Plus BaseMapper自动提供 -->
|
||||||
|
<mapper namespace="com.wangpan.common.dal.mapper.LoginLogMapper">
|
||||||
|
|
||||||
|
</mapper>
|
||||||
Binary file not shown.
@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
|
||||||
|
<!-- 电影 Mapper XML:存放复杂SQL,简单CRUD由MyBatis-Plus BaseMapper自动提供 -->
|
||||||
|
<mapper namespace="com.wangpan.common.dal.mapper.MovieMapper">
|
||||||
|
|
||||||
|
<!-- 查找用户所有网盘下的重复电影(按 title 分组,出现次数 > 1) -->
|
||||||
|
<select id="findDuplicateMovies" resultType="java.util.Map">
|
||||||
|
SELECT title, COUNT(*) AS cnt
|
||||||
|
FROM wp_movie
|
||||||
|
WHERE user_id = #{userId} AND status = 'active'
|
||||||
|
GROUP BY title
|
||||||
|
HAVING COUNT(*) > 1
|
||||||
|
ORDER BY cnt DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 查找所有配置了TMDB API Key的用户(供定时刮削使用) -->
|
||||||
|
<select id="findUsersWithTmdbKey" resultType="java.util.Map">
|
||||||
|
SELECT u.id, u.username
|
||||||
|
FROM wp_user u
|
||||||
|
WHERE u.tmdb_api_key IS NOT NULL
|
||||||
|
AND u.tmdb_api_key != ''
|
||||||
|
AND u.status = 1
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 查找指定用户需要刮削的电影 -->
|
||||||
|
<select id="findMoviesNeedingScrape" resultType="com.wangpan.common.dal.dataobject.MovieDO">
|
||||||
|
SELECT m.* FROM wp_movie m
|
||||||
|
LEFT JOIN wp_tmdb_cache c ON m.id = c.movie_id AND c.status = 'active'
|
||||||
|
WHERE m.user_id = #{userId}
|
||||||
|
AND m.title IS NOT NULL AND m.title != ''
|
||||||
|
AND m.scrape_type != 'none'
|
||||||
|
AND (c.id IS NULL OR c.cache_time < DATE_SUB(NOW(), INTERVAL #{expireDays} DAY))
|
||||||
|
ORDER BY m.update_time DESC
|
||||||
|
LIMIT 100
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
Binary file not shown.
@ -0,0 +1,90 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
|
||||||
|
<!-- TMDB刮削缓存 Mapper XML:存放复杂SQL,简单CRUD由MyBatis-Plus BaseMapper自动提供 -->
|
||||||
|
<mapper namespace="com.wangpan.common.dal.mapper.TmdbCacheMapper">
|
||||||
|
|
||||||
|
<!-- 根据电影ID查询有效缓存 -->
|
||||||
|
<select id="findByMovieId" resultType="com.wangpan.common.dal.dataobject.TmdbCacheDO">
|
||||||
|
SELECT * FROM wp_tmdb_cache WHERE movie_id = #{movieId} AND status = 'active'
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 根据TMDB ID查询有效缓存列表 -->
|
||||||
|
<select id="findByTmdbId" resultType="com.wangpan.common.dal.dataobject.TmdbCacheDO">
|
||||||
|
SELECT * FROM wp_tmdb_cache WHERE tmdb_id = #{tmdbId} AND status = 'active'
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 分页查询用户缓存(含关联电影标题) -->
|
||||||
|
<select id="queryByUserId" resultType="java.util.Map">
|
||||||
|
SELECT c.*, m.title AS movie_title
|
||||||
|
FROM wp_tmdb_cache c
|
||||||
|
LEFT JOIN wp_movie m ON c.movie_id = m.id
|
||||||
|
WHERE c.user_id = #{userId} AND c.status = 'active'
|
||||||
|
ORDER BY c.cache_time DESC
|
||||||
|
LIMIT #{pageSize} OFFSET #{pageNo}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 查询所有过期的缓存ID列表 -->
|
||||||
|
<select id="findExpiredCaches" resultType="java.lang.Long">
|
||||||
|
SELECT id FROM wp_tmdb_cache WHERE cache_time < #{expireBefore} AND status = 'active'
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 新增或更新缓存记录(UPSERT) -->
|
||||||
|
<insert id="saveOrUpdate" parameterType="map">
|
||||||
|
INSERT INTO wp_tmdb_cache (
|
||||||
|
movie_id, user_id, tmdb_id, title, original_title, overview,
|
||||||
|
release_date, poster_path, backdrop_path, local_poster_url,
|
||||||
|
tmdb_rating, vote_count, runtime, genres, directors, casts,
|
||||||
|
original_language, production_countries, status, cache_time
|
||||||
|
) VALUES (
|
||||||
|
#{data.movieId}, #{data.userId}, #{data.tmdbId}, #{data.title}, #{data.originalTitle}, #{data.overview},
|
||||||
|
#{data.releaseDate}, #{data.posterPath}, #{data.backdropPath}, #{data.localPosterUrl},
|
||||||
|
#{data.tmdbRating}, #{data.voteCount}, #{data.runtime}, #{data.genres}, #{data.directors}, #{data.casts},
|
||||||
|
#{data.originalLanguage}, #{data.productionCountries}, 'active', NOW()
|
||||||
|
)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
tmdb_id = VALUES(tmdb_id),
|
||||||
|
title = VALUES(title),
|
||||||
|
original_title = VALUES(original_title),
|
||||||
|
overview = VALUES(overview),
|
||||||
|
release_date = VALUES(release_date),
|
||||||
|
poster_path = VALUES(poster_path),
|
||||||
|
backdrop_path = VALUES(backdrop_path),
|
||||||
|
local_poster_url = VALUES(local_poster_url),
|
||||||
|
tmdb_rating = VALUES(tmdb_rating),
|
||||||
|
vote_count = VALUES(vote_count),
|
||||||
|
runtime = VALUES(runtime),
|
||||||
|
genres = VALUES(genres),
|
||||||
|
directors = VALUES(directors),
|
||||||
|
casts = VALUES(casts),
|
||||||
|
original_language = VALUES(original_language),
|
||||||
|
production_countries = VALUES(production_countries),
|
||||||
|
status = 'active',
|
||||||
|
cache_time = NOW()
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<!-- 批量将缓存标记为过期 -->
|
||||||
|
<update id="markAsExpired">
|
||||||
|
UPDATE wp_tmdb_cache SET status = 'expired' WHERE id IN
|
||||||
|
<foreach collection="cacheIds" item="id" open="(" separator="," close=")">
|
||||||
|
#{id}
|
||||||
|
</foreach>
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<!-- 删除指定电影的缓存 -->
|
||||||
|
<delete id="deleteByMovieId">
|
||||||
|
DELETE FROM wp_tmdb_cache WHERE movie_id = #{movieId}
|
||||||
|
</delete>
|
||||||
|
|
||||||
|
<!-- 获取用户的缓存统计信息 -->
|
||||||
|
<select id="getStatistics" resultType="java.util.Map">
|
||||||
|
SELECT
|
||||||
|
COUNT(*) AS totalCached,
|
||||||
|
SUM(CASE WHEN cache_time >= DATE_SUB(NOW(), INTERVAL 1 DAY) THEN 1 ELSE 0 END) AS todayCached,
|
||||||
|
SUM(CASE WHEN cache_time < DATE_SUB(NOW(), INTERVAL 7 DAY) THEN 1 ELSE 0 END) AS expiredCount
|
||||||
|
FROM wp_tmdb_cache
|
||||||
|
WHERE user_id = #{userId}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
Binary file not shown.
@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
|
||||||
|
<!-- TMDB定时刮削执行记录 Mapper XML:存放复杂SQL,简单CRUD由MyBatis-Plus BaseMapper自动提供 -->
|
||||||
|
<mapper namespace="com.wangpan.common.dal.mapper.TmdbScraperLogMapper">
|
||||||
|
|
||||||
|
<!-- 插入一条执行记录 -->
|
||||||
|
<insert id="insertLog" parameterType="map">
|
||||||
|
INSERT INTO wp_tmdb_scraper_log (
|
||||||
|
trigger_type, status, total_processed, total_success, total_skipped,
|
||||||
|
total_failed, user_count, detail_json, error_message, duration_ms,
|
||||||
|
start_time, end_time
|
||||||
|
) VALUES (
|
||||||
|
#{log.triggerType}, #{log.status}, #{log.totalProcessed}, #{log.totalSuccess}, #{log.totalSkipped},
|
||||||
|
#{log.totalFailed}, #{log.userCount}, #{log.detailJson}, #{log.errorMessage}, #{log.durationMs},
|
||||||
|
#{log.startTime}, #{log.endTime}
|
||||||
|
)
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<!-- 分页查询最近的执行记录 -->
|
||||||
|
<select id="queryRecent" resultType="java.util.Map">
|
||||||
|
SELECT * FROM wp_tmdb_scraper_log ORDER BY start_time DESC
|
||||||
|
LIMIT #{pageSize} OFFSET #{pageNo}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 查询最近的N条执行记录 -->
|
||||||
|
<select id="queryRecentByLimit" resultType="java.util.Map">
|
||||||
|
SELECT * FROM wp_tmdb_scraper_log ORDER BY start_time DESC LIMIT #{limit}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 根据ID查询单条记录 -->
|
||||||
|
<select id="findById" resultType="java.util.Map">
|
||||||
|
SELECT * FROM wp_tmdb_scraper_log WHERE id = #{id}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
Binary file not shown.
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
|
||||||
|
<!-- 用户令牌 Mapper XML:存放复杂SQL,简单CRUD由MyBatis-Plus BaseMapper自动提供 -->
|
||||||
|
<mapper namespace="com.wangpan.common.dal.mapper.TokenMapper">
|
||||||
|
|
||||||
|
</mapper>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user