refactor(files): reorganize backend package layout

This commit is contained in:
yoyuzh
2026-04-09 16:00:34 +08:00
parent da576e0253
commit 3906a523fd
118 changed files with 4722 additions and 978 deletions

View File

@@ -4,15 +4,35 @@
本文只记录当前还没有完全收口的工作,方便后续窗口快速接上。新会话仍必须先读 `memory.md``docs/architecture.md``docs/api-reference.md`,再读本文。
## 当前 files 后端结构
`backend/src/main/java/com/yoyuzh/files` 已完成一次只改结构、不改语义的包清理,当前按职责拆为:
- `com.yoyuzh.files.core`
- `com.yoyuzh.files.upload`
- `com.yoyuzh.files.share`
- `com.yoyuzh.files.search`
- `com.yoyuzh.files.events`
- `com.yoyuzh.files.tasks`
- `com.yoyuzh.files.storage`
- `com.yoyuzh.files.policy`
注意:
- 旧的 API 路径、数据库表/字段、响应结构、前端调用方式都没改
- 后续做 files 相关工作时,先按上面职责分区找类,不要再默认去平铺的 `com.yoyuzh.files.*` 根包下找
- `FileService` 仍然是当前 files 子系统最重的协调类,后续若继续做结构优化,应优先评估它和 `FileController` 是否值得继续按用例拆分
## 当前 Git 状态
- 当前分支:`dev`
- 已推送到:`gitea/dev`
- 最近已推送提交:`977eb60 feat(files): add v2 task and metadata workflows`
- 当前未提交文件:
- `docs/superpowers/plans/2026-04-09-frontend-redesign-generation-spec.md`
- 当前 worktree 不是干净状态:除本次后端 multipart / 后台任务相关文件外,还混有前端重构中的改动与若干新增文件;提交前先用 `git status` 逐项确认,不要批量回滚
- 当前未跟踪但不要默认处理:
- `.claude/`
- `docs/superpowers/plans/2026-04-09-multi-user-platform-upgrade-phase-2.md`
- 前端重构过程中新增的 `front/src/components/ui/*``front/src/mobile-pages/*``front/src/pages/files/*` 等文件
## 已写好的前端重设计说明书
@@ -90,22 +110,24 @@ npm run test
不要在仓库根目录运行 `npm` 命令。
### P1阶段 6 后台任务继续真实化
### P1阶段 6 后台任务继续收口
当前状态:
- `/api/v2/tasks/**` 后端骨架已落地
- worker 会领取 `QUEUED` 任务并切换状态
- `MEDIA_META` 已有最小真实 handler会写入基础媒体 metadata 和图片宽高
- `ARCHIVE` / `EXTRACT` 仍是 no-op handler
- `ARCHIVE` 已有真实 handler会派生 `outputPath/outputFilename`,生成 zip 并回写同级目录
- `EXTRACT` 已有真实 handler当前只支持 zip-compatible 归档,会派生 `outputPath/outputDirectoryName`,剥离共享根目录,并在批量导入失败时清理已写入 blob
- `/api/v2/tasks/**` 已有最小 progress 字段:`publicStateJson.phase` 会经历 `queued -> running -> archiving/extracting/extracting-metadata -> completed/failed/cancelled`
- `ARCHIVE/EXTRACT` 还会暴露真实计数字段:`processedFileCount/totalFileCount``processedDirectoryCount/totalDirectoryCount` 与真实 `progressPercent`
- 后台任务已有按任务类型区分的自动重试:`ARCHIVE` 默认最多 4 次、`EXTRACT` 最多 3 次、`MEDIA_META` 最多 2 次;失败会归类为 `UNSUPPORTED_INPUT/DATA_STATE/TRANSIENT_INFRASTRUCTURE/RATE_LIMITED/UNKNOWN`,自动重试时会写入 `retryScheduled/nextRetryAt/retryDelaySeconds/lastFailureMessage/lastFailureAt/failureCategory`
- `/api/v2/tasks/{id}/retry` 已支持最小手动重试:仅 `FAILED` 任务可由当前用户重置回 `QUEUED`,并清空 `finishedAt/errorMessage`
- worker 已有 heartbeat 与多实例 lease运行中会暴露 `workerOwner/heartbeatAt/leaseExpiresAt/startedAt`,服务启动和调度前都只回收 lease 已过期的 `RUNNING` 任务
- `BackgroundTaskV2ControllerIntegrationTest` 已补 worker 驱动的 archive/extract 完成态与 extract 失败态端到端覆盖
后续未完成:
- 真实压缩任务
- 真实解压任务
- 任务重试策略
- 服务重启后的 `RUNNING` 任务恢复策略
- 更完整的任务进度字段
- 前端任务面板自动轮询或 SSE 化
- 移动端任务入口
@@ -115,11 +137,10 @@ npm run test
- v2 分享后端骨架已落地
- 支持密码、过期时间、导入开关、下载次数相关字段、我的分享、删除
- `allowDownload`落库并返回
- `allowDownload`接入 `GET /api/v2/shares/{token}?download=1`,会校验密码、过期、下载开关和下载次数并递增 `downloadCount`
后续未完成:
- 独立 v2 下载路由消费 `allowDownload`
- 前端分享二期设置界面
- 公开分享页的密码输入与过期/次数提示体验
@@ -127,14 +148,15 @@ npm run test
当前状态:
- v2 upload session 创建、查询、取消、complete、part metadata 和过期清理骨架已落地
- 当前只记录会话和 part metadata不做真实对象存储 multipart
- v2 upload session 后端已补齐创建、查询、取消、prepare-part、record-part、complete 和过期清理
- `FileContentStorage` 已新增 multipart 抽象;`S3FileContentStorage` 已实现 create/upload-part/complete/abort
- 默认 S3 存储策略现在会声明 `multipartUpload=true`;创建会话时会生成 `multipartUploadId`v2 响应会返回 `multipartUpload`
- `GET /api/v2/files/upload-sessions/{sessionId}/parts/{partIndex}/prepare` 已可返回单分片直传地址;`POST /complete` 会先提交 multipart complete再复用旧 `FileService.completeUpload()` 落库
- 过期清理已从普通 `deleteBlob` 升级为对未完成 multipart 执行 abort
- 前端上传队列仍未切到 v2 upload session
后续未完成:
-`FileContentStorage` 抽象层补 multipart 能力语义
- S3 multipart 的 create/upload-part/complete/abort
- 过期清理从普通 `deleteBlob` 升级为可 abort 未完成 multipart
- 前端上传队列切到 v2 session
### P2存储策略继续推进
@@ -149,8 +171,7 @@ npm run test
- 管理台新增/编辑/停用策略
- 多策略迁移任务
- 按策略能力决定上传路径
- 真正启用 multipart 能力
- 按策略能力决定上传路径与前端上传策略
## 当前本地运行状态

