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

7.2 KiB
Raw Blame History

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 并删除此计划文件。