Files
2026-09-24 14:24:34 +08:00

79 lines
9.9 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.
# 项目长期记忆:强宝小助手
## 项目概况
- 名称:`qiangbao_accounting`(强宝小助手),一个 Flutter 记账类 App。
- 定位:由 uniApp 项目 `pages/login/login.vue` 复刻迁移而来(见 `login_page.dart` 顶部注释)。
- 环境:Flutter **3.47.4** stable / Dart **3.13.3**(Windows 开发)。
- 目标平台:仅 `android/`、`ios/`、`web/`,未配置桌面端。
## 技术栈与约定
- 状态管理:**flutter_riverpod ^3.0.0**(`Notifier` / `NotifierProvider`,非旧版 StateNotifier)。
- 网络:**dio ^5.8.0**,API 层按模块拆分在 `lib/api/`,每个模块导出 `xxxApiProvider = Provider<XxxApi>(...)`。
- 本地存储:`shared_preferences`,统一封装在 `lib/core/storage/local_storage.dart`。
- 其他依赖:`image_picker`(OCR 选图)、`intl`、`cupertino_icons`。
- UI:Material 3(Flutter 3.16+ 默认启用),未显式设置 `useMaterial3`。
## 架构与关键文件
- `lib/app.dart`:根组件 `QiangbaoApp extends ConsumerWidget`。配置 M3 主题,用 `auth.isLoggedIn` 在 `MainShell` / `LoginPage` 之间分发。全局 `navigatorKey` 已抽到 `lib/core/navigation/navigator_key.dart`(避免 app.dart ↔ 网络层循环依赖),供网络层 401 跳转与全局 toast 使用。
- `lib/core/network/auth_redirect.dart`:网络层与 Riverpod 的桥接(`configureAuthRedirect(container)` 在 `main.dart` 调用),提供 `forceLogout()` / `logoutAndGoLogin()`。
- `lib/pages/shell/main_shell.dart`:主框架,4 个 Tab 是**角色**不是功能 —— 首页(仪表盘)/记录(全局录入)/助手(AI)/我的,用 `IndexedStack` 保留各页状态,`NavigationBar`(M3)承载导航,默认 `_index = 0`(首页)。`initState` 里有登录态兜底跳转。
- `lib/pages/login/login_page.dart`:登录/注册页,含毛玻璃卡片(`BackdropFilter`)、动画背景圆球(`AnimationController` + `Stack` + `Positioned` + `Transform.translate`)、记住密码(`utils/crypto.dart` 加解密)。
- 首页、统计、我的**都已是真实实现**(不是占位页)。记账录入页在 `pages/add/`(`AddPage`,手动记账 / OCR 双 Tab)。
### 模块框架(2026-09-23 做的「阶段 A」)
- **`lib/core/module/app_module.dart` 是模块注册表**,`appModules` 列表是唯一的真相来源:首页仪表盘的卡片、`MinePage` 的「功能模块」列表都从它生成。**新增一个模块 = 往列表加一条 + 写页面**,导航/首页/设置页都不用改。
- 每个 `AppModule` 有:`id`(稳定标识,将来做排序/开关/AI 可读范围时持久化用)、`name`、`blurb`、`icon`(emoji,和项目卡片风格一致)、`open`(打开入口页的 `WidgetBuilder`)、**可选的 `summary`**(首页卡片上的摘要 widget,由模块自己提供,`null` 就不显示)。
- **具体模块不进底部 Tab**。代价是打开 App 多一次点击才能看到账单,换来的是「以后加模块不用改布局」。这是用户明确选的。
- 目录按模块划分:`pages/bill/`(记账:首页/统计/编辑 + `widgets/`)、`pages/note/`、`pages/add/`(记录 Tab 的录入)、`pages/home/`(全局仪表盘)、`pages/assistant/`(AI 占位)。
- 「记录」Tab 目前直接就是记账录入 —— 只有一种记录类型。**等第二种记录类型(运动/投资)出现时才需要在它上面加一层「选类型」**。
- 统计**不再是顶层 Tab**,从记账首页右上角图标进(`BillStatisticsPage`)。
- `pages/bill/bill_home_page.dart` 和 `bill_statistics_page.dart` 都是被 push 进来的,所以顶部自绘了返回箭头,底部留白也按 `Navigator.canPop()` 判断(有导航栏时留 90,没有时留 24)。
### AI 助手(2026-09-23 阶段 B)
- 链路:`lib/api/ai_api.dart`(SSE 客户端)→ `lib/providers/ai_provider.dart`(`assistantProvider` + `chatSessionsProvider` + `aiStatusProvider`)→ `lib/pages/assistant/assistant_page.dart`(真对话页)。
- **SSE 是手动解析的**:dio 没有内置 SSE 支持,用 `ResponseType.stream` 拿字节流,再 `cast<List<int>>().transform(utf8.decoder).transform(LineSplitter())` 按 `event:` / `data:` / 空行切。`cast` 不能省 —— dio 给的是 `Stream<Uint8List>`,Dart 泛型不允许它直接喂给 `StreamTransformer<List<int>, String>`。
- **`sessionId` 传 null 开新会话**,后端在 `done` 事件里才把新 ID 返回,`AssistantNotifier.send` 必须把它记下来,否则下一条消息又会开一个新会话。
- 历史会话存后端(`chat_session` / `chat_message` 两张表),App 只发 `sessionId + content`。**工具调用的中间过程不入库**,所以 App 不用理解 tool 消息格式。
- 助手气泡用 `MarkdownBody` 渲染(回答里常有列表/代码块),用户气泡用 `SelectableText`。
- 后端未配 key 时不报错:`/api/ai/status` 返回 `enabled:false`,`/chat` 推一个 error 事件,页面顶部显示红色提示条。
- **AI 写库后必须让 App 侧的数据失效**(2026-09-24 修的真实 bug)。`AssistantNotifier.send` 里跟踪 SSE 的 `tool` 事件,命中 `_mutatingTools`(`create_bill` / `create_note`)就在对话结束后调 `invalidateBillData(ref)` 或失效那四个笔记 provider。
**为什么必须显式做**:`IndexedStack` 让首页仪表盘和「我的」页常驻,它们 watch 的 `monthlyStatisticsProvider` / `budgetProvider` / `accountProvider` **永远不会被 autoDispose 回收**,只靠「写操作后显式失效」更新。AI 是在服务端直接写库的,App 不知情 → 这三个一直是旧数。而 `billsProvider` 只被 push 出来的记账首页 watch,会被回收重取 —— 于是现象是**「账单列表对了、汇总和预算环没对」**,很有迷惑性。
回归测试在 `test/ai_refresh_test.dart`(用假 AiApi 吐 SSE 事件,断言 provider 的重建次数)。**加新的写工具时记得同步 `_mutatingTools`**,否则会静默失效。
## 设计规范(配色)
- 品牌主色:`#C8956E`(驼色/焦糖色),在 `app.dart` 中作为 `ColorScheme.fromSeed` 的 seedColor。
- 深棕文字色:`#5D4037`;辅助文字:`#8D6E63`;页面底色:`#F8F8F8`。
- 登录页另用暖橙渐变 `#FFE5CC → #FFD4A8 → #FFC08A`,点缀金色 `#FFD700`、粉色 `#FFB6C1`。
- 注意:项目中颜色多为**硬编码字面量**,尚未统一走 `Theme.of(context).colorScheme`(有优化空间)。
## 网络环境配置(重要)
- 环境切换在 `lib/core/network/api_env.dart`:`baseUrl` 三级优先级 = `--dart-define=API_BASE_URL` > `!kDebugMode` 取 `kProdBaseUrl` > `kLocalBaseUrl`。
- 消费链:`dio_client.dart` 的 `dioProvider` → `BaseOptions.baseUrl`;API 层使用相对路径(如 `/auth/login`)。
- 生产地址当前为 `http://103.36.220.231:12345/api`(明文 HTTP + 裸 IP)。**与注释及 `docs/Flutter打包与真机安装指南.md` 中记录的 `https://accounting.aqroid.cn/api` 不一致。**
- `ios/Runner/Info.plist` 无任何 ATS 例外配置 → iOS 包请求 http 会被拦截。
- `android/app/src/main/AndroidManifest.xml` 已设 `android:usesCleartextTraffic="true"`。
- 已知风险:① `--dart-define` 在 release 下同样生效,可击穿"正式包走生产"的保证;② `kDebugMode` 在 profile 模式为 false → profile 包会连生产,宜改用 `kReleaseMode`。
## 待办 / 可优化点
- **模块的排序与开关还没做**(注册表里 `id` 已为此预留);AI 的「可读范围」也需要按 `id` 配置。
- **`test/page_layout_test.dart` 在守布局**:用 320 / 393 / 480 三种宽度断言仪表盘和记账首页不溢出。改这两个页面的横向布局后跑一下 —— 它已经抓到过一次真 bug(记账首页顶部四个元素挤一行,在 393 宽的常规机上溢出 32px)。
- 硬编码颜色可迁移为 M3 角色色(`colorScheme.primary` 等),并补暗色模式。
- `api_env.dart` 优先用 `kReleaseMode` 显式判断,并统一生产地址(域名 + HTTPS);多环境改用 `--dart-define-from-file`;加运行时自检/启动日志显示当前 baseUrl。
- iOS 若需访问明文 HTTP,需在 `Info.plist` 增加 ATS 例外(`NSAllowsLocalNetworking` 或域例外)。
- `MainShell` 未做响应式:宽屏(≥600dp/840dp)应切换 `NavigationRail` / `NavigationDrawer`。
- `MainShell.initState` 的 `addPostFrameCallback` 回调里可加 `if (!mounted) return;`。
- Flutter 3.47 起 Material/Cupertino 已解耦为 `material_ui` / `cupertino_ui` 包,`package:flutter/material.dart` 仍可用,暂无需迁移。
## 应用名称与图标(已落地)
- 显示名统一为 **强宝小助手**:`AndroidManifest.xml` 的 `android:label`、`Info.plist` 的 `CFBundleDisplayName`、`web/index.html` 的 `<title>` 与 `apple-mobile-web-app-title`;`web/manifest.json` 用 `short_name = 强宝助手`(4 字以内避免桌面截断)。
- 图标链路:`tools/make_app_icon.py`(去水印 + 抠图 + 缩放)→ `assets/icon/app_icon.png` + `app_icon_fg.png` → `dart run flutter_launcher_icons`。
- 图标配置在 `pubspec.yaml` 末尾的 `flutter_launcher_icons` 段;dev 依赖已加 `flutter_launcher_icons: ^0.14.0`。
- 素材归档在 `assets/icon/_source/`(`raw_bear.png` 采用、`raw_bear_dog.png` 备选)。`assets/icon/` 未声明为 Flutter assets,不会进包。
- **`assets/images/logo.png`(1120×928 贴纸海报)不可用作图标**:非正方形、带虚化底纹、含大字,48px 下不可读。
- 改图标 = 重跑上面两条命令;改名字后需卸载重装才刷新桌面。
## 用户画像与偏好
- 用户正在**系统学习 Flutter 基础概念**,常就单个文件/代码片段提问(如 `GlobalKey`、`ConsumerWidget`、`ref`、`Stack`)。
- 沟通偏好:希望用**中文**解释,讲清「是什么 + 为什么这么写 + 在本项目中的上下文」,并配合结构化的表格/分层说明。
- 用户对 Material 3、多端适配、官方文档出处感兴趣,回答时应附**官方文档链接**。