fix: 统一 wp_drive_movie 表映射,移除废弃的 wp_movie 表引用
- MovieDO/MovieMapper 改为映射 wp_drive_movie 表 - 移除 wp_movie 建表逻辑,由 drive-service 统一管理 - 修复 TMDB 刮削查询条件,放开 scrape_type=none 限制 - 修复 api-doc.html 中 9 处与实际代码不一致 - 统一所有服务 Jasypt 加密算法 - 移除默认加密密钥,生产环境需通过环境变量设置 - 新增 PROJECT_REFERENCE.md 项目规范文件 - 新增 IconCleanupScheduler 定时清理图片 - 调整日志级别为 INFO,移除 MyBatis-Plus SQL 日志
This commit is contained in:
parent
f79c193b09
commit
49cce2dbb5
475
PROJECT_REFERENCE.md
Normal file
475
PROJECT_REFERENCE.md
Normal file
@ -0,0 +1,475 @@
|
|||||||
|
# 网盘系统 (WangPan) - 项目全局参考文件
|
||||||
|
|
||||||
|
> **用途**:本文档记录项目规范、命名约定、常见问题与解决方案,供 TraeAI 在每次调整代码前阅读参考。
|
||||||
|
>
|
||||||
|
> **规则**:每次修改项目代码前,TraeAI 必须读取本文件以确保遵循项目规范。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、项目概览
|
||||||
|
|
||||||
|
| 项目 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 名称 | WangPan(网盘系统) |
|
||||||
|
| 架构 | 微服务(Spring Boot 3.2.5 + Spring Cloud Gateway) |
|
||||||
|
| 语言 | Java 17 |
|
||||||
|
| 构建工具 | Maven |
|
||||||
|
| 包名根路径 | `com.wangpan` |
|
||||||
|
| 数据库 | MySQL 8.0.33 |
|
||||||
|
| 对象存储 | MinIO |
|
||||||
|
| ORM | MyBatis-Plus 3.5.7 |
|
||||||
|
| 配置加密 | Jasypt(Jasypt Spring Boot Starter) |
|
||||||
|
|
||||||
|
### 模块结构与端口
|
||||||
|
|
||||||
|
| 模块 | 端口 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `wangpan-common` | - | 公共模块(DO/Mapper/Repository/Entity/Util/Interceptor) |
|
||||||
|
| `wangpan-user-service` | 8081 | 用户认证服务(登录、注册、Token管理) |
|
||||||
|
| `wangpan-drive-service` | 8082 | 网盘管理服务(文件管理、电影管理) |
|
||||||
|
| `wangpan-tmdb-service` | 8083 | TMDB 刮削服务(电影元数据抓取) |
|
||||||
|
| `wangpan-gateway` | 8888 | API 网关(统一路由入口) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、命名规范
|
||||||
|
|
||||||
|
### 2.1 包命名
|
||||||
|
|
||||||
|
```
|
||||||
|
com.wangpan # 根包
|
||||||
|
├── common # 公共模块
|
||||||
|
│ ├── annotation # 自定义注解
|
||||||
|
│ ├── config # 配置类
|
||||||
|
│ ├── dal # 数据访问层
|
||||||
|
│ │ ├── dataobject # 数据对象(DO)
|
||||||
|
│ │ ├── mapper # MyBatis Mapper 接口
|
||||||
|
│ │ └── repository # Repository 层(封装数据访问)
|
||||||
|
│ ├── entity # 业务实体
|
||||||
|
│ ├── interceptor # 拦截器
|
||||||
|
│ ├── listener # 监听器 / 定时任务
|
||||||
|
│ └── util # 工具类
|
||||||
|
├── controller # 控制器(各子模块各自持有)
|
||||||
|
├── service # 业务服务层(各子模块各自持有)
|
||||||
|
└── Application.java # 启动类(各子模块各自持有)
|
||||||
|
```
|
||||||
|
|
||||||
|
**规则**:
|
||||||
|
- 各子服务的 Controller 和 Service 放在各自模块的 `src/main/java/com/wangpan/` 下
|
||||||
|
- 公共的 DO、Mapper、Repository、Entity、Util 放在 `wangpan-common` 中
|
||||||
|
- 所有 Mapper XML 文件统一放在 `wangpan-common/src/main/resources/com/wangpan/common/dal/mapper/` 下
|
||||||
|
|
||||||
|
### 2.2 类命名
|
||||||
|
|
||||||
|
| 类型 | 命名规则 | 示例 |
|
||||||
|
|------|---------|------|
|
||||||
|
| DO(数据对象) | `{实体名}DO` | `DriveMovieDO`, `UserDO`, `MovieDO` |
|
||||||
|
| Mapper 接口 | `{实体名}Mapper` | `DriveMovieMapper`, `UserMapper` |
|
||||||
|
| Repository | `{实体名}Repository` | `DriveMovieRepository`, `UserRepository` |
|
||||||
|
| Entity(业务实体) | `{实体名}` | `DriveMovie`, `User`, `Movie` |
|
||||||
|
| Controller | `{模块名}Controller` | `DriveController`, `AuthController`, `TmdbController` |
|
||||||
|
| Service | `{模块名}Service` | `DriveService`, `UserService`, `TmdbService` |
|
||||||
|
| 工具类 | `{功能}Util` | `TokenUtil`, `MinioUtil`, `AuthUtil`, `EncryptUtil` |
|
||||||
|
| 配置类 | `{功能}Config` | `WebMvcConfig`, `MyBatisPlusConfig` |
|
||||||
|
| 启动类 | `{模块名}Application` | `UserServiceApplication`, `DriveServiceApplication` |
|
||||||
|
| 拦截器 | `{功能}Interceptor` | `AuthInterceptor` |
|
||||||
|
| 定时任务 | `{功能}Scheduler` | `TmdbScheduler`, `IconCleanupScheduler` |
|
||||||
|
|
||||||
|
### 2.3 方法命名
|
||||||
|
|
||||||
|
| 操作 | 命名规则 | 示例 |
|
||||||
|
|------|---------|------|
|
||||||
|
| 查询单个 | `findBy{条件}` | `findById`, `findByMovieId` |
|
||||||
|
| 查询列表 | `queryBy{条件}` | `queryByUserId`, `queryByDriveAndFolder` |
|
||||||
|
| 统计数量 | `countBy{条件}` | `countByDrive`, `countByUser` |
|
||||||
|
| 插入 | `insert` | `insert(DriveMovie movie)` |
|
||||||
|
| 更新 | `update` / `update{字段}` | `update`, `updateScrapeInfo` |
|
||||||
|
| 逻辑删除 | `logicalDelete` | `logicalDelete(Long id, Long userId)` |
|
||||||
|
| 物理删除 | `delete` | `delete(Long id, Long userId)` |
|
||||||
|
| 移动 | `moveTo{目标}` | `moveToFolder` |
|
||||||
|
|
||||||
|
### 2.4 数据库命名
|
||||||
|
|
||||||
|
| 元素 | 命名规则 | 示例 |
|
||||||
|
|------|---------|------|
|
||||||
|
| 表名 | `wp_{模块}_{实体}` | `wp_drive_movie`, `wp_user`, `wp_tmdb_cache` |
|
||||||
|
| 字段名 | 小写 + 下划线分隔 | `drive_id`, `user_id`, `create_time`, `movie_title` |
|
||||||
|
| 主键 | `id`(BIGINT AUTO_INCREMENT) | `id` |
|
||||||
|
| 索引 | `idx_{字段名}` | `idx_drive_id`, `idx_user_id`, `idx_status` |
|
||||||
|
| 状态字段 | 字符串枚举值 | `'active'`, `'deleted'`, `'expired'` |
|
||||||
|
|
||||||
|
### 2.5 API 命名
|
||||||
|
|
||||||
|
| 模块 | 路径前缀 | 示例 |
|
||||||
|
|------|---------|------|
|
||||||
|
| 用户服务 | `/api/login`, `/api/register`, `/api/token/**`, `/api/users/**`, `/api/login-logs/**` | `POST /api/login` |
|
||||||
|
| 网盘服务 | `/api/drive/**`, `/api/icon/**` | `GET /api/drive/movies/duplicates` |
|
||||||
|
| TMDB服务 | `/api/movies/**`, `/api/tmdb/**`, `/api/tmdb-scraper/**` | `GET /api/movies/{id}` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、技术架构模式
|
||||||
|
|
||||||
|
### 3.1 分层架构
|
||||||
|
|
||||||
|
```
|
||||||
|
Controller → 接收请求、参数校验、调用 Service、返回 Result
|
||||||
|
↓
|
||||||
|
Service → 业务逻辑处理、事务管理
|
||||||
|
↓
|
||||||
|
Repository → 数据访问封装、Entity ↔ DO 转换、MyBatis-Plus LambdaQueryWrapper 查询
|
||||||
|
↓
|
||||||
|
Mapper → MyBatis-Plus BaseMapper 接口 + XML 复杂 SQL
|
||||||
|
↓
|
||||||
|
DO → 数据库表映射(@TableName 注解)
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键规则**:
|
||||||
|
- Controller 直接返回 `Result<T>` 对象,不要直接返回原始数据
|
||||||
|
- Repository 继承 `ServiceImpl<Mapper, DO>`,自动获得 MyBatis-Plus CRUD 能力
|
||||||
|
- Mapper 继承 `BaseMapper<DO>`,简单 CRUD 自动生成
|
||||||
|
- 复杂 SQL 写在 Mapper XML 中,方法签名定义在 Mapper 接口中
|
||||||
|
- Repository 负责 Entity ↔ DO 转换,对外暴露 Entity 或 Map 类型
|
||||||
|
|
||||||
|
### 3.2 Result 统一响应
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 成功
|
||||||
|
Result.success("操作成功", data);
|
||||||
|
|
||||||
|
// 各类错误
|
||||||
|
Result.badRequest("参数错误"); // 400
|
||||||
|
Result.unauthorized("登录已过期"); // 401
|
||||||
|
Result.forbidden("权限不足"); // 403
|
||||||
|
Result.notFound("资源不存在"); // 404
|
||||||
|
Result.tooManyRequests("请求过于频繁"); // 429
|
||||||
|
Result.error("服务器错误"); // 500
|
||||||
|
Result.error(code, "自定义错误"); // 自定义状态码
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 认证模式
|
||||||
|
|
||||||
|
- 使用 `@RequireAuth` 注解标记需要登录的接口
|
||||||
|
- 使用 `@RequireAdmin` 注解标记需要管理员权限的接口
|
||||||
|
- `AuthInterceptor` 拦截器统一处理认证逻辑
|
||||||
|
- Controller 通过 `request.getAttribute("currentUser")` 获取当前用户
|
||||||
|
- Token 使用 `TokenUtil` 生成和验证
|
||||||
|
|
||||||
|
### 3.4 MyBatis-Plus 查询模式
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 简单查询 → LambdaQueryWrapper
|
||||||
|
var wrapper = new LambdaQueryWrapper<DriveMovieDO>()
|
||||||
|
.eq(DriveMovieDO::getDriveId, driveId)
|
||||||
|
.eq(DriveMovieDO::getUserId, userId)
|
||||||
|
.eq(DriveMovieDO::getStatus, "active")
|
||||||
|
.orderByDesc(DriveMovieDO::getCreateTime);
|
||||||
|
|
||||||
|
// 分页查询
|
||||||
|
var page = new Page<DriveMovieDO>(pageNo, pageSize);
|
||||||
|
var result = page(page, wrapper);
|
||||||
|
|
||||||
|
// 更新 → LambdaUpdateWrapper
|
||||||
|
var wrapper = new LambdaUpdateWrapper<DriveMovieDO>()
|
||||||
|
.eq(DriveMovieDO::getId, id)
|
||||||
|
.set(DriveMovieDO::getStatus, "deleted");
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 Jasypt 配置加密模式
|
||||||
|
|
||||||
|
所有服务(必须一致):
|
||||||
|
```yaml
|
||||||
|
jasypt:
|
||||||
|
encryptor:
|
||||||
|
password: ${JASYPT_ENCRYPTOR_PASSWORD} # 必须通过环境变量传入
|
||||||
|
algorithm: PBEWITHHMACSHA512ANDAES_256 # 统一使用强加密算法
|
||||||
|
key-obtention-iterations: 1000
|
||||||
|
pool-size: 1
|
||||||
|
salt-generator-classname: org.jasypt.salt.RandomSaltGenerator
|
||||||
|
iv-generator-classname: org.jasypt.iv.RandomIvGenerator
|
||||||
|
string-output-type: base64
|
||||||
|
```
|
||||||
|
|
||||||
|
**加密密钥**:`wangpan-secret-key-2024`
|
||||||
|
|
||||||
|
**启动命令**(必须带 Jasypt 密钥参数):
|
||||||
|
```bash
|
||||||
|
java -DJASYPT_ENCRYPTOR_PASSWORD=wangpan-secret-key-2024 -jar xxx.jar
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.6 MinIO 存储模式
|
||||||
|
|
||||||
|
- `MinioUtil` 作为 Spring Bean,使用 `@PostConstruct` 初始化
|
||||||
|
- 支持静态方法调用和 Spring Bean 注入两种方式
|
||||||
|
- 预签名 URL 有效期:7 天
|
||||||
|
- 存储桶按服务区分:`wangpan`(网盘服务)、`tmdb`(TMDB服务)
|
||||||
|
|
||||||
|
### 3.7 定时任务模式
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Component
|
||||||
|
public class TmdbScheduler {
|
||||||
|
@Scheduled(fixedDelay = 6 * 60 * 60 * 1000, initialDelay = 5 * 60 * 1000)
|
||||||
|
public void executeScrape() { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、MyBatis XML 映射规则(重要!)
|
||||||
|
|
||||||
|
### 4.1 列别名规则
|
||||||
|
|
||||||
|
XML 中 `SELECT` 返回的列别名 **必须与 Java 代码中 `Map.get()` 使用的 key 完全一致**:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<!-- XML 中 -->
|
||||||
|
<select id="findDuplicateMovies" resultType="java.util.Map">
|
||||||
|
SELECT title, COUNT(*) AS cnt <!-- 注意:列别名是 cnt -->
|
||||||
|
FROM wp_drive_movie
|
||||||
|
...
|
||||||
|
</select>
|
||||||
|
```
|
||||||
|
|
||||||
|
```java
|
||||||
|
// Java 代码中必须使用相同的 key
|
||||||
|
g.get("cnt") // ✅ 正确 - 与 XML 别名 cnt 一致
|
||||||
|
g.get("count") // ❌ 错误 - 别名是 cnt 不是 count
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 数值类型转换
|
||||||
|
|
||||||
|
XML 中 `resultType="java.util.Map"` 返回的数值,Java 中获取时需要安全转换:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ✅ 推荐:使用 Number 父类转换
|
||||||
|
((Number) g.get("cnt")).intValue()
|
||||||
|
|
||||||
|
// ❌ 不推荐:直接强制转换可能失败
|
||||||
|
(int) g.get("cnt")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 分页查询
|
||||||
|
|
||||||
|
XML 中使用 `LIMIT #{pageSize} OFFSET #{pageNo}` 时,分页偏移量计算规则:
|
||||||
|
```java
|
||||||
|
// 前端传 pageNo 从 1 开始,XML 中 offset = (pageNo - 1) * pageSize
|
||||||
|
// 或者前端直接传 offset 值
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、已发现的问题与解决方案
|
||||||
|
|
||||||
|
### 问题 1:SQL 列别名与 Java Map key 不匹配导致 NPE
|
||||||
|
|
||||||
|
**日期**:2026-06-15
|
||||||
|
|
||||||
|
**问题描述**:调用 `GET /api/drive/movies/duplicates` 返回 500,报错:
|
||||||
|
```
|
||||||
|
java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()"
|
||||||
|
because the return value of "java.util.Map.get(Object)" is null
|
||||||
|
```
|
||||||
|
|
||||||
|
**根因**:`DriveMovieMapper.xml` 中 SQL 别名是 `cnt`,但 `DriveController.java` 中使用的 key 是 `count`。
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
1. 将 Controller 中的 `g.get("count")` 改为 `g.get("cnt")`
|
||||||
|
2. 使用安全转换 `((Number) g.get("cnt")).intValue()` 替代 `(int) g.get("cnt")`
|
||||||
|
|
||||||
|
**影响文件**:
|
||||||
|
- `wangpan-drive-service/.../controller/DriveController.java` 第 676 行
|
||||||
|
- `wangpan-common/.../mapper/DriveMovieMapper.xml` 第 10 行(SQL 别名 `cnt`)
|
||||||
|
|
||||||
|
**教训**:修改 XML 中的 SQL 列别名时,必须同步修改所有引用该结果集的 Java 代码中的 Map key。
|
||||||
|
|
||||||
|
### 问题 2:网关 Jasypt 加密算法不一致
|
||||||
|
|
||||||
|
**日期**:2026-06-15
|
||||||
|
|
||||||
|
**问题描述**:网关服务使用 `PBEWithMD5AndDES` 算法,其他服务使用 `PBEWITHHMACSHA512ANDAES_256`,不一致且 MD5+DES 算法已过时。
|
||||||
|
|
||||||
|
**解决方案**:统一所有服务使用 `PBEWITHHMACSHA512ANDAES_256`。
|
||||||
|
|
||||||
|
**影响文件**:`wangpan-gateway/src/main/resources/application.yml`
|
||||||
|
|
||||||
|
### 问题 3:硬编码默认 Jasypt 密钥
|
||||||
|
|
||||||
|
**日期**:2026-06-15
|
||||||
|
|
||||||
|
**问题描述**:所有服务配置了 `${JASYPT_ENCRYPTOR_PASSWORD:wangpan-secret-key-2024}` 默认值,存在安全风险。
|
||||||
|
|
||||||
|
**解决方案**:移除默认值,改为 `${JASYPT_ENCRYPTOR_PASSWORD}`,启动时强制通过命令行参数 `-DJASYPT_ENCRYPTOR_PASSWORD=xxx` 传入。
|
||||||
|
|
||||||
|
**影响文件**:所有服务的 `application.yml`
|
||||||
|
|
||||||
|
### 问题 4:日志级别过高
|
||||||
|
|
||||||
|
**日期**:2026-06-15
|
||||||
|
|
||||||
|
**问题描述**:所有服务日志级别为 `DEBUG`,生产环境会产生大量日志。
|
||||||
|
|
||||||
|
**解决方案**:改为 `INFO` 级别。
|
||||||
|
|
||||||
|
### 问题 5:MyBatis-Plus SQL 日志输出到控制台
|
||||||
|
|
||||||
|
**日期**:2026-06-15
|
||||||
|
|
||||||
|
**问题描述**:配置了 `log-impl: org.apache.ibatis.logging.stdout.StdOutImpl`,生产环境会打印所有 SQL。
|
||||||
|
|
||||||
|
**解决方案**:移除该配置。
|
||||||
|
|
||||||
|
### 问题 6:启动时 Jasypt 环境变量未传递
|
||||||
|
|
||||||
|
**日期**:2026-06-15
|
||||||
|
|
||||||
|
**问题描述**:使用 `$env:JASYPT_ENCRYPTOR_PASSWORD="xxx"` + `Start-Process` 启动服务时,子进程未继承环境变量,导致 `Failed to bind properties under 'spring.datasource.password'`。
|
||||||
|
|
||||||
|
**解决方案**:使用 `-DJASYPT_ENCRYPTOR_PASSWORD=xxx` 命令行参数替代环境变量。
|
||||||
|
|
||||||
|
### 问题 7:TMDB 刮削查询了错误的电影表
|
||||||
|
|
||||||
|
**日期**:2026-06-18
|
||||||
|
|
||||||
|
**问题描述**:TMDB 刮削器(`MovieMapper`/`MovieDO`/`MovieRepository`)映射的是 `wp_movie` 表,但项目中实际使用的电影表是 `wp_drive_movie`(由 drive-service 管理)。`wp_movie` 表无数据,导致刮削始终返回"没有需要刮削的电影"。
|
||||||
|
|
||||||
|
**根因**:项目中存在两张结构相同的电影表(`wp_movie` 和 `wp_drive_movie`),但实际只有 `wp_drive_movie` 有数据。
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
1. 将 `MovieDO` 的 `@TableName` 从 `wp_movie` 改为 `wp_drive_movie`
|
||||||
|
2. 将所有 `MovieMapper` 的 SQL(含 XML 和注解)中的 `wp_movie` 改为 `wp_drive_movie`
|
||||||
|
3. 将 `TmdbCacheMapper.xml` 中 JOIN 的 `wp_movie` 改为 `wp_drive_movie`
|
||||||
|
4. 移除 `MovieMapper.CREATE_TABLE_SQL`(`wp_drive_movie` 由 `DriveMovieMapper` 负责建表)
|
||||||
|
5. 从 tmdb-service 的 `DatabaseInitializer` 中移除 `wp_movie` 表创建
|
||||||
|
6. 修改 `findMoviesNeedingScrape` 查询条件,移除 `scrape_type != 'none'` 限制,允许未刮削的电影被选中
|
||||||
|
|
||||||
|
**影响文件**:
|
||||||
|
- `wangpan-common/.../dataobject/MovieDO.java` — `@TableName` 改为 `wp_drive_movie`
|
||||||
|
- `wangpan-common/.../mapper/MovieMapper.java` — 移除 `CREATE_TABLE_SQL`,SQL 改表名
|
||||||
|
- `wangpan-common/.../mapper/MovieMapper.xml` — SQL 表名改为 `wp_drive_movie`,放开 `scrape_type` 限制
|
||||||
|
- `wangpan-common/.../mapper/TmdbCacheMapper.xml` — JOIN 表名改为 `wp_drive_movie`
|
||||||
|
- `wangpan-common/.../mapper/TmdbCacheMapper.java` — 注释更新
|
||||||
|
- `wangpan-common/.../entity/Movie.java` — 注释更新
|
||||||
|
- `wangpan-tmdb-service/.../config/DatabaseInitializer.java` — 移除 `wp_movie` 建表
|
||||||
|
- `wangpan-tmdb-service/.../service/TmdbScraperService.java` — 注释更新
|
||||||
|
|
||||||
|
**教训**:项目中统一使用 `wp_drive_movie` 作为电影表,所有模块(drive-service、tmdb-service)都应操作同一张表。`wp_movie` 表已被废弃,不应再使用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、配置文件关键信息
|
||||||
|
|
||||||
|
### 6.1 数据库连接
|
||||||
|
|
||||||
|
- 地址:`118.178.238.159:33306`
|
||||||
|
- 数据库名:`wangpan`
|
||||||
|
- 连接参数:`useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true`
|
||||||
|
- 所有密码使用 Jasypt `ENC(...)` 加密
|
||||||
|
|
||||||
|
### 6.2 MinIO 连接
|
||||||
|
|
||||||
|
- 地址:`118.178.238.159:39000`
|
||||||
|
- 访问密钥和秘密密钥使用 Jasypt `ENC(...)` 加密
|
||||||
|
- 存储桶:`wangpan`(网盘服务)、`tmdb`(TMDB服务)
|
||||||
|
|
||||||
|
### 6.3 网关路由规则
|
||||||
|
|
||||||
|
| 路由路径 | 转发目标 |
|
||||||
|
|---------|---------|
|
||||||
|
| `/api/login`, `/api/register`, `/api/token/**`, `/api/login-logs/**`, `/api/users/**` | `http://localhost:8081` |
|
||||||
|
| `/api/drive/**`, `/api/icon/**` | `http://localhost:8082` |
|
||||||
|
| `/api/movies/**`, `/api/tmdb/**`, `/api/tmdb-scraper/**` | `http://localhost:8083` |
|
||||||
|
|
||||||
|
所有路由 `StripPrefix=0`(不裁剪路径前缀)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、TraeAI 操作规则
|
||||||
|
|
||||||
|
### 7.1 每次修改代码前必须执行
|
||||||
|
|
||||||
|
1. **读取本文件**(`PROJECT_REFERENCE.md`)了解项目规范
|
||||||
|
2. 查找相关代码,理解现有模式后再修改
|
||||||
|
3. 修改代码时遵循项目命名规范
|
||||||
|
|
||||||
|
### 7.2 修改规范
|
||||||
|
|
||||||
|
1. **禁止**创建不必要的新文件,优先编辑现有文件
|
||||||
|
2. **禁止**创建文档(*.md)除非用户明确要求
|
||||||
|
3. 修改 SQL 列别名时,**必须同步检查**所有 Java 代码中对 Map key 的引用
|
||||||
|
4. 修改配置文件时,**确保所有服务配置一致**(Jasypt 算法、日志级别等)
|
||||||
|
5. 使用 `Result` 类统一返回,不要返回原始类型
|
||||||
|
6. 数据库操作使用 MyBatis-Plus LambdaQueryWrapper/LambdaUpdateWrapper,不要拼接 SQL 字符串
|
||||||
|
7. 复杂 SQL 写在 Mapper XML 中,不要写在 Java 代码中
|
||||||
|
|
||||||
|
### 7.3 编译与启动
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 编译
|
||||||
|
mvn clean install -DskipTests
|
||||||
|
|
||||||
|
# 启动单个服务
|
||||||
|
java -DJASYPT_ENCRYPTOR_PASSWORD=wangpan-secret-key-2024 -jar xxx\target\xxx-1.0-SNAPSHOT.jar
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.4 遇到新问题时
|
||||||
|
|
||||||
|
在本文档 **第五章** 中追加新问题记录,包含:
|
||||||
|
- 日期
|
||||||
|
- 问题描述
|
||||||
|
- 根因
|
||||||
|
- 解决方案
|
||||||
|
- 影响文件
|
||||||
|
- 教训
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、MyBatis-Plus 配置要点
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
mybatis-plus:
|
||||||
|
mapper-locations: classpath*:com/wangpan/common/dal/mapper/**/*.xml
|
||||||
|
type-aliases-package: com.wangpan.common.dal.dataobject
|
||||||
|
configuration:
|
||||||
|
map-underscore-to-camel-case: true # 自动驼峰映射
|
||||||
|
global-config:
|
||||||
|
db-config:
|
||||||
|
id-type: auto # 主键自增
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**:`mapper-locations` 使用 `classpath*:` 前缀,因为 Mapper XML 在 `wangpan-common` 模块中,所有子服务都需要加载。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、api-doc.html 维护规范
|
||||||
|
|
||||||
|
### 9.1 文件位置
|
||||||
|
`wangpan-gateway/src/main/resources/META-INF/resources/api-doc.html`
|
||||||
|
|
||||||
|
### 9.2 历史不一致问题记录
|
||||||
|
|
||||||
|
**问题 8:api-doc.html 与代码多处不一致**
|
||||||
|
|
||||||
|
**日期**:2026-06-18
|
||||||
|
|
||||||
|
**发现的不一致**:
|
||||||
|
|
||||||
|
| 接口 | 文档描述 | 实际代码 | 严重程度 |
|
||||||
|
|------|---------|---------|---------|
|
||||||
|
| `GET /api/movies` 查询参数 `status` | `pending / scraped / failed` | `active / deleted`(status 是记录状态,scrapeType 是刮削状态) | **高** |
|
||||||
|
| `GET /api/movies` 缺少参数 | 无 `sortBy`/`sortOrder` | 支持 `sortBy` 和 `sortOrder` 排序参数 | 中 |
|
||||||
|
| `POST /api/movies` 请求体 | 只有 `{ "title": "..." }` | 支持 title, subtitle, resourceLibrary, maintainer, status, category, scrapeType, rating, posterUrl, tags | **高** |
|
||||||
|
| `PUT /api/movies/{id}` 请求体 | 空白 | 支持全部字段的部分更新 | **高** |
|
||||||
|
| `GET /api/movies/stats` 响应 | 空白 | 返回 totalCount, scrapedCount, pendingCount | 中 |
|
||||||
|
| `POST /api/tmdb-scraper/scrape` | 仅请求体方式 | 同时支持 URL 参数 `?movieId=1` 和请求体 | 中 |
|
||||||
|
| `POST /api/tmdb-scraper/batch-scrape` | `movieIds` 为必填 | `movieIds` 可选,不传则自动查找 | 中 |
|
||||||
|
| 多个 `/api/tmdb-scraper/*` 端点 | 响应文档空白 | stats, trigger, scraper-logs, cache delete 等缺少响应 | 中 |
|
||||||
|
| 路由表缺少 `/api/icon/**` | 未列出 | `/api/icon/**` → drive-service | 低 |
|
||||||
|
|
||||||
|
**解决方案**:逐一修复 api-doc.html 中所有不一致的字段、参数和响应示例。
|
||||||
|
|
||||||
|
**教训**:每次新增或修改 Controller 接口时,必须同步更新 api-doc.html。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **最后更新**:2026-06-18
|
||||||
@ -10,13 +10,14 @@ import java.time.LocalDateTime;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 电影数据对象(Data Object)
|
* 电影数据对象(Data Object)
|
||||||
* 对应数据库表:wp_movie
|
* 对应数据库表:wp_drive_movie(网盘电影表)
|
||||||
*
|
*
|
||||||
* 设计说明:
|
* 设计说明:
|
||||||
* - DO 字段名使用 Java 驼峰命名,MyBatis-Plus 自动映射为数据库 snake_case 列
|
* - DO 字段名使用 Java 驼峰命名,MyBatis-Plus 自动映射为数据库 snake_case 列
|
||||||
* - DO 仅负责数据映射,不包含业务逻辑
|
* - DO 仅负责数据映射,不包含业务逻辑
|
||||||
|
* - 注意:本 DO 仅映射 wp_drive_movie 表中 TMDB 刮削相关的字段,其他字段由 DriveMovieDO 映射
|
||||||
*/
|
*/
|
||||||
@TableName(value = "wp_movie", autoResultMap = true)
|
@TableName(value = "wp_drive_movie", autoResultMap = true)
|
||||||
public class MovieDO implements Serializable {
|
public class MovieDO implements Serializable {
|
||||||
|
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|||||||
@ -18,41 +18,6 @@ import java.util.Map;
|
|||||||
@Mapper
|
@Mapper
|
||||||
public interface MovieMapper extends BaseMapper<MovieDO> {
|
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 分组)
|
* 查找用户所有网盘下的重复电影(按 title 分组)
|
||||||
* 复杂SQL,在XML中实现
|
* 复杂SQL,在XML中实现
|
||||||
@ -74,24 +39,24 @@ public interface MovieMapper extends BaseMapper<MovieDO> {
|
|||||||
/**
|
/**
|
||||||
* 更新电影评分
|
* 更新电影评分
|
||||||
*/
|
*/
|
||||||
@Update("UPDATE wp_movie SET rating = #{rating}, update_time = NOW() WHERE id = #{id}")
|
@Update("UPDATE wp_drive_movie SET rating = #{rating}, update_time = NOW() WHERE id = #{id}")
|
||||||
int updateRating(@Param("id") Long id, @Param("rating") Integer rating);
|
int updateRating(@Param("id") Long id, @Param("rating") Integer rating);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新电影海报URL
|
* 更新电影海报URL
|
||||||
*/
|
*/
|
||||||
@Update("UPDATE wp_movie SET poster_url = #{posterUrl}, update_time = NOW() WHERE id = #{id}")
|
@Update("UPDATE wp_drive_movie SET poster_url = #{posterUrl}, update_time = NOW() WHERE id = #{id}")
|
||||||
int updatePosterUrl(@Param("id") Long id, @Param("posterUrl") String posterUrl);
|
int updatePosterUrl(@Param("id") Long id, @Param("posterUrl") String posterUrl);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新电影标签
|
* 更新电影标签
|
||||||
*/
|
*/
|
||||||
@Update("UPDATE wp_movie SET tags = #{tags}, update_time = NOW() WHERE id = #{id}")
|
@Update("UPDATE wp_drive_movie SET tags = #{tags}, update_time = NOW() WHERE id = #{id}")
|
||||||
int updateTags(@Param("id") Long id, @Param("tags") String tags);
|
int updateTags(@Param("id") Long id, @Param("tags") String tags);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新刮削类型标记
|
* 更新刮削类型标记
|
||||||
*/
|
*/
|
||||||
@Update("UPDATE wp_movie SET scrape_type = #{scrapeType}, update_time = NOW() WHERE id = #{id}")
|
@Update("UPDATE wp_drive_movie SET scrape_type = #{scrapeType}, update_time = NOW() WHERE id = #{id}")
|
||||||
int updateScrapeType(@Param("id") Long id, @Param("scrapeType") String scrapeType);
|
int updateScrapeType(@Param("id") Long id, @Param("scrapeType") String scrapeType);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,7 @@ public interface TmdbCacheMapper extends BaseMapper<TmdbCacheDO> {
|
|||||||
String CREATE_TABLE_SQL = """
|
String CREATE_TABLE_SQL = """
|
||||||
CREATE TABLE IF NOT EXISTS wp_tmdb_cache (
|
CREATE TABLE IF NOT EXISTS wp_tmdb_cache (
|
||||||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '记录ID',
|
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '记录ID',
|
||||||
movie_id BIGINT NOT NULL COMMENT '关联wp_movie表的电影ID',
|
movie_id BIGINT NOT NULL COMMENT '关联wp_drive_movie表的电影ID',
|
||||||
user_id BIGINT NOT NULL COMMENT '关联用户ID',
|
user_id BIGINT NOT NULL COMMENT '关联用户ID',
|
||||||
tmdb_id INT DEFAULT NULL COMMENT 'TMDB电影ID',
|
tmdb_id INT DEFAULT NULL COMMENT 'TMDB电影ID',
|
||||||
title VARCHAR(200) NOT NULL COMMENT '电影标题(来自TMDB)',
|
title VARCHAR(200) NOT NULL COMMENT '电影标题(来自TMDB)',
|
||||||
|
|||||||
@ -146,16 +146,17 @@ public class DriveMovieRepository extends ServiceImpl<DriveMovieMapper, DriveMov
|
|||||||
* 更新电影基本信息
|
* 更新电影基本信息
|
||||||
* 使用 MyBatis-Plus LambdaUpdateWrapper 构建更新条件
|
* 使用 MyBatis-Plus LambdaUpdateWrapper 构建更新条件
|
||||||
*/
|
*/
|
||||||
public boolean update(Long id, String title, String subtitle, String description,
|
public boolean update(Long id, String title, String originalName, String subtitle, String description,
|
||||||
Integer rating, String tags, String category) {
|
Integer rating, String tags, String category) {
|
||||||
var wrapper = new LambdaUpdateWrapper<DriveMovieDO>()
|
var wrapper = new LambdaUpdateWrapper<DriveMovieDO>()
|
||||||
.eq(DriveMovieDO::getId, id)
|
.eq(DriveMovieDO::getId, id)
|
||||||
.set(DriveMovieDO::getTitle, title)
|
.set(title != null, DriveMovieDO::getTitle, title)
|
||||||
.set(DriveMovieDO::getSubtitle, subtitle)
|
.set(originalName != null, DriveMovieDO::getOriginalName, originalName)
|
||||||
.set(DriveMovieDO::getDescription, description)
|
.set(subtitle != null, DriveMovieDO::getSubtitle, subtitle)
|
||||||
.set(DriveMovieDO::getRating, rating)
|
.set(description != null, DriveMovieDO::getDescription, description)
|
||||||
.set(DriveMovieDO::getTags, tags)
|
.set(rating != null, DriveMovieDO::getRating, rating)
|
||||||
.set(DriveMovieDO::getCategory, category)
|
.set(tags != null, DriveMovieDO::getTags, tags)
|
||||||
|
.set(category != null, DriveMovieDO::getCategory, category)
|
||||||
.set(DriveMovieDO::getUpdateTime, LocalDateTime.now());
|
.set(DriveMovieDO::getUpdateTime, LocalDateTime.now());
|
||||||
return update(wrapper);
|
return update(wrapper);
|
||||||
}
|
}
|
||||||
@ -287,7 +288,7 @@ public class DriveMovieRepository extends ServiceImpl<DriveMovieMapper, DriveMov
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DO → Map 转换(用于 API 返回)
|
* DO → Map 转换(用于 API 返回,包含所有字段)
|
||||||
*/
|
*/
|
||||||
private Map<String, Object> toMap(DriveMovieDO movieDO) {
|
private Map<String, Object> toMap(DriveMovieDO movieDO) {
|
||||||
var map = new LinkedHashMap<String, Object>();
|
var map = new LinkedHashMap<String, Object>();
|
||||||
@ -308,8 +309,10 @@ public class DriveMovieRepository extends ServiceImpl<DriveMovieMapper, DriveMov
|
|||||||
map.put("scrapeType", movieDO.getScrapeType());
|
map.put("scrapeType", movieDO.getScrapeType());
|
||||||
map.put("rating", movieDO.getRating());
|
map.put("rating", movieDO.getRating());
|
||||||
map.put("tags", movieDO.getTags());
|
map.put("tags", movieDO.getTags());
|
||||||
map.put("updateTime", movieDO.getUpdateTime() != null ? movieDO.getUpdateTime().toString() : null);
|
map.put("posterUrl", movieDO.getPosterUrl());
|
||||||
|
map.put("minioUrl", movieDO.getMinioUrl());
|
||||||
map.put("createTime", movieDO.getCreateTime() != null ? movieDO.getCreateTime().toString() : null);
|
map.put("createTime", movieDO.getCreateTime() != null ? movieDO.getCreateTime().toString() : null);
|
||||||
|
map.put("updateTime", movieDO.getUpdateTime() != null ? movieDO.getUpdateTime().toString() : null);
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -116,6 +116,27 @@ public class IconRepository extends ServiceImpl<IconMapper, IconDO> {
|
|||||||
return remove(wrapper);
|
return remove(wrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 查询所有在用 logoUrl ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询所有 Icon 的 logoUrl(用于清理无用图片)
|
||||||
|
*
|
||||||
|
* @return 所有在用 logoUrl 列表(去重)
|
||||||
|
*/
|
||||||
|
public List<String> findAllLogoUrls() {
|
||||||
|
var wrapper = new LambdaQueryWrapper<IconDO>()
|
||||||
|
.select(IconDO::getLogoUrl)
|
||||||
|
.isNotNull(IconDO::getLogoUrl)
|
||||||
|
.ne(IconDO::getLogoUrl, "");
|
||||||
|
|
||||||
|
var list = list(wrapper);
|
||||||
|
return list.stream()
|
||||||
|
.map(IconDO::getLogoUrl)
|
||||||
|
.filter(url -> url != null && !url.isEmpty())
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Entity ↔ DO 转换 ====================
|
// ==================== Entity ↔ DO 转换 ====================
|
||||||
|
|
||||||
private IconDO toIconDO(Icon icon) {
|
private IconDO toIconDO(Icon icon) {
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import java.util.Objects;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 电影实体类
|
* 电影实体类
|
||||||
* 对应数据库表:wp_movie(用户电影库表)
|
* 对应数据库表:wp_drive_movie(网盘电影表)
|
||||||
*
|
*
|
||||||
* 功能:
|
* 功能:
|
||||||
* 1. 封装电影的基本信息(标题、评分、海报等)
|
* 1. 封装电影的基本信息(标题、评分、海报等)
|
||||||
|
|||||||
@ -186,6 +186,19 @@ public class MinioUtil {
|
|||||||
return instance != null ? instance.bucketName : null;
|
return instance != null ? instance.bucketName : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除 MinIO 中的文件(静态方法)
|
||||||
|
*
|
||||||
|
* @param objectName 对象名称(文件路径)
|
||||||
|
* @return 删除成功返回 true,失败返回 false
|
||||||
|
*/
|
||||||
|
public static boolean deleteFile(String objectName) {
|
||||||
|
if (instance == null) {
|
||||||
|
throw new IllegalStateException("MinioUtil未初始化,请确保Spring容器已启动");
|
||||||
|
}
|
||||||
|
return instance.doDeleteFile(objectName);
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 内部实现方法 ====================
|
// ==================== 内部实现方法 ====================
|
||||||
|
|
||||||
private String doUpload(InputStream inputStream, String objectName, String contentType) {
|
private String doUpload(InputStream inputStream, String objectName, String contentType) {
|
||||||
@ -238,4 +251,25 @@ public class MinioUtil {
|
|||||||
return endpoint + "/" + bucketName + "/" + objectName;
|
return endpoint + "/" + bucketName + "/" + objectName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除 MinIO 中的文件(内部实现)
|
||||||
|
*/
|
||||||
|
private boolean doDeleteFile(String objectName) {
|
||||||
|
try {
|
||||||
|
getClient().removeObject(
|
||||||
|
RemoveObjectArgs.builder()
|
||||||
|
.bucket(bucketName)
|
||||||
|
.object(objectName)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
LOGGER.info("[MinioUtil] 文件删除成功: " + objectName);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.log(Level.WARNING, "[MinioUtil] 文件删除失败: " + objectName, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
<!-- 查找用户所有网盘下的重复电影(按 title 分组,出现次数 > 1) -->
|
<!-- 查找用户所有网盘下的重复电影(按 title 分组,出现次数 > 1) -->
|
||||||
<select id="findDuplicateMovies" resultType="java.util.Map">
|
<select id="findDuplicateMovies" resultType="java.util.Map">
|
||||||
SELECT title, COUNT(*) AS cnt
|
SELECT title, COUNT(*) AS cnt
|
||||||
FROM wp_movie
|
FROM wp_drive_movie
|
||||||
WHERE user_id = #{userId} AND status = 'active'
|
WHERE user_id = #{userId} AND status = 'active'
|
||||||
GROUP BY title
|
GROUP BY title
|
||||||
HAVING COUNT(*) > 1
|
HAVING COUNT(*) > 1
|
||||||
@ -24,14 +24,14 @@
|
|||||||
AND u.status = 1
|
AND u.status = 1
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!-- 查找指定用户需要刮削的电影 -->
|
<!-- 查找指定用户需要刮削的电影(包含 scrape_type='none' 的未刮削电影) -->
|
||||||
<select id="findMoviesNeedingScrape" resultType="com.wangpan.common.dal.dataobject.MovieDO">
|
<select id="findMoviesNeedingScrape" resultType="com.wangpan.common.dal.dataobject.MovieDO">
|
||||||
SELECT m.* FROM wp_movie m
|
SELECT m.* FROM wp_drive_movie m
|
||||||
LEFT JOIN wp_tmdb_cache c ON m.id = c.movie_id AND c.status = 'active'
|
LEFT JOIN wp_tmdb_cache c ON m.id = c.movie_id AND c.status = 'active'
|
||||||
WHERE m.user_id = #{userId}
|
WHERE m.user_id = #{userId}
|
||||||
AND m.title IS NOT NULL AND m.title != ''
|
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))
|
AND (c.id IS NULL OR c.cache_time < DATE_SUB(NOW(), INTERVAL #{expireDays} DAY))
|
||||||
|
AND m.status = 'active'
|
||||||
ORDER BY m.update_time DESC
|
ORDER BY m.update_time DESC
|
||||||
LIMIT 100
|
LIMIT 100
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
<select id="queryByUserId" resultType="java.util.Map">
|
<select id="queryByUserId" resultType="java.util.Map">
|
||||||
SELECT c.*, m.title AS movie_title
|
SELECT c.*, m.title AS movie_title
|
||||||
FROM wp_tmdb_cache c
|
FROM wp_tmdb_cache c
|
||||||
LEFT JOIN wp_movie m ON c.movie_id = m.id
|
LEFT JOIN wp_drive_movie m ON c.movie_id = m.id
|
||||||
WHERE c.user_id = #{userId} AND c.status = 'active'
|
WHERE c.user_id = #{userId} AND c.status = 'active'
|
||||||
ORDER BY c.cache_time DESC
|
ORDER BY c.cache_time DESC
|
||||||
LIMIT #{pageSize} OFFSET #{pageNo}
|
LIMIT #{pageSize} OFFSET #{pageNo}
|
||||||
|
|||||||
@ -347,8 +347,16 @@ public class DriveController {
|
|||||||
// ==================== 电影操作 ====================
|
// ==================== 电影操作 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建电影(上传文件)
|
* 创建电影(只传文件名)
|
||||||
* POST /api/drive/{driveId}/movies
|
* POST /api/drive/{driveId}/movies
|
||||||
|
*
|
||||||
|
* 请求体:
|
||||||
|
* {
|
||||||
|
* "title": "文件名.mp4",
|
||||||
|
* "folderId": 0
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* 说明:只记录文件名,不上传文件数据,后续通过 TMDB 刮削补充信息
|
||||||
*/
|
*/
|
||||||
@PostMapping("/{driveId}/movies")
|
@PostMapping("/{driveId}/movies")
|
||||||
public Result<?> createMovie(@PathVariable("driveId") Long driveId,
|
public Result<?> createMovie(@PathVariable("driveId") Long driveId,
|
||||||
@ -363,7 +371,7 @@ public class DriveController {
|
|||||||
|
|
||||||
String title = (String) data.get("title");
|
String title = (String) data.get("title");
|
||||||
if (title == null || title.isBlank()) {
|
if (title == null || title.isBlank()) {
|
||||||
return Result.error("创建电影失败:电影标题不能为空");
|
return Result.error("创建电影失败:文件名不能为空");
|
||||||
}
|
}
|
||||||
|
|
||||||
Long folderId = 0L;
|
Long folderId = 0L;
|
||||||
@ -372,54 +380,11 @@ public class DriveController {
|
|||||||
folderId = ((Number) folderIdObj).longValue();
|
folderId = ((Number) folderIdObj).longValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
String subtitle = (String) data.get("subtitle");
|
|
||||||
String description = (String) data.get("description");
|
|
||||||
String fileDataBase64 = (String) data.get("fileData");
|
|
||||||
|
|
||||||
if (fileDataBase64 == null || fileDataBase64.isBlank()) {
|
|
||||||
return Result.error("创建电影失败:电影文件数据不能为空,请上传有效的视频文件");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解码 Base64 文件数据
|
|
||||||
byte[] fileBytes;
|
|
||||||
try {
|
try {
|
||||||
fileBytes = Base64.getDecoder().decode(fileDataBase64);
|
// 创建电影记录(只记录文件名,不上传文件数据)
|
||||||
} catch (Exception e) {
|
DriveMovie movie = new DriveMovie(driveId, folderId, user.getId(), title.trim(), title.trim());
|
||||||
return Result.error("创建电影失败:文件数据格式错误,请确保 fileData 是有效的 Base64 编码");
|
movie.setMimeType(detectMimeType(title));
|
||||||
}
|
|
||||||
|
|
||||||
// 构建 MinIO 对象名称
|
|
||||||
String objectName = "drive/" + driveId + "/" + folderId + "/" + System.currentTimeMillis() + "_" + title;
|
|
||||||
|
|
||||||
try {
|
|
||||||
String mimeType = detectMimeType(title);
|
|
||||||
try (var inputStream = new ByteArrayInputStream(fileBytes)) {
|
|
||||||
String uploadedName = MinioUtil.upload(inputStream, objectName, mimeType);
|
|
||||||
|
|
||||||
if (uploadedName == null) {
|
|
||||||
return Result.error("创建电影失败:文件上传到存储服务失败,请检查存储配置");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取文件访问 URL
|
|
||||||
String fileUrl = MinioUtil.getFileUrl(objectName);
|
|
||||||
|
|
||||||
// 创建电影记录
|
|
||||||
DriveMovie movie = new DriveMovie(driveId, folderId, user.getId(), title.trim(), title);
|
|
||||||
movie.setSubtitle(subtitle);
|
|
||||||
movie.setMinioObjectName(objectName);
|
|
||||||
movie.setMinioUrl(fileUrl);
|
|
||||||
movie.setMimeType(mimeType);
|
|
||||||
movie.setFileSize((long) fileBytes.length);
|
|
||||||
movie.setExtension(getExtension(title));
|
movie.setExtension(getExtension(title));
|
||||||
movie.setDescription(description);
|
|
||||||
|
|
||||||
// 处理可选字段
|
|
||||||
Object ratingObj = data.get("rating");
|
|
||||||
if (ratingObj != null) {
|
|
||||||
movie.setRating(((Number) ratingObj).intValue());
|
|
||||||
}
|
|
||||||
movie.setTags((String) data.get("tags"));
|
|
||||||
|
|
||||||
Long id = movieRepository.insert(movie);
|
Long id = movieRepository.insert(movie);
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
@ -493,6 +458,19 @@ public class DriveController {
|
|||||||
/**
|
/**
|
||||||
* 更新电影信息
|
* 更新电影信息
|
||||||
* PUT /api/drive/{driveId}/movies/{movieId}
|
* PUT /api/drive/{driveId}/movies/{movieId}
|
||||||
|
*
|
||||||
|
* 请求体(所有字段可选):
|
||||||
|
* {
|
||||||
|
* "title": "新标题",
|
||||||
|
* "originalName": "新文件名.mp4",
|
||||||
|
* "subtitle": "副标题",
|
||||||
|
* "description": "描述",
|
||||||
|
* "rating": 85,
|
||||||
|
* "tags": "标签1,标签2",
|
||||||
|
* "category": "分类"
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* 说明:用于 TMDB 刮削有误时,用户自定义修改文件名等信息
|
||||||
*/
|
*/
|
||||||
@PutMapping("/{driveId}/movies/{movieId}")
|
@PutMapping("/{driveId}/movies/{movieId}")
|
||||||
public Result<?> updateMovie(@PathVariable("driveId") Long driveId,
|
public Result<?> updateMovie(@PathVariable("driveId") Long driveId,
|
||||||
@ -507,6 +485,7 @@ public class DriveController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String title = (String) data.get("title");
|
String title = (String) data.get("title");
|
||||||
|
String originalName = (String) data.get("originalName");
|
||||||
String subtitle = (String) data.get("subtitle");
|
String subtitle = (String) data.get("subtitle");
|
||||||
String description = (String) data.get("description");
|
String description = (String) data.get("description");
|
||||||
Object ratingObj = data.get("rating");
|
Object ratingObj = data.get("rating");
|
||||||
@ -517,7 +496,7 @@ public class DriveController {
|
|||||||
String tags = (String) data.get("tags");
|
String tags = (String) data.get("tags");
|
||||||
String category = (String) data.get("category");
|
String category = (String) data.get("category");
|
||||||
|
|
||||||
boolean success = movieRepository.update(movieId, title, subtitle, description, rating, tags, category);
|
boolean success = movieRepository.update(movieId, title, originalName, subtitle, description, rating, tags, category);
|
||||||
if (success) {
|
if (success) {
|
||||||
return Result.success("更新成功");
|
return Result.success("更新成功");
|
||||||
} else {
|
} else {
|
||||||
@ -694,7 +673,7 @@ public class DriveController {
|
|||||||
result.put("totalGroups", duplicates.size());
|
result.put("totalGroups", duplicates.size());
|
||||||
|
|
||||||
int totalDuplicates = duplicates.stream()
|
int totalDuplicates = duplicates.stream()
|
||||||
.mapToInt(g -> (int) g.get("count"))
|
.mapToInt(g -> ((Number) g.get("cnt")).intValue())
|
||||||
.sum();
|
.sum();
|
||||||
result.put("totalDuplicates", totalDuplicates);
|
result.put("totalDuplicates", totalDuplicates);
|
||||||
|
|
||||||
|
|||||||
@ -7,10 +7,12 @@ import com.wangpan.common.entity.Icon;
|
|||||||
import com.wangpan.common.entity.User;
|
import com.wangpan.common.entity.User;
|
||||||
import com.wangpan.common.interceptor.AuthInterceptor;
|
import com.wangpan.common.interceptor.AuthInterceptor;
|
||||||
import com.wangpan.common.util.IconLogoFetcher;
|
import com.wangpan.common.util.IconLogoFetcher;
|
||||||
|
import com.wangpan.common.util.MinioUtil;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -358,4 +360,172 @@ public class IconController {
|
|||||||
return Result.error("删除失败:" + e.getMessage());
|
return Result.error("删除失败:" + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 上传自定义图标 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传自定义图标图片
|
||||||
|
* POST /api/icon/{id}/logo
|
||||||
|
*
|
||||||
|
* 请求:multipart/form-data,字段名 "file"
|
||||||
|
* 支持格式:png、jpg、jpeg、gif、webp、ico
|
||||||
|
* 最大大小:1MB
|
||||||
|
*
|
||||||
|
* 流程:
|
||||||
|
* 1. 校验文件合法性(格式、大小)
|
||||||
|
* 2. 上传新图片到 MinIO
|
||||||
|
* 3. 更新数据库中的 logoUrl
|
||||||
|
* 4. 删除旧的 MinIO 图片(防止硬盘过满)
|
||||||
|
*/
|
||||||
|
@PostMapping("/{id}/logo")
|
||||||
|
public Result<?> uploadLogo(@PathVariable Long id, @RequestParam("file") MultipartFile file, HttpServletRequest request) {
|
||||||
|
User user = getCurrentUser(request);
|
||||||
|
log.info("用户 [{}] 上传自定义图标,Icon ID={}", user.getUsername(), id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 校验文件
|
||||||
|
if (file.isEmpty()) {
|
||||||
|
return Result.error("文件不能为空");
|
||||||
|
}
|
||||||
|
if (file.getSize() > 1024 * 1024) {
|
||||||
|
return Result.error("文件大小不能超过 1MB");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验文件格式
|
||||||
|
String originalFilename = file.getOriginalFilename();
|
||||||
|
String ext = getFileExtension(originalFilename);
|
||||||
|
if (!isAllowedImageType(ext)) {
|
||||||
|
return Result.error("仅支持 png、jpg、jpeg、gif、webp、ico 格式的图片");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验 Icon 归属
|
||||||
|
Icon icon = iconRepository.findByIdAndUserId(id, user.getId());
|
||||||
|
if (icon == null) {
|
||||||
|
return Result.error("Icon 不存在或无权访问");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录旧的 logoUrl(用于后续删除)
|
||||||
|
String oldLogoUrl = icon.getLogoUrl();
|
||||||
|
|
||||||
|
// 上传新图片到 MinIO
|
||||||
|
String contentType = file.getContentType();
|
||||||
|
if (contentType == null || contentType.isEmpty()) {
|
||||||
|
contentType = guessContentType(ext);
|
||||||
|
}
|
||||||
|
String objectName = "icons/" + user.getId() + "/" + System.currentTimeMillis() + "." + ext;
|
||||||
|
String uploadedObjectName = MinioUtil.uploadFile(objectName, file.getBytes(), contentType);
|
||||||
|
if (uploadedObjectName == null) {
|
||||||
|
return Result.error("文件上传失败,请重试");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取新图片的访问 URL
|
||||||
|
String newLogoUrl = MinioUtil.getFileUrl(uploadedObjectName);
|
||||||
|
if (newLogoUrl == null) {
|
||||||
|
return Result.error("获取文件 URL 失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新数据库中的 logoUrl
|
||||||
|
iconRepository.updateLogoUrl(id, newLogoUrl);
|
||||||
|
log.info("图标更新成功,新 URL: {}", newLogoUrl);
|
||||||
|
|
||||||
|
// 异步删除旧的 MinIO 图片(防止硬盘过满)
|
||||||
|
if (oldLogoUrl != null && !oldLogoUrl.isEmpty()) {
|
||||||
|
String oldObjectName = extractObjectName(oldLogoUrl);
|
||||||
|
if (oldObjectName != null) {
|
||||||
|
boolean deleted = MinioUtil.deleteFile(oldObjectName);
|
||||||
|
if (deleted) {
|
||||||
|
log.info("旧图标删除成功: {}", oldObjectName);
|
||||||
|
} else {
|
||||||
|
log.warn("旧图标删除失败: {}", oldObjectName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回更新后的 Icon 信息
|
||||||
|
Icon updatedIcon = iconRepository.findByIdAndUserId(id, user.getId());
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("id", updatedIcon.getId());
|
||||||
|
result.put("name", updatedIcon.getName());
|
||||||
|
result.put("logoUrl", updatedIcon.getLogoUrl());
|
||||||
|
result.put("updateTime", updatedIcon.getUpdateTime() != null ? updatedIcon.getUpdateTime().toString() : null);
|
||||||
|
|
||||||
|
return Result.success("图标上传成功", result);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("用户 [{}] 上传自定义图标失败,ID={}", user.getUsername(), id, e);
|
||||||
|
return Result.error("上传失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 私有辅助方法 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件扩展名(小写)
|
||||||
|
*/
|
||||||
|
private String getFileExtension(String filename) {
|
||||||
|
if (filename == null || !filename.contains(".")) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验是否为允许的图片格式
|
||||||
|
*/
|
||||||
|
private boolean isAllowedImageType(String ext) {
|
||||||
|
return ext.equals("png") || ext.equals("jpg") || ext.equals("jpeg")
|
||||||
|
|| ext.equals("gif") || ext.equals("webp") || ext.equals("ico");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据扩展名猜测 Content-Type
|
||||||
|
*/
|
||||||
|
private 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 "ico" -> "image/x-icon";
|
||||||
|
default -> "application/octet-stream";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 MinIO 访问 URL 中提取 objectName
|
||||||
|
*
|
||||||
|
* 例如:http://minio:9000/bucket/icons/1/123456.png -> icons/1/123456.png
|
||||||
|
*/
|
||||||
|
private String extractObjectName(String fileUrl) {
|
||||||
|
if (fileUrl == null || fileUrl.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String bucketName = MinioUtil.getBucketName();
|
||||||
|
String endpoint = MinioUtil.getEndpoint();
|
||||||
|
if (bucketName == null || endpoint == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// 构建前缀:endpoint/bucketName/
|
||||||
|
String prefix = endpoint + "/" + bucketName + "/";
|
||||||
|
if (fileUrl.startsWith(prefix)) {
|
||||||
|
return fileUrl.substring(prefix.length());
|
||||||
|
}
|
||||||
|
// 尝试不带 endpoint 的情况(如预签名 URL 可能包含查询参数)
|
||||||
|
int bucketIndex = fileUrl.indexOf("/" + bucketName + "/");
|
||||||
|
if (bucketIndex >= 0) {
|
||||||
|
String afterBucket = fileUrl.substring(bucketIndex + bucketName.length() + 2);
|
||||||
|
// 去掉查询参数
|
||||||
|
int queryIndex = afterBucket.indexOf('?');
|
||||||
|
if (queryIndex >= 0) {
|
||||||
|
return afterBucket.substring(0, queryIndex);
|
||||||
|
}
|
||||||
|
return afterBucket;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("提取 objectName 失败: {}", fileUrl, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,291 @@
|
|||||||
|
package com.wangpan.listener;
|
||||||
|
|
||||||
|
import com.wangpan.common.dal.repository.IconRepository;
|
||||||
|
import com.wangpan.common.util.MinioUtil;
|
||||||
|
import io.minio.ListObjectsArgs;
|
||||||
|
import io.minio.Result;
|
||||||
|
import io.minio.messages.Item;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.Timer;
|
||||||
|
import java.util.TimerTask;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon 图片定时清理器
|
||||||
|
*
|
||||||
|
* 功能:
|
||||||
|
* 1. 定时扫描 MinIO 中 icons/ 目录下的所有图片
|
||||||
|
* 2. 对比数据库中在用的 logoUrl,找出无用的图片
|
||||||
|
* 3. 删除无用图片,释放硬盘空间
|
||||||
|
*
|
||||||
|
* 调度策略:
|
||||||
|
* - 首次延迟:启动后 10 分钟
|
||||||
|
* - 执行间隔:每 24 小时执行一次
|
||||||
|
* - 防止并发:同一时间只允许一个清理任务运行
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class IconCleanupScheduler {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(IconCleanupScheduler.class);
|
||||||
|
|
||||||
|
private final IconRepository iconRepository;
|
||||||
|
private Timer timer;
|
||||||
|
private final AtomicBoolean isRunning = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
/** 首次执行延迟(毫秒):启动后 10 分钟 */
|
||||||
|
private static final long INITIAL_DELAY_MS = 10 * 60 * 1000L;
|
||||||
|
|
||||||
|
/** 执行间隔(毫秒):每 24 小时执行一次 */
|
||||||
|
private static final long PERIOD_MS = 24 * 60 * 60 * 1000L;
|
||||||
|
|
||||||
|
/** MinIO 中 icon 图片的前缀路径 */
|
||||||
|
private static final String ICONS_PREFIX = "icons/";
|
||||||
|
|
||||||
|
public IconCleanupScheduler(IconRepository iconRepository) {
|
||||||
|
this.iconRepository = iconRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用启动后自动启动定时任务
|
||||||
|
*/
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动定时清理任务
|
||||||
|
*/
|
||||||
|
public void start() {
|
||||||
|
if (timer != null) {
|
||||||
|
log.warn("[IconCleanupScheduler] 清理器已在运行中,跳过重复启动");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
timer = new Timer("Icon-Cleanup-Scheduler", true); // daemon 线程
|
||||||
|
timer.scheduleAtFixedRate(new CleanupTask(), INITIAL_DELAY_MS, PERIOD_MS);
|
||||||
|
|
||||||
|
log.info("[IconCleanupScheduler] 定时清理任务已启动 | 首次延迟={}分钟 | 间隔={}小时",
|
||||||
|
INITIAL_DELAY_MS / 60000, PERIOD_MS / 3600000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止定时清理任务
|
||||||
|
*/
|
||||||
|
public void stop() {
|
||||||
|
if (timer != null) {
|
||||||
|
timer.cancel();
|
||||||
|
timer = null;
|
||||||
|
log.info("[IconCleanupScheduler] 定时清理任务已停止");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动触发一次清理(供管理接口调用)
|
||||||
|
*
|
||||||
|
* @return 清理结果统计
|
||||||
|
*/
|
||||||
|
public String manualCleanup() {
|
||||||
|
return executeCleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定时任务内部类
|
||||||
|
*/
|
||||||
|
private class CleanupTask extends TimerTask {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
executeCleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行清理逻辑
|
||||||
|
*
|
||||||
|
* 流程:
|
||||||
|
* 1. 查询数据库中所有在用的 logoUrl
|
||||||
|
* 2. 列出 MinIO 中 icons/ 目录下的所有文件
|
||||||
|
* 3. 对比找出不在数据库中的文件(无用文件)
|
||||||
|
* 4. 删除无用文件
|
||||||
|
*
|
||||||
|
* @return 清理结果统计字符串
|
||||||
|
*/
|
||||||
|
private String executeCleanup() {
|
||||||
|
// 防止并发执行
|
||||||
|
if (!isRunning.compareAndSet(false, true)) {
|
||||||
|
log.warn("[IconCleanupScheduler] 上次清理仍在进行中,跳过本次");
|
||||||
|
return "上次清理仍在进行中,已跳过";
|
||||||
|
}
|
||||||
|
|
||||||
|
int totalFiles = 0;
|
||||||
|
int deletedFiles = 0;
|
||||||
|
int failedFiles = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.info("[IconCleanupScheduler] 开始执行 Icon 图片清理任务");
|
||||||
|
|
||||||
|
// 第 1 步:查询数据库中所有在用的 logoUrl
|
||||||
|
Set<String> inUseObjectNames = getInUseObjectNames();
|
||||||
|
log.info("[IconCleanupScheduler] 数据库中共有 {} 个在用的 Icon 图片", inUseObjectNames.size());
|
||||||
|
|
||||||
|
// 第 2 步:列出 MinIO 中 icons/ 目录下的所有文件
|
||||||
|
Set<String> minioObjectNames = listMinioIcons();
|
||||||
|
totalFiles = minioObjectNames.size();
|
||||||
|
log.info("[IconCleanupScheduler] MinIO 中共有 {} 个 Icon 图片文件", totalFiles);
|
||||||
|
|
||||||
|
// 第 3 步:找出无用文件(在 MinIO 中但不在数据库中)
|
||||||
|
Set<String> orphanedFiles = new HashSet<>(minioObjectNames);
|
||||||
|
orphanedFiles.removeAll(inUseObjectNames);
|
||||||
|
log.info("[IconCleanupScheduler] 发现 {} 个无用文件需要删除", orphanedFiles.size());
|
||||||
|
|
||||||
|
// 第 4 步:删除无用文件
|
||||||
|
for (String objectName : orphanedFiles) {
|
||||||
|
try {
|
||||||
|
boolean deleted = MinioUtil.deleteFile(objectName);
|
||||||
|
if (deleted) {
|
||||||
|
deletedFiles++;
|
||||||
|
log.debug("[IconCleanupScheduler] 删除无用文件成功: {}", objectName);
|
||||||
|
} else {
|
||||||
|
failedFiles++;
|
||||||
|
log.warn("[IconCleanupScheduler] 删除无用文件失败: {}", objectName);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
failedFiles++;
|
||||||
|
log.warn("[IconCleanupScheduler] 删除无用文件异常: {}", objectName, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String result = String.format("清理完成 | 总文件数=%d | 删除成功=%d | 删除失败=%d",
|
||||||
|
totalFiles, deletedFiles, failedFiles);
|
||||||
|
log.info("[IconCleanupScheduler] {}", result);
|
||||||
|
return result;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[IconCleanupScheduler] 清理任务执行异常", e);
|
||||||
|
return "清理任务执行异常: " + e.getMessage();
|
||||||
|
} finally {
|
||||||
|
isRunning.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取数据库中所有在用的 Icon 图片 objectName 集合
|
||||||
|
*/
|
||||||
|
private Set<String> getInUseObjectNames() {
|
||||||
|
Set<String> objectNames = new HashSet<>();
|
||||||
|
try {
|
||||||
|
var logoUrls = iconRepository.findAllLogoUrls();
|
||||||
|
for (String logoUrl : logoUrls) {
|
||||||
|
String objectName = extractObjectName(logoUrl);
|
||||||
|
if (objectName != null && !objectName.isEmpty()) {
|
||||||
|
objectNames.add(objectName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[IconCleanupScheduler] 查询数据库中的 logoUrl 失败", e);
|
||||||
|
}
|
||||||
|
return objectNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列出 MinIO 中 icons/ 目录下的所有文件 objectName
|
||||||
|
*/
|
||||||
|
private Set<String> listMinioIcons() {
|
||||||
|
Set<String> objectNames = new HashSet<>();
|
||||||
|
try {
|
||||||
|
String bucketName = MinioUtil.getBucketName();
|
||||||
|
if (bucketName == null) {
|
||||||
|
log.warn("[IconCleanupScheduler] 无法获取 MinIO bucket 名称");
|
||||||
|
return objectNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 MinIO SDK 列出对象
|
||||||
|
var client = getMinioClient();
|
||||||
|
if (client == null) {
|
||||||
|
log.warn("[IconCleanupScheduler] 无法获取 MinIO 客户端");
|
||||||
|
return objectNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
var iterable = client.listObjects(
|
||||||
|
ListObjectsArgs.builder()
|
||||||
|
.bucket(bucketName)
|
||||||
|
.prefix(ICONS_PREFIX)
|
||||||
|
.recursive(true)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
for (Result<Item> result : iterable) {
|
||||||
|
Item item = result.get();
|
||||||
|
objectNames.add(item.objectName());
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[IconCleanupScheduler] 列出 MinIO icons 目录失败", e);
|
||||||
|
}
|
||||||
|
return objectNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 MinIO 客户端实例(通过反射访问 MinioUtil 的私有方法)
|
||||||
|
*
|
||||||
|
* 注意:这是一个临时方案,后续可以考虑在 MinioUtil 中暴露 getClient() 方法
|
||||||
|
*/
|
||||||
|
private io.minio.MinioClient getMinioClient() {
|
||||||
|
try {
|
||||||
|
// 通过反射获取 MinioUtil 的 client 字段
|
||||||
|
var field = MinioUtil.class.getDeclaredField("client");
|
||||||
|
field.setAccessible(true);
|
||||||
|
return (io.minio.MinioClient) field.get(null);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[IconCleanupScheduler] 反射获取 MinioClient 失败", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 MinIO 访问 URL 中提取 objectName
|
||||||
|
*
|
||||||
|
* 例如:http://minio:9000/bucket/icons/1/123456.png -> icons/1/123456.png
|
||||||
|
*/
|
||||||
|
private String extractObjectName(String fileUrl) {
|
||||||
|
if (fileUrl == null || fileUrl.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String bucketName = MinioUtil.getBucketName();
|
||||||
|
String endpoint = MinioUtil.getEndpoint();
|
||||||
|
if (bucketName == null || endpoint == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建前缀:endpoint/bucketName/
|
||||||
|
String prefix = endpoint + "/" + bucketName + "/";
|
||||||
|
if (fileUrl.startsWith(prefix)) {
|
||||||
|
return fileUrl.substring(prefix.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试不带 endpoint 的情况(如预签名 URL 可能包含查询参数)
|
||||||
|
int bucketIndex = fileUrl.indexOf("/" + bucketName + "/");
|
||||||
|
if (bucketIndex >= 0) {
|
||||||
|
String afterBucket = fileUrl.substring(bucketIndex + bucketName.length() + 2);
|
||||||
|
// 去掉查询参数
|
||||||
|
int queryIndex = afterBucket.indexOf('?');
|
||||||
|
if (queryIndex >= 0) {
|
||||||
|
return afterBucket.substring(0, queryIndex);
|
||||||
|
}
|
||||||
|
return afterBucket;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[IconCleanupScheduler] 提取 objectName 失败: {}", fileUrl, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,6 +5,10 @@ server:
|
|||||||
spring:
|
spring:
|
||||||
application:
|
application:
|
||||||
name: wangpan-drive-service
|
name: wangpan-drive-service
|
||||||
|
servlet:
|
||||||
|
multipart:
|
||||||
|
max-file-size: 10MB
|
||||||
|
max-request-size: 10MB
|
||||||
datasource:
|
datasource:
|
||||||
url: ENC(nJXPBEsg+NwtZt+qnD9A0AwRIdPFwex6kYEfGDetJ+N42ha/dH7V3Lq/hSCYgZa08cocXV5N+AaiisG/gh+i8CcNYjXzFV8CMSRfLwx03cxxNwnDzak+2ZEY5nb8UTbC/a6b3pWuAwg9lnHF9ziroFIrOn8+s/SYN7e+oMh4tZFmfCT4h5ATsEEuB6RkYGTzf0oImBVmet1jRR4CRdHPSeh6jSWa7y1TutJoeBWyDpzHNgqE/JRLd1VNoc583/Bn)
|
url: ENC(nJXPBEsg+NwtZt+qnD9A0AwRIdPFwex6kYEfGDetJ+N42ha/dH7V3Lq/hSCYgZa08cocXV5N+AaiisG/gh+i8CcNYjXzFV8CMSRfLwx03cxxNwnDzak+2ZEY5nb8UTbC/a6b3pWuAwg9lnHF9ziroFIrOn8+s/SYN7e+oMh4tZFmfCT4h5ATsEEuB6RkYGTzf0oImBVmet1jRR4CRdHPSeh6jSWa7y1TutJoeBWyDpzHNgqE/JRLd1VNoc583/Bn)
|
||||||
username: ENC(9cCYl0WIh2m3gQhAfq4ykWAm5+PUvr6yPcuGKPwYDPpADnTZKVv9eYhwT806vYti)
|
username: ENC(9cCYl0WIh2m3gQhAfq4ykWAm5+PUvr6yPcuGKPwYDPpADnTZKVv9eYhwT806vYti)
|
||||||
@ -21,7 +25,7 @@ minio:
|
|||||||
# Jasypt 加密配置
|
# Jasypt 加密配置
|
||||||
jasypt:
|
jasypt:
|
||||||
encryptor:
|
encryptor:
|
||||||
password: ${JASYPT_ENCRYPTOR_PASSWORD:wangpan-secret-key-2024}
|
password: ${JASYPT_ENCRYPTOR_PASSWORD}
|
||||||
algorithm: PBEWITHHMACSHA512ANDAES_256
|
algorithm: PBEWITHHMACSHA512ANDAES_256
|
||||||
key-obtention-iterations: 1000
|
key-obtention-iterations: 1000
|
||||||
pool-size: 1
|
pool-size: 1
|
||||||
@ -31,7 +35,7 @@ jasypt:
|
|||||||
|
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
com.wangpan: DEBUG
|
com.wangpan: INFO
|
||||||
|
|
||||||
# MyBatis-Plus 配置
|
# MyBatis-Plus 配置
|
||||||
mybatis-plus:
|
mybatis-plus:
|
||||||
@ -39,7 +43,6 @@ mybatis-plus:
|
|||||||
type-aliases-package: com.wangpan.common.dal.dataobject
|
type-aliases-package: com.wangpan.common.dal.dataobject
|
||||||
configuration:
|
configuration:
|
||||||
map-underscore-to-camel-case: true
|
map-underscore-to-camel-case: true
|
||||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
|
||||||
global-config:
|
global-config:
|
||||||
db-config:
|
db-config:
|
||||||
id-type: auto
|
id-type: auto
|
||||||
|
|||||||
@ -346,7 +346,9 @@
|
|||||||
<tr><th>请求路径</th><th>转发服务</th><th>说明</th></tr>
|
<tr><th>请求路径</th><th>转发服务</th><th>说明</th></tr>
|
||||||
<tr><td>/api/login, /api/register</td><td>user-service</td><td>登录注册</td></tr>
|
<tr><td>/api/login, /api/register</td><td>user-service</td><td>登录注册</td></tr>
|
||||||
<tr><td>/api/token/**, /api/login-logs/**</td><td>user-service</td><td>令牌/日志</td></tr>
|
<tr><td>/api/token/**, /api/login-logs/**</td><td>user-service</td><td>令牌/日志</td></tr>
|
||||||
|
<tr><td>/api/users/**</td><td>user-service</td><td>用户管理</td></tr>
|
||||||
<tr><td>/api/drive/**</td><td>drive-service</td><td>网盘管理</td></tr>
|
<tr><td>/api/drive/**</td><td>drive-service</td><td>网盘管理</td></tr>
|
||||||
|
<tr><td>/api/icon/**</td><td>drive-service</td><td>Icon管理</td></tr>
|
||||||
<tr><td>/api/movies/**</td><td>tmdb-service</td><td>电影管理</td></tr>
|
<tr><td>/api/movies/**</td><td>tmdb-service</td><td>电影管理</td></tr>
|
||||||
<tr><td>/api/tmdb/**, /api/tmdb-scraper/**</td><td>tmdb-service</td><td>TMDB刮削</td></tr>
|
<tr><td>/api/tmdb/**, /api/tmdb-scraper/**</td><td>tmdb-service</td><td>TMDB刮削</td></tr>
|
||||||
</table>
|
</table>
|
||||||
@ -941,7 +943,7 @@
|
|||||||
<div class="api-header" onclick="toggleDetail(this)">
|
<div class="api-header" onclick="toggleDetail(this)">
|
||||||
<span class="method POST">POST</span>
|
<span class="method POST">POST</span>
|
||||||
<span class="api-path">/api/drive/{driveId}/movies</span>
|
<span class="api-path">/api/drive/{driveId}/movies</span>
|
||||||
<span class="api-desc">上传电影文件</span><span class="auth-badge">需Token</span>
|
<span class="api-desc">创建电影(只传文件名)</span><span class="auth-badge">需Token</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="api-detail">
|
<div class="api-detail">
|
||||||
<div class="detail-section">
|
<div class="detail-section">
|
||||||
@ -954,31 +956,21 @@
|
|||||||
<div class="detail-section">
|
<div class="detail-section">
|
||||||
<h4>请求体</h4>
|
<h4>请求体</h4>
|
||||||
<div class="code-block">{
|
<div class="code-block">{
|
||||||
<span class="key">"title"</span>: <span class="string">"电影名称"</span>, <span style="color:#718096">// 必填</span>
|
<span class="key">"title"</span>: <span class="string">"文件名.mp4"</span>, <span style="color:#718096">// 必填,文件名(同时作为电影标题)</span>
|
||||||
<span class="key">"folderId"</span>: <span class="number">0</span>, <span style="color:#718096">// 可选,默认0(根目录)</span>
|
<span class="key">"folderId"</span>: <span class="number">0</span> <span style="color:#718096">// 可选,默认0(根目录)</span>
|
||||||
<span class="key">"subtitle"</span>: <span class="string">"副标题"</span>, <span style="color:#718096">// 可选</span>
|
|
||||||
<span class="key">"description"</span>: <span class="string">"描述"</span>, <span style="color:#718096">// 可选</span>
|
|
||||||
<span class="key">"fileData"</span>: <span class="string">"Base64编码的文件数据"</span>, <span style="color:#718096">// 必填</span>
|
|
||||||
<span class="key">"rating"</span>: <span class="number">85</span>, <span style="color:#718096">// 可选,评分0-100</span>
|
|
||||||
<span class="key">"tags"</span>: <span class="string">"标签1,标签2"</span> <span style="color:#718096">// 可选</span>
|
|
||||||
}</div>
|
}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-section">
|
<div class="detail-section">
|
||||||
<h4>参数说明</h4>
|
<h4>参数说明</h4>
|
||||||
<table class="param-table">
|
<table class="param-table">
|
||||||
<tr><th>参数</th><th>类型</th><th>必填</th><th>说明</th></tr>
|
<tr><th>参数</th><th>类型</th><th>必填</th><th>说明</th></tr>
|
||||||
<tr><td>title</td><td>String</td><td>是</td><td>电影名称(同时作为文件名)</td></tr>
|
<tr><td>title</td><td>String</td><td>是</td><td>文件名(同时作为电影标题,后续通过 TMDB 刮削补充信息)</td></tr>
|
||||||
<tr><td>folderId</td><td>Long</td><td>否</td><td>目标文件夹ID,默认0(根目录)</td></tr>
|
<tr><td>folderId</td><td>Long</td><td>否</td><td>目标文件夹ID,默认0(根目录)</td></tr>
|
||||||
<tr><td>subtitle</td><td>String</td><td>否</td><td>副标题</td></tr>
|
|
||||||
<tr><td>description</td><td>String</td><td>否</td><td>描述</td></tr>
|
|
||||||
<tr><td>fileData</td><td>String</td><td>是</td><td>Base64编码的视频文件数据</td></tr>
|
|
||||||
<tr><td>rating</td><td>Integer</td><td>否</td><td>评分</td></tr>
|
|
||||||
<tr><td>tags</td><td>String</td><td>否</td><td>标签</td></tr>
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-section">
|
<div class="detail-section">
|
||||||
<h4>说明</h4>
|
<h4>说明</h4>
|
||||||
<p>文件通过 Base64 编码上传,服务端解码后存储到 MinIO,返回文件访问 URL。自动检测 MIME 类型(mp4/mkv/avi → video/mp4)。</p>
|
<p>只记录文件名,不上传文件数据。创建后通过 TMDB 刮削接口补充电影的海报、描述、评分等信息。自动检测 MIME 类型(mp4/mkv/avi → video/mp4)。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1088,9 +1080,10 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-section">
|
<div class="detail-section">
|
||||||
<h4>请求体(部分更新)</h4>
|
<h4>请求体(部分更新,所有字段可选)</h4>
|
||||||
<div class="code-block">{
|
<div class="code-block">{
|
||||||
<span class="key">"title"</span>: <span class="string">"新电影名称"</span>,
|
<span class="key">"title"</span>: <span class="string">"新电影名称"</span>,
|
||||||
|
<span class="key">"originalName"</span>: <span class="string">"新文件名.mp4"</span>,
|
||||||
<span class="key">"subtitle"</span>: <span class="string">"新副标题"</span>,
|
<span class="key">"subtitle"</span>: <span class="string">"新副标题"</span>,
|
||||||
<span class="key">"description"</span>: <span class="string">"新描述"</span>,
|
<span class="key">"description"</span>: <span class="string">"新描述"</span>,
|
||||||
<span class="key">"rating"</span>: <span class="number">90</span>,
|
<span class="key">"rating"</span>: <span class="number">90</span>,
|
||||||
@ -1098,6 +1091,23 @@
|
|||||||
<span class="key">"category"</span>: <span class="string">"新分类"</span>
|
<span class="key">"category"</span>: <span class="string">"新分类"</span>
|
||||||
}</div>
|
}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="detail-section">
|
||||||
|
<h4>参数说明</h4>
|
||||||
|
<table class="param-table">
|
||||||
|
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
|
||||||
|
<tr><td>title</td><td>String</td><td>电影名称</td></tr>
|
||||||
|
<tr><td>originalName</td><td>String</td><td>原始文件名(用于 TMDB 刮削有误时修改)</td></tr>
|
||||||
|
<tr><td>subtitle</td><td>String</td><td>副标题</td></tr>
|
||||||
|
<tr><td>description</td><td>String</td><td>描述</td></tr>
|
||||||
|
<tr><td>rating</td><td>Integer</td><td>评分 0-100</td></tr>
|
||||||
|
<tr><td>tags</td><td>String</td><td>标签</td></tr>
|
||||||
|
<tr><td>category</td><td>String</td><td>分类</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="detail-section">
|
||||||
|
<h4>说明</h4>
|
||||||
|
<p>用于 TMDB 刮削有误时,用户可自定义修改文件名等信息。所有字段均为可选,只更新传入的字段。</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -1463,6 +1473,64 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 上传自定义图标 -->
|
||||||
|
<div class="api-item">
|
||||||
|
<div class="api-header" onclick="toggleDetail(this)">
|
||||||
|
<span class="method POST">POST</span>
|
||||||
|
<span class="api-path">/api/icon/{id}/logo</span>
|
||||||
|
<span class="api-desc">上传自定义图标图片</span><span class="auth-badge">需Token</span>
|
||||||
|
</div>
|
||||||
|
<div class="api-detail">
|
||||||
|
<div class="detail-section">
|
||||||
|
<h4>功能说明</h4>
|
||||||
|
<p>上传自定义图标图片替换自动抓取的 logo。上传后会自动删除旧的 MinIO 图片,防止硬盘空间浪费。系统每 24 小时自动清理无用图片。</p>
|
||||||
|
</div>
|
||||||
|
<div class="detail-section">
|
||||||
|
<h4>路径参数</h4>
|
||||||
|
<table class="param-table">
|
||||||
|
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
|
||||||
|
<tr><td>id</td><td>Long</td><td>Icon ID</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="detail-section">
|
||||||
|
<h4>请求格式</h4>
|
||||||
|
<p><code>multipart/form-data</code></p>
|
||||||
|
<table class="param-table">
|
||||||
|
<tr><th>字段</th><th>类型</th><th>必填</th><th>说明</th></tr>
|
||||||
|
<tr><td>file</td><td>File</td><td>是</td><td>图片文件(png/jpg/jpeg/gif/webp/ico,最大 1MB)</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="detail-section">
|
||||||
|
<h4>请求示例(curl)</h4>
|
||||||
|
<div class="code-block">curl -X POST http://localhost:8888/api/icon/1/logo \
|
||||||
|
-H "Authorization: Bearer <token>" \
|
||||||
|
-F "file=@/path/to/icon.png"</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-section">
|
||||||
|
<h4>成功响应(200)</h4>
|
||||||
|
<div class="code-block">{
|
||||||
|
<span class="key">"code"</span>: <span class="number">200</span>,
|
||||||
|
<span class="key">"message"</span>: <span class="string">"图标上传成功"</span>,
|
||||||
|
<span class="key">"data"</span>: {
|
||||||
|
<span class="key">"id"</span>: <span class="number">1</span>,
|
||||||
|
<span class="key">"name"</span>: <span class="string">"百度"</span>,
|
||||||
|
<span class="key">"logoUrl"</span>: <span class="string">"http://118.178.238.159:39000/wangpan/icons/2/1718265600000.png"</span>,
|
||||||
|
<span class="key">"updateTime"</span>: <span class="string">"2026-06-15T10:00:00"</span>
|
||||||
|
}
|
||||||
|
}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-section">
|
||||||
|
<h4>错误码</h4>
|
||||||
|
<table class="param-table">
|
||||||
|
<tr><th>code</th><th>说明</th></tr>
|
||||||
|
<tr><td>400</td><td>文件为空/格式不支持/超过 1MB</td></tr>
|
||||||
|
<tr><td>401</td><td>未登录或 Token 无效</td></tr>
|
||||||
|
<tr><td>500</td><td>Icon 不存在或无权访问/上传失败</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TMDB服务 -->
|
<!-- TMDB服务 -->
|
||||||
@ -1554,9 +1622,11 @@
|
|||||||
<table class="param-table">
|
<table class="param-table">
|
||||||
<tr><th>参数</th><th>类型</th><th>必填</th><th>说明</th></tr>
|
<tr><th>参数</th><th>类型</th><th>必填</th><th>说明</th></tr>
|
||||||
<tr><td>keyword</td><td>String</td><td>否</td><td>搜索关键词</td></tr>
|
<tr><td>keyword</td><td>String</td><td>否</td><td>搜索关键词</td></tr>
|
||||||
<tr><td>status</td><td>String</td><td>否</td><td>pending / scraped / failed</td></tr>
|
<tr><td>status</td><td>String</td><td>否</td><td>电影状态:active(正常)/ deleted(已删除)</td></tr>
|
||||||
<tr><td>category</td><td>String</td><td>否</td><td>分类</td></tr>
|
<tr><td>category</td><td>String</td><td>否</td><td>分类</td></tr>
|
||||||
<tr><td>scrapeType</td><td>String</td><td>否</td><td>刮削类型</td></tr>
|
<tr><td>scrapeType</td><td>String</td><td>否</td><td>刮削类型:tmdb / manual / none</td></tr>
|
||||||
|
<tr><td>sortBy</td><td>String</td><td>否</td><td>排序字段(如 createTime、rating、title)</td></tr>
|
||||||
|
<tr><td>sortOrder</td><td>String</td><td>否</td><td>排序方向:asc / desc</td></tr>
|
||||||
<tr><td>pageNo</td><td>int</td><td>否</td><td>页码,默认 1</td></tr>
|
<tr><td>pageNo</td><td>int</td><td>否</td><td>页码,默认 1</td></tr>
|
||||||
<tr><td>pageSize</td><td>int</td><td>否</td><td>每页条数,默认 20</td></tr>
|
<tr><td>pageSize</td><td>int</td><td>否</td><td>每页条数,默认 20</td></tr>
|
||||||
</table>
|
</table>
|
||||||
@ -1581,8 +1651,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="api-detail">
|
<div class="api-detail">
|
||||||
<div class="detail-section">
|
<div class="detail-section">
|
||||||
<h4>请求体</h4>
|
<h4>请求体(所有字段可选,title 必填)</h4>
|
||||||
<div class="code-block">{ <span class="key">"title"</span>: <span class="string">"电影名称"</span> }</div>
|
<div class="code-block">{
|
||||||
|
<span class="key">"title"</span>: <span class="string">"电影名称"</span>, <span style="color:#718096">// 必填</span>
|
||||||
|
<span class="key">"subtitle"</span>: <span class="string">"副标题"</span>, <span style="color:#718096">// 可选</span>
|
||||||
|
<span class="key">"resourceLibrary"</span>: <span class="string">"资源库"</span>, <span style="color:#718096">// 可选</span>
|
||||||
|
<span class="key">"maintainer"</span>: <span class="string">"维护者"</span>, <span style="color:#718096">// 可选</span>
|
||||||
|
<span class="key">"status"</span>: <span class="string">"active"</span>, <span style="color:#718096">// 可选:active / deleted</span>
|
||||||
|
<span class="key">"category"</span>: <span class="string">"动作"</span>, <span style="color:#718096">// 可选</span>
|
||||||
|
<span class="key">"scrapeType"</span>: <span class="string">"tmdb"</span>, <span style="color:#718096">// 可选:tmdb / manual / none</span>
|
||||||
|
<span class="key">"rating"</span>: <span class="number">85</span>, <span style="color:#718096">// 可选:0-100</span>
|
||||||
|
<span class="key">"posterUrl"</span>: <span class="string">"海报URL"</span>, <span style="color:#718096">// 可选</span>
|
||||||
|
<span class="key">"tags"</span>: <span class="string">"标签1,标签2"</span> <span style="color:#718096">// 可选</span>
|
||||||
|
}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1593,7 +1674,30 @@
|
|||||||
<span class="api-path">/api/movies/{id}</span>
|
<span class="api-path">/api/movies/{id}</span>
|
||||||
<span class="api-desc">更新电影</span><span class="auth-badge">需Token</span>
|
<span class="api-desc">更新电影</span><span class="auth-badge">需Token</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="api-detail"></div>
|
<div class="api-detail">
|
||||||
|
<div class="detail-section">
|
||||||
|
<h4>路径参数</h4>
|
||||||
|
<table class="param-table">
|
||||||
|
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
|
||||||
|
<tr><td>id</td><td>Long</td><td>电影ID</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="detail-section">
|
||||||
|
<h4>请求体(部分更新,所有字段可选)</h4>
|
||||||
|
<div class="code-block">{
|
||||||
|
<span class="key">"title"</span>: <span class="string">"新电影名称"</span>,
|
||||||
|
<span class="key">"subtitle"</span>: <span class="string">"新副标题"</span>,
|
||||||
|
<span class="key">"resourceLibrary"</span>: <span class="string">"新资源库"</span>,
|
||||||
|
<span class="key">"maintainer"</span>: <span class="string">"新维护者"</span>,
|
||||||
|
<span class="key">"status"</span>: <span class="string">"active"</span>,
|
||||||
|
<span class="key">"category"</span>: <span class="string">"新分类"</span>,
|
||||||
|
<span class="key">"scrapeType"</span>: <span class="string">"tmdb"</span>,
|
||||||
|
<span class="key">"rating"</span>: <span class="number">90</span>,
|
||||||
|
<span class="key">"posterUrl"</span>: <span class="string">"新海报URL"</span>,
|
||||||
|
<span class="key">"tags"</span>: <span class="string">"新标签"</span>
|
||||||
|
}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="api-item">
|
<div class="api-item">
|
||||||
@ -1625,7 +1729,23 @@
|
|||||||
<span class="api-path">/api/movies/stats</span>
|
<span class="api-path">/api/movies/stats</span>
|
||||||
<span class="api-desc">电影统计</span><span class="auth-badge">需Token</span>
|
<span class="api-desc">电影统计</span><span class="auth-badge">需Token</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="api-detail"></div>
|
<div class="api-detail">
|
||||||
|
<div class="detail-section">
|
||||||
|
<h4>功能说明</h4>
|
||||||
|
<p>获取当前用户的电影数量统计,按刮削类型和分类分组。</p>
|
||||||
|
</div>
|
||||||
|
<div class="detail-section">
|
||||||
|
<h4>响应示例</h4>
|
||||||
|
<div class="code-block">{
|
||||||
|
<span class="key">"code"</span>: <span class="number">200</span>,
|
||||||
|
<span class="key">"data"</span>: {
|
||||||
|
<span class="key">"totalCount"</span>: <span class="number">10</span>,
|
||||||
|
<span class="key">"scrapedCount"</span>: <span class="number">8</span>,
|
||||||
|
<span class="key">"pendingCount"</span>: <span class="number">2</span>
|
||||||
|
}
|
||||||
|
}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="api-item">
|
<div class="api-item">
|
||||||
@ -1659,9 +1779,25 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="api-detail">
|
<div class="api-detail">
|
||||||
<div class="detail-section">
|
<div class="detail-section">
|
||||||
<h4>请求体</h4>
|
<h4>请求方式</h4>
|
||||||
|
<p>支持两种方式传递 movieId:</p>
|
||||||
|
<p>1. <strong>URL 查询参数</strong>:<code>?movieId=1</code></p>
|
||||||
|
<p>2. <strong>请求体 JSON</strong>:</p>
|
||||||
<div class="code-block">{ <span class="key">"movieId"</span>: <span class="number">1</span> }</div>
|
<div class="code-block">{ <span class="key">"movieId"</span>: <span class="number">1</span> }</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="detail-section">
|
||||||
|
<h4>响应示例</h4>
|
||||||
|
<div class="code-block">{
|
||||||
|
<span class="key">"code"</span>: <span class="number">200</span>,
|
||||||
|
<span class="key">"message"</span>: <span class="string">"刮削完成"</span>,
|
||||||
|
<span class="key">"data"</span>: {
|
||||||
|
<span class="key">"movieId"</span>: <span class="number">1</span>,
|
||||||
|
<span class="key">"title"</span>: <span class="string">"电影名称"</span>,
|
||||||
|
<span class="key">"rating"</span>: <span class="number">85</span>,
|
||||||
|
<span class="key">"posterUrl"</span>: <span class="string">"海报URL"</span>
|
||||||
|
}
|
||||||
|
}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -1673,9 +1809,20 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="api-detail">
|
<div class="api-detail">
|
||||||
<div class="detail-section">
|
<div class="detail-section">
|
||||||
<h4>请求体</h4>
|
<h4>请求体(movieIds 可选)</h4>
|
||||||
<div class="code-block">{ <span class="key">"movieIds"</span>: [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>] }</div>
|
<div class="code-block">{ <span class="key">"movieIds"</span>: [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>] } <span style="color:#718096">// 可选:不传则自动查找需要刮削的电影</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="detail-section">
|
||||||
|
<h4>响应示例</h4>
|
||||||
|
<div class="code-block">{
|
||||||
|
<span class="key">"code"</span>: <span class="number">200</span>,
|
||||||
|
<span class="key">"message"</span>: <span class="string">"批量刮削完成"</span>,
|
||||||
|
<span class="key">"data"</span>: {
|
||||||
|
<span class="key">"successCount"</span>: <span class="number">2</span>,
|
||||||
|
<span class="key">"failCount"</span>: <span class="number">0</span>,
|
||||||
|
<span class="key">"total"</span>: <span class="number">2</span>
|
||||||
|
}
|
||||||
|
}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -1703,7 +1850,20 @@
|
|||||||
<span class="api-path">/api/tmdb-scraper/stats</span>
|
<span class="api-path">/api/tmdb-scraper/stats</span>
|
||||||
<span class="api-desc">刮削统计</span><span class="auth-badge">需Token</span>
|
<span class="api-desc">刮削统计</span><span class="auth-badge">需Token</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="api-detail"></div>
|
<div class="api-detail">
|
||||||
|
<div class="detail-section">
|
||||||
|
<h4>响应示例</h4>
|
||||||
|
<div class="code-block">{
|
||||||
|
<span class="key">"code"</span>: <span class="number">200</span>,
|
||||||
|
<span class="key">"data"</span>: {
|
||||||
|
<span class="key">"totalCaches"</span>: <span class="number">5</span>,
|
||||||
|
<span class="key">"activeCaches"</span>: <span class="number">3</span>,
|
||||||
|
<span class="key">"expiredCaches"</span>: <span class="number">2</span>,
|
||||||
|
<span class="key">"userId"</span>: <span class="number">2</span>
|
||||||
|
}
|
||||||
|
}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="api-item">
|
<div class="api-item">
|
||||||
@ -1712,7 +1872,24 @@
|
|||||||
<span class="api-path">/api/tmdb-scraper/trigger</span>
|
<span class="api-path">/api/tmdb-scraper/trigger</span>
|
||||||
<span class="api-desc">手动触发定时任务</span><span class="auth-badge">需Token</span>
|
<span class="api-desc">手动触发定时任务</span><span class="auth-badge">需Token</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="api-detail"></div>
|
<div class="api-detail">
|
||||||
|
<div class="detail-section">
|
||||||
|
<h4>功能说明</h4>
|
||||||
|
<p>手动触发 TMDB 定时扫描任务,查找需要刮削的电影并执行刮削。无需请求体。</p>
|
||||||
|
</div>
|
||||||
|
<div class="detail-section">
|
||||||
|
<h4>响应示例</h4>
|
||||||
|
<div class="code-block">{
|
||||||
|
<span class="key">"code"</span>: <span class="number">200</span>,
|
||||||
|
<span class="key">"message"</span>: <span class="string">"扫描任务已触发"</span>,
|
||||||
|
<span class="key">"data"</span>: {
|
||||||
|
<span class="key">"message"</span>: <span class="string">"扫描任务已触发"</span>,
|
||||||
|
<span class="key">"triggeredBy"</span>: <span class="number">2</span>,
|
||||||
|
<span class="key">"triggerType"</span>: <span class="string">"manual"</span>
|
||||||
|
}
|
||||||
|
}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="api-item">
|
<div class="api-item">
|
||||||
@ -1730,6 +1907,26 @@
|
|||||||
<tr><td>pageSize</td><td>20</td><td>每页条数</td></tr>
|
<tr><td>pageSize</td><td>20</td><td>每页条数</td></tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="detail-section">
|
||||||
|
<h4>响应示例</h4>
|
||||||
|
<div class="code-block">{
|
||||||
|
<span class="key">"code"</span>: <span class="number">200</span>,
|
||||||
|
<span class="key">"data"</span>: {
|
||||||
|
<span class="key">"total"</span>: <span class="number">10</span>,
|
||||||
|
<span class="key">"list"</span>: [
|
||||||
|
{
|
||||||
|
<span class="key">"id"</span>: <span class="number">1</span>,
|
||||||
|
<span class="key">"scanTime"</span>: <span class="string">"2026-06-18T10:00:00"</span>,
|
||||||
|
<span class="key">"totalMovies"</span>: <span class="number">5</span>,
|
||||||
|
<span class="key">"successCount"</span>: <span class="number">4</span>,
|
||||||
|
<span class="key">"failCount"</span>: <span class="number">1</span>
|
||||||
|
}
|
||||||
|
],
|
||||||
|
<span class="key">"pageNo"</span>: <span class="number">1</span>,
|
||||||
|
<span class="key">"pageSize"</span>: <span class="number">20</span>
|
||||||
|
}
|
||||||
|
}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -1739,7 +1936,27 @@
|
|||||||
<span class="api-path">/api/tmdb-scraper/cache/{movieId}</span>
|
<span class="api-path">/api/tmdb-scraper/cache/{movieId}</span>
|
||||||
<span class="api-desc">删除缓存</span><span class="auth-badge">需Token</span>
|
<span class="api-desc">删除缓存</span><span class="auth-badge">需Token</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="api-detail"></div>
|
<div class="api-detail">
|
||||||
|
<div class="detail-section">
|
||||||
|
<h4>路径参数</h4>
|
||||||
|
<table class="param-table">
|
||||||
|
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
|
||||||
|
<tr><td>movieId</td><td>Long</td><td>电影ID</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="detail-section">
|
||||||
|
<h4>功能说明</h4>
|
||||||
|
<p>删除指定电影的 TMDB 刮削缓存,下次刮削时将重新从 TMDB 获取数据。</p>
|
||||||
|
</div>
|
||||||
|
<div class="detail-section">
|
||||||
|
<h4>响应示例</h4>
|
||||||
|
<div class="code-block">{
|
||||||
|
<span class="key">"code"</span>: <span class="number">200</span>,
|
||||||
|
<span class="key">"message"</span>: <span class="string">"缓存删除成功"</span>,
|
||||||
|
<span class="key">"data"</span>: <span class="keyword">null</span>
|
||||||
|
}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,8 @@ server:
|
|||||||
spring:
|
spring:
|
||||||
application:
|
application:
|
||||||
name: wangpan-gateway
|
name: wangpan-gateway
|
||||||
|
codec:
|
||||||
|
max-in-memory-size: 10MB
|
||||||
cloud:
|
cloud:
|
||||||
gateway:
|
gateway:
|
||||||
routes:
|
routes:
|
||||||
@ -33,10 +35,15 @@ spring:
|
|||||||
# Jasypt 加密配置
|
# Jasypt 加密配置
|
||||||
jasypt:
|
jasypt:
|
||||||
encryptor:
|
encryptor:
|
||||||
password: ${JASYPT_ENCRYPTOR_PASSWORD:wangpan-secret-key-2024}
|
password: ${JASYPT_ENCRYPTOR_PASSWORD}
|
||||||
algorithm: PBEWithMD5AndDES
|
algorithm: PBEWITHHMACSHA512ANDAES_256
|
||||||
|
key-obtention-iterations: 1000
|
||||||
|
pool-size: 1
|
||||||
|
salt-generator-classname: org.jasypt.salt.RandomSaltGenerator
|
||||||
|
iv-generator-classname: org.jasypt.iv.RandomIvGenerator
|
||||||
|
string-output-type: base64
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
com.wangpan: DEBUG
|
com.wangpan: INFO
|
||||||
org.springframework.cloud.gateway: DEBUG
|
org.springframework.cloud.gateway: INFO
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
package com.wangpan.config;
|
package com.wangpan.config;
|
||||||
|
|
||||||
import com.wangpan.common.dal.mapper.MovieMapper;
|
|
||||||
import com.wangpan.common.dal.mapper.TmdbCacheMapper;
|
import com.wangpan.common.dal.mapper.TmdbCacheMapper;
|
||||||
import com.wangpan.common.dal.mapper.TmdbScraperLogMapper;
|
import com.wangpan.common.dal.mapper.TmdbScraperLogMapper;
|
||||||
import com.wangpan.common.util.DBUtil;
|
import com.wangpan.common.util.DBUtil;
|
||||||
@ -74,11 +73,10 @@ public class DatabaseInitializer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 创建TMDB相关表(使用各 Mapper 中定义的建表SQL)
|
// 创建TMDB相关表(使用各 Mapper 中定义的建表SQL)
|
||||||
SchemaUtil.createTableIfNotExists(conn, "wp_movie", MovieMapper.CREATE_TABLE_SQL);
|
// wp_drive_movie 表由 drive-service 的 DatabaseInitializer 负责创建,此处不再重复创建
|
||||||
SchemaUtil.createTableIfNotExists(conn, "wp_tmdb_cache", TmdbCacheMapper.CREATE_TABLE_SQL);
|
SchemaUtil.createTableIfNotExists(conn, "wp_tmdb_cache", TmdbCacheMapper.CREATE_TABLE_SQL);
|
||||||
SchemaUtil.createTableIfNotExists(conn, "wp_tmdb_scraper_log", TmdbScraperLogMapper.CREATE_TABLE_SQL);
|
SchemaUtil.createTableIfNotExists(conn, "wp_tmdb_scraper_log", TmdbScraperLogMapper.CREATE_TABLE_SQL);
|
||||||
|
|
||||||
System.out.println(" ✓ [wp_movie] 表创建完成");
|
|
||||||
System.out.println(" ✓ [wp_tmdb_cache] 表创建完成");
|
System.out.println(" ✓ [wp_tmdb_cache] 表创建完成");
|
||||||
System.out.println(" ✓ [wp_tmdb_scraper_log] 表创建完成");
|
System.out.println(" ✓ [wp_tmdb_scraper_log] 表创建完成");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -112,9 +112,9 @@ public class TmdbScraperService {
|
|||||||
* 5. 获取电影详情
|
* 5. 获取电影详情
|
||||||
* 6. 下载海报图片到本地
|
* 6. 下载海报图片到本地
|
||||||
* 7. 缓存刮削结果到数据库
|
* 7. 缓存刮削结果到数据库
|
||||||
* 8. 更新wp_movie表的相关字段
|
* 8. 更新wp_drive_movie表的相关字段
|
||||||
*
|
*
|
||||||
* @param movieId 要刮削的电影ID(wp_movie表主键)
|
* @param movieId 要刮削的电影ID(wp_drive_movie表主键)
|
||||||
* @param userId 用户ID(用于获取API Key)
|
* @param userId 用户ID(用于获取API Key)
|
||||||
* @return 刮削结果Map(包含success、message、data等)
|
* @return 刮削结果Map(包含success、message、data等)
|
||||||
*/
|
*/
|
||||||
@ -191,7 +191,7 @@ public class TmdbScraperService {
|
|||||||
var cacheData = buildCacheData(detailResult != null ? detailResult : bestMatch, movieId, userId, localPosterPath);
|
var cacheData = buildCacheData(detailResult != null ? detailResult : bestMatch, movieId, userId, localPosterPath);
|
||||||
tmdbCacheRepository.saveOrUpdate(cacheData);
|
tmdbCacheRepository.saveOrUpdate(cacheData);
|
||||||
|
|
||||||
// ========== 第9步:更新wp_movie表的相关字段 ==========
|
// ========== 第9步:更新wp_drive_movie表的相关字段 ==========
|
||||||
updateMovieFromScrape(movie, cacheData, localPosterPath);
|
updateMovieFromScrape(movie, cacheData, localPosterPath);
|
||||||
|
|
||||||
LOGGER.info("[TmdbScraper] 刮削完成: " + cacheData.get("title"));
|
LOGGER.info("[TmdbScraper] 刮削完成: " + cacheData.get("title"));
|
||||||
@ -567,7 +567,7 @@ public class TmdbScraperService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据刮削结果更新wp_movie表的相关字段
|
* 根据刮削结果更新wp_drive_movie表的相关字段
|
||||||
*/
|
*/
|
||||||
private void updateMovieFromScrape(Movie movie, Map<String, Object> cacheData, String encryptedPosterUrl) {
|
private void updateMovieFromScrape(Movie movie, Map<String, Object> cacheData, String encryptedPosterUrl) {
|
||||||
// 更新评分(如果之前没有或更低)
|
// 更新评分(如果之前没有或更低)
|
||||||
@ -580,7 +580,7 @@ public class TmdbScraperService {
|
|||||||
|
|
||||||
// 更新海报URL(优先使用 MinIO 存储的加密 URL)
|
// 更新海报URL(优先使用 MinIO 存储的加密 URL)
|
||||||
if (encryptedPosterUrl != null) {
|
if (encryptedPosterUrl != null) {
|
||||||
// 解密后存储到 wp_movie 表(前端可直接使用)
|
// 解密后存储到 wp_drive_movie 表(前端可直接使用)
|
||||||
try {
|
try {
|
||||||
var decryptedUrl = EncryptUtil.decrypt(encryptedPosterUrl);
|
var decryptedUrl = EncryptUtil.decrypt(encryptedPosterUrl);
|
||||||
movieRepository.updatePosterUrl(movie.getId(), decryptedUrl);
|
movieRepository.updatePosterUrl(movie.getId(), decryptedUrl);
|
||||||
|
|||||||
@ -21,7 +21,7 @@ minio:
|
|||||||
# Jasypt 加密配置
|
# Jasypt 加密配置
|
||||||
jasypt:
|
jasypt:
|
||||||
encryptor:
|
encryptor:
|
||||||
password: ${JASYPT_ENCRYPTOR_PASSWORD:wangpan-secret-key-2024}
|
password: ${JASYPT_ENCRYPTOR_PASSWORD}
|
||||||
algorithm: PBEWITHHMACSHA512ANDAES_256
|
algorithm: PBEWITHHMACSHA512ANDAES_256
|
||||||
key-obtention-iterations: 1000
|
key-obtention-iterations: 1000
|
||||||
pool-size: 1
|
pool-size: 1
|
||||||
@ -31,7 +31,7 @@ jasypt:
|
|||||||
|
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
com.wangpan: DEBUG
|
com.wangpan: INFO
|
||||||
|
|
||||||
# MyBatis-Plus 配置
|
# MyBatis-Plus 配置
|
||||||
mybatis-plus:
|
mybatis-plus:
|
||||||
@ -39,7 +39,6 @@ mybatis-plus:
|
|||||||
type-aliases-package: com.wangpan.common.dal.dataobject
|
type-aliases-package: com.wangpan.common.dal.dataobject
|
||||||
configuration:
|
configuration:
|
||||||
map-underscore-to-camel-case: true
|
map-underscore-to-camel-case: true
|
||||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
|
||||||
global-config:
|
global-config:
|
||||||
db-config:
|
db-config:
|
||||||
id-type: auto
|
id-type: auto
|
||||||
|
|||||||
@ -14,7 +14,7 @@ spring:
|
|||||||
# Jasypt 加密配置
|
# Jasypt 加密配置
|
||||||
jasypt:
|
jasypt:
|
||||||
encryptor:
|
encryptor:
|
||||||
password: ${JASYPT_ENCRYPTOR_PASSWORD:wangpan-secret-key-2024}
|
password: ${JASYPT_ENCRYPTOR_PASSWORD}
|
||||||
algorithm: PBEWITHHMACSHA512ANDAES_256
|
algorithm: PBEWITHHMACSHA512ANDAES_256
|
||||||
key-obtention-iterations: 1000
|
key-obtention-iterations: 1000
|
||||||
pool-size: 1
|
pool-size: 1
|
||||||
@ -24,7 +24,7 @@ jasypt:
|
|||||||
|
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
com.wangpan: DEBUG
|
com.wangpan: INFO
|
||||||
|
|
||||||
# MyBatis-Plus 配置
|
# MyBatis-Plus 配置
|
||||||
mybatis-plus:
|
mybatis-plus:
|
||||||
@ -32,7 +32,6 @@ mybatis-plus:
|
|||||||
type-aliases-package: com.wangpan.common.dal.dataobject
|
type-aliases-package: com.wangpan.common.dal.dataobject
|
||||||
configuration:
|
configuration:
|
||||||
map-underscore-to-camel-case: true
|
map-underscore-to-camel-case: true
|
||||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
|
||||||
global-config:
|
global-config:
|
||||||
db-config:
|
db-config:
|
||||||
id-type: auto
|
id-type: auto
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user