java/PROJECT_REFERENCE.md
liRQ 49cce2dbb5 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 日志
2026-06-18 19:02:43 +08:00

475 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 网盘系统 (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 |
| 配置加密 | JasyptJasypt 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 值
```
---
## 五、已发现的问题与解决方案
### 问题 1SQL 列别名与 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` 级别。
### 问题 5MyBatis-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` 命令行参数替代环境变量。
### 问题 7TMDB 刮削查询了错误的电影表
**日期**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 历史不一致问题记录
**问题 8api-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