feat(portal): land files platform and frontend workspace refresh

This commit is contained in:
yoyuzh
2026-04-09 18:35:03 +08:00
parent 67cd0f6e6f
commit 99e00cd7f7
68 changed files with 5795 additions and 2911 deletions

View File

@@ -151,27 +151,36 @@ npm run test
- v2 upload session 后端已补齐创建、查询、取消、prepare-part、record-part、complete 和过期清理
- `FileContentStorage` 已新增 multipart 抽象;`S3FileContentStorage` 已实现 create/upload-part/complete/abort
- 默认 S3 存储策略现在会声明 `multipartUpload=true`;创建会话时会生成 `multipartUploadId`v2 响应会返回 `multipartUpload`
- v2 会话现在还会返回 `directUpload``uploadMode=PROXY|DIRECT_SINGLE|DIRECT_MULTIPART`
- v2 会话响应还会返回 `strategy`,显式给出当前模式应该走的 `prepareUrl/proxyContentUrl/partPrepareUrlTemplate/partRecordUrlTemplate/completeUrl`
- `GET /api/v2/files/upload-sessions/{sessionId}/prepare` 已支持 `DIRECT_SINGLE` 模式下的整文件直传
- `POST /api/v2/files/upload-sessions/{sessionId}/content` 已支持 `PROXY` 模式下的整文件代理上传
- `GET /api/v2/files/upload-sessions/{sessionId}/parts/{partIndex}/prepare` 已可返回单分片直传地址;`POST /complete` 会先提交 multipart complete再复用旧 `FileService.completeUpload()` 落库
- 过期清理已从普通 `deleteBlob` 升级为对未完成 multipart 执行 abort
- 前端上传队列仍未切到 v2 upload session
- `/api/files/upload/initiate` 现在也会尊重默认策略 `directUpload/maxObjectSize`
- 桌面端 `FilesPage`、移动端 `MobileFilesPage``saveFileToNetdisk()` 已切到 v2 upload session并按 `uploadMode + strategy` 自动选择 `PROXY / DIRECT_SINGLE / DIRECT_MULTIPART`
- 前端现已接上 `create/get/cancel/prepare/content/part-prepare/part-record/complete` 全套 v2 upload session helper
后续未完成:
- 前端上传队列切到 v2 session
- 头像上传等非 files 子系统上传入口仍在走旧 `/api/**` 上传链路
### P2存储策略继续推进
当前状态:
- `StoragePolicy` 和能力声明骨架已落地
- 管理台只读展示已落地
- 管理台查看、新增、编辑、启停非默认策略的后端接口已落地
- `POST /api/admin/storage-policies/migrations` 已可创建 `STORAGE_POLICY_MIGRATION` 后台任务
- `STORAGE_POLICY_MIGRATION` 现在会在当前活动存储后端内真实复制对象数据到新的 `policies/{targetPolicyId}/blobs/...` key并更新 `FileBlob``FileEntity.VERSION`
- 迁移任务会暴露 `migrationStage``processedEntityCount/totalEntityCount``processedStoredFileCount/totalStoredFileCount``migratedEntityCount``migratedStoredFileCount` 和真实 `progressPercent`
- 如果迁移过程中失败handler 会删除本轮新写对象,并依赖事务回滚元数据;旧对象清理则在成功提交后 best-effort 执行
- 物理实体可追踪 `storagePolicyId`
后续未完成:
- 管理台新增/编辑/停用策略
- 多策略迁移任务
- 按策略能力决定上传路径与前端上传策略
- 跨不同运行时后端类型的真正 provider 级迁移
- 继续把按策略能力的上传体验外扩到其他非 files 子系统上传入口
## 当前本地运行状态

View File

