Files
QiuSWandCursor 534c88993e feat: add local hexagram content for offline reading
Load a versioned Wikisource jing plus project-authored plain drafts so results can show labeled original and vernacular texts without unauthorized modern translations.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-19 11:48:16 +08:00

198 lines
11 KiB
Markdown
Raw Permalink 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.
# Android 系统架构
> 状态:已接受的 MVP 架构
> 技术方向:Kotlin + Jetpack Compose,单模块起步
> 骨架来源:[Android Architecture Starter Template — base](https://github.com/android/architecture-templates/tree/base)
## 1. 架构目标
- 本地起卦是确定、可测试、无 Android/网络依赖的纯领域逻辑。
- UI、内容、存储和 AI 可以独立替换,不改变已生成的 `CastResult`。
- 核心流程离线可用,AI 故障只降级解释能力。
- 包边界对人和编码代理都清楚,并能通过测试或静态规则检查。
- MVP 保持单 Gradle 模块,避免过早多模块化;代码仍按领域和层分包。
Android 官方建议新应用采用清晰的 UI/数据分层、单向数据流、ViewModel、协程与 Flow;大型复杂业务才按需要增加 domain 层。本项目因为起卦不变量重要,保留轻量 domain 层。[Android 架构指南](https://developer.android.com/topic/architecture)
## 2. 系统上下文
```text
┌──────────────── Android App ────────────────┐
│ Compose UI │
│ ↓ intent ↑ immutable state │
│ ViewModel / state holder │
│ ↓ │
│ Domain: CastEngine / explanation use cases │
│ ↓ interfaces │
│ Repositories │
│ ├─ Assets / prepackaged content │
│ ├─ Room history │
│ ├─ DataStore settings │
│ └─ AI gateway ───────────────→ App backend│
└─────────────────────────────────────────────┘
```
只有 AI 解释需要网络。起念、投币、起卦、卦库读取和本地解释都位于 Android 应用内部。
## 3. 建议目录
工程初始化后建议采用:
```text
app/src/main/java/net/opcapp/flash/
├── app/
│ ├── LingjiApplication.kt
│ ├── MainActivity.kt
│ └── LingjiApp.kt
├── core/
│ ├── model/ # 跨层不可变领域模型
│ ├── designsystem/ # 主题、字体、间距、卦象 Canvas
│ └── common/ # 极少量通用结果类型/调度器
├── domain/
│ ├── casting/ # CastEngine、映射与校验
│ └── explanation/ # 解释用例与端口
├── data/
│ ├── content/ # assets 卦库解析与版本校验
│ ├── history/ # Room entity、DAO、repository
│ ├── settings/ # DataStore
│ └── ai/ # 网络 DTO、gateway、响应校验
└── feature/
├── onboarding/
├── home/ # 回访首页、最近一次、已上线次级入口
├── question/
├── casting/
├── result/
├── explanation/
├── history/
└── settings/
```
测试按相同包结构镜像放入 `src/test` 和 `src/androidTest`。
当前已落地的包:`app/`、`core/model`、`core/designsystem`、`domain/casting`、`data/content`(assets 解析器与测试 fake)、`data/history`、`data/settings`、`feature/onboarding`、`feature/home`、`feature/question`、`feature/casting`、`feature/result`、`feature/settings`、`feature/content`。尚未创建 `feature/history`、`feature/explanation`、`domain/explanation` 和 `data/ai`。首页不为问卦簿或「解」生成空占位。
## 4. 依赖方向
允许:
```text
feature UI → feature ViewModel → domain use case → repository interface
data implementation → repository interface + core model
app/navigation → feature public route
core/designsystem → Compose/Material + core model(仅绘制需要)
```
禁止:
- `domain` 依赖 Compose、Activity、ViewModel、Room、Retrofit/Ktor 或具体 AI SDK。
- `CastEngine` 读取系统时间、随机数、网络、数据库或问题文本。
- Composable 直接访问 DAO、assets、网络客户端或 Hilt entry point。
- `data/ai` 引用或调用 `CastEngine`。
- AI DTO 直接成为 UI 状态;必须先校验并映射为领域结果。
- feature 之间直接引用对方的内部 ViewModel 或 screen 实现。
- 一个“Utils”包承载无边界的杂项业务逻辑。
工程具备代码后,应通过架构测试或静态检查机械执行这些禁止项,不能只依赖评审记忆。
## 5. 核心组件职责
### `CastEngine`
纯 Kotlin、无副作用。输入六轮铜币,使用 API 固定的 `coin-v1` 约定输出不可变 `CastComputation`;随后由记录工厂附加 `contentVersion` 与 `createdAt`,组成不可变 `CastResult`。方法版本不是调用方可随意传入的自由字符串,记录元数据也不能进入计算。所有规则来自[领域规则](domain-rules.md)。
### `HexagramContentRepository`
按卦号和 `contentVersion` 返回已校验的本地内容。缺失、重复或版本不兼容属于数据完整性错误,不使用 AI 猜补。
### `CastingSessionViewModel`
维护草稿问题、六轮录入和状态机。通过 `SavedStateHandle` 保存可恢复的进行中状态;只在第六轮确认时调用 `CastEngine`。
### `HistoryRepository`
接收第六爻锁定事件与当次 `HistorySavePolicy` 快照,在 `autoSaveHistory=true` 时持久化会话;总开关关闭时仍支持结果页明确发出的“保存本次”命令。策略包含 `saveQuestion`、`saveExplanation` 和 `saveActionNote`,生产默认值均为 `true`,由 DataStore 提供但不得泄露到领域起卦逻辑。
Repository 提供倒序历史与最新一条的 `Flow` 查询,并把随后产生的本地或 AI 解读事务性关联到同一 session。重新解读新增版本或执行经过明确确认的替换命令,禁止普通 upsert 静默覆盖。DAO 事务负责 session、explanation 和 action 的一致写入与级联删除;首页直接订阅最新记录的投影,不维护第二份敏感正文缓存。Room 模型不得泄露到 UI;数据库迁移必须有测试。
### `HomeViewModel` / `HistoryViewModel`
`HomeViewModel` 组合 onboarding 状态、最新历史投影、保存策略摘要和已启用功能入口,只输出真实可进入的入口;不为未来功能生成禁用占位。首页固定输出本机保存/AI 发送边界文案和设置入口。`HistoryViewModel` 维护列表、空态、详情、单条删除和清空全部确认状态,通过 `HistoryRepository` 完成操作。导航使用一次性 UI 事件或 NavHost 回调,返回后恢复列表位置。
### `ExplanationRepository`
提供统一接口:本地解释和 AI 解释返回相同的可展示领域结构,并带明确 `source`。AI 实现遵循[AI 解释与安全](ai-safety.md)。
## 6. 单向数据流
每个 feature 使用不可变 `UiState` 和显式 `UiAction`:
```text
user action → ViewModel → use case/repository → state update → Compose render
```
一次性结果也应建模为状态及消费动作,避免裸 `Channel`/事件在配置变化时丢失。导航由 UI 根据已处理状态触发,并保证重复收集不会重复提交 AI 请求。
领域计算成功后 `CastResult` 为只读值。解释状态与起卦状态分开存储:
```text
ResultUiState(
castResult = immutable,
content = immutable,
explanationState = Idle | Loading | Local | Ai | Failed
)
```
## 7. 数据和存储选择
- **assets 或预置 Room**:版本化的 64 卦及爻辞。首版如果仅按编号读取,JSON assets 更简单;需要全文检索或内容迁移时再采用预置 Room。
- **Room**:按保存策略自动写入或由用户当次明确保存的会话、解释和行动记录。Android 官方推荐 Room 管理非平凡结构化数据。[Room 文档](https://developer.android.com/training/data-storage/room)
- **DataStore**:方法说明是否已读、主题、AI 同意版本和历史保存策略等少量设置;不保存大型历史或完整卦库。[DataStore 文档](https://developer.android.com/topic/libraries/architecture/datastore)
- **SavedStateHandle**:恢复当前未完成流程;不是长期历史数据库。
历史 Room 数据库及其辅助文件必须通过 Android backup/data-extraction 配置排除系统自动备份和设备迁移。此边界由构建配置测试机械验证,不能只依赖界面文案;引入导出、账号或同步前必须新增 ADR。
详细契约见[数据与内容](data-content.md)。
## 8. 网络与 AI 边界
正式发行版本通过自有后端代理模型服务:
```text
App → HTTPS backend → model provider
```
开发者密钥不能放入 APK。网络层只在用户主动选择 AI 解读后工作,设置合理连接/读取超时、取消传播和有限重试。不得后台预取解释,不得在用户继续编辑问题时偷偷重发。
如果未来支持用户自带密钥,它是单独的产品模式,需要 Android Keystore、清晰风险说明和独立决策记录,不能与默认发布路径混合。
## 9. 安全与隐私
- 仅请求联网所需权限;起卦不需要相机、定位、联系人、传感器或存储权限。
- 所有请求使用 TLS,服务端执行认证、限流和滥用保护。
- 日志不得包含问题原文、完整提示词、模型回复或 API 密钥。
- 崩溃与分析事件只记录枚举状态、耗时桶和匿名错误码。
- 用户删除本地记录后,不保留隐藏副本;服务端数据保留策略需在 AI 上线前单独确认。
- 任何来自 assets、数据库或网络的数据在边界处解析和校验,失败后进入有恢复路径的错误状态。
## 10. 依赖和构建政策
- 版本集中在 Gradle Version Catalog。
- 优先使用 AndroidX、Kotlin 官方组件和维护活跃的小型依赖。
- 新依赖必须说明用途、维护状态、许可证、体积和是否可由现有能力替代。
- 禁止直接引入完整“算命 SDK”或无法审计的卦象计算库。
- Compose 依赖使用 BOM 对齐版本。
- `compileSdk`/`targetSdk` 使用实现时最新稳定且满足商店要求的版本,不在方案文档硬编码会迅速过期的数值。
- `minSdk=26` 已由 [ADR-014](decisions.md#adr-014正式-android-身份与最低版本) 确认;提高最低版本必须以依赖要求或覆盖率数据另立决策。
## 11. 多模块化触发条件
满足以下至少两个条件后再评估从 `base` 迁移为多模块:
- 两名以上开发者长期并行修改独立 feature;
- 构建时间已经影响反馈循环;
- 有可独立发布/复用的设计系统或领域库;
- 历史、内容浏览、账户等功能使单模块边界难以机械约束;
- 需要独立基准测试或测试应用。
迁移前必须新增 ADR;“项目看起来更专业”不是拆模块理由。