Rainytoken/PLAN.md
WaterRain 14083f4be1
refactor: migrate UsageCache from DataStore JSON to Room
- New: UsageRecordEntity (Room Entity, indexed on workspaceId+timeCreated)
- New: UsageDao (CRUD + time-range queries)
- New: UsageDatabase (Room Database, version 1)
- New: migrateDataStoreToRoom() — one-time migration from old DataStore JSON
- Rewrite: UsageCache internal impl from JSON serialize/deserialize to Room DAO
- UsageCache public API unchanged (getAll/insertAll/getRecords/count/etc.)
- Old DataStore key cleared after successful migration
- SharedPreferences flag prevents re-migration
- Room 2.7.1 + KSP compatible
- libs.versions.toml + build.gradle.kts updated
2026-07-06 23:21:43 +00:00

198 lines
7.2 KiB
Markdown
Raw 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.

# RainyToken 改进计划
> 临时计划文件,执行完毕后删除。
---
## 1. 迁移 UsageCache 到 Room
**现状问题**:
- ~3700 条记录序列化为一个 JSON 字符串存入 DataStore 单 key
- 每次 `insertAll` → `loadAll()` 全量读取 → 追加 → `persist()` 全量序列化写回
- APP 冷启动时内存缓存丢失,需反序列化整个 JSON 字符串
- 数据只增不减,半年后可能上万条,写入和启动读取会线性恶化
- `getRecords(workspaceId, fromTs, toTs)` 是全量遍历后过滤,Room 用索引查询是 O(log n)
**目标**:
- 用 Room 替换 DataStore JSON 方案
- `UsageRecord` 作为 Entity,`workspaceId` + `timeCreated` 建索引
- `UsageCache` 接口不变(`getAll` / `insertAll` / `getRecords` / `getStatsByModel` / `getStatsByDay` / `getOverview` / `deleteByWorkspaceId` / `count` / `getDistinctModels` / `getLatest` / `getAllIds`),内部实现从 JSON 序列化改为 Room DAO 查询
- 保留内存缓存层(`@Volatile cachedAll`),但改为按需查询而非全量加载
- 数据迁移:首次启动时检查旧 DataStore 数据,一次性导入 Room,然后清理旧 key
**涉及文件**:
- 新建:`data/local/UsageDatabase.kt`(Room Database + DAO)
- 新建:`data/local/UsageRecordEntity.kt`(Entity 定义,从现有 `UsageRecord` 迁移)
- 修改:`data/local/UsageCache.kt`(内部实现改为 Room DAO 调用,接口不变)
- 修改:`di/NetworkModule.kt`(@Provides RoomDatabase 实例)
- 修改:`app/build.gradle.kts`(添加 Room 依赖)
**验证标准**:
- APP 启动后 OCGO / CCGO 用量数据正常显示
- 下拉刷新触发增量同步后新数据正确入库
- CCGO 清除按钮能正确删除并重新全量同步
- 图表、总览、原始数据页面正常渲染
- 冷启动不再有全量 JSON 反序列化卡顿
---
## 2. 加解析单元测试
**现状问题**:
- `OllamaRepository.parseUsage()` 和 `OpenCodeGoRepository.parseWindows()` 用正则解析 HTML
- 对方改一次页面结构就失效,且失效表现是静默的(显示 0% 而非报错)
- `CredentialEditViewModel.load()` 每次加新服务都要改回显分支,漏了用户看不到已存凭据
**目标**:
- 纯 JVM 单元测试,不需要 Android 设备
- 测试用例用之前抓到的真实 HTML(Ollama settings 页)作为 fixture
**涉及文件**:
- 新建:`app/src/test/java/com/rainy/token/data/repository/OllamaRepositoryTest.kt`
- 测试 plan 解析(Pro / Max / Free)
- 测试 session/weekly 百分比解析(含小数 35.1%)
- 测试 data-time 重置时间戳解析
- 测试 data-model + data-requests 模型级数据解析
- 测试无效 Cookie / 空页面 / HTML 结构变更场景
- 新建:`app/src/test/java/com/rainy/token/data/repository/OpenCodeGoRepositoryTest.kt`
- 测试 rollingUsage / weeklyUsage / monthlyUsage 解析
- 测试嵌套对象的括号匹配
- 测试 HTML 无目标数据时的 fallback
**验证标准**:
- `./gradlew test` 全部通过
- 测试覆盖正则解析的主要路径和边界情况
---
## 3. DashboardScreen 拆分
**现状问题**:
- DashboardScreen.kt 1121 行,包含主框架 + 所有服务卡片 + 拖拽逻辑 + 工具函数
- 加一个新服务要改这一个文件至少 3 处
**目标**:
- 拆成 3 个文件:
- `DashboardScreen.kt` — 主框架 + Scaffold + PullToRefresh(~200 行)
- `ServiceBalanceCards.kt` — 各服务的 MainBalance + UsageWindows Composable
- `DashboardDragState.kt` — 拖拽状态管理类 + 工具函数
**涉及文件**:
- 修改:`ui/dashboard/DashboardScreen.kt`(大幅精简)
- 新建:`ui/dashboard/ServiceBalanceCards.kt`
- 新建:`ui/dashboard/DashboardDragState.kt`
**验证标准**:
- 编译通过
- Dashboard 功能不变(卡片显示、拖拽排序、下拉刷新、自适应布局)
- 新增服务时只需改 `ServiceBalanceCards.kt` + `secondaryLine`
---
## 4. 重复函数提取
**现状问题**:
- `formatAmount(Double)` — DashboardScreen.kt + ServiceDetailScreen.kt 各一份
- `normalizeWindowLabel(String)` — DashboardScreen.kt + WidgetProvider 各一份
- `formatResetInSec(Long)` / `formatReset(Long)` — DashboardScreen.kt + ServiceDetailScreen.kt + WidgetProvider 三份
- `parseCookieString(String)` — CredentialEditViewModel.kt 独一份,但应该在公共位置
**目标**:
- 抽取到 `ui/components/FormatUtils.kt`(UI 层共享)
- `parseCookieString` 抽到 `data/repository/CookieUtils.kt`
**涉及文件**:
- 新建:`ui/components/FormatUtils.kt`
- 新建:`data/repository/CookieUtils.kt`
- 修改:DashboardScreen.kt / ServiceDetailScreen.kt / OpenCodeGoWidgetProvider.kt / CredentialEditViewModel.kt(删除重复定义,改用 import)
**验证标准**:
- 编译通过,无行为变化
---
## 5. Repository 加重试
**现状问题**:
- 所有 Repository 的 `fetchBalance()` 只做一次请求,失败就返回 `Result.failure`
- 网络抖动时用户需手动下拉刷新
**目标**:
- 对 `RepositoryError.Network` 和 `RepositoryError.ServerError(5xx)` 加一次指数退避重试(最多 2 次)
- 401/403/429 不重试
- 可以做成 OkHttp Interceptor 或在 Repository 层包一层 `retryOnce`
**涉及文件**:
- 新建或修改:`data/repository/RetryHelper.kt`(通用重试包装)
- 修改:各 Repository 的 `fetchBalance()`(包一层 retry)
**验证标准**:
- 编译通过
- 网络抖动时自动重试一次而非直接失败
- 401/403 仍然立即失败不重试
---
## 6. CredentialEditViewModel 模板化
**现状问题**:
- `saveOpenCodeGoSession()` / `testAndSaveOpenCodeGo()` / `saveOllamaCredential()` / `testAndSaveOllama()` / `saveCommandCodeGoCredential()` / `testAndSaveCommandCodeGo()` 等方法成对出现,结构几乎一样
**目标**:
- 抽取通用的 `saveAndTestCredential(service, saveBlock, testBlock)` 模板方法
- 各服务只传不同的 save + test lambda
**涉及文件**:
- 修改:`ui/settings/CredentialEditViewModel.kt`
**验证标准**:
- 编译通过
- 各服务的保存/测试功能不变
- 新增服务时只需写 save + test 两个 lambda,不再写完整的成对方法
---
## 7. 删除 Jsoup 依赖
**现状问题**:
- `build.gradle.kts` 引入了 `org.jsoup:jsoup`,代码里 0 处 import
**涉及文件**:
- 修改:`app/build.gradle.kts`(删除 `implementation(libs.jsoup)`)
**验证标准**:
- 编译通过
---
## 8. ChartSettingsStore 改 Flow
**现状问题**:
- `ChartSettingsStore.getUseUtc8()` 用 `runBlocking` 同步读 DataStore,在 UI 线程调用
**目标**:
- 改成 `StateFlow` 暴露,Composable 层 `collectAsState`
**涉及文件**:
- 修改:`data/local/ChartSettingsStore.kt`
- 修改:调用 `getUseUtc8()` 的 Composable 层
**验证标准**:
- 编译通过
- 图表页 UTC 偏好切换正常
- 退出再进入能恢复上次偏好
---
## 执行顺序
1. 迁移 Room(基础设施,后续都依赖它)
2. 加解析单元测试(在改其他代码前先有回归保护)
3. DashboardScreen 拆分
4. 重复函数提取
5. Repository 加重试
6. CredentialEditViewModel 模板化
7. 删除 Jsoup 依赖
8. ChartSettingsStore 改 Flow
每步完成后 `./gradlew assembleDebug` 验证编译,全部完成后执行 `git commit` 并删除此计划文件。