上个月月底我家那位第三次在睡前问我同一个问题“这个月我们到底花了多少钱”我翻开两个手机的支付账单对着 Excel 手工汇总了快一个小时得到的数字还被质疑“超市那笔你漏了”。也就是从那一刻起我下定决心自己动手做一款真正适合两个人的情侣共享记账系统。项目从零开始代号 Money Tracker Pro核心定位就三个两个人共用一本账、账单实时同步、AI 自动生成消费分析。这篇文章把完整的开发过程、数据模型设计、AI 分析模块的实现思路以及我踩进去又爬出来的坑全部摊开来讲。如果你正在做自己的第一个全栈项目或者想给对象做一个真正能落地使用的记账工具这篇内容值得你从头看到尾。1. 为什么非要自己造轮子情侣记账的真实痛点先别急着动手写代码做任何项目之前得先搞清楚一个问题市面上现成的记账 App 那么多随手一搜能列出十几个凭什么还要自己开发一个1.1 市面产品解决不了“两个人的钱”这件事市面上的记账软件大致分两类。一类是个人记账工具功能做得很重什么股票持仓、公积金、信用卡管理全都塞进来但它的核心数据模型是“一个人的账本”两个人共同记一笔账得手动切换账号根本没有“共同账本”的概念。另一类是引入社交功能的社区型记账产品把记账变成打卡晒账单的秀场每记一笔账都要纠结要不要公开而且首页塞满理财推荐和广告我用了几次就觉得它不是让我记账的是让我来买基金的。最关键的一点是绝大多数现成产品里的“智能分析”就是个统计报表最多帮你画个饼图、按分类排个序。真正能回答“我们这钱花得合理吗”“这个月比上个月超支了多少”这类问题的产品不是没有但有这个功能的基本都藏在付费墙后面一个月会员十几二十块用起来还总是推送额度满减。我想要的不是图表是一个能看懂我们账本的人——或者说一个能基于账本数据帮我们做判断的 AI 助手。1.2 目标收敛三个核心能力想清楚了要解决什么问题我把项目目标收敛成三条后面所有开发决策都围绕这三条来一本共同账本情侣双方通过邀请码加入同一个账本两个人都能记账、改账、看账所有操作实时同步。账单结构要灵活别小看“谁付的钱”这件事。两个人吃饭有时 AA有时这顿你请下顿我请还有一起存旅行基金的情况。数据模型必须支持“一笔账单多人分摊”的逻辑否则记到后面账必然对不上。AI 消费分析不是输出柱状图和饼图而是用大模型对一段时间的账单做语义级分析生成人话版消费报告指出异常消费给出调整建议。项目定完这三个目标我心里其实已经清楚了一件事这不是一个“三天撸完”的玩具项目它是一个真正需要认真设计数据模型、认真处理并发和隐私问题的全栈应用。但反过来它的复杂度又刚好卡在“一个人 hold 得住”的范围里。接下来就是选技术栈。2. 技术选型为什么最后用了这套组合技术选型这件事很多新手容易犯一个毛病什么火用什么项目写到一半发现生态不熟到处踩坑最后烂尾。我的原则很简单——选自己最熟、社区最稳、文档最全的组合够用就好。2.1 客户端Flutter一套代码覆盖两端之所以选 Flutter 而不是纯原生 Android 加 iOS 双开发核心原因就一个字省。情侣记账这种工具类应用没有特别复杂的系统级交互不需要整天调底层 APIFlutter 的跨端能力完全够用。而且 Flutter 的图表生态里有个 fl_chart画月度趋势图、分类占比图都很顺手我们后面做 AI 报告的图文展示时省了不少事。有人可能会问为什么不直接做成微信小程序说实话小程序确实更快但有两个硬伤一是涉账单数据这种相对私密的信息全部走微信生态总让我不放心二是我希望这套代码以后能扩展成独立 App 上架小程序改造成本反而更高。所以最后还是选了 Flutter。2.2 服务端Spring Boot 3 Spring AI服务端我选了 Java 系的 Spring Boot 3没有别的原因就是这套技术我在生产环境里摸了好几年它的事务管理、生态成熟度、排查问题的手段我都心里有数。做账本系统最怕的就是金额出问题而 Spring 的声明式事务Transactional能让我在处理分摊、平账这类多表操作时把正确性牢牢锁住。AI 这一层我直接用 Spring AI 框架来做统一接入。很多人一听说“接入大模型”就觉得很高大上其实到了 2024、2025 年这个节点大模型 API 的接入早就标准化了。Spring AI 做的事情很简单就是把 OpenAI、通义、DeepSeek 这些模型提供方的接口做了一层抽象你在代码里只需要面向ChatClient编程换模型供应商只需要改配置不用改业务代码。这点对我们的项目来说很关键因为我一开始就不确定最后会用哪家模型先跑通流程再说。2.3 存储与缓存MySQL Redis数据这块没悬念MySQL 是账务数据最稳妥的选择事务和 ACID 都靠它兜底。Redis 主要干三件事缓存用户的登录态、缓存 AI 分析报告的临时结果、做接口限流。账本这种数据其实不太适合放 Redis 缓存因为读写频率高、一致性要求也高我干脆不缓存实时账单只在 AI 报告这种“生成一次、短时间反复查看”的数据上做缓存。为了照顾可能没接触过这套组合的读者我先把整体架构画成一句话Flutter 客户端通过 HTTPS 调用 Spring Boot 的 REST 接口服务端用 MySQL 存账本和账单用 Redis 管会话与报告缓存AI 分析模块通过 Spring AI 调用外部大模型 API产出的报告结果落库。3. 共享账本的数据模型设计钱要分得清也要记得住这是整个项目里我认为最核心、也最值得拿出来分享的部分。功能界面是皮数据模型才是骨。特别是当你面对的是“两个人共同记账”这个场景时数据模型的好坏直接决定后面积不积重难返。3.1 第一个铁律金额一律用“分”存储所有做过账务系统的工程师都会告诉你一句话货币计算永远不要用浮点数。Java 的double在表示 0.1 的时候就已经不精确了如果你用double存金额记账记到第三笔就会出现 0.30000000000000004 这种魔幻数字。也不要偷懒用BigDecimal到处传虽然它精度没问题但在数据库里映射、JSON 序列化、前端展示这些环节都很啰嗦。我的做法很粗暴也很标准数据库字段和 Java 实体里金额统一用Long类型的“分”存储。比如用户输入 12.35 元前端转成 1235 分传给后端后端存 1235查询出来再转回 12.35 元给前端展示。这样不仅完全没有精度问题聚合查询 SUM 起来也是整数运算性能还好。public class Bill { private Long id; private Long ledgerId; private Long payerId; // 付款人 private Long categoryId; // 分类 private Long amountCents; // 金额单位分 private Integer splitType; // 1-均摊 2-按金额 3-自定义 private String splitJson; // 分摊明细如 {userA: 500, userB: 700} private LocalDateTime billTime; // 账单时间 private String note; // 备注 private Integer version; // 乐观锁版本号 }前端那边我封装了一个MoneyUtil负责把用户输入的字符串“12.35”解析成1235再把1235格式化回“12.35”。一进一出都走这个工具类项目从头到尾没有一处直接拿浮点数做金额运算。3.2 账本、成员、账单的关系三张核心表情侣记账和我们平时用的个人账本最大的区别是“共享”。这就逼着我设计了三张核心业务表而不是简单地在账单上加一个userId就完事。第一张是账本表ledger它代表“两个人共同拥有的一本账”。字段很简单账本名称、创建人、邀请码。邀请码是关键它是一串 6 位随机字符新用户扫码或输入邀请码就能加入这本账。第二张是账本成员表ledger_member记录哪个用户属于哪个账本。为什么要专门一张表而不直接在账本表里塞两个userId因为账本以后可能不止两个人用甚至是家人、室友共用一个账本用关联表才具备扩展性。而且我们后面所有鉴权都基于这张表你只能查询自己加入的账本的账单。第三张就是账单表bill每笔消费一行记录。注意一个设计细节账单表里没有存“这笔账记给谁”而是拆成了splitType和splitJson两个字段。一笔 300 元的聚餐如果 AA那splitJson里就记两个人各 150如果今天这顿是某一方请客那splitJson就记请客方 300、另一方 0。这个设计让“谁欠谁”的问题在数据层一次解决后续做平账统计时不用再猜。3.3 月度结算与“谁花得多”不吵架的算法有了上面的数据模型我们就可以算一本总账了。每个月月初系统会跑一个定时任务对上一月的数据做结算结果存一张monthly_settlement表。结算的核心逻辑是统计这个月双方各自付款的总金额即payerId维度的 SUM。统计双方各自应承担的金额即按splitJson做反解累加每个人分摊到的部分。应付减去实付得到每个人当月的净差额。A 的净差额是正数代表 A 替 B 垫付了钱月底可以商量是转账还还是记到下月。这套算法发送出来的“月度结算单”比任何一句“我觉得我花得多”都有说服力。因为每一分钱都来自账单明细数据摆在那里想吵都吵不起来。这也是我做完这个项目之后最大的感受很多情侣矛盾不是因为谁花得多谁花得少而是因为没有一笔清清楚楚的账。4. AI 消费分析模块从统计报表到“会说话”的账单这个模块是整个项目的点睛之笔也是标题里“附 AI 消费分析”的真正落点。我做这个功能前想明白了一件事市面上所有记账 App 的分析模块本质上都在做“数据可视化”而用户真正需要的是“数据解读”。饼图不会告诉你“这个月咖啡支出异常偏高”但 AI 会。4.1 分析流程拆解先结构化再上大模型AI 分析模块我拆成了五步流水线每一步都有明确职责数据聚合写 SQL 从账单表里按月、按分类、按付款人做聚合统计同时拉取上个月的同口径数据做环比。上下文组装把聚合结果拼成一份结构化的 JSON 或文本作为给大模型看的“账本摘要”。提示词模板用一套精心设计的 Prompt 让模型按我们期望的格式输出分析报告。模型调用与解析通过 Spring AI 调用大模型拿到 JSON 格式的结构化报告解析后渲染到前端。落库与推送报告存进数据库同时给双方的 App 推送一条通知“你们的月度消费报告已生成”。核心就在前两步你给模型的数据必须是聚合好的、干净的结构化数据而不是一坨原始流水。最开始我犯过一个错误想直接把一个月的几百条账单明细全塞进 Prompt让模型“自己看”。结果上下文长度爆了API 报错费用也飙升。后来我才意识到大模型擅长的是文本理解和归纳不是给你做数据库聚合查询。聚合是数据库的活模型只负责基于聚合结果做解读。4.2 提示词模板把规则写死把发挥空间留给模型很多人对 Prompt 的理解是“问问题”但在工程化项目里Prompt 是一段需要反复调优的代码。我最终稳定下来的模板大致长这样你是一个专业的家庭财务分析师。请基于以下某情侣账本某个月的消费聚合数据生成一份月度消费分析报告。 数据 { month: 2025-05, total_spent_cents: 864350, last_month_total_cents: 771200, top_categories: [ {category: 餐饮, amount_cents: 236000, ratio: 0.27}, {category: 住房, amount_cents: 180000, ratio: 0.21} ], anomalies: [...] } 要求 1. 输出 JSON 格式包含 summary、good_points、issues、advice 四个字段。 2. summary 在 80 字以内语气自然不要用“亲爱的”之类的称呼。 3. issues 必须结合数据指出具体问题比如“餐饮支出环比上涨 32%”。 4. 每条 advice 必须具体可执行不要写“理性消费”这种空话。注意最后一条要求——“不要写‘理性消费’这种空话”是血的教训。如果不加这一句模型会输出一堆“建议合理规划支出、避免冲动消费”的正确的废话用户看完等于没看。加了之后模型才会老老实实从数据里找问题给出“下个月试着把外卖次数从 12 次降到 8 次”这种有信息量的建议。4.3 异常消费识别规则和大模型配合而不是靠大模型裸奔AI 模块上线之后我又发现一个问题如果完全靠大模型从聚合数据里找异常它经常找不准因为大模型看到的只是统计数字它不知道我们这对用户的“正常”是什么。所以我加了一个前置的规则引擎用简单的统计学方法先筛出异常再把这些异常点连同聚合数据一起交给大模型。规则引擎的逻辑很简单主要查四类问题单笔大额单笔消费超过当月日均消费 10 倍的账单拉出来让模型判断是否合理。分类突变某分类本月消费额比近三个月均值高出 50% 以上标记为“分类支出异动”。深夜消费账单时间在 23:00 到次日 05:00 之间的消费单独汇总很多非理性消费都发生在这个时段。高频商户同一个商户描述在一个月内出现 5 次以上说明可能存在无意识的习惯性消费。规则引擎筛出这些点之后我把它们作为anomalies字段塞进 Prompt 里大模型只需要基于这些浓缩过的异常点做归因和建议准确率高了很多。我在实际跑数据的时候遇到过这样一个案例规则引擎发现“便利店”一个月出现了 14 次大模型一眼看穿这是每天下班顺路买饮料的习惯性消费建议改成周度批量购买一个月真省下了一百多块。这种洞察纯靠规则或者纯靠大模型都很难产出。4.4 AI 接口不稳定的兜底方案接过大模型 API 的都知道就算你再小心也一定会在某个深夜遇到超时、限流、返回格式错误。所以 AI 分析模块我做了三层兜底第一层是缓存兜底。月度报告生成一次之后直接把结果写进ai_report表用户反复打开是直接从库里读不重复调用模型。只有新月份第一次进入分析页面才会触发模型调用。第二层是规则兜底。如果模型调用失败就返回一份基于模板拼出来的基础统计报告把环比涨跌幅、分类占比这些硬数据用固定模板呈现。虽然不如大模型写得有温度但至少用户能看到核心数字不至于白屏。第三层是降级兜底。Spring AI 配置里我设置了超时时间和重试次数第一次超时自动切换到备用模型供应商两次都失败才会返回兜底报告。同时在日志里记录失败原因方便我第二天排查。这一套组合拳打下来AI 分析模块的可用率稳定在 98% 以上。我一直觉得做 AI 功能最重要的不是模型选得多强而是把“模型挂了怎么办”想清楚。模型是不可控的但你的系统必须是可控的。5. 开发过程中踩过的关键坑每一条都是真金白银这部分我要单独拿出来写因为有些坑不是你读官方文档就能避开的非得亲自踩一脚才长记性。5.1 两个人同时记账的并发冲突共享账本有一个很现实的问题两个人可能在同一天、同一时刻往账本里记不同的账或者更麻烦的——一起来修改同一笔历史账单。刚开始我没做任何并发控制结果出现了两次严重的丢数据问题一次是女朋友在改一笔外卖账单的分摊比例时我正在旁边同步加一笔超市消费她的提交把旧数据覆盖回去了出来的账就对不上了。解决方案分两层。账单一类的新增操作本身没有并发冲突走正常 INSERT 就行。真正需要防的是 UPDATE 操作我在bill表上加了version字段做乐观锁每次更新时先比较版本号不匹配就提示用户“这笔账单刚刚被对方修改过请刷新后再试”。这也是我第一次在实际项目中体会到账务系统的并发控制不是技术选型问题是感情问题。数据丢了可以再补信任裂了很难修复。Redis 在这个场景里也帮了忙。我在写操作入口加了一个简单的分布式锁同一个账本同一时刻只允许一个写事务在跑。虽然牺牲了一点点并发性能但换来的是账单数据百分之百的一致性。两个人用一个账本的写入频率根本撑不爆这个限制完全没有必要为了所谓的性能去引入复杂方案。5.2 月份查询的时区陷阱这个坑特别隐蔽我排查了整整一个下午。需求是“查询 5 月份的账单”我的第一版 SQL 写的是WHERE bill_time BETWEEN 2025-05-01 00:00:00 AND 2025-05-31 23:59:59。表面看没问题但它有两个隐患。第一个隐患23:59:59这种写法会漏掉23:59:59.500这种带毫秒的记录。五彩斑斓的漏数据在账务系统的对账环节就是灾难。正确的写法应该是bill_time 2025-05-01 00:00:00 AND bill_time 2025-06-01 00:00:00用左闭右开区间把整个月完整框进去。第二个隐患更严重时区。我在数据库里统一用UTC存储时间但用户在手机上录入账单用的是北京时间。如果查询时没有把查询参数从北京时间转换成 UTC5 月 1 日早上 8 点的账单就会被算到 4 月 30 日里去。最终的方案是存储统一用 UTC查询时把请求里的时区信息一起带过来由后端统一做转换前端彻底不管时区逻辑。5.3 大模型输出的 JSON 总是带 Markdown 代码块这个问题折磨了我一整天。我要求模型输出纯 JSON模型也确实输出了 JSON但它把 JSON 包在了一个 Markdown 的代码块里前面是json后面是。我第一版解析器直接JSON.parse()看到第一个字符是反引号就报错。方案有两个。一个是解析前做一次字符串清理把首尾的json 和去掉再解析。这个方法快但等于在跟模型的行为打补丁换一个模型供应商可能又不灵了。另一个方案是利用 Spring AI 的Structured Output能力在调用时声明一个 POJO 类让框架自己去把模型输出映射成对象解析错误时框架会自动重试一次。两个方案我最后都用了先用框架的结构化输出双保险再兜一层字符串清洗。还有一个相关的坑模型偶尔会输出空字符串或一长串重复的无意义内容。Spring AI 的响应里带有finishReason字段如果等于LENGTH因为长度限制被截断我就直接判定这次生成无效走兜底方案不回传空数据给前端。5.4 隐私与权限每一条 SQL 都要带上账本 ID做共享记账系统隐私是一碰就炸的红线。两个用户虽然在一个账本里但并不意味着他们可以看对方的个人消费明细以外的、不属于这个账本的数据。我在所有查询接口上都加了一个强制约束必须带ledgerId且当前用户必须是该账本的成员否则一律返回 403。这个逻辑看起来简单但实现的时候很容易漏。比如某次加了一个“查询账单详情”的接口图方便只按billId查询了忘了校验当前用户是否属于这笔账单所属的账本。这就意味着任何已经登录的用户只要拿到一个 billId 就能看到别人的账单。这种越权漏洞在记账场景里等于裸奔。所以我后来把“校验成员资格”抽成了一个公共注解和拦截器对所有涉及账本数据的接口统一生效不允许任何绕过。另外AI 分析涉及的隐私问题我也专门处理过。前面说过每次调模型只传聚合后的统计结果绝不上传原始账单明细。像“美团外卖 35 元”“某超市 120 元”这种单笔信息属于用户非常私密的数据哪怕模型厂商宣称不保留数据我也不会冒这个险。6. 实测结果与可以继续做的方向项目从设计到上线前后花了大概三周业余时间。页面 UI 走的是极简风没有花里胡哨的皮肤和动效就两个字快、准。你现在问我这个项目值不值得做我会毫不犹豫地说值得原因不是技术难度有多高而是它的反馈链路实在太短了。6.1 真实使用感受AI 报告真的改变了我们的沟通方式我自己试用、加上女朋友一起用了半个月之后有三个直观变化。第一我们终于能在一分钟内回答“这个月花了多少钱”了打开 App 首页就是本月花销和预算进度条。第二AI 月度报告生成后我们俩会花五分钟一起读一遍这个动作本身变成了一个很有仪式感的“家庭财务复盘”很多以前会憋在心里的消费分歧在数据面前都变得可以讨论了。第三也是我最意外的异常消费规则里那个“便利店高频消费”的提醒真的让我戒掉了每天下班顺手买饮料的习惯一个月省了差不多 200 块。从数据上看系统的读写性能完全没问题账单量在一万笔以内时所有接口响应都在 100 毫秒以内。AI 报告平均生成时间在 5 秒左右配上缓存之后第二次打开基本秒开。作为两个普通人日常记账的小系统这个体量绰绰有余。6.2 后续扩展方向账单 OCR、预算预警、多账本项目做到这里我可以很负责任地说MVP 已经完整了。但如果以后有空我列了几个明确的扩展方向按优先级排序账单 OCR 识别拍一张小票照片自动识别金额和分类并生成账单。现在各家大模型都有很强的多模态能力这个功能实现难度不高但能大幅降低记账门槛。预算超支预警在 AI 分析的基础上做预算当某个分类的消费超过月度预算的 80% 时推送一条预警通知把“事后分析”升级为“事中提醒”。多账本支持现在是一对情侣一本账以后可以拆出“旅行账本”“家庭账本”这种独立子账本让 AI 分析按场景分开跑。更本地化的模型部署当前 AI 分析走的是云上 API等账单数据积累到一定程度可以尝试在本地部署一个小参数模型专门做数据归纳把最敏感的原始数据完全留在本地只把聚合结果传给云端模型做深度分析。我个人的经验是做这种偏工具类的全栈项目不要一开始就贪大贪全。先把“两个人如何记好一本账”这件事做到极致再加上一个真正有洞察力的 AI 分析模块就已经比市面上大多数产品好用了。技术上的难度从来都不是最大的门槛最大的门槛是你愿不愿意为一个真实的需求耐心地把每一个细节抠到位。Money Tracker Pro 的代码还在我的仓库里每次打开那个熟悉的目录我都会想起那个晚上她把手机递给我说“你算算我们到底花了多少钱”时的表情。现在这个问题的答案三秒就能算出来了。