View File

@@ -177,7 +177,7 @@
控制器:
- `backend/src/main/java/com/yoyuzh/files/FileController.java`
- `backend/src/main/java/com/yoyuzh/files/core/FileController.java`
### 3.1 上传相关
@@ -246,6 +246,25 @@
- 登录用户可将分享内容导入自己的网盘
- 普通文件导入时会新建自己的 `StoredFile` 并复用源 `FileBlob`,不会再次写入物理文件
### 3.6 v2 上传会话
- `POST /api/v2/files/upload-sessions`
- `GET /api/v2/files/upload-sessions/{sessionId}`
- `DELETE /api/v2/files/upload-sessions/{sessionId}`
- `GET /api/v2/files/upload-sessions/{sessionId}/parts/{partIndex}/prepare`
- `PUT /api/v2/files/upload-sessions/{sessionId}/parts/{partIndex}`
- `POST /api/v2/files/upload-sessions/{sessionId}/complete`
说明:
- 需要登录,只允许操作当前用户自己的上传会话
- 会话响应返回 `sessionId``objectKey``multipartUpload``path``filename``contentType``size``storagePolicyId``status``chunkSize``chunkCount``expiresAt``createdAt``updatedAt`
- 默认 S3 存储策略下,创建会话时会立即初始化 multipart upload并把 `multipartUpload=true` 返回给客户端;本地策略仍会返回 `multipartUpload=false`
- `GET /parts/{partIndex}/prepare` 会返回当前分片的直传信息:`direct``uploadUrl``method``headers``storageName`
- `PUT /parts/{partIndex}` 请求体仍为 `{ "etag": "...", "size": 8388608 }`,只负责记录 part 元数据,不直接接收字节流
- `POST /complete` 会先按已记录的 part 元数据提交 multipart complete再复用旧上传完成链路写入 `FileBlob + StoredFile + FileEntity.VERSION`
- 后端每小时清理过期且未完成的会话;若会话已绑定 multipart upload会优先向对象存储发送 abort
## 4. 快传模块
控制器:
@@ -389,7 +408,7 @@
- 需要管理员登录
- 返回当前存储策略的只读列表和结构化能力声明
- 当前仅用于管理台查看默认策略、启用状态、存储类型和能力矩阵,不支持新增、编辑、启停或删除策略
- `capabilities.multipartUpload` 当前仍为能力声明字段,不代表真实对象存储 multipart 已启用
- `capabilities.multipartUpload` 现在会反映默认策略是否支持 v2 上传会话 multipart当前默认 S3 策略为 `true`,本地策略为 `false`
## 6. 前端公开路由与接口关系
@@ -452,11 +471,11 @@
- 本阶段不新增对外 API`/api/files/**`、分享、回收站、快传导入等响应结构保持不变。
- 后端在旧接口内部开始双写实体模型:上传完成、外部导入、分享导入和网盘复制会继续写 `FileBlob`,同时创建或复用 `FileEntity.VERSION`,并写入 `StoredFile.primaryEntity``StoredFileEntity(PRIMARY)`
- 下载、分享详情、回收站、ZIP 下载仍读取 `StoredFile.blob`;后续阶段稳定后再切换到 `primaryEntity` 读取。
- 2026-04-08 阶段 3 第一小步 API 补充:新增受保护的 v2 上传会话骨架接口,`POST /api/v2/files/upload-sessions` 创建会话,`GET /api/v2/files/upload-sessions/{sessionId}` 查询当前用户自己的会话,`DELETE /api/v2/files/upload-sessions/{sessionId}` 取消会话。当前响应返回 `sessionId``objectKey`、路径、文件名、状态、分片大小、分片数量和时间字段;实际文件内容仍走旧上传链路,尚未开放 v2 分片上传/完成接口
- 2026-04-08 阶段 3 第二小步 API 补充:新增 `POST /api/v2/files/upload-sessions/{sessionId}/complete`用于把当前用户自己的上传会话提交完成。该接口当前不接收请求体,会复用会话里的 `objectKey/path/filename/contentType/size`用旧上传完成落库链路,成功后返回 `COMPLETED` 状态的 v2 会话响应;分片内容上传端点仍未开放
- 2026-04-08 阶段 3 第三小步 API 补充:新增 `PUT /api/v2/files/upload-sessions/{sessionId}/parts/{partIndex}`请求体为 `{ "etag": "...", "size": 8388608 }`,用于记录当前用户上传会话的 part 元数据并返回 v2 会话响应。该接口会校验 part 范围和会话状态,当前只更新 `uploadedPartsJson`,不接收或合并真实文件分片内容
- 2026-04-08 阶段 3 第四小步 API 补充:本小步没有新增对外 API。后端新增上传会话过期清理任务,只处理未完成且已过期的会话,并把它们标记为 `EXPIRED`已完成会话和旧 `/api/files/**` 上传接口响应不变
- 2026-04-08 阶段 4 第一小步 API 补充:本小步没有新增存储策略管理 API。v2 上传会话响应新增 `storagePolicyId`,用于标识该会话绑定的默认存储策略;当前该字段只服务后续 multipart/多策略迁移,旧 `/api/files/**` 上传下载接口响应不变
- 2026-04-08 阶段 3 第一小步 API 补充:新增受保护的 v2 上传会话接口`POST /api/v2/files/upload-sessions` 创建会话,`GET /api/v2/files/upload-sessions/{sessionId}` 查询当前用户自己的会话,`DELETE /api/v2/files/upload-sessions/{sessionId}` 取消会话。当前响应返回 `sessionId``objectKey``multipartUpload`路径、文件名、状态、分片大小、分片数量和时间字段。
- 2026-04-08 阶段 3 第二小步 API 补充:`POST /api/v2/files/upload-sessions/{sessionId}/complete` 用于把当前用户自己的上传会话提交完成。当前默认 S3 策略下,该接口会先完成 multipart 合并,再复用旧上传完成链路落库,成功后返回 `COMPLETED` 状态的 v2 会话响应。
- 2026-04-08 阶段 3 第三小步 API 补充:`PUT /api/v2/files/upload-sessions/{sessionId}/parts/{partIndex}` 请求体为 `{ "etag": "...", "size": 8388608 }`,用于记录当前用户上传会话的 part 元数据并返回 v2 会话响应`GET /api/v2/files/upload-sessions/{sessionId}/parts/{partIndex}/prepare` 则返回该分片的直传地址和请求头。字节流仍直接上传到对象存储,不经过后端转发
- 2026-04-08 阶段 3 第四小步 API 补充:本小步没有新增额外资源类型。后端新增上传会话过期清理任务,只处理未完成且已过期的会话,并把它们标记为 `EXPIRED`若会话绑定了 multipart upload还会在清理时发起 abort
- 2026-04-08 阶段 4 第一小步 API 补充:本小步没有新增存储策略管理 API。v2 上传会话响应新增 `storagePolicyId`,用于标识该会话绑定的默认存储策略;该字段现在也用于区分会话是否应按策略能力走 multipart 上传
## 2026-04-08 阶段 5 文件搜索第一小步
@@ -546,13 +565,26 @@
需要登录。取消当前用户自己的任务,`QUEUED` / `RUNNING` 会转为 `CANCELLED` 并写入 `finishedAt`,终态任务保持原样。
`POST /api/v2/tasks/{id}/retry`
需要登录。仅允许当前用户重试自己处于 `FAILED` 的后台任务。
补充说明:
- 成功后任务状态会重置为 `QUEUED`
- `finishedAt``errorMessage` 会被清空
- `publicStateJson.phase` 会重置为 `queued`
- `publicStateJson.attemptCount` 会重置为 `0`
- 公开 state 会按服务端保存的 `privateStateJson` 重建,因此失败执行时写入的瞬时字段不会保留
-`FAILED` 任务调用会返回 `400`
`POST /api/v2/tasks/archive`
需要登录。创建 `ARCHIVE` 类型的 `QUEUED` 任务;`fileId` 必须属于当前用户且未删除,`path` 必须匹配服务端派生逻辑路径,暂允许文件和目录;当前 worker 只做 no-op 占位完成,不执行真实压缩
需要登录。创建 `ARCHIVE` 类型的 `QUEUED` 任务;`fileId` 必须属于当前用户且未删除,`path` 必须匹配服务端派生逻辑路径,暂允许文件和目录;当前 worker 会生成 zip 并把归档结果回写到原文件同级目录
`POST /api/v2/tasks/extract`
需要登录。创建 `EXTRACT` 类型的 `QUEUED` 任务;`fileId` 必须属于当前用户且未删除,`path` 必须匹配服务端派生逻辑路径,并拒绝目录和非压缩包类文件;当前 worker 只做 no-op 占位完成,不执行真实解压
需要登录。创建 `EXTRACT` 类型的 `QUEUED` 任务;`fileId` 必须属于当前用户且未删除,`path` 必须匹配服务端派生逻辑路径,并拒绝目录和非压缩包类文件;当前 worker 只支持 zip-compatible 归档,会剥离共享根目录,并把解压结果恢复到原文件父目录
`POST /api/v2/tasks/media-metadata`
@@ -561,6 +593,12 @@
补充说明:
- worker 会定时领取少量 `QUEUED` 任务并切换为 `RUNNING`,完成后标记 `COMPLETED`,异常时标记 `FAILED` 并写入 `errorMessage`
- `publicStateJson.phase` 当前会经历 `queued -> running -> archiving/extracting/extracting-metadata -> completed/failed/cancelled` 这样的最小阶段流转。
- `publicStateJson` 还会暴露 `attemptCount/maxAttempts`;当前默认预算为 `ARCHIVE=4``EXTRACT=3``MEDIA_META=2`
- 任务进入 `RUNNING` 后,`publicStateJson` 会额外暴露 `workerOwner/heartbeatAt/leaseExpiresAt/startedAt`,用于描述当前 worker 的 lease 和 heartbeat终态或重排回队列后会移除运行态 owner/lease 字段。
- `ARCHIVE/EXTRACT` 任务还会在 `publicStateJson` 里暴露 `processedFileCount/totalFileCount``processedDirectoryCount/totalDirectoryCount` 与真实 `progressPercent``MEDIA_META` 会额外暴露 `metadataStage`
- 当 worker 命中失败时,任务会按失败分类写入 `failureCategory``TRANSIENT_INFRASTRUCTURE``RATE_LIMITED` 与部分 `UNKNOWN` 失败会按任务类型退避自动重排回 `QUEUED`,并在 `publicStateJson` 写入 `retryScheduled=true``nextRetryAt``retryDelaySeconds``lastFailureMessage``lastFailureAt``UNSUPPORTED_INPUT``DATA_STATE` 这类确定性失败不会自动重试。
- 已取消或其他终态任务不会被重新执行。
- 服务重启后,只有 lease 已过期或历史上没有 lease 的 `RUNNING` 任务会在启动完成时被重置回 `QUEUED`,避免多实例下误抢仍在运行的 worker。
- 创建成功后的任务 state 使用服务端文件信息,至少包含 `fileId``path``filename``directory``contentType``size`
- 桌面端 `Files` 页面会拉取最近 10 条任务、提供 `QUEUED/RUNNING` 取消按钮,并可为当前选中文件创建 `MEDIA_META` 任务;移动端与 archive/extract 的前端入口暂未接入。