@@ -251,6 +251,8 @@
- `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}/prepare`
- `POST /api/v2/files/upload-sessions/{sessionId}/content`
- `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`
@@ -258,12 +260,17 @@
说明:
- 需要登录,只允许操作当前用户自己的上传会话
- 会话响应返回 `sessionId``objectKey``multipartUpload``path``filename``contentType``size``storagePolicyId``status``chunkSize``chunkCount``expiresAt``createdAt``updatedAt`
- 默认 S3 存储策略下,创建会话时会立即初始化 multipart upload并把 `multipartUpload=true` 返回给客户端;本地策略仍会返回 `multipartUpload=false`
- 会话响应返回 `sessionId``objectKey``directUpload``multipartUpload``uploadMode``path``filename``contentType``size``storagePolicyId``status``chunkSize``chunkCount``expiresAt``createdAt``updatedAt`,以及一个面向前端消费的 `strategy` 对象
- `uploadMode` 目前有三种:`PROXY``DIRECT_SINGLE``DIRECT_MULTIPART`
- 默认 S3 存储策略下,创建会话时会立即初始化 multipart upload并把 `directUpload=true``multipartUpload=true``uploadMode=DIRECT_MULTIPART` 返回给客户端;若默认策略 `directUpload=true``multipartUpload=false`,会返回 `DIRECT_SINGLE`;若 `directUpload=false`,则返回 `PROXY`
- `strategy` 会把当前会话下一步该调用的后端入口显式返回出来:`DIRECT_SINGLE` 返回 `prepareUrl` + `completeUrl``PROXY` 返回 `proxyContentUrl` + `proxyFormField=file` + `completeUrl``DIRECT_MULTIPART` 返回 `partPrepareUrlTemplate``partRecordUrlTemplate``completeUrl`
- `GET /{sessionId}/prepare` 仅用于 `DIRECT_SINGLE`,返回整文件直传所需的 `direct/uploadUrl/method/headers/storageName`
- `POST /{sessionId}/content` 仅用于 `PROXY`,以 multipart 表单上传整文件内容到当前 upload session 绑定的 `objectKey`
- `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
- 当前前端网盘上传主链路已经消费这套 v2 接口:桌面/移动文件页和“存入网盘”入口都会按 `uploadMode + strategy` 自动选择代理上传、单请求直传或 multipart 分片上传
## 4. 快传模块
@@ -401,13 +408,21 @@
### 5.4 存储策略
`GET /api/admin/storage-policies`
- `GET /api/admin/storage-policies`
- `POST /api/admin/storage-policies`
- `PUT /api/admin/storage-policies/{policyId}`
- `PATCH /api/admin/storage-policies/{policyId}/status`
- `POST /api/admin/storage-policies/migrations`
说明:
- 需要管理员登录
- 返回当前存储策略的只读列表和结构化能力声明
- 当前仅用于管理台查看默认策略、启用状态、存储类型和能力矩阵,不支持新增、编辑、启停或删除策略
- 返回当前存储策略列表和结构化能力声明
- 新增/编辑接口当前允许维护名称、类型、bucket/endpoint/region、前缀、凭证模式、最大对象大小、能力声明与启用状态
- `PATCH /status` 用于启用或停用非默认策略;默认策略不能被停用
- `POST /migrations` 需要管理员登录,请求体为 `sourcePolicyId``targetPolicyId` 与可选 `correlationId`;当前会创建一个 `STORAGE_POLICY_MIGRATION` 后台任务,返回值沿用 `/api/v2/tasks/{id}` 的任务响应形状
- 当前迁移任务会在“当前活动存储后端”内执行真实对象迁移:复制旧对象到新的 target-policy object key更新 `FileBlob``FileEntity.VERSION`,并在事务提交后清理旧对象;如果源/目标策略类型与当前运行时存储后端不匹配,任务会失败
- 当前仍不支持删除策略、切换默认策略或通过管理接口暴露实际凭证内容
- `capabilities.multipartUpload` 现在会反映默认策略是否支持 v2 上传会话 multipart当前默认 S3 策略为 `true`,本地策略为 `false`
## 6. 前端公开路由与接口关系

View File

@@ -166,7 +166,8 @@
- 定时清理任务会删除超过 10 天的回收站条目;只有当某个 `FileBlob` 的最后一个逻辑引用随之消失时,才真正删除底层对象
- 应用启动时会把旧 `portal_file.storage_name` 行自动回填到新的 `blob_id` 引用,保证存量数据能继续读取
- 当前线上网盘文件存储已切到多吉云对象存储,后端先通过多吉云临时密钥 API 换取短期 S3 会话,再访问底层 COS 兼容桶
- v2 上传会话后端现已支持按存储策略能力走真实 multipart默认 S3 策略会在创建会话时初始化 `multipartUploadId`,分片上传通过预签名 `UploadPart` 直传对象存储,完成时先提交 multipart complete再复用旧 `FileService.completeUpload()` 落库;本地策略仍保持 `multipartUpload=false`
- v2 上传会话后端现已按默认策略能力明确区分三种上传模式:`PROXY``DIRECT_SINGLE``DIRECT_MULTIPART`。默认 S3 策略会走 `DIRECT_MULTIPART`在创建会话时初始化 `multipartUploadId`,分片上传通过预签名 `UploadPart` 直传对象存储,完成时先提交 multipart complete再复用旧 `FileService.completeUpload()` 落库;若默认策略 `directUpload=true``multipartUpload=false`,则通过 `GET /api/v2/files/upload-sessions/{sessionId}/prepare` 返回整文件直传信息;若 `directUpload=false`,则通过 `POST /api/v2/files/upload-sessions/{sessionId}/content` 走代理上传。当前会话响应还会附带 `strategy`,把当前模式下应调用的后续接口模板显式返回给前端,减少前端自己硬编码 `uploadMode -> endpoint` 映射
- 前端 files 子系统上传入口现已消费这套 v2 upload session桌面端 `FilesPage`、移动端 `MobileFilesPage``saveFileToNetdisk()` 统一通过共享 helper 按 `uploadMode + strategy` 自动选路,并在 multipart 模式下逐片调用 `prepare -> direct upload -> record -> complete`;因此网盘上传主链路已经不再依赖旧 `/api/files/upload/**`
- 前端会缓存目录列表和最后访问路径
- 桌面网盘页在左侧树状目录栏底部固定展示回收站入口;移动端在网盘页顶部提供回收站入口;两端共用独立 `RecycleBin` 页面调用 `/api/files/recycle-bin` 与恢复接口
@@ -240,6 +241,7 @@ Android 壳补充说明:
- 当前邀请码由后端返回给管理台展示
- 用户列表会展示每个用户的已用空间 / 配额
- 管理员修改用户密码后,旧密码应立即失效,新密码可直接重新登录
- 管理台当前已可查看、新增、编辑并启停非默认 `StoragePolicy`,也可创建 `STORAGE_POLICY_MIGRATION` 后台任务;策略能力继续以结构化 `StoragePolicyCapabilities` 持久化和回显。当前迁移任务会在“当前活动存储后端”内复制对象数据到新的 target-policy object key、更新 `FileBlob/FileEntity.VERSION` 元数据,并在事务提交后清理旧对象;但仍不支持跨不同运行时后端类型的真正 provider 级迁移。默认策略切换和策略删除仍未落地
- JWT 过滤器在受保护接口鉴权成功后,会把当天首次上线的用户写入管理统计表,只保留最近 7 天
- 管理台请求折线图只渲染当天已发生的小时,不再为未来小时补空点
@@ -469,6 +471,8 @@ Android 壳补充说明:
- 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` 资源展示能力矩阵。该能力只做配置可视化,不改变旧上传下载路径,也不暴露凭证或提供策略编辑能力。
- 2026-04-09 存储策略管理补充:`AdminController` 现已补 `POST /api/admin/storage-policies``PUT /api/admin/storage-policies/{policyId}``PATCH /api/admin/storage-policies/{policyId}/status``POST /api/admin/storage-policies/migrations`。当前允许新增、编辑、启停非默认策略,并沿用 `StoragePolicyCapabilities` 作为强类型能力声明;迁移接口会为管理员创建 `STORAGE_POLICY_MIGRATION` 后台任务worker 只校验源/目标策略并重算候选 `FileEntity.VERSION` / `StoredFile` 数量,不直接移动对象数据。默认策略仍不能被停用,也还不支持删除策略或切换默认策略。
- 2026-04-09 存储策略迁移补充:`StoragePolicyMigrationBackgroundTaskHandler` 现在会在当前活动存储后端内执行真实对象迁移。它要求源/目标策略类型一致且与运行时后端匹配,复制旧 object key 的字节内容到新的 `policies/{targetPolicyId}/blobs/...` key更新 `FileBlob.objectKey``FileEntity.VERSION.storagePolicyId/objectKey`,并在事务提交后清理旧对象;若中途失败,会删除本轮新写对象,依赖事务回滚数据库状态。
- 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 文件搜索第一小步

View File

@@ -0,0 +1,446 @@
# Multi User Platform Upgrade Phase 2 Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 在当前“邀请制多用户个人盘”稳定后,把系统升级到最小可用的“空间 + 成员角色 + 文件 ACL + 站内共享 + 审计”平台模型,同时不破坏现有个人网盘、公开分享、快传和管理台主链路。
**Architecture:** 继续保留现有 `portal_user + StoredFile + FileBlob/FileEntity` 主模型,不推翻个人网盘语义,而是在其上增加 `Space``SpaceMember``FilePermissionEntry``AuditLog` 这四层扩展。个人网盘先通过“每个用户自动拥有一个 personal space”兼容到新模型桌面端和管理台先接入空间与权限入口移动端只保兼容、不在本阶段新增完整协作 UI。
**Tech Stack:** Spring Boot 3.3.8, Spring Data JPA, H2/MySQL, React 19, TypeScript, Vite 6, Tailwind CSS v4, existing `/api/v2` patterns, existing `mvn test`, `npm run test`, `npm run lint`, @test-driven-development.
---
## Scope And Sequencing
- 本计划默认在当前 `2026-04-08-cloudreve-inspired-upgrade-and-refactor.md` 的既定升级、尤其是阶段 6 和全站前端重设计完成并合入后执行。
- 本计划只覆盖桌面 Web、现有管理台、后端模型/API、现有测试体系不在本阶段实现移动端协作页、WebDAV 权限联动、组织/部门层级、团队回收站、第三方 OAuth scope 细化。
- 所有新能力优先挂在 `/api/v2/**`;旧 `/api/files/**``/api/admin/**` 只做必要兼容,不强制一次性重写。
- 执行中始终使用 @test-driven-development先写失败测试再补最小实现再跑最小验证最后补全集验证。
### Task 1: Add Space Domain Skeleton And Default Personal Space
**Files:**
- Create: `backend/src/main/java/com/yoyuzh/files/space/Space.java`
- Create: `backend/src/main/java/com/yoyuzh/files/space/SpaceType.java`
- Create: `backend/src/main/java/com/yoyuzh/files/space/SpaceRole.java`
- Create: `backend/src/main/java/com/yoyuzh/files/space/SpaceMember.java`
- Create: `backend/src/main/java/com/yoyuzh/files/space/SpaceRepository.java`
- Create: `backend/src/main/java/com/yoyuzh/files/space/SpaceMemberRepository.java`
- Create: `backend/src/main/java/com/yoyuzh/files/space/SpaceService.java`
- Modify: `backend/src/main/java/com/yoyuzh/auth/User.java`
- Modify: `backend/src/main/java/com/yoyuzh/files/StoredFile.java`
- Modify: `backend/src/main/java/com/yoyuzh/files/StoredFileRepository.java`
- Create: `backend/src/test/java/com/yoyuzh/files/space/SpaceServiceTest.java`
- Modify: `backend/src/test/java/com/yoyuzh/files/FileServiceTest.java`
- [ ] **Step 1: Write the failing space-domain tests**
Run: `cd backend && mvn test -Dtest=SpaceServiceTest,FileServiceTest`
Expected: FAIL because `Space`, `SpaceMember`, and `StoredFile.space` support do not exist yet.
- [ ] **Step 2: Add the minimal schema and repository layer**
Implement:
- `Space` with `PERSONAL` and `COLLABORATIVE` types
- `SpaceMember` with `OWNER`, `MANAGER`, `EDITOR`, `VIEWER`
- `StoredFile.space` as nullable-to-required migration target
- repository methods for “default personal space by user” and “space membership by user”
- [ ] **Step 3: Add default personal-space bootstrap**
Implement `SpaceService.ensurePersonalSpace(User)` so:
- existing users can lazily receive a personal space
- new users automatically get one after registration/bootstrap
- files created through current private flows can still resolve to a valid space
- [ ] **Step 4: Re-run the targeted backend tests**
Run: `cd backend && mvn test -Dtest=SpaceServiceTest,FileServiceTest`
Expected: PASS for personal-space bootstrap and `StoredFile` compatibility assertions.
- [ ] **Step 5: Commit**
```bash
git add backend/src/main/java/com/yoyuzh/files/space backend/src/main/java/com/yoyuzh/auth/User.java backend/src/main/java/com/yoyuzh/files/StoredFile.java backend/src/main/java/com/yoyuzh/files/StoredFileRepository.java backend/src/test/java/com/yoyuzh/files/space/SpaceServiceTest.java backend/src/test/java/com/yoyuzh/files/FileServiceTest.java
git commit -m "feat(files): add space domain skeleton"
```
### Task 2: Route Existing File Ownership Through Spaces Without Breaking Private Disk
**Files:**
- Modify: `backend/src/main/java/com/yoyuzh/files/FileService.java`
- Modify: `backend/src/main/java/com/yoyuzh/files/FileSearchService.java`
- Modify: `backend/src/main/java/com/yoyuzh/files/ShareV2Service.java`
- Modify: `backend/src/main/java/com/yoyuzh/files/BackgroundTaskService.java`
- Modify: `backend/src/main/java/com/yoyuzh/files/StoredFileRepository.java`
- Create: `backend/src/test/java/com/yoyuzh/files/StoredFileSpaceCompatibilityTest.java`
- Modify: `backend/src/test/java/com/yoyuzh/files/FileSearchServiceTest.java`
- Modify: `backend/src/test/java/com/yoyuzh/files/BackgroundTaskServiceTest.java`
- [ ] **Step 1: Write failing compatibility tests for old personal-disk flows**
Run: `cd backend && mvn test -Dtest=StoredFileSpaceCompatibilityTest,FileSearchServiceTest,BackgroundTaskServiceTest`
Expected: FAIL because current create/search/task flows still assume “user owns everything directly”.
- [ ] **Step 2: Thread `spaceId` through internal file creation and lookup**
Implement the smallest compatibility rules:
- private files default to current user personal space
- current search/list/task flows keep returning the same personal files as before
- share import, archive/extract, upload complete, and metadata tasks preserve the owning space
- [ ] **Step 3: Keep old API semantics stable**
Do not change:
- existing personal path semantics
- public share token behavior
- anonymous transfer behavior
- current admin file listing shape
Only add internal `space` routing needed for future collaborative flows.
- [ ] **Step 4: Re-run targeted compatibility tests**
Run: `cd backend && mvn test -Dtest=StoredFileSpaceCompatibilityTest,FileSearchServiceTest,BackgroundTaskServiceTest`
Expected: PASS with no regression on existing personal-disk behavior.
- [ ] **Step 5: Commit**
```bash
git add backend/src/main/java/com/yoyuzh/files/FileService.java backend/src/main/java/com/yoyuzh/files/FileSearchService.java backend/src/main/java/com/yoyuzh/files/ShareV2Service.java backend/src/main/java/com/yoyuzh/files/BackgroundTaskService.java backend/src/main/java/com/yoyuzh/files/StoredFileRepository.java backend/src/test/java/com/yoyuzh/files/StoredFileSpaceCompatibilityTest.java backend/src/test/java/com/yoyuzh/files/FileSearchServiceTest.java backend/src/test/java/com/yoyuzh/files/BackgroundTaskServiceTest.java
git commit -m "refactor(files): route private disk through personal spaces"
```
### Task 3: Add V2 Space APIs And Membership Management
**Files:**
- Create: `backend/src/main/java/com/yoyuzh/api/v2/spaces/SpaceV2Controller.java`
- Create: `backend/src/main/java/com/yoyuzh/api/v2/spaces/SpaceResponse.java`
- Create: `backend/src/main/java/com/yoyuzh/api/v2/spaces/SpaceMemberResponse.java`
- Create: `backend/src/main/java/com/yoyuzh/api/v2/spaces/CreateSpaceRequest.java`
- Create: `backend/src/main/java/com/yoyuzh/api/v2/spaces/AddSpaceMemberRequest.java`
- Create: `backend/src/main/java/com/yoyuzh/api/v2/spaces/UpdateSpaceMemberRoleRequest.java`
- Modify: `backend/src/main/java/com/yoyuzh/config/SecurityConfig.java`
- Modify: `backend/src/main/java/com/yoyuzh/auth/UserDetailsServiceImpl.java`
- Create: `backend/src/test/java/com/yoyuzh/api/v2/spaces/SpaceV2ControllerIntegrationTest.java`
- [ ] **Step 1: Write failing API tests for creating and listing spaces**
Run: `cd backend && mvn test -Dtest=SpaceV2ControllerIntegrationTest`
Expected: FAIL because `/api/v2/spaces/**` endpoints and security wiring do not exist.
- [ ] **Step 2: Implement the minimal V2 space endpoints**
Implement:
- `POST /api/v2/spaces`
- `GET /api/v2/spaces`
- `GET /api/v2/spaces/{id}`
- `POST /api/v2/spaces/{id}/members`
- `PATCH /api/v2/spaces/{id}/members/{userId}`
- `DELETE /api/v2/spaces/{id}/members/{userId}`
Rules:
- only authenticated users can list their spaces
- only `OWNER`/`MANAGER` can manage members
- personal spaces cannot remove the owner or add arbitrary members
- [ ] **Step 3: Return DTOs only**
Do not expose JPA entities directly. Keep API outputs small:
- space identity
- type
- display name
- current user role
- member summaries
- [ ] **Step 4: Re-run targeted V2 API tests**
Run: `cd backend && mvn test -Dtest=SpaceV2ControllerIntegrationTest`
Expected: PASS for create/list/member add/remove/role change rules.
- [ ] **Step 5: Commit**
```bash
git add backend/src/main/java/com/yoyuzh/api/v2/spaces backend/src/main/java/com/yoyuzh/config/SecurityConfig.java backend/src/main/java/com/yoyuzh/auth/UserDetailsServiceImpl.java backend/src/test/java/com/yoyuzh/api/v2/spaces/SpaceV2ControllerIntegrationTest.java
git commit -m "feat(api): add v2 space and member management"
```
### Task 4: Add File ACL Entries And Permission Evaluation
**Files:**
- Create: `backend/src/main/java/com/yoyuzh/files/permission/FilePermissionEntry.java`
- Create: `backend/src/main/java/com/yoyuzh/files/permission/FilePermissionRole.java`
- Create: `backend/src/main/java/com/yoyuzh/files/permission/FilePermissionSubjectType.java`
- Create: `backend/src/main/java/com/yoyuzh/files/permission/FilePermissionEntryRepository.java`
- Create: `backend/src/main/java/com/yoyuzh/files/permission/FilePermissionService.java`
- Create: `backend/src/main/java/com/yoyuzh/files/permission/FileAccessEvaluator.java`
- Modify: `backend/src/main/java/com/yoyuzh/files/FileService.java`
- Modify: `backend/src/main/java/com/yoyuzh/files/FileSearchService.java`
- Modify: `backend/src/main/java/com/yoyuzh/files/ShareV2Service.java`
- Create: `backend/src/test/java/com/yoyuzh/files/permission/FilePermissionServiceTest.java`
- Modify: `backend/src/test/java/com/yoyuzh/files/FileServiceEdgeCaseTest.java`
- [ ] **Step 1: Write failing ACL tests**
Run: `cd backend && mvn test -Dtest=FilePermissionServiceTest,FileServiceEdgeCaseTest`
Expected: FAIL because file access is still governed only by owner/admin checks.
- [ ] **Step 2: Implement the minimal ACL model**
Implement:
- subject types: `USER`, `SPACE`
- permission roles: `VIEWER`, `EDITOR`, `MANAGER`
- inheritance flag for directory-to-descendant lookup
Rules:
- owner/personal-space owner always has full access
- collaborative space role provides the default baseline
- explicit file ACL can grant additional access to a user
- [ ] **Step 3: Enforce ACL at service boundaries**
Apply permission checks to:
- list directory
- upload into folder
- rename/move/copy/delete
- create public share
- import into target folder
- start background tasks on a file
- [ ] **Step 4: Re-run the ACL and edge-case tests**
Run: `cd backend && mvn test -Dtest=FilePermissionServiceTest,FileServiceEdgeCaseTest`
Expected: PASS with clear denials for users outside the owning space or without explicit grants.
- [ ] **Step 5: Commit**
```bash
git add backend/src/main/java/com/yoyuzh/files/permission backend/src/main/java/com/yoyuzh/files/FileService.java backend/src/main/java/com/yoyuzh/files/FileSearchService.java backend/src/main/java/com/yoyuzh/files/ShareV2Service.java backend/src/test/java/com/yoyuzh/files/permission/FilePermissionServiceTest.java backend/src/test/java/com/yoyuzh/files/FileServiceEdgeCaseTest.java
git commit -m "feat(files): add acl-based permission evaluation"
```
### Task 5: Add In-App Sharing And “Shared With Me” V2 Views
**Files:**
- Create: `backend/src/main/java/com/yoyuzh/api/v2/files/ShareFileToUserV2Request.java`
- Create: `backend/src/main/java/com/yoyuzh/api/v2/files/SharedFileV2Response.java`
- Create: `backend/src/main/java/com/yoyuzh/api/v2/files/FilePermissionsV2Controller.java`
- Modify: `backend/src/main/java/com/yoyuzh/api/v2/files/FileSearchV2Controller.java`
- Modify: `backend/src/main/java/com/yoyuzh/files/permission/FilePermissionService.java`
- Modify: `backend/src/main/java/com/yoyuzh/files/StoredFileRepository.java`
- Create: `backend/src/test/java/com/yoyuzh/api/v2/files/FilePermissionsV2ControllerIntegrationTest.java`
- Modify: `backend/src/test/java/com/yoyuzh/files/FileShareControllerIntegrationTest.java`
- [ ] **Step 1: Write failing tests for in-app share and shared-with-me listing**
Run: `cd backend && mvn test -Dtest=FilePermissionsV2ControllerIntegrationTest,FileShareControllerIntegrationTest`
Expected: FAIL because there is no station-internal share grant endpoint or “shared with me” query.
- [ ] **Step 2: Add minimal in-app sharing endpoints**
Implement:
- `GET /api/v2/files/{fileId}/permissions`
- `PUT /api/v2/files/{fileId}/permissions`
- `DELETE /api/v2/files/{fileId}/permissions/{entryId}`
- `GET /api/v2/files/shared-with-me`
Rules:
- only `MANAGER` or owner can grant/revoke
- station-internal share writes ACL entries, not public share tokens
- `shared-with-me` excludes files already owned through the current users own personal space
- [ ] **Step 3: Keep public sharing separate**
Do not merge station-internal sharing into `FileShareLink`.
Continue using:
- `FileShareLink` for public token shares
- `FilePermissionEntry` for logged-in user-to-user sharing
- [ ] **Step 4: Re-run targeted integration tests**
Run: `cd backend && mvn test -Dtest=FilePermissionsV2ControllerIntegrationTest,FileShareControllerIntegrationTest`
Expected: PASS for grant, revoke, and `shared-with-me` listing behavior.
- [ ] **Step 5: Commit**
```bash
git add backend/src/main/java/com/yoyuzh/api/v2/files backend/src/main/java/com/yoyuzh/files/permission/FilePermissionService.java backend/src/main/java/com/yoyuzh/files/StoredFileRepository.java backend/src/test/java/com/yoyuzh/api/v2/files/FilePermissionsV2ControllerIntegrationTest.java backend/src/test/java/com/yoyuzh/files/FileShareControllerIntegrationTest.java
git commit -m "feat(files): add in-app sharing and shared-with-me"
```
### Task 6: Add Cross-Cutting Audit Logs And Admin Audit Read API
**Files:**
- Create: `backend/src/main/java/com/yoyuzh/common/audit/AuditLogEntry.java`
- Create: `backend/src/main/java/com/yoyuzh/common/audit/AuditAction.java`
- Create: `backend/src/main/java/com/yoyuzh/common/audit/AuditLogEntryRepository.java`
- Create: `backend/src/main/java/com/yoyuzh/common/audit/AuditLogService.java`
- Create: `backend/src/main/java/com/yoyuzh/admin/AdminAuditLogResponse.java`
- Modify: `backend/src/main/java/com/yoyuzh/admin/AdminController.java`
- Modify: `backend/src/main/java/com/yoyuzh/admin/AdminService.java`
- Modify: `backend/src/main/java/com/yoyuzh/files/FileService.java`
- Modify: `backend/src/main/java/com/yoyuzh/files/space/SpaceService.java`
- Modify: `backend/src/main/java/com/yoyuzh/files/permission/FilePermissionService.java`
- Modify: `backend/src/test/java/com/yoyuzh/admin/AdminControllerIntegrationTest.java`
- Create: `backend/src/test/java/com/yoyuzh/common/audit/AuditLogServiceTest.java`
- [ ] **Step 1: Write failing audit tests**
Run: `cd backend && mvn test -Dtest=AuditLogServiceTest,AdminControllerIntegrationTest`
Expected: FAIL because no audit entity/service exists and admin cannot query audit logs.
- [ ] **Step 2: Implement the smallest useful audit model**
Persist at least:
- actor user id
- action type
- target type
- target id
- summary text
- created at
Log these events:
- space create/member change
- file permission grant/revoke
- public share create/delete
- file delete/restore
- [ ] **Step 3: Add admin read API only**
Implement a read-only admin endpoint:
- `GET /api/admin/audit-logs?page=0&size=20&query=...`
Do not add end-user audit UI in this phase.
- [ ] **Step 4: Re-run audit tests**
Run: `cd backend && mvn test -Dtest=AuditLogServiceTest,AdminControllerIntegrationTest`
Expected: PASS for audit persistence and admin listing.
- [ ] **Step 5: Commit**
```bash
git add backend/src/main/java/com/yoyuzh/common/audit backend/src/main/java/com/yoyuzh/admin/AdminController.java backend/src/main/java/com/yoyuzh/admin/AdminService.java backend/src/main/java/com/yoyuzh/files/FileService.java backend/src/main/java/com/yoyuzh/files/space/SpaceService.java backend/src/main/java/com/yoyuzh/files/permission/FilePermissionService.java backend/src/test/java/com/yoyuzh/admin/AdminControllerIntegrationTest.java backend/src/test/java/com/yoyuzh/common/audit/AuditLogServiceTest.java
git commit -m "feat(admin): add audit log backbone"
```
### Task 7: Add Desktop-Web Space, Permission, And Shared-With-Me UI
**Files:**
- Modify: `front/src/lib/types.ts`
- Modify: `front/src/lib/api.ts`
- Create: `front/src/lib/spaces.ts`
- Create: `front/src/lib/spaces.test.ts`
- Create: `front/src/lib/file-permissions.ts`
- Create: `front/src/lib/file-permissions.test.ts`
- Modify: `front/src/App.tsx`
- Modify: `front/src/components/layout/Layout.tsx`
- Modify: `front/src/pages/files/FilesDirectoryRail.tsx`
- Modify: `front/src/pages/files/FilesToolbar.tsx`
- Modify: `front/src/pages/files/useFilesDirectoryState.ts`
- Create: `front/src/pages/Spaces.tsx`
- Create: `front/src/pages/SharedWithMe.tsx`
- Create: `front/src/pages/files/SpaceMembersDialog.tsx`
- Create: `front/src/pages/files/FilePermissionsDialog.tsx`
- Create: `front/src/pages/spaces-state.ts`
- Create: `front/src/pages/spaces-state.test.ts`
- Create: `front/src/pages/shared-with-me-state.ts`
- Create: `front/src/pages/shared-with-me-state.test.ts`
- [ ] **Step 1: Write failing frontend helper tests**
Run: `cd front && npm run test -- src/lib/spaces.test.ts src/lib/file-permissions.test.ts src/pages/spaces-state.test.ts src/pages/shared-with-me-state.test.ts`
Expected: FAIL because the new API helpers and page state modules do not exist.
- [ ] **Step 2: Add typed API helpers first**
Implement the smallest helpers for:
- list/create spaces
- list/update members
- get/update file permissions
- list shared-with-me files
Do not start with JSX-heavy pages before helpers and tests exist.
- [ ] **Step 3: Add desktop-only navigation and management UI**
Implement:
- `Spaces` page for list/create/member management
- `SharedWithMe` page for station-internal shares
- file page dialogs for members and permissions
- a space switcher in the file rail or toolbar
Rules:
- keep current private disk route working
- personal space remains default landing target
- mobile app can stay unchanged in this phase
- [ ] **Step 4: Re-run targeted frontend tests**
Run: `cd front && npm run test -- src/lib/spaces.test.ts src/lib/file-permissions.test.ts src/pages/spaces-state.test.ts src/pages/shared-with-me-state.test.ts`
Expected: PASS with helpers and page state stabilized before broad UI verification.
- [ ] **Step 5: Commit**
```bash
git add front/src/lib/types.ts front/src/lib/api.ts front/src/lib/spaces.ts front/src/lib/spaces.test.ts front/src/lib/file-permissions.ts front/src/lib/file-permissions.test.ts front/src/App.tsx front/src/components/layout/Layout.tsx front/src/pages/files/FilesDirectoryRail.tsx front/src/pages/files/FilesToolbar.tsx front/src/pages/files/useFilesDirectoryState.ts front/src/pages/Spaces.tsx front/src/pages/SharedWithMe.tsx front/src/pages/files/SpaceMembersDialog.tsx front/src/pages/files/FilePermissionsDialog.tsx front/src/pages/spaces-state.ts front/src/pages/spaces-state.test.ts front/src/pages/shared-with-me-state.ts front/src/pages/shared-with-me-state.test.ts
git commit -m "feat(front): add spaces and shared-with-me ui"
```
### Task 8: Full Verification And Documentation Handoff
**Files:**
- Modify: `docs/architecture.md`
- Modify: `docs/api-reference.md`
- Modify: `memory.md`
- Modify only if verification reveals gaps: `backend/**`, `front/**`
- [ ] **Step 1: Run full backend verification**
Run: `cd backend && mvn test`
Expected: PASS with no regressions in auth, files, tasks, shares, admin, and API v2 tests.
- [ ] **Step 2: Run full frontend verification**
Run: `cd front && npm run test`
Expected: PASS with no regressions in files, transfer, admin, and new spaces/shared state tests.
- [ ] **Step 3: Run frontend type/lint verification**
Run: `cd front && npm run lint`
Expected: PASS with no TypeScript errors.
- [ ] **Step 4: Update project memory and architecture docs**
Document:
- space model
- member roles
- ACL rules
- in-app sharing vs public share split
- admin audit log availability
- mobile deferred scope
- [ ] **Step 5: Final commit**
```bash
git add docs/architecture.md docs/api-reference.md memory.md
git commit -m "docs: record multi-user platform phase 2 architecture"
```
## Deferred Explicitly
- 移动端 `MobileFiles` / `MobileOverview` / `MobileApp` 的空间协作 UI
- 组织/部门/用户组层级,不在本计划混入
- WebDAV、OAuth scope、桌面同步客户端权限联动
- 团队回收站、空间级生命周期策略、空间级存储策略切换
- 文件评论、审批流、在线协作文档
## Success Criteria
- 现有个人网盘用户登录后仍能像今天一样使用自己的私有文件空间。
- 每个用户可看到自己的 personal space且可创建 collaborative space。
- collaborative space 支持最小成员角色与目录/文件 ACL。
- 站内用户可通过 ACL 被共享文件,并在 “Shared With Me” 中看到结果。
- 审计日志能覆盖空间、权限、分享、删除/恢复等关键动作。
- 旧公开分享、快传、上传会话、后台任务和管理台文件列表不被打断。