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

9.9 KiB
Raw Permalink Blame History

项目长期记忆:强宝小助手

项目概况

  • 名称: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、多端适配、官方文档出处感兴趣,回答时应附官方文档链接。