上个月月底,我家那位第三次在睡前问我同一个问题:“这个月我们到底花了多少钱?”我翻开两个手机的支付账单,对着 Excel 手工汇总了快一个小时,得到的数字还被质疑“超市那笔你漏了”。也就是从那一刻起,我下定决心自己动手做一款真正适合两个人的情侣共享记账系统。项目从零开始,代号 Money Tracker Pro,核心定位就三个:两个人共用一本账、账单实时同步、AI 自动生成消费分析。这篇文章把完整的开发过程、数据模型设计、AI 分析模块的实现思路,以及我踩进去又爬出来的坑,全部摊开来讲。如果你正在做自己的第一个全栈项目,或者想给对象做一个真正能落地使用的记账工具,这篇内容值得你从头看到尾。
1. 为什么非要自己造轮子:情侣记账的真实痛点
先别急着动手写代码,做任何项目之前,得先搞清楚一个问题:市面上现成的记账 App 那么多,随手一搜能列出十几个,凭什么还要自己开发一个?
1.1 市面产品解决不了“两个人的钱”这件事
市面上的记账软件大致分两类。一类是个人记账工具,功能做得很重,什么股票持仓、公积金、信用卡管理全都塞进来,但它的核心数据模型是“一个人的账本”,两个人共同记一笔账得手动切换账号,根本没有“共同账本”的概念。另一类是引入社交功能的社区型记账产品,把记账变成打卡晒账单的秀场,每记一笔账都要纠结要不要公开,而且首页塞满理财推荐和广告,我用了几次就觉得它不是让我记账的,是让我来买基金的。
最关键的一点是,绝大多数现成产品里的“智能分析”就是个统计报表,最多帮你画个饼图、按分类排个序。真正能回答“我们这钱花得合理吗”“这个月比上个月超支了多少”这类问题的产品不是没有,但有这个功能的基本都藏在付费墙后面,一个月会员十几二十块,用起来还总是推送额度满减。我想要的不是图表,是一个能看懂我们账本的人——或者说,一个能基于账本数据帮我们做判断的 AI 助手。
1.2 目标收敛:三个核心能力
想清楚了要解决什么问题,我把项目目标收敛成三条,后面所有开发决策都围绕这三条来:
- 一本共同账本:情侣双方通过邀请码加入同一个账本,两个人都能记账、改账、看账,所有操作实时同步。
- 账单结构要灵活:别小看“谁付的钱”这件事。两个人吃饭有时 AA,有时这顿你请下顿我请,还有一起存旅行基金的情况。数据模型必须支持“一笔账单多人分摊”的逻辑,否则记到后面账必然对不上。
- AI 消费分析:不是输出柱状图和饼图,而是用大模型对一段时间的账单做语义级分析,生成人话版消费报告,指出异常消费,给出调整建议。
项目定完这三个目标,我心里其实已经清楚了一件事:这不是一个“三天撸完”的玩具项目,它是一个真正需要认真设计数据模型、认真处理并发和隐私问题的全栈应用。但反过来,它的复杂度又刚好卡在“一个人 hold 得住”的范围里。接下来就是选技术栈。
2. 技术选型:为什么最后用了这套组合
技术选型这件事,很多新手容易犯一个毛病:什么火用什么,项目写到一半发现生态不熟,到处踩坑,最后烂尾。我的原则很简单——选自己最熟、社区最稳、文档最全的组合,够用就好。
2.1 客户端:Flutter,一套代码覆盖两端
之所以选 Flutter 而不是纯原生 Android 加 iOS 双开发,核心原因就一个字:省。情侣记账这种工具类应用,没有特别复杂的系统级交互,不需要整天调底层 API,Flutter 的跨端能力完全够用。而且 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存储时间,但用户在手机上录入账单,用的是北京时间。如果查询时没有把查询参数从北京时间转换成 UTC,5 月 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 的代码还在我的仓库里,每次打开那个熟悉的目录,我都会想起那个晚上,她把手机递给我,说“你算算我们到底花了多少钱”时的表情。现在,这个问题的答案,三秒就能算出来了。