1. 项目概述从128行原型到全功能AI家计簿的蜕变最近在做一个挺有意思的项目我们团队在开发一个叫“自分株式会社”的AI生活管理应用目标是把Notion、Evernote、MoneyForward、Slack这些你日常用的21个SaaS工具全都整合到一个地方。这想法听起来有点野心但做起来确实能解决信息碎片化的大问题。就在前几天看到亚马逊的Rufus AI推出了“Buy for Me”功能能帮你分析购买决策这让我突然意识到我们手头的家计管理模块还只是个128行的静态展示页面实在太简陋了。作为一个财务管理和效率工具的聚合平台没有点智能化的家计分析功能实在说不过去。于是我决定用Flutter Web在几天内把这个“摆设”页面彻底重构成一个带有AI节建议和未来资产模拟的、真正能用的家计AI顾问。这个新页面的核心目标很明确不仅要能像MoneyForward那样清晰地记录和分类收支更要利用AI去理解你的消费模式主动给出省钱的实操建议并且能让你直观地看到如果坚持某个储蓄或投资计划未来5年、10年你的资产会变成什么样。最终我把一个原本只有几个数字卡片的页面扩展成了包含4个核心标签页、超过750行代码的完整功能模块。整个过程没有增加新的后端服务完全复用现有的AI能力用纯Dart实现了复杂的财务计算并且保持了代码库的绝对整洁flutter analyze和deno lint都是0警告。如果你也在用Flutter做Web应用并且想引入AI能力或处理复杂的业务逻辑我踩过的坑和总结的模式或许能帮你省下不少时间。2. 架构设计与技术选型背后的思考2.1 为什么是Flutter Web Supabase组合选择Flutter Web作为前端对我们来说几乎是必然的。我们的核心应用是跨平台的一套代码能跑在移动端和Web端维护成本大大降低。Flutter Web经过几个大版本的迭代现在的性能和体验已经足够支撑这种数据密集型的后台管理页面。渲染图表、频繁更新状态比如用户调整预算滑块时实时更新进度条都很流畅。更重要的是Flutter丰富的UI组件库和高度自定义的能力让我们能快速构建出体验一致且美观的财务数据看板。后端选择Supabase则主要基于其“一体化”和“无服务器优先”的特性。我们的应用涉及用户认证、实时数据、AI接口调用等多个层面。Supabase的Auth、Postgres数据库、Realtime、Storage以及Edge Functions正好覆盖了所有这些需求。特别是Edge Functions它让我们能用TypeScript或Deno快速部署无服务器函数处理像AI对话这类需要调用外部API如Anthropic的Claude的敏感或复杂逻辑而无需自己管理服务器。这次家计AI顾问的核心——节建议生成就是直接复用了我们已有的一个通用ai-assistantEdge Function实现了零成本的功能扩展。2.2 数据层设计通用表与源标识模式在数据存储设计上我们采用了一个非常灵活且节省资源的模式。通常遇到“预算计划”、“实际支出”这类新功能第一反应可能是创建budget_plans和expenses这样的专用表。但我们没有这么做而是选择复用了现有的app_analytics通用事件表。这个表结构很简单核心字段有user_id、timestamp、source和metadataJSONB类型。source字段就是关键我们用不同的字符串来区分数据用途。例如// 保存用户设定的2024年7月“餐饮”预算 await supabaseClient.from(app_analytics).insert({ user_id: currentUser.id, source: budget_plan, metadata: { month: 2024-07, category: 餐饮, amount: 50000 } }); // 保存一笔2024年7月“餐饮”类的实际支出 await supabaseClient.from(app_analytics).insert({ user_id: currentUser.id, source: budget_expense, metadata: { month: 2024-07, category: 餐饮, amount: 3800, description: 周五部门聚餐 } });这么做的几个核心好处避免Schema爆炸每加一个小功能就建新表长期来看数据库会变得难以维护。用source字段区分逻辑清晰扩展时无需频繁执行ALTER TABLE。节省Supabase资源Supabase的免费和收费计划对数据库表数量有限制。复用现有表相当于在配额内做了最大化利用。灵活的数据结构metadata作为JSONB字段可以存储任意结构的数据。今天预算只需要amount明天如果想加个color标签直接存进去就行前端解析处理即可后端完全不用动。统一的查询接口所有财务相关数据的读写都通过同一张表简化了数据访问层的代码。当然这种模式不适合数据量极大、需要复杂关联查询或强事务保证的场景。但对于我们这种用户个人财务数据量级每月几十到几百条记录和查询模式主要是按用户、月份、来源筛选它提供了最佳的开发速度和灵活性。3. 核心功能模块的深度实现解析3.1 四标签页布局与状态管理策略UI上我们采用了经典的顶部标签栏TabBar加内容区TabBarView的布局四个标签分别是概览、预算、AI节建议、未来模拟。状态管理是这里的一个小挑战因为每个标签的数据概览的KPI、预算的设置与进度、AI建议内容、模拟计算结果都是独立获取和更新的而且有些操作比如在“预算”页调整金额需要实时反映在“概览”页的进度条上。我们没有引入复杂的状态管理库如Bloc、Riverpod因为当前模块的复杂度可控。而是使用了Flutter内置的ValueNotifier配合Consumer来自provider包来实现局部的、高效的状态响应。具体来说我们为整个财务页面创建了一个FinancialDataController类它内部管理着多个ValueNotifierclass FinancialDataController { final ValueNotifierMapString, double monthlyBudgetNotifier ValueNotifier({}); final ValueNotifierListExpenseRecord currentMonthExpensesNotifier ValueNotifier([]); final ValueNotifierString? aiAdviceNotifier ValueNotifier(null); final ValueNotifierdouble? simulationResultNotifier ValueNotifier(null); // 加载预算数据的方法 Futurevoid loadBudget(String month) async { final data await _fetchBudgetFromSupabase(month); monthlyBudgetNotifier.value data; // 更新Notifier所有监听它的Widget会自动重建 } // 更新单项预算的方法 Futurevoid updateBudget(String category, double newAmount) async { await _saveBudgetToSupabase(category, newAmount); // 先更新本地内存中的数据 final newMap MapString, double.from(monthlyBudgetNotifier.value); newMap[category] newAmount; monthlyBudgetNotifier.value newMap; // 触发UI更新 // 同时概览页的进度条Widget监听了这个Notifier也会自动更新 } }在UI中对于只关心预算数据的Widget我们用ValueListenableBuilder包裹这样只有当monthlyBudgetNotifier变化时这个Widget才会重建性能最优。这种“细粒度响应式”的模式在Flutter Web这种单页面应用里能有效避免不必要的全局重建保持界面流畅。3.2 AI节建议生成低成本接入大语言模型这是本项目的亮点之一。我们并没有为这个功能单独开发一个新的后端API或Edge Function而是巧妙地复用了项目中已有的一个通用AI助手函数ai-assistant。实现步骤数据准备在Flutter前端我们将用户指定月份如“2024-07”的财务数据汇总并格式化成一段清晰的文本。这包括总收入、总支出、以及分门别类的支出明细例如“餐饮: ¥85,000交通: ¥25,000娱乐: ¥18,000 ...”。构建提示词Prompt这是让AI输出高质量建议的关键。我们设计了一个结构化的提示词请扮演一位专业的个人理财顾问。请分析以下用户[2024-07]月份的家计数据并提供三条具体、可立即行动的节建议。 数据概览 - 总收入¥450,000 - 总支出¥380,000 - 主要支出类别 餐饮¥85,000 (占支出22.4%) 交通¥25,000 娱乐¥18,000 ...其他类别 要求 1. 请基于上述数据指出最有可能节省开支的1-2个类别。 2. 针对这些类别提出三条非常具体、实操性强的建议例如“尝试每周自带午餐3次预计每月可节省约¥12,000”而非“减少餐饮支出”。 3. 每条建议请用一句话说明预估每月可节省的金额范围。 4. 输出格式严格遵循仅输出三条建议每条以‘• ’开头使用中文。这个提示词明确了AI的角色、输入数据的结构、输出要求三条、具体、带金额预估和格式。通过限制输出条数和格式我们能得到稳定、整洁、可直接在UI上展示的结果无需复杂的后处理。调用Edge Function通过Supabase客户端库调用ai-assistant函数将上述提示词作为消息体发送。FutureString fetchAiAdvice(String month, FinancialSummary summary) async { final prompt _buildAdvicePrompt(month, summary); // 构建上述提示词 try { final response await supabase.functions.invoke(ai-assistant, body: { action: chat, message: prompt, }); return response.data[reply] as String; // 假设返回结构为 {“reply”: “...”} } catch (e) { // 处理网络或API错误返回友好提示 return AI分析暂时不可用请稍后重试。; } }前端展示将返回的文本三条带•的建议用Text组件渲染或者进一步用正则表达式拆分后放入ListView中提升视觉效果。避坑心得提示词工程是关键最初的版本只是简单地把数据扔给AI结果它可能回复一段冗长的分析文章或者建议数量不固定。通过精确的提示词约束才能得到产品化所需的结构化输出。错误处理必须友好AI API调用可能因为网络、额度、内容策略等原因失败。前端一定要做好try-catch给用户明确的反馈如“分析中...”、“服务繁忙”而不是让界面卡死或崩溃。成本控制复用现有Edge Function避免了新函数的冷启动开销和额外的监控负担。同时在提示词中限制输出长度也能有效控制每次调用消耗的Token数从而控制成本。3.3 未来资产模拟纯Dart实现的复利计算器“未来模拟”标签页的核心是一个复利计算器。用户输入初始金额、每月追加投资额、预期年化回报率和投资年限点击计算后就能看到期末的总资产预估。这个功能完全在前端用Dart实现不依赖任何后端服务或复杂库。核心算法实现我们采用按月复利计算的方式更贴近大多数基金定投的实际情景。核心函数如下/// 计算复利终值按月计算 /// [principal] 初始本金 /// [monthlyAddition] 每月追加金额 /// [annualRate] 预期年化收益率百分比如5.0表示5% /// [years] 投资年数 double calculateCompoundInterest( double principal, double monthlyAddition, double annualRate, int years) { // 1. 将年利率转换为月利率小数形式 double monthlyRate annualRate / 100 / 12; int totalMonths years * 12; double futureValue principal; // 2. 按月循环计算 for (int i 0; i totalMonths; i) { // 每月先计算利息上月本金 * 月利率 // 然后加上本月追加的投资额 futureValue futureValue * (1 monthlyRate) monthlyAddition; } // 3. 返回最终结果 return futureValue; }为什么选择循环计算而非公式标准的复利终值公式是FV P*(1r)^n PMT*[((1r)^n - 1)/r]。虽然公式更高效但对于大多数用户来说理解“每月投入、按月复利”这个过程循环计算在概念上更直观。而且对于几十年的计算最多几百次循环在浏览器的JavaScript/Dart引擎上性能开销完全可以忽略不计代码的可读性和可维护性收益更大。一个生动的例子假设用户有100万日元初始资金计划每月追加投资3万日元预期年化回报率为5%投资20年。总投入本金 1,000,000 (30,000 * 12 * 20) 8,200,000日元。通过上述函数计算20年后的资产总额约为15,440,000日元。利息收益部分约为7,240,000日元。这个数字直观地展示了“时间复利”的威力利息收益几乎接近本金总额。我们在UI上特意将这个“利息部分”高亮显示对用户是非常有力的储蓄激励。UI交互细节我们使用了TextFormField来接收用户输入并为其添加了输入验证确保是正数、利率合理等。当任何输入框的值发生变化时我们使用onChanged回调来触发重新计算并实时更新显示结果给用户即时的反馈。同时我们预设了几个“快速设置”按钮如“保守型3%”、“进取型7%”方便用户快速切换场景进行对比。3.4 预算管理与进度可视化预算页面允许用户在15个预设的生活类别如住房、餐饮、交通、娱乐、学习等中设置月度预算。数据通过前面提到的通用表模式保存到Supabase。可视化实现每个预算条目都是一个ListTile包含类别图标、名称、预算金额输入框和一个线性进度条LinearProgressIndicator。进度条的长度根据“实际支出 / 预算金额”的比例动态计算。LinearProgressIndicator( value: expenseAmount / budgetAmount, // 比例超过1.0则显示为满格可考虑颜色变红 backgroundColor: Colors.grey[200], valueColor: AlwaysStoppedAnimationColor( (expenseAmount / budgetAmount) 1.0 ? Colors.blue : Colors.red, ), )当用户在概览页记录一笔新支出时该类别对应的进度条会实时更新。这个“实时性”得益于我们之前提到的ValueNotifier状态管理。支出记录保存后会触发currentMonthExpensesNotifier更新而预算页的Widget监听相关数据会自动重绘进度条。注意事项数据一致性预算和支出都按“年月”如‘2024-07’严格区分。查询时务必带上时间范围避免把上月的支出算到本月。进度条超限处理当支出超过预算比例1.0时我们把进度条颜色设为红色并且值固定为1.0填满这样既能直观告警又不会让进度条“溢出”UI组件。4. Flutter Web开发与代码质量维护的实战要点4.1 保持flutter analyze 0警告的纪律在团队协作和长期维护中保持代码静态分析零警告至关重要。这次重构我特别关注了Flutter 3.19当前稳定版中analysis_options.yaml里require_trailing_commas这条规则。它要求在多行的集合字面量、函数调用参数列表的每一行末尾都加上逗号。为什么这个规则重要版本控制友好当你在集合中添加一个新元素时只需要新增一行上一行的末尾因为已有逗号所以这行修改在git diff中只会显示为“添加了一行”而不是“修改了上一行添加逗号 新增一行”。这让代码审查更清晰。格式统一自动格式化工具如dart format能更好地工作代码风格完全一致。错误示例和正确示例// ❌ 错误最后一行参数后面缺少逗号flutter analyze会报错 Widget _buildKpiCard(String title, double value, Color bgColor, Color textColor, IconData icon) { return Card( color: bgColor, child: Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ Icon(icon, color: textColor), Text(title, style: TextStyle(color: textColor)), Text(formatCurrency(value), style: TextStyle(...)), ], // - children 列表的 ] 前面也应该有逗号但这里先关注参数 ), ), ); } // ✅ 正确所有多行参数列表、集合的末尾都有逗号 Widget _buildKpiCard( String title, double value, Color bgColor, Color textColor, IconData icon, // - 参数列表最后一项也有逗号 ) { return Card( color: bgColor, child: Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ Icon(icon, color: textColor), Text(title, style: TextStyle(color: textColor)), Text(formatCurrency(value), style: TextStyle(...)), ], // - children 列表的 ] 前面也有逗号 ), ), ); }养成这个习惯后代码会整洁很多。建议在IDEVSCode或Android Studio中配置保存时自动运行dart format并定期在终端运行flutter analyze确保团队代码规范。4.2 适配Flutter版本DropdownButtonFormField的变迁另一个在实际开发中遇到的细节是DropdownButtonFormField的API变化。在Flutter 3.3之后直接设置value属性来预选值的方式被标记为弃用deprecated转而推荐使用initialValue。旧方式已弃用String _selectedCategory 餐饮; DropdownButtonFormFieldString( value: _selectedCategory, // 在Flutter 3.3会提示deprecated items: categories.map((String category) { return DropdownMenuItem(value: category, child: Text(category)); }).toList(), onChanged: (newValue) { setState(() { _selectedCategory newValue!; }); }, );新方式推荐final _categoryController TextEditingController(text: 餐饮); // 通过Controller设置初始值 DropdownButtonFormFieldString( // 不再使用value属性 items: categories.map((String category) { return DropdownMenuItem(value: category, child: Text(category)); }).toList(), onChanged: (newValue) { setState(() { _categoryController.text newValue!; }); }, controller: _categoryController, // 使用controller // 或者如果与Form关联可以使用initialValue // initialValue: 餐饮, );这个改动是为了更好地将下拉菜单集成到Flutter的Form生态中使其行为与其他表单字段如TextFormField一致。如果你在升级Flutter版本后遇到相关警告按照新方式修改即可。4.3 性能优化列表渲染与数据分页当支出记录越来越多时直接在ListView中渲染所有条目可能会导致滚动卡顿。我们采用了ListView.builder来按需构建子项这是Flutter处理长列表的标准做法。更进一步如果数据量巨大虽然家计数据通常不会可以考虑集成Supabase的实时分页查询。基础优化示例ValueListenableBuilderListExpenseRecord( valueListenable: financialController.currentMonthExpensesNotifier, builder: (context, expenses, child) { if (expenses.isEmpty) return _buildEmptyState(); return ListView.builder( itemCount: expenses.length, itemBuilder: (context, index) { final expense expenses[index]; return ExpenseListItem(expense: expense); // 使用独立的StatelessWidget }, ); }, )将列表项抽离成独立的StatelessWidget如ExpenseListItem可以最小化重绘范围。当只有某一条目的数据变化时只有那个对应的ListItem会重建而不是整个列表。5. 部署、测试与未来迭代方向5.1 Flutter Web的构建与部署开发完成后使用flutter build web命令生成优化的发布包。我们选择部署到Firebase Hosting因为它与Flutter工具链集成良好部署简单快捷。# 1. 构建生产版本 flutter build web --release --web-renderer canvaskit # 使用CanvasKit渲染器以获得更好的浏览器兼容性 # 2. 部署到Firebase (需先安装并登录Firebase CLI) firebase deploy --only hosting--web-renderer canvaskit是一个重要选项。CanvasKit渲染器能确保UI在不同浏览器中具有最高的一致性特别是对于自定义图形和文本渲染。虽然初始加载体积会比html渲染器稍大但对于我们这种包含自定义图表和复杂布局的应用来说稳定性优先。5.2 核心功能测试策略对于这样一个工具测试重点在于逻辑正确性和用户体验。复利计算单元测试为calculateCompoundInterest函数编写Dart单元测试验证常见场景零本金、零利率、长期投资下的计算结果是否正确特别是与已知的财务计算器结果进行对比。AI提示词与解析测试模拟不同的财务数据输入检查生成的提示词是否符合预期格式并模拟Edge Function返回各种格式的文本包括可能出现的错误信息测试前端解析和显示逻辑的健壮性。UI交互测试使用flutter_test进行Widget测试模拟用户点击标签页、输入预算、点击计算按钮等操作验证界面状态是否正确更新。集成测试关键编写一个简单的集成测试模拟用户从登录到查看AI建议的完整流程。这能确保前端与Supabase Auth、Database、Functions的集成是可靠的。5.3 可能的未来扩展方向这个家计AI顾问模块已经具备了核心功能但还有很大的深化空间数据可视化增强引入charts_flutter库在概览页增加月度收支趋势折线图、支出类别占比饼图让数据更直观。AI能力深化消费预测基于历史数据让AI预测下个月在各类别的大致支出。个性化建议不仅分析月度数据还能结合用户的长期目标如“两年内存够100万日元旅行基金”给出阶段性的储蓄和支出调整建议。收据图像识别通过Supabase Storage上传收据图片利用Edge Function调用OCR和AI服务自动提取金额、类别、商家信息实现“拍照记账”。多账户与家庭共享扩展数据模型支持用户管理多个账户如个人账户、家庭共同账户并实现家庭成员间的预算共享和支出可见在隐私授权前提下。与日历/待办事项集成这是我们“AI生活管理应用”的终极愿景。例如识别到日历中有“朋友生日”事件AI可以提前一周给出合理的礼物预算建议或者当某类别支出快超预算时在待办事项中生成一条“本周减少外出就餐”的提醒。从128行的静态页面到如今功能丰富的AI家计顾问这次重构让我深刻体会到利用好现有的强大工具链Flutter、Supabase复用已有能力AI Edge Function并专注于解决用户真实痛点清晰的预算、可操作的节建议、可视化的未来激励完全可以在短时间内打造出体验出色且功能扎实的产品模块。整个过程中保持代码的整洁和可维护性是为未来迭代铺平道路的关键。