
家里财政大权一直是我管但管得稀里糊涂。月初还信誓旦旦要做预算月底一看账单就沉默——钱花哪儿了查记账App又嫌麻烦分类不对、入口太深、广告弹窗坚持两个月就卸载。后来在小区业主群看到有人晒自家做的家庭账本突然就来了劲与其等别人把产品做好不如自己动手写一个反正我手里同时攒着小程序、Python和Android的技术栈干脆做成一个家里三个人都能用的记账系统。这个项目前后折腾了大概三周代号010610zkv5c成果是一套完整的家庭理财记账系统微信小程序端负责老婆日常随手记账Android端负责我自己的批量录入和账目管理后端用PythonFlask提供统一API再把收支数据做成可视化报表折线图、环形图、预算进度条一应俱全。今天就把这套系统的完整设计思路、核心代码和踩坑过程写出来希望能帮到想自己搭一套记账系统、或者正在做类似Python小程序Android多端项目的朋友。1. 为什么我要自己做一套家庭记账系统1.1 市面记账软件的痛点不是不好用是不合我家用市面上记账软件并不少随手记、挖财、鲨鱼记账我都用过。它们的共性是做成了给所有人用的产品而不是给某一家用的工具。具体痛点有三个分类体系固定死板。我家支出大头是孩子的奶粉尿布、老人的医药费、两个人的餐饮交通但主流App的默认分类里宝宝赡养往往是二级甚至三级菜单每次记一笔要连点好几下。数据在别人的服务器上。虽然方便但我不放心而且导出Excel之后字段乱得没法看。多端体验割裂。手机上记的账想在电脑上汇总分析要么开网页版要么导出很别扭。所以我的需求很明确一套数据完全自有的、分类可自定义的、手机端多个入口都能记账、图表能直观呈现收支结构的家庭账本。后端存自己的数据库接口自己定义想怎么折腾怎么折腾。1.2 项目目标三个家庭成员、三个使用场景我把使用场景拆成了三类分别对应不同的终端老婆日常买菜、网购、给孩子买东西频率高、金额小要的是打开就能记、三秒完成。适合放在微信小程序里随手打开就记不用下载App。我负责月底对账、补录大额支出、管理家庭预算需要在Android端有一个功能完整的记账管理界面能看明细、能筛选、能批量操作。两个人共同的需求月底看报表这个月收入多少、支出花在哪几类、预算还剩多少。这部分做成可视化页面小程序和Android都展示同一套接口的数据。明确了这三个场景之后技术选型就顺理成章了。2. 技术选型的思考小程序、Python、Android各司其职2.1 终端选择为什么同时保留Android和小程序很多人的第一反应是做一个App不就行了。但App有一个天然劣势——用户不愿意为了记一笔账专门打开一个冷启动要两三秒的应用。而小程序在微信里通过发现-小程序或者下拉菜单就能唤起路径短、心理负担小这是记账这种高频轻量操作最需要的体验。Android端则承担管理职能。我平时对账的时候需要在电脑旁放着一台Android平板竖屏显示账目列表点进去看每一笔的详情、修改分类、补记漏掉的支出。小程序端做不了太复杂的交互Android原生做列表筛选、底部弹窗、日期选择器都更顺手。还有一层考虑是数据冗余。如果只做小程序万一微信接口调整或者账号出问题整个系统就瘫了。Android端作为独立入口同时连着同一个后端相当于多了一条使用路径踏实。2.2 后端为什么用Python轻量记账系统谈不上高并发核心需求是开发效率高、写接口快、能方便处理后续的数据统计。Python的Flask框架在这一场景下几乎是首选Flask路由写法简单一个记账服务撑死也就十几个接口用Flask写比Spring Boot省一半时间。Python做日期处理和聚合统计非常顺手Pandas甚至可以直接对查询结果做分组汇总我后期做图表接口时几乎没费劲。部署简单一台小服务器就够了Gunicorn Flask 跑起来稳得很。很多人纠结Python性能不行那是拿Python跟C比高并发。家庭记账系统一天最多几百次请求Python绰绰有余。真正要花心思的是接口设计得清楚、数据格式统一而不是纠结框架本身的性能。2.3 可视化方案选型为什么是ECharts 自绘进度条可视化部分我最开始考虑过三条路后端用Matplotlib或Plotly生成图片返回给前端 —— 方案简单但交互差不能hover看数据。前端用原生Canvas手搓图表 —— 能做但工作量太大一个月内我不可能把折线图、饼图的动画和交互都调好。前端用ECharts —— 微信小程序和Android WebView都能跑配置成熟社区案例多这是性价比最高的方案。最后我选了ECharts。小程序端用Taro或者原生组件引入echarts-for-weixinAndroid端用一个WebView加载本地HTML页面内部也是ECharts。这样两个端共用一套图表配置代码只是外部包了一层不同的容器。预算进度条更简单不需要图表库直接在Android端用ProgressBar自定义样式小程序端用viewCSS宽度百分比搞定。3. 系统架构与数据模型设计3.1 整体架构一后端、两前端、一数据库整个系统的物理结构很清晰微信小程序 (记账入口) ↓ HTTPS JSON Python Flask API ↓ SQLite (后期可换MySQL) ↑ Android App (管理和对账入口)数据库选了SQLite起步。家庭账本数据量很小一年撑死几千条记录SQLite完全扛得住而且备份方便——直接把文件拷走就行。后期如果想让老婆手机远程访问再把SQLite的数据迁移到MySQLFlask的SQLAlchemy层不用大改。3.2 数据表设计四张表搞定核心业务设计数据模型的时候我遵循能简单就不复杂的原则最终落在四张表上用户表users就存昵称和头像URL。一家人没必要搞注册登录那套复杂权限通过一个简单的家庭成员编码区分就行。类别表categories这是整个系统最关键的灵活点。每条记录有type字段expense或income、name字段类别名、icon字段图标编码。我们把食品交通育儿医疗人情其他等作为默认支出分类老婆可以在小程序端自己加宠物美容这种个性化分类这样记账时才能做到点两下就记完。账单表transactions字段包括字段类型说明idINTEGER主键user_idINTEGER记账人category_idINTEGER关联分类amountREAL金额单位元typeTEXTexpense/incomenoteTEXT备注trans_dateTEXT交易日期YYYY-MM-DDcreated_atTEXT创建时间updated_atTEXT更新时间这里特别说明一下交易日期和创建时间分开存。因为经常有隔天补记的情况——比如昨天买的东西今天才记账如果不区分这两个时间月底统计本日支出就会乱掉。预算表budgets字段是month2025-06这种格式、category_id、amount。按类别做月度预算比只做一个总额预算更实用比如6月食品预算1500元6月交通预算300元。这套模型我用了大概两天调整最大的经验是记账系统的核心不是怎么存钱而是怎么把类别做灵活。分类一旦写死后面想加就牵一发动全身。4. Android端与小程序端的核心实现4.1 Android端对账和批量操作的主战场Android端我用的技术栈是Kotlin MVVM Room。本来可以直接调后端的REST API但我发现一个体验问题——网络不稳定时账目列表载入太慢于是加了一层Room本地缓存每次打开App先展示本地缓存再后台请求增量数据更新。核心的账目列表界面用RecyclerView实现每条item显示金额、分类图标、备注、日期。批量补记功能是长按一条item进入多选模式然后统一修改分类或删除这在月底对账时非常管用。记账录入页的核心逻辑不太复杂但有个细节值得提一下// 金额输入框绑定数字格式化 binding.amountEditText.doAfterTextChanged { editable - if (editable.isNullOrBlank()) { binding.amountPreview.text ¥0.00 returndoAfterTextChanged } val clean editable.toString().replace(¥, ).replace(,, ) val amount clean.toDoubleOrNull() ?: 0.0 binding.amountPreview.text ¥%.2f.format(amount) }这段代码做了金额输入的实时格式化预览。还有一个容易被忽略的问题EditText默认弹出的键盘是字母键盘记账时输入数字还要手动切换太蠢了所以录入页的XML里必须加上android:inputTypenumberDecimal4.2 微信小程序端三秒完成一笔记账小程序端的定位是极简记账所以首页就是一个大的加号按钮点击进入记账页。页面结构就三个输入域金额顶部大字号、分类网格中部、备注底部一行。分类网格从后端拉取按类型支出/收入切换展示两组分类。这个页面其实是最花心思的。因为手机的输入体验天然不如电脑所以所有交互都要为单手操作考虑金额默认聚焦键盘用户打开页面就能直接输数字。分类图标用简单的emoji文字符号就行我试过加载图片发现第一次启动时图片没缓存白屏一瞬间很影响体验。分类数据通过wx.request拉取存到globalData里做缓存这样每次打开记账页不用重新请求。代码如下function loadCategories() { const app getApp() if (app.globalData.categories.length 0) { return } wx.request({ url: ${app.globalData.baseUrl}/api/categories, success: (res) { app.globalData.categories res.data.data } }) }这里有个细节baseUrl在小程序端不能写localhost必须写后端服务器的局域网IP或公网域名。我一开始图省事写了127.0.0.1结果真机测试时请求全部失败——因为小程序真机里的localhost指的是手机自己而不是开发电脑。4.3 数据同步与接口约定为了两个端共用同一套数据逻辑我把API设计成了完全RESTful风格。核心接口如下方法路径说明GET/api/transactions?month2025-06获取某月账单POST/api/transactions新增账单PUT/api/transactions/{id}修改账单DELETE/api/transactions/{id}删除账单GET/api/categories?typeexpense获取分类列表GET/api/statistics/summary?month2025-06月度汇总数据GET/api/statistics/trend?months6近6月收支趋势接口返回格式统一为{ code: 0, msg: success, data: {} }统一返回值格式对前端的好处是解析逻辑只需要写一遍。code0表示成功非0表示业务错误。msg是给用户看的提示data是具体数据。这个约定在前后端联调时能省掉大量扯皮时间。5. 可视化报表的实现细节5.1 月度汇总收入、支出、结余一目了然月度汇总接口返回的核心字段是四个数字总收入、总支出、结余、预算剩余。Android端顶部放了三个大数字卡片中间用分隔线隔开点击结余可以展开当月每天的收支小柱状图。小程序端则是用一个简单的仪表盘样式展示预算使用率例如6月食品预算1500元已用980元剩余34.7%。这个进度条的宽度直接用CSS计算.budget-bar { height: 12rpx; border-radius: 6rpx; background: linear-gradient(90deg, #4facfe, #00f2fe); width: 65.3%; /* 根据剩余百分比动态设置 */ }5.2 收支趋势折线图用ECharts看清半年走势最让我兴奋的是趋势图部分。我需要让用户一眼看出这半年每个月的收入、支出变化曲线折线图是唯一合适的方案。小程序端引入echarts-for-weixin组件后配置代码和Web端基本一致const trendChart echarts.init(canvasNode) trendChart.setOption({ color: [#4facfe, #ff6b6b], tooltip: { trigger: axis, formatter: (params) { let res params[0].axisValue br/ params.forEach(item { res item.marker item.seriesName : ¥ item.value br/ }) return res } }, xAxis: { type: category, data: trendData.map(item item.month) }, yAxis: { type: value, axisLabel: { formatter: (value) ¥ value } }, series: [ { name: 收入, type: line, smooth: true, data: trendData.map(item item.income) }, { name: 支出, type: line, smooth: true, data: trendData.map(item item.expense) } ] })这一段的真实经验是ECharts在小程序里渲染canvas时必须先拿到canvas节点的宽高再初始化否则图表会以默认宽度绘制拉伸变形。我的解决方式是先wx.createSelectorQuery()查询节点宽度再初始化。Android端的话我选择了更省事的方案——直接在WebView里加载一个本地HTML文件动态传入JSON数据由HTML里的ECharts渲染。这样Android端不需要额外引入图表库代码量反而更少两张端共用的只是ECharts的option配置思路。5.3 分类占比环形图钱到底花在哪儿环形图用来展示当月支出各类别的占比。这是老婆最爱看的一张图因为看看钱是不是都花在吃上了。实现同样用ECharts核心配置是series.type: pie加radius: [40%, 70%]形成环形。环形图的重点在于图例和百分比标签的排版尤其是当类别较多时10类图例会挤压图表区域。我的处理是把图例放到图表下方使用滚动式图例百分比标签只在占比大于5%时才显示。这里附上后端聚合数据的核心Python代码用Pandas处理非常直观def get_category_ratio(month): transactions Transaction.query.filter( Transaction.type expense, Transaction.trans_date.like(f{month}-%) ).all() df pd.DataFrame([ {category_id: t.category_id, amount: t.amount} for t in transactions ]) if df.empty: return [] summary df.groupby(category_id)[amount].sum().reset_index() total summary[amount].sum() result [] for _, row in summary.iterrows(): category Category.query.get(row[category_id]) result.append({ name: category.name, value: round(row[amount], 2), ratio: round(row[amount] / total * 100, 2) }) return result很多人觉得Pandas在这里杀鸡用牛刀但我的理由是当数据量到几千条时用Python的for循环逐条计算会很慢而Pandas的groupby是向量化计算几乎瞬时完成。虽然家庭账本的数据量谈不上压力但这种写法让代码更简洁也方便以后扩展。5.4 可视化建设中的一个坑图表色号不统一这个坑必须拿出来单说。一开始我在小程序端和Android端分别定义了两个不同的色板小程序用#4facfe到#00f2fe的蓝青渐变Android WebView里却用了ECharts默认的颜色。结果两边的同类图表颜色完全不一样老婆截图对比后问我是不是两套数据。后来我把颜色方案统一提取到一个JSON配置里放在后端接口中返回。这样图表主题色由后端统一控制两端只需要读取默认配置即可。项目维护顺序从改完这一端忘那一端变成了只改一处全局生效。6. 实测阶段的一天两位家庭成员的真实反馈系统做好之后我没有自己闷头测而是让老婆和岳母分别用了一天。这一步收获非常大问题暴露得也相当真实。6.1 老婆的反馈记账速度还不够快她原话是能直接用但我还是想更快一点。我想了半天发现问题的根源在于每次打开必须从首页-点击记账按钮-选择分类-输入金额这条路径走完。于是做了两个优化在小程序首页常驻显眼位置放一个快速记账悬浮按钮点击直达记账页。记账页默认分类记录上一次选择的分类。比如上次选了餐饮这次打开还是餐饮大部分情况下可以直接输金额就完事了。这个记住上一次的功能实现起来就是把全局变量lastCategoryId存到Storage里下次进入页面时预先选中。效果立竿见影老婆实测记账耗时从8秒降到了3秒左右。6.2 岳母的反馈字数太大看不清岳母用的是放大字体模式。结果小程序界面布局全乱了金额数字截断、分类图标重叠。这也是我之前忽略的点——小程序的rpx单位看似适配屏幕但没有适配系统字体缩放。我用了两种方式解决对关键金额展示用text-overflow: ellipsis加overflow: hidden兜底防止数字被截断。对分类网格布局改用flex-wrap并且每个分类项设定最小宽度让它在放大字体时自动换行而不是挤在一起。6.3 我自己测试时发现的时间bug有一笔钱记到一半我突然发现列表里时间和实际时间对不上。排查半天根因是Android端提交账单时前端拼的日期字符串是本地时间而后端服务器用的UTC时间。因为我的服务器时区设置问题导致晚上12点后记的账系统时间会比实际早了8小时。解决方法是统一约定前端提交的就是一个不带时区的普通字符串YYYY-MM-DD由前端在用户选择交易日期时生成后端不主动生成时间。如果用户没有手动选择日期前端自动填入当前日期的本地时间。把时间生成的职责全部交给前端之后这个问题就彻底消失了。7. 复盘这套记账系统可以复用的几个通用方案7.1 自定义分类体系是记账类项目的核心设计决策我这套系统最大的一个设计决策就是把分类做成了用户可以自定义的主表。新手做记账项目时很容易把分类做成常量枚举但实际使用中一定会遇到没有我想要的分类的窘境。我的方案是后端在初始化时自动写入一套默认分类用户可以对这套默认分类做增删改但有一个约束——已有账单引用的分类不能被删除只能被合并/停用。这个约束是因为删除分类会导致历史账单的category_id悬空统计时会出现无类别脏数据。这是所有记账类项目必须处理的深坑如果你在做类似系统设计数据模型时一定要把分类和账单的关联方式考虑进去。7.2 预算算法的两个关键点预算模块我反复调了几轮最后总结出两个关键点预算按类别独立设置而不是总额。一个总预算在家庭场景中几乎没用因为没法控制单一品类超支。按类别的预算才能起到真正的提醒作用。超支警告用两个阈值触发。当某类支出达到预算的80%时给一次文本提醒达到100%时再给一次强提醒让用户有缓冲时间。这个80%预算警告的细节是从信用卡消费提醒里学来的很实用。7.3 后端接口设计的通用约定最后分享一个我这次实践后深以为然的经验所有接口严格统一错误码和返回格式。还要把鉴权简化到极致——直接用家庭成员编码作为请求头参数不做登录态管理理由很简单这是家庭内部工具没有多用户权限管理需求。如果你也要做类似的项目建议在动手写代码之前先花一个晚上把所有接口请求和响应的示例JSON手写出来发给自己看看是否直观。这一步能省掉后面至少三天联调时间。最后再分享一个小技巧给家庭成员设好唯独自己可用的批量补录入口。家庭记账最怕的不是某天忘记而是月底发现漏了十几笔如果只能一笔笔补录体验非常崩溃。我在Android端加了从微信/支付宝账单文本粘贴导入的功能——因为很多支付记录可以导出文本解析之后批量存入系统。现在每次月底对账我花十分钟就能把全家人的账理清。这个功能虽然不在最初的规划里却成了全家认为最实用的一个功能也算是一个意外的收获了。