Fix Android WebView API access and mobile shell layout

This commit is contained in:
yoyuzh
2026-04-03 14:37:21 +08:00
parent f02ff9342f
commit 56f2a9fe0d
121 changed files with 4751 additions and 700 deletions

View File

@@ -158,6 +158,8 @@
- 兼容普通上传和 OSS 直传
- 前端会优先尝试“初始化上传 -> 直传/代理 -> 完成上传”
- `upload/initiate` 返回的 `storageName` 现在是一次上传对应的 opaque blob object key新文件会落到全局 `blobs/...` key而不是用户目录路径 key
- `upload/complete` 必须回传这个 opaque blob key后端会据此创建 `FileBlob` 并把新 `StoredFile` 绑定到该 blob
### 3.2 目录与列表
@@ -192,6 +194,7 @@
- `move` 用于移动到目标路径
- `copy` 用于复制到目标路径
- 文件和文件夹都支持移动 / 复制
- 普通文件的 `move` / `rename` / `copy` 只改逻辑元数据;`copy` 会复用原有 `FileBlob`,不会复制底层对象
### 3.5 分享链接
@@ -204,6 +207,7 @@
- 已登录用户可为自己的文件或文件夹创建分享链接
- 公开访客可查看分享详情
- 登录用户可将分享内容导入自己的网盘
- 普通文件导入时会新建自己的 `StoredFile` 并复用源 `FileBlob`,不会再次写入物理文件
## 4. 快传模块
@@ -231,7 +235,7 @@
说明:
- 接收端通过 6 位取件码查找会话
- 未登录用户只能查找在线快传
- 在线快传和离线快传都允许未登录用户查找
### 4.3 加入会话
@@ -241,7 +245,7 @@
- 在线快传会占用一次性会话
- 离线快传返回可下载文件清单,不需要建立 P2P 通道
- 未登录用户只能加入在线快传
- 在线快传和离线快传都允许未登录用户加入
### 4.4 信令交换
@@ -281,7 +285,7 @@
说明:
- 需要登录
- 需要登录
- 离线文件在有效期内可以被重复下载
### 4.7 存入网盘
@@ -308,6 +312,16 @@
- 用户总数
- 文件总数
- 当前邀请码
- 今日请求次数
- 今日按小时请求折线图
- 最近 7 天每日上线人数和用户名单
- 当前离线快传占用与上限
补充说明:
- `requestTimeline` 现在只返回当天已经过去的小时,例如当天只到 `07:xx` 时只会返回 `00:00``07:00`
- `dailyActiveUsers` 固定返回最近 7 天,按日期升序排列;每项包含日期、展示标签、当天去重后的上线人数和用户名列表
- “上线”定义为用户成功通过 JWT 鉴权访问受保护接口后的当天首次记录
### 5.2 用户管理

View File