View File

@@ -74,7 +74,14 @@
后端包结构:
- `com.yoyuzh.auth`
- `com.yoyuzh.files`
- `com.yoyuzh.files.core`
- `com.yoyuzh.files.upload`
- `com.yoyuzh.files.share`
- `com.yoyuzh.files.search`
- `com.yoyuzh.files.events`
- `com.yoyuzh.files.tasks`
- `com.yoyuzh.files.storage`
- `com.yoyuzh.files.policy`
- `com.yoyuzh.transfer`
- `com.yoyuzh.admin`
- `com.yoyuzh.config`
@@ -124,8 +131,15 @@
核心文件:
- `backend/src/main/java/com/yoyuzh/files/FileController.java`
- `backend/src/main/java/com/yoyuzh/files/FileService.java`
- `backend/src/main/java/com/yoyuzh/files/core/FileController.java`
- `backend/src/main/java/com/yoyuzh/files/core/FileService.java`
- `backend/src/main/java/com/yoyuzh/files/upload/UploadSessionService.java`
- `backend/src/main/java/com/yoyuzh/files/share/ShareV2Service.java`
- `backend/src/main/java/com/yoyuzh/files/search/FileSearchService.java`
- `backend/src/main/java/com/yoyuzh/files/events/FileEventService.java`
- `backend/src/main/java/com/yoyuzh/files/tasks/BackgroundTaskService.java`
- `backend/src/main/java/com/yoyuzh/files/policy/StoragePolicyService.java`
- `backend/src/main/java/com/yoyuzh/api/v2/files/UploadSessionV2Controller.java`
- `backend/src/main/java/com/yoyuzh/files/storage/*`
- `front/src/pages/Files.tsx`
@@ -140,6 +154,7 @@
关键实现说明:
- `com.yoyuzh.files` 已按职责拆成 `core/upload/share/search/events/tasks/storage/policy` 八个子包,控制器路径、数据库表结构、接口路径和前端调用方式保持不变;这次调整只做包重组与引用修正,不改业务语义
- 文件元数据在数据库
- 文件内容通过独立 `FileBlob` 实体映射到底层对象;`StoredFile` 只负责用户、目录、文件名、路径、分享关系等逻辑元数据
- 新文件的物理对象 key 使用全局 `blobs/...` 命名,不再把 `userId/path` 编进对象 key
@@ -151,6 +166,7 @@
- 定时清理任务会删除超过 10 天的回收站条目;只有当某个 `FileBlob` 的最后一个逻辑引用随之消失时,才真正删除底层对象
- 应用启动时会把旧 `portal_file.storage_name` 行自动回填到新的 `blob_id` 引用,保证存量数据能继续读取
- 当前线上网盘文件存储已切到多吉云对象存储,后端先通过多吉云临时密钥 API 换取短期 S3 会话,再访问底层 COS 兼容桶
- v2 上传会话后端现已支持按存储策略能力走真实 multipart默认 S3 策略会在创建会话时初始化 `multipartUploadId`,分片上传通过预签名 `UploadPart` 直传对象存储,完成时先提交 multipart complete再复用旧 `FileService.completeUpload()` 落库;本地策略仍保持 `multipartUpload=false`
- 前端会缓存目录列表和最后访问路径
- 桌面网盘页在左侧树状目录栏底部固定展示回收站入口;移动端在网盘页顶部提供回收站入口;两端共用独立 `RecycleBin` 页面调用 `/api/files/recycle-bin` 与恢复接口
@@ -446,13 +462,14 @@ Android 壳补充说明:
- `StoredFile.blob` 仍是当前生产读取路径;`StoredFile.primaryEntity` 与关系表暂时只作为兼容迁移数据,不影响旧 `/api/files/**` DTO 和前端调用。
- `portal_stored_file_entity.stored_file_id``portal_file` 删除级联清理;`portal_file_entity.created_by` 在用户删除时置空,避免实体审计关系阻塞用户清理。
- 2026-04-08 阶段 3 第一小步补充:后端新增上传会话二期最小骨架。`UploadSession` 记录用户、目标路径、文件名、对象键、分片大小、分片数量、状态、过期时间和已上传分片占位 JSON`/api/v2/files/upload-sessions` 目前只提供创建、查询、取消会话,不承接实际分片内容上传,也不替换旧 `/api/files/upload/**` 生产链路。
- 2026-04-08 阶段 3 第二小步补充:上传会话新增完成状态机。`UploadSessionService.completeOwnedSession()` 会复用旧 `FileService.completeUpload()` 完成对象确认、目录补齐、配额/冲突校验和 `FileBlob + StoredFile + FileEntity.VERSION` 双写落库,然后把会话标记为 `COMPLETED`;失败时标记 `FAILED`,过期时标记 `EXPIRED`。当仍没有独立 v2 分片内容写入端点。
- 2026-04-08 阶段 3 第二小步补充:上传会话新增完成状态机。`UploadSessionService.completeOwnedSession()` 会复用旧 `FileService.completeUpload()` 完成对象确认、目录补齐、配额/冲突校验和 `FileBlob + StoredFile + FileEntity.VERSION` 双写落库,然后把会话标记为 `COMPLETED`;失败时标记 `FAILED`,过期时标记 `EXPIRED`。当仍没有独立 v2 分片内容写入端点。
- 2026-04-08 阶段 3 第三小步补充:上传会话新增 part 状态记录。`UploadSessionService.recordUploadedPart()` 会校验会话归属、状态、过期时间和 part 范围,把 `etag/size/uploadedAt` 写入 `uploadedPartsJson`,并将新会话推进到 `UPLOADING`。当前实现是会话状态跟踪,不是跨存储驱动的分片内容写入/合并实现。
- 2026-04-08 阶段 3 第四小步补充:上传会话新增定时过期清理。`UploadSessionService.pruneExpiredSessions()` 每小时扫描未完成且已过期的 `CREATED/UPLOADING/COMPLETING` 会话,尝试删除 `objectKey` 对应的临时 blob然后标记为 `EXPIRED`。已完成文件不参与清理,避免误删已经落库的生产对象。
- 2026-04-08 阶段 4 第一小步补充:后端新增存储策略骨架。`StoragePolicyService` 作为 `CommandLineRunner` 在启动时确保存在默认策略,并把当前 `FileStorageProperties` 映射为 `LOCAL``S3_COMPATIBLE` 策略及 `StoragePolicyCapabilities` JSON能力声明 `multipartUpload=false`用于明确真实对象存储分片写入/合并还没有启用。`UploadSession.storagePolicyId` 开始记录默认策略 ID `FileContentStorage` 仍保持单对象上传/校验抽象,`/api/files/**` 生产路径不切换。
- 2026-04-08 `files/storage` 合并补充S3 存储实现拆出多吉云临时密钥客户端与运行期会话提供器。`S3FileContentStorage` 现在通过 `S3SessionProvider.currentSession()` 获取当前 bucket、`S3Client``S3Presigner`,避免每次操作重复内联多吉云 token 解析逻辑;测试环境可直接注入 mock S3 client/presigner。该改动没有引入 multipart仍是单对象 PUT/HEAD/GET/COPY/DELETE 路径。
- 2026-04-08 阶段 4 第一小步补充:后端新增存储策略骨架。`StoragePolicyService` 作为 `CommandLineRunner` 在启动时确保存在默认策略,并把当前 `FileStorageProperties` 映射为 `LOCAL``S3_COMPATIBLE` 策略及 `StoragePolicyCapabilities` JSON能力声明里的 `multipartUpload=false` 用于明确真实对象存储分片写入/合并还没有启用。`UploadSession.storagePolicyId` 开始记录默认策略 ID但旧 `/api/files/**` 生产路径当时仍不切换。
- 2026-04-08 `files/storage` 合并补充S3 存储实现拆出多吉云临时密钥客户端与运行期会话提供器。`S3FileContentStorage` 现在通过 `S3SessionProvider.currentSession()` 获取当前 bucket、`S3Client``S3Presigner`,避免每次操作重复内联多吉云 token 解析逻辑;测试环境可直接注入 mock S3 client/presigner。当时该改动没有引入 multipart仍是单对象 PUT/HEAD/GET/COPY/DELETE 路径。
- 2026-04-08 阶段 4 第二小步补充:`FileService` 在创建新的 `FileEntity.VERSION` 时会通过 `StoragePolicyService.ensureDefaultPolicy()` 写入默认 `storagePolicyId``FileEntityBackfillService` 对历史 `FileBlob` 回填新实体时也写入同一默认策略。复用已有实体时保持原策略字段不变,只增加引用计数,避免在兼容迁移阶段覆盖历史数据。
- 2026-04-08 阶段 4 第三小步补充:管理台新增只读存储策略列表。`AdminController` 暴露 `GET /api/admin/storage-policies``AdminService` 通过白名单 DTO 返回策略基础字段和结构化 `StoragePolicyCapabilities`;前端 `react-admin` 新增 `storagePolicies` 资源展示能力矩阵。该能力只做配置可视化,不改变旧上传下载路径,不暴露凭证,不启用策略编辑或 multipart
- 2026-04-08 阶段 4 第三小步补充:管理台新增只读存储策略列表。`AdminController` 暴露 `GET /api/admin/storage-policies``AdminService` 通过白名单 DTO 返回策略基础字段和结构化 `StoragePolicyCapabilities`;前端 `react-admin` 新增 `storagePolicies` 资源展示能力矩阵。该能力只做配置可视化,不改变旧上传下载路径,不暴露凭证或提供策略编辑能力
- 2026-04-09 上传会话二期补充:`FileContentStorage` 抽象已新增 `createMultipartUpload/prepareMultipartPartUpload/completeMultipartUpload/abortMultipartUpload``S3FileContentStorage` 基于预签名 `UploadPart` 与 S3 `Complete/AbortMultipartUpload` 实现真实 multipart。`UploadSession` 新增 `multipartUploadId``UploadSessionService.createSession()` 会在默认策略声明 `multipartUpload=true` 时初始化 uploadId并通过 `GET /api/v2/files/upload-sessions/{sessionId}/parts/{partIndex}/prepare` 返回单分片直传地址。会话完成时先按 `uploadedPartsJson` 提交 multipart complete再复用旧上传完成链路落库过期清理则改为优先 abort 未完成 multipart。
## 2026-04-08 阶段 5 文件搜索第一小步
@@ -469,8 +486,8 @@ Android 壳补充说明:
- 旧分享仍保留在 `/api/files/share-links/**`,用于兼容当前前端公开分享页和旧导入路径。
- 新 v2 分享位于 `com.yoyuzh.api.v2.shares``ShareV2Service``FileShareLink` 新增 `passwordHash``expiresAt``maxDownloads``downloadCount``viewCount``allowImport``allowDownload``shareName` 策略字段。
- 公开端点包括 `GET /api/v2/shares/{token}``POST /api/v2/shares/{token}/verify-password`;创建、导入、我的分享列表和删除仍需要登录。
- 密码分享在校验前隐藏 `file` 详情v2 导入会在复用旧导入落库链路前校验过期时间、密码、`allowImport``maxDownloads`当前 `allowDownload` 只落库和返回,尚未接入独立 v2 下载路由
- 公开端点包括 `GET /api/v2/shares/{token}``POST /api/v2/shares/{token}/verify-password`,以及 `GET /api/v2/shares/{token}?download=1`;创建、导入、我的分享列表和删除仍需要登录。
- 密码分享在校验前隐藏 `file` 详情v2 导入会在复用旧导入落库链路前校验过期时间、密码、`allowImport``maxDownloads`v2 下载也会统一校验过期时间、密码、`allowDownload``maxDownloads`,成功后复用现有文件下载链路并递增 `downloadCount`
## 2026-04-08 阶段 5 文件事件流最小闭环
@@ -482,8 +499,14 @@ Android 壳补充说明:
## 2026-04-08 阶段 6 任务框架与 worker 后端最小骨架
- 后端新增 `BackgroundTask` / `BackgroundTaskType` / `BackgroundTaskStatus` / `BackgroundTaskRepository` / `BackgroundTaskService`,用于承载后续压缩、解压、缩略图、媒体元数据和清理类后台工作。
- 新增受保护的 `/api/v2/tasks/**``GET /api/v2/tasks``GET /api/v2/tasks/{id}``DELETE /api/v2/tasks/{id}`,以及 `POST /api/v2/tasks/archive``POST /api/v2/tasks/extract``POST /api/v2/tasks/media-metadata` 占位创建接口。
- 任务创建入口集中在 `BackgroundTaskService` 校验 `StoredFile``fileId` 必须属于当前用户且未删除,请求 `path` 必须匹配由 `StoredFile.path + filename` 派生的真实逻辑路径;`ARCHIVE` 允许文件和目录,`EXTRACT` 仅允许压缩包类文件`MEDIA_META` 仅允许媒体类文件。任务 public/private state 使用服务端派生的 `fileId``path``filename``directory``contentType``size`
- 当前实现新增了 worker 最小调度:定时扫描少量 `QUEUED` 任务,通过状态条件更新完成 claim`MEDIA_META` 任务会进入独立 handler 写入基础媒体元数据与图片宽高,其余任务类型执行 no-op handler 后标记 `COMPLETED`handler 异常会标记 `FAILED` 并记录错误原因,已取消任务不会被领取
- 当前仍不包含真实压缩、解压、缩略图、媒体元数据解析、重试/恢复策略或前端队列展示
- 新增受保护的 `/api/v2/tasks/**``GET /api/v2/tasks``GET /api/v2/tasks/{id}``DELETE /api/v2/tasks/{id}``POST /api/v2/tasks/{id}/retry`,以及 `POST /api/v2/tasks/archive``POST /api/v2/tasks/extract``POST /api/v2/tasks/media-metadata` 创建接口。
- 任务创建入口集中在 `BackgroundTaskService` 校验 `StoredFile``fileId` 必须属于当前用户且未删除,请求 `path` 必须匹配由 `StoredFile.path + filename` 派生的真实逻辑路径;`ARCHIVE` 允许文件和目录,`EXTRACT` 当前只允许 zip-compatible 文件(`.zip/.jar/.war` 或 zip/java archive 内容类型)`MEDIA_META` 仅允许媒体类文件。任务 public/private state 使用服务端派生的 `fileId``path``filename``directory``contentType``size`;其中 `ARCHIVE` 还会写入 `outputPath/outputFilename``EXTRACT` 会写入 `outputPath/outputDirectoryName`
- 当前实现新增了 worker 调度与多实例 lease定时先回收 lease 已过期的 `RUNNING` 任务,再扫描少量 `QUEUED` 任务,通过状态条件更新完成 claim并写入持久化 `leaseOwner/leaseExpiresAt/heartbeatAt` 与公开 `workerOwner/heartbeatAt/leaseExpiresAt/startedAt`。运行中所有 progress/完成/失败更新都要求 owner 匹配,丢失 lease 的旧 worker 不会覆盖新状态
- `MEDIA_META` 任务会进入独立 handler 写入基础媒体元数据与图片宽高,并在公开 state 写入 `metadataStage``ARCHIVE` 任务会调用 `FileService.buildArchiveBytes(...)` 生成 zip 并回写同级目录;`EXTRACT` 任务会读取 zip-compatible 归档、剥离共享根目录或把单文件直接恢复到父目录,再通过 `FileService.importExternalFilesAtomically(...)` 做预检、批量导入和失败 blob 清理
- `BackgroundTaskService` 还会在 `publicStateJson` 里统一维护最小进度阶段 `phase`:创建时是 `queued`claim 后进入 `running`worker 开始执行时按任务类型细化成 `archiving` / `extracting` / `extracting-metadata`,完成/失败/取消时再收口为 `completed` / `failed` / `cancelled`
- `ARCHIVE``EXTRACT` 任务现在会在运行和完成阶段暴露真实条目计数:`processedFileCount/totalFileCount``processedDirectoryCount/totalDirectoryCount`,并基于真实总量计算 `progressPercent`。其中 `ARCHIVE` 按实际写入 zip entry 推进,`EXTRACT` 按实际创建目录和导入文件推进;`MEDIA_META` 则暴露阶段型 `metadataStage`
- 当前 `POST /api/v2/tasks/{id}/retry` 已支持最小手动重试:只有 `FAILED` 任务可以被当前用户重置回 `QUEUED`,并清空 `finishedAt/errorMessage`,按 `privateStateJson` 重建公开 state同时把 `attemptCount` 重置回 0。
- `BackgroundTaskStartupRecovery` 现在只会在服务启动完成后回收 lease 已过期或历史上缺少 lease 的 `RUNNING` 任务,恢复时按 `privateStateJson` 重建公开 state不会再无条件重排所有 `RUNNING` 任务。
- worker 现在会按失败分类和任务类型做自动重试:失败会归到 `UNSUPPORTED_INPUT``DATA_STATE``TRANSIENT_INFRASTRUCTURE``RATE_LIMITED``UNKNOWN`;其中 `ARCHIVE` 默认最多 4 次、`EXTRACT` 最多 3 次、`MEDIA_META` 最多 2 次,公开 state 会暴露 `attemptCount/maxAttempts/retryScheduled/nextRetryAt/retryDelaySeconds/lastFailureMessage/lastFailureAt/failureCategory`
- 当前仍不包含非 zip 解压格式、缩略图/视频时长任务,以及 archive/extract 的前端入口。
- 桌面端 `front/src/pages/Files.tsx` 已接入最近 10 条后台任务查看与取消入口,并可为当前选中文件创建 `MEDIA_META` 任务;移动端与 archive/extract 的前端入口仍未接入。