@@ -10,6 +10,8 @@
2. Spring Boot 后端 API
3. 文件存储层(本地文件系统或 S3 兼容对象存储)
当前前端除了作为 Web 站点发布外,也已支持通过 Capacitor 打包成 Android WebView 壳应用。
业务主线已经从旧教务方向切换为:
- 账号系统
@@ -33,6 +35,7 @@
- 快传发/收流程
- 管理台前端
- 生产环境 API 基址拼装与调用
- Android WebView 壳的静态资源承载与 Capacitor 同步
关键入口:
@@ -41,6 +44,8 @@
- `front/src/main.tsx`
- `front/src/lib/api.ts`
- `front/src/components/layout/Layout.tsx`
- `front/capacitor.config.ts`
- `front/android/`
主要页面:
@@ -133,11 +138,26 @@
关键实现说明:
- 文件元数据在数据库
- 文件内容走存储层抽象
- 文件内容通过独立 `FileBlob` 实体映射到底层对象;`StoredFile` 只负责用户、目录、文件名、路径、分享关系等逻辑元数据
- 新文件的物理对象 key 使用全局 `blobs/...` 命名,不再把 `userId/path` 编进对象 key
- 支持本地磁盘和 S3 兼容对象存储
- 分享导入与网盘复制会直接复用源文件的 `FileBlob`,不会再次写入字节内容
- 文件重命名、移动只更新 `StoredFile` 元数据,不会移动底层对象
- 删除文件时会先删除 `StoredFile` 引用;只有最后一个引用消失时,才真正删除 `FileBlob` 对应的底层对象
- 应用启动时会把旧 `portal_file.storage_name` 行自动回填到新的 `blob_id` 引用,保证存量数据能继续读取
- 当前线上网盘文件存储已切到多吉云对象存储,后端先通过多吉云临时密钥 API 换取短期 S3 会话,再访问底层 COS 兼容桶
- 前端会缓存目录列表和最后访问路径
Android 壳补充说明:
- Android 客户端当前使用 Capacitor 直接承载 `front/dist`,不单独维护原生业务页面
- 当前包名是 `xyz.yoyuzh.portal`
- 前端 API 基址在 Web 与 Android 壳上分开解析:网页继续走相对 `/api`Capacitor `localhost` 壳在 `http://localhost``https://localhost` 下都默认改走 `https://api.yoyuzh.xyz/api`
- 后端 CORS 默认放行 `http://localhost``https://localhost``http://127.0.0.1``https://127.0.0.1``capacitor://localhost`,以兼容 Web 开发环境和 Android WebView 壳
- Web 端构建完成后,通过 `npx cap sync android` 把静态资源复制到 `front/android/app/src/main/assets/public`
- Android 调试包当前通过 `cd front/android && ./gradlew assembleDebug` 生成,输出路径是 `front/android/app/build/outputs/apk/debug/app-debug.apk`
- 由于当前开发机直连 `dl.google.com` 与 Google Android Maven 仓库存在 TLS 握手失败,本地 Android 构建仓库源已切到可访问镜像;如果后续重新生成 Capacitor 工程,需要重新确认镜像配置仍存在
### 3.3 快传模块
核心文件:
@@ -166,8 +186,10 @@
- 多文件或文件夹可走 ZIP 下载
- 在线快传是一次性浏览器 P2P 传输,首个接收者进入后即占用该会话
- 离线快传会把文件内容落到站点存储,线上环境使用多吉云对象存储,默认保留 7 天并支持重复接收
- 登录页提供直达快传入口;匿名用户允许创建在线快传接收在线快传,离线快传相关操作仍要求登录
- 登录页提供直达快传入口;匿名用户允许创建在线快传接收在线快传和接收离线快传,离线快传的发送以及“存入网盘”仍要求登录
- 已登录发送端可在快传页查看自己未过期的离线快传记录,并重新打开取件码 / 二维码 / 分享链接详情弹层
- 生产环境当前已经部署 `GET /api/transfer/sessions/offline/mine`,用于驱动“我的离线快传”列表
- 前端默认内置 STUN 服务器,并支持通过 `VITE_TRANSFER_ICE_SERVERS_JSON` 追加 TURN / ICE 配置;未配置 TURN 时,跨运营商或手机蜂窝网络下的在线 P2P 直连不保证成功
### 3.4 管理台模块
@@ -182,7 +204,7 @@
- 管理用户
- 管理文件
- 查看邀请码
- 展示总存储量、下载流量、今日请求次数、快传使用量、离线快传占用请求折线图
- 展示总存储量、下载流量、今日请求次数、快传使用量、离线快传占用请求折线图和最近 7 天上线记录
- 调整离线快传总上限
关键实现说明:
@@ -191,6 +213,8 @@
- 当前邀请码由后端返回给管理台展示
- 用户列表会展示每个用户的已用空间 / 配额
- 管理员修改用户密码后,旧密码应立即失效,新密码可直接重新登录
- JWT 过滤器在受保护接口鉴权成功后,会把当天首次上线的用户写入管理统计表,只保留最近 7 天
- 管理台请求折线图只渲染当天已发生的小时,不再为未来小时补空点
## 4. 关键业务流程
@@ -198,6 +222,7 @@
- 前端主入口会在 `main.tsx` 按屏幕宽度选择桌面壳或移动壳
- 当前规则为:宽度小于 `768px` 时渲染 `MobileApp`,否则渲染桌面 `App`
- 移动端 `MobileFiles``MobileTransfer` 独立维护页面级动态光晕层,视觉上与桌面端网盘/快传保持同一背景语言
### 4.1 登录流程
@@ -226,16 +251,19 @@
1. 前端在 `Files` 页面选择文件或文件夹
2. 前端优先调用 `/api/files/upload/initiate`
3. 如果存储支持直传,则浏览器直接上传到对象存储
4. 前端再调用 `/api/files/upload/complete`
5. 如果直传失败,会回退到代理上传接口 `/api/files/upload`
3. 后端为新文件预留一个全局 blob object key`blobs/...`)并返回给前端
4. 如果存储支持直传,则浏览器直接把字节上传到该 blob key
5. 前端再调用 `/api/files/upload/complete`
6. 如果直传失败,会回退到代理上传接口 `/api/files/upload`
7. 后端创建 `FileBlob`,再创建指向该 blob 的 `StoredFile`
### 4.4 文件分享流程
1. 登录用户创建分享链接
2. 后端生成 token
3. 公开用户通过 `/share/:token` 查看详情
4. 登录用户可以导入到自己的网盘
4. 登录用户导入时会新建自己的 `StoredFile`
5. 若源对象是普通文件,则新条目直接复用源 `FileBlob`,不会复制物理内容
### 4.5 快传流程
@@ -258,7 +286,7 @@
补充说明:
- 离线快传的创建、查找、加入和下载都要求登录
- 离线快传只有“创建会话 / 上传文件 / 存入网盘”要求登录;匿名用户可以查找、加入和下载离线快传
- 匿名用户进入 `/transfer` 时默认落在发送页,但仅会看到在线模式
- 登录用户可通过 `/api/transfer/sessions/offline/mine` 拉取自己仍在有效期内的离线快传会话,用于在快传页回看历史取件信息

View File

@@ -0,0 +1,115 @@
# Shared File Blob Storage 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:** 将网盘文件模型改造成 `StoredFile -> FileBlob` 引用关系,让分享导入与网盘复制复用同一份底层对象,而不是再次写入物理文件。
**Architecture:** 新增独立 `FileBlob` 实体承载真实对象 key、大小和内容类型`StoredFile` 只保留逻辑目录元数据并引用 `FileBlob`。网盘上传为每个新文件创建新 blob分享导入和文件复制直接复用 blob删除时按引用是否归零决定是否删除底层对象。补一个面向旧 `portal_file.storage_name` 数据的一次性回填路径,避免线上旧数据在新模型下失联。
**Tech Stack:** Spring Boot 3.3.8, Spring Data JPA, Java 17, Maven, H2 tests, local filesystem storage, S3-compatible object storage
---
### Task 1: Define Blob Data Model
**Files:**
- Create: `backend/src/main/java/com/yoyuzh/files/FileBlob.java`
- Create: `backend/src/main/java/com/yoyuzh/files/FileBlobRepository.java`
- Modify: `backend/src/main/java/com/yoyuzh/files/StoredFile.java`
- Modify: `backend/src/main/java/com/yoyuzh/files/StoredFileRepository.java`
- [ ] **Step 1: Write the failing tests**
Add/update backend tests that expect file copies and share imports to preserve a shared blob id rather than a per-user storage name.
- [ ] **Step 2: Run test to verify it fails**
Run: `cd /Users/mac/Documents/my_site/backend && mvn test -Dtest=FileServiceTest,FileShareControllerIntegrationTest`
Expected: FAIL because `StoredFile` does not yet expose blob references and current logic still duplicates file content.
- [ ] **Step 3: Write minimal implementation**
Add `FileBlob` entity/repository, move file-object ownership from `StoredFile.storageName` to `StoredFile.blob`, and add repository helpers for blob reference counting / physical size queries.
- [ ] **Step 4: Run test to verify it passes**
Run: `cd /Users/mac/Documents/my_site/backend && mvn test -Dtest=FileServiceTest,FileShareControllerIntegrationTest`
Expected: PASS for data-model expectations.
### Task 2: Refactor File Storage Flow To Blob Keys
**Files:**
- Modify: `backend/src/main/java/com/yoyuzh/files/FileService.java`
- Modify: `backend/src/main/java/com/yoyuzh/files/storage/FileContentStorage.java`
- Modify: `backend/src/main/java/com/yoyuzh/files/storage/LocalFileContentStorage.java`
- Modify: `backend/src/main/java/com/yoyuzh/files/storage/S3FileContentStorage.java`
- Test: `backend/src/test/java/com/yoyuzh/files/FileServiceTest.java`
- Test: `backend/src/test/java/com/yoyuzh/files/FileServiceEdgeCaseTest.java`
- [ ] **Step 1: Write the failing tests**
Add/update tests for:
- upload creates a new blob
- share import reuses the source blob and does not store bytes again
- file copy reuses the source blob and does not copy bytes again
- deleting a non-final reference keeps the blob/object alive
- deleting the final reference removes the blob/object
- [ ] **Step 2: Run test to verify it fails**
Run: `cd /Users/mac/Documents/my_site/backend && mvn test -Dtest=FileServiceTest,FileServiceEdgeCaseTest`
Expected: FAIL because current service still calls `readFile/storeImportedFile/copyFile/moveFile/renameFile` for ordinary files.
- [ ] **Step 3: Write minimal implementation**
Refactor file upload/download/import/copy/delete to operate on blob object keys. Keep directory behavior metadata-only. Ensure file rename/move/copy no longer trigger physical object mutations, and deletion only removes the object when the last `StoredFile` reference disappears.
- [ ] **Step 4: Run test to verify it passes**
Run: `cd /Users/mac/Documents/my_site/backend && mvn test -Dtest=FileServiceTest,FileServiceEdgeCaseTest`
Expected: PASS.
### Task 3: Backfill Old File Rows Into Blob References
**Files:**
- Create: `backend/src/main/java/com/yoyuzh/files/FileBlobBackfillService.java`
- Modify: `backend/src/main/java/com/yoyuzh/files/StoredFileRepository.java`
- Test: `backend/src/test/java/com/yoyuzh/files/FileShareControllerIntegrationTest.java`
- [ ] **Step 1: Write the failing test**
Add an integration-path expectation that persisted file rows still download/import correctly after the blob model change.
- [ ] **Step 2: Run test to verify it fails**
Run: `cd /Users/mac/Documents/my_site/backend && mvn test -Dtest=FileShareControllerIntegrationTest`
Expected: FAIL because legacy `portal_file` rows created without blobs can no longer resolve file content.
- [ ] **Step 3: Write minimal implementation**
Add a startup/on-demand backfill that creates `FileBlob` rows for existing non-directory files using their legacy object key and attaches them to `StoredFile` rows with missing blob references.
- [ ] **Step 4: Run test to verify it passes**
Run: `cd /Users/mac/Documents/my_site/backend && mvn test -Dtest=FileShareControllerIntegrationTest`
Expected: PASS.
### Task 4: Update Docs And Full Verification
**Files:**
- Modify: `memory.md`
- Modify: `docs/architecture.md`
- Modify: `docs/api-reference.md` (only if API semantics changed)
- [ ] **Step 1: Document the new storage model**
Record that logical file metadata now points to shared blobs, that share import and copy reuse blobs, and that physical keys are global rather than user-path keys.
- [ ] **Step 2: Run full backend verification**
Run: `cd /Users/mac/Documents/my_site/backend && mvn test`
Expected: PASS.
- [ ] **Step 3: Summarize migration requirement**
In the final handoff, call out that old production data must be backfilled to `FileBlob` rows before or during rollout; otherwise pre-existing files cannot resolve blob references under the new model.

View File

@@ -0,0 +1,115 @@
# Transfer Simple Peer Integration 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:** Replace the hand-rolled online transfer WebRTC peer wiring with `simple-peer` while preserving the current pickup-code flow, backend signaling APIs, and offline transfer mode.
**Architecture:** Keep the existing product boundaries: Spring Boot remains a dumb signaling relay and session store, while the React frontend owns online-transfer peer creation and file streaming. Instead of manually managing `RTCPeerConnection`, ICE candidates, and SDP state across sender/receiver pages, introduce a thin `simple-peer` adapter that serializes peer signals through the existing `/api/transfer/sessions/{id}/signals` endpoints and reuses the current transfer control/data protocol.
**Tech Stack:** Vite 6, React 19, TypeScript, node:test, `simple-peer`, existing Spring Boot transfer signaling API.
---
### Task 1: Lock the new peer adapter contract with failing tests
**Files:**
- Create: `front/src/lib/transfer-peer.test.ts`
- Create: `front/src/lib/transfer-peer.ts`
- [ ] **Step 1: Write the failing test**
Add tests that assert:
- local `simple-peer` signal events serialize into a backend-friendly payload
- incoming backend signal payloads are routed back into the peer instance
- peer connection lifecycle maps to app-friendly callbacks without exposing raw browser SDP/ICE handling to pages
- [ ] **Step 2: Run test to verify it fails**
Run: `cd front && npm run test -- src/lib/transfer-peer.test.ts`
- [ ] **Step 3: Write minimal implementation**
Create a focused adapter around `simple-peer` with:
- sender/receiver construction
- `signal` event forwarding
- `connect`, `data`, `close`, and `error` callbacks
- send helpers used by the existing transfer pages
- [ ] **Step 4: Run test to verify it passes**
Run: `cd front && npm run test -- src/lib/transfer-peer.test.ts`
### Task 2: Replace online sender wiring in the desktop and mobile transfer pages
**Files:**
- Modify: `front/src/pages/Transfer.tsx`
- Modify: `front/src/mobile-pages/MobileTransfer.tsx`
- Modify: `front/src/lib/transfer.ts`
- [ ] **Step 1: Write the failing test**
Add or extend focused tests for the new signaling payload shape if needed so the sender path no longer depends on manual offer/answer/ICE branches.
- [ ] **Step 2: Run test to verify it fails**
Run: `cd front && npm run test`
- [ ] **Step 3: Write minimal implementation**
Use the adapter in both sender pages:
- build an initiator peer instead of a raw `RTCPeerConnection`
- post serialized peer signals through the existing backend endpoint
- keep the current file manifest and binary chunk sending protocol
- [ ] **Step 4: Run test to verify it passes**
Run: `cd front && npm run test`
### Task 3: Replace online receiver wiring while keeping the current receive UX
**Files:**
- Modify: `front/src/pages/TransferReceive.tsx`
- [ ] **Step 1: Write the failing test**
Add or update tests around receiver signal handling and data delivery if gaps remain after Task 2.
- [ ] **Step 2: Run test to verify it fails**
Run: `cd front && npm run test`
- [ ] **Step 3: Write minimal implementation**
Use the adapter in the receiver page:
- build a non-initiator peer
- feed backend-delivered signals into it
- keep the current control messages, archive flow, and netdisk save flow unchanged
- [ ] **Step 4: Run test to verify it passes**
Run: `cd front && npm run test`
### Task 4: Verification and release
**Files:**
- Modify if required by validation failures: `front/package.json`, `front/package-lock.json`
- [ ] **Step 1: Install the dependency**
Run: `cd front && npm install simple-peer @types/simple-peer`
- [ ] **Step 2: Run frontend tests**
Run: `cd front && npm run test`
- [ ] **Step 3: Run frontend typecheck**
Run: `cd front && npm run lint`
- [ ] **Step 4: Run frontend build**
Run: `cd front && npm run build`
- [ ] **Step 5: Publish the frontend**
Run: `node scripts/deploy-front-oss.mjs